参考原文

安装

使用 Homebrew 安装 proxychains-ng

1
~ $ brew install proxychains-ng

编辑配置文件

1
vim /usr/local/etc/proxychains.conf

在 [ProxyList] 下面(也就是末尾)加入代理类型,代理地址和端口
注释掉原来的代理并添加, 1080 就是默认的shadowSocks 本地代理端口号,

1
socks5 127.0.0.1 1080

如果有的软件支持socks5代理, 比如 dropbox ,可以直接设置,

我们在这里搞这么多飞机就是因为, iTerm2 不支持配置代理,而且就算你在系统设置了也不管用.

如果所在的网络很复杂,可能需要在配置文件中启用
dynamic_chain - 按照列表中出现的代理服务器的先后顺序组成一条链,如果有代理服务器失效,则自动将其排除,但至少要有一个是有效的
然后在 [ProxyList] 下添加多个代理
默认:strict_chain - 按照后面列表中出现的代理服务器的先后顺序组成一条链,要求所有的代理服务器都是有效的

测试

proxychains4 curl google.com
注意:proxychains支持的是socks,http, https协议.它们以tcp或者udp协议为基础, ping命令用的是 ICMP 协议, proxychains 不支持;

优化

alias 命令别名
直接在命令行中输入

1
~ $ alias pc="proxychains4”

iTerm中前缀补全
你输了很长一段命令,然后你突然想使用代理功能,怎么办?
我想你应该会复制一下,然后重写;
现在你可以这样做:

在 iTerm -> Preferences -> Profiles -> Keys 中,新建一个快捷键,例如 ⌥(alt) + p ,Action 选择 Send Hex Code,键值为 0x1 0x70 0x63 0x20 0xd,保存生效。这个 HexCode 用 hexCodeToAsciiText 就是 “pc”两个字母

以后命令要代理就直接敲命令,然后 ⌥ + p 即可,这样命令补全也能保留了。
比如 git clone, npm install 加速等等.

推荐(福利)

最后推荐一个下载视频的神器 you-get可以下载国内外主流的视频网站的视频,made by python3. 也可以配置 proxychains4使用

1
~ $ pc you-get https://www.youtube.com/watch?v=fwoDwajwqdo

谜团

关于使用shadowSocks 时,dns是不是一起走了代理了,这里面还有很多网络知识不是特别明白

Native-Module 基本分为两种

  • 可见的Module. (显示,原生动画)
    View 本身继承自RCTView,如果想要在JS中使用,需要再实现一个 Manager
  • 不可见的Module. (数据,网络,modeling)
    需要实现 delegate

今天我们来讲讲第二种不可见的Module

Module 和 method的关系

One Module

-> MethodA
-> MethodB

-> MethodN

官方指引

  1. 正常的参数, 所有json格式的参数
    而且有个强大的Util RCTConvert.h 从字体,颜色,string,number都可以转换
  2. 支持回调 callback, 可以传function进去
    原生类型是 RCTResponseSenderBlock
    和传参都是 用RCT_EXPORT_METHOD 声明
  3. 支持Promise
    但是需要用到 RCT_REMAP_METHOD 声明,再辅助两个block
    RCTPromiseResolveBlock/ RCTPromiseRejectBlock
    JS 层面 用 ES2016 async/await 去写, 这个需要看一下

  4. 接下来讲多线程
    methodQueue 是在一个Module下的所有method共享的, 要么这个module是在主线程上执行,要么就是在某个线程上执行. 而且大家可以共享这个线程
    如果我有一个方法需要单独在新的线程上执行怎么办,比如相对耗时的存储等等.我可以在方法里面 用dispatch_async包一下执行代码,执行完之后可以用 2.里面提到的callback通知外面
    最后提到了 在不同的Module中共享线程,比较高深

  5. 导出常量
    返回Dictionary的native method 在初始化的时候完成赋值,运行时的时候再改变他,js也拿不到最新的值.
    如果要枚举常量,他建议新建一个RCTConvert的extension

  6. 向JS发送Event
    如何使用 bridge.eventDispatcher, 如何在native send 以及如何在JS subscription. 这样Native 也可作用在JS上了,这是除了 callback的另一种实现.
    采用的是观察者模式. 如果想用JS作用在Native上直接调取Native 用RCT_EXPORT_METHOD 暴露出来的函数就好了,各种回调,promise,sender伺候你.

这里多数一句,我们在OC里常用的传递消息的方式有很多 KVO,Delegate,Block. 还有高逼格的NSOperation,GCD 等等.基本上RN都用了.可见facebook的原生功力还是比较深厚的

  1. 最后提到了如何在swift 用RN,然并卵.

RN = ReactNative

————

有兴趣的同学可以读一下具体 Module 和 Method 的原生实现

关于Bridge的一篇facebook的内部剖析
如何优化RNApp性能的官方介绍

next

下一部分准备按照官方的指引写一个Demo 把这些点传起来

scala里使用Option类型来封装一个可能为null的引用,和Java引用相比,它避免了直接操作null引发的异常。一般来说可以用switch/case来处理Option,比如要把一个Int对象转成一个String对象:

1
2
3
4
5
6
def f(p: Option[Int]): Option[String] = {
p match {
case Some(i) => i.toString
case None => None
}
}

和Java里判断引用是否为空的代码其实是类似的,不过Option的封装保证了程序员不会忘记处理空的情况--除非程序员使用get取值,非常不推荐这样做:

1
p.get.toString

跟Java一样,这样写代码很容易收获一个空引用异常。
不过通过Option的map函数能把代码写得跟用get求值一样简洁,还不会引发空引用异常。

1
def f(p: Option[Int]): Option[String] = p.map(_.toString)

map函数只在Option为Some()的时候调用传入的函数,也就是说,None被直接“短路”到输出了,这对于大部分情况都是适用的。


scala里用Future来封装IO操作,代表”未来可能会获得某个值“。比如发起一个HTTP请求或者访问数据库,函数会返回一个Future,和Option有点类似,Future也有俩状态,一个是成功一个是失败。所以常用的方法也是map。

1
def f(p: Future[Int]): Future[String] = p.map(_.toString)

当IO成功的时候,获得的值会被用做参数,调用map里传入的函数,失败则直接“短路”到输出。
我们还可以用flatMap简单的把IO“串联”起来:

1
2
3
4
5
6
7
def readById(id: Int): Future[String] = ...
def readByName(name: String): Future[Result] = ...
def getResult(p: Future[Int]): Future[Result] = p.flatMap(id => {
readById(id).flatMap(name => {
readByName(name)
})
})

这么写在形式上有个弱点,类似回调函数,每次串联都会带来一层缩进,影响可读性。scala提供了for来处理这个问题,下面这段代码和上面的多层flatMap是等价的:

1
2
3
4
5
6
7
def readById(id: Int): Future[String] = ...
def readByName(name: String): Future[Result] = ...
def getResult(p: Future[Int]): Future[Result] = for {
id <- p
name <- readById(id)
result <- readByName(name)
} yield(result)


不过真实世界里的代码会稍微复杂一些,我们经常会混用Future和Option,最常见的是IO返回的对象里,某些属性可能为空,我们用Option来表示这些属性的话,意味着代码逻辑里会出现Future[Option[T]]这样的类型。我们当然可以在Future的map/flatMap里用match/case来写逻辑,其实也比较清晰,但是缩进的层数会比较多,影响可读性。来看一段实际的例子,这是从真实世界的业务代码里拷贝/粘贴出来的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def cityCodeFriendly(term: Option[String]): Future[Option[JsValue]] = {
term match {
case Some(cityCode) => {
AreaService.findOneByCode(cityCode) flatMap { cityOption =>
cityOption match {
case Some(city) => {
AreaService.findOneByCode(city.parentCode.getOrElse("")) map { provinceOption =>
provinceOption match {
case Some(province) => {
Some(JsObject(Seq("provinceName" -> JsString(province.name.getOrElse("")),
"cityName" -> JsString(city.name.getOrElse("")),
"cityCode" -> JsString(cityCode))))
}
case None => None
}
}
}
case None => Future(None)
}
}
}
case None => Future(None)
}
}

代码逻辑还是比较清晰的:传入一个城市id(可能为空),然后从数据库里依次读取城市的名称和所属省份的名称,最后把结果组装成一个json对象输出,只要中间有一步读到空值,就输出一个空的json对象。代码风格看起来有两个问题,一是缩进太多,第二是case None => None看起来很多余。
试试用Option的flatMap来改造一下,得到如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def cityCodeFriendly(term: Option[String]): Future[Option[JsValue]] = {
term.map(cityCode => {
AreaService
.findOneByCode(cityCode)
.flatMap(cityOption => {
cityOption.map(city => {
city.parentCode.map(parentCode => {
AreaService.findOneByCode(parentCode).map(provinceOption => {
provinceOption.map(province =>
JsObject(Seq("provinceName" -> JsString(province.name.getOrElse("")),
"cityName" -> JsString(city.name.getOrElse("")),
"cityCode" -> JsString(cityCode))))
})
}).getOrElse(Future(None))
}).getOrElse(Future(None))
})
}).getOrElse(Future(None))
}

看起来更糟糕了,Future的flatMap只能和Future级联,Option的flatMap只能和Option的级联,所以两者都用flatMap其实并无好处,反而需要用.getOrElse(Future(None))来处理分支的情形,比switch/case更不清晰,缩进只是略少,总体来说得不偿失。
因为Future和Option的flatMap互相不能级联,所以我们没发用for把它们串联起来,看看flatMap写法里的getOrElse就知道了。
但是从逻辑上来说,应该存在一种方法,能把Future和Option串起来,中间只要有Future.failure就短路到Future.failure,只要有Future(None)就短路到Future(None),这是一个很机械的逻辑,编译器应该能处理。
scalaz提供的optionT函数,就能做到这一点。改造后实现看起来清爽多了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def cityCodeFriendly(term: Option[String]): Future[Option[JsValue]] = {
def wrapCityCode: Future[Option[String]] = Future(term)
def wrapJsObject(province: Area, city:Area, cityCode: String): Future[Option[JsValue]] =
Future(Some(JsObject(Seq(
"provinceName" -> JsString(province.name.getOrElse("")),
"cityName" -> JsString(city.name.getOrElse("")),
"cityCode" -> JsString(cityCode)))))

(for {
cityCode <- optionT(wrapCityCode)
city <- optionT(AreaService.findOneByCode(cityCode))
parentCode <- optionT(Future(city.parentCode))
province <- optionT(AreaService.findOneByCode(parentCode))
res <- optionT(wrapJsObject(province, city, cityCode))
} yield res).run
}

optionT函数接收一个Future[Option[T]]类型,返回一个封装好的OptionT类型,OptionT类型相当于“混合”了Future和Option的特性,它提供了map/flatMap函数,只针对成功并有值的情况调用传入的函数,否则直接“短路”到输出。这样的写法不但让代码变得简洁(平板的结构,很少的缩进),而且在编写过程中不需要再去关注错误处理(不需要再加入case None => None 或者 getOrElse之类的玩意了)。

排序算法是算法的基础部分。
本文讲用函数式的方法,用Scala实现4种常用的排序算法。
由于是用函数式的方式,所以使用List代替Array。下文中统一由“列表”代替。

选择排序

从一个列表中选出最小或最大的元素,插入到列表头或列表尾。
循环一次,对列表中的每个元素都做同样的处理。
平均时间复杂度为N的平方,N为列表的长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def selectionSort[T <% Ordered[T]](list: List[T]): List[T] = {
def remove(e: T, list: List[T]): List[T] =
list match {
case Nil => Nil
case x :: xs => if (x == e) xs else x :: remove(e, xs)
}

list match {
case Nil => Nil
case _ =>
val min = list.min
min :: selectionSort(remove(min, list))
}
}

插入排序

像平时我们打扑克时抓牌,并依次按照顺序插入到列表中相应的位置。
平均时间复杂度为N的平方。

1
2
3
4
5
6
7
8
def insertionSort[T <% Ordered[T]](list: List[T]): List[T] = {
def insert(e: T, list: List[T]): List[T] =
list match {
case Nil => List(e)
case x :: xs => if (x < e) x :: insert(e, xs) else e :: list
}
list.foldLeft(List.empty[T])((res, init) => insert(init, res))
}

归并排序

递归的将列表分成长度更小的列表。当每个子列表的长度小于等于1时,将列表们归并在一起。
平均时间复杂度为NlogN。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def mergeSort[T <% Ordered[T]](list: List[T]): List[T] = {
def merge(l1: List[T], l2: List[T]): List[T] =
(l1, l2) match {
case (Nil, l) => l
case (l, Nil) => l
case (x :: xs, y :: ys) =>
if (x < y) x :: merge(xs, l2)
else y :: merge(l1, ys)
}
val n = list.length / 2
if (n == 0) list
else {
val (ys, zs) = list.splitAt(n)
merge(mergeSort(ys), mergeSort(zs))
}
}

快速排序

从一个列表中随机选出一个元素作为基准,分别在过滤出小于它的元素,大于等于它的元素,
递归的对这些列表调用该算法,最后将列表合并。
这里并没有随机从列表中取元素,只取第一个元素作为基准元素。
平均时间复杂度为NlogN。

1
2
3
4
5
6
7
8
9
10
11
12
def quickSort[T <% Ordered[T]](list: List[T]): List[T] =
list match {
case Nil => Nil
case x :: _ =>
val (smaller, eqs, bigger) = list.foldLeft((List.empty[T], List.empty[T], List.empty[T])) {
(res, init) =>
if (init < x) (init :: res._1, res._2, res._3)
else if (init == x) (res._1, init :: res._2, res._3)
else (res._1, res._2, init :: res._3)
}
quickSort(smaller) ::: eqs ::: quickSort(bigger)
}

注1:图片来源于网络
注2:T <% Ordered[T]为View Bound,表示“I can use any T, so long as T can be treated as an Ordered[T].”

AsyncDisplayKit是Facebook开源的一套用于提高iOS界面流畅的异步绘制UI的框架。其核心思想是对CPU以及GPU的优化,CPU方面则是在将一些高消耗运算放在后台线程中进行,尽量减轻主线程负担;GPU方面则是将每一帧的内容渲染成一张texture,这个工作可以在后台线程中完成。

AsyncDisplayKit的基本单元是node,Node是UIView以及对应Layer的抽象层。与UIView的最大区别在于,Node是线程安全的,并可以设置对应的Node内部层次后在后台线程中运行,而UIView只能将大量的耗时操作全都放在主线程上进行,下面是AsyncDisplayKit和UIKit的一些类对应关系。

ASDisplayNode. Counterpart to UIView — subclass to make custom nodes.
ASControlNode. Analogous to UIControl — subclass to make buttons.
ASImageNode. Like UIImageView — decodes images asynchronously.
ASTextNode. Like UITextView — built on TextKit with full-featured rich text support.
ASTableView and ASCollectionView. UITableView and UICollectionView subclasses that support nodes.

ASDisplayNode是线程安全的,它可以在后台线程创建和修改。Node刚创建时,并不会在内部创建UIView和CALayer,直到第一次在主线程访问view或者layer属性时,它才会在内部生成对应的对象。当它的属性(比如frame/transform)改变后,它并不会立刻同步到其持有的view或layer去,而是把被改变的属性保存到内部的一个中间变量,稍后在需要时,再通过某个机制一次性设置到内部的view或layer。实际的绘制代码在:ASDisplayNode和ASDisplayNode+AsyncDisplay.mm。

从主线程中剥离出来的耗时运算比较重要的就是图像、布局还有绘制。
图像(解压与处理):一般UIImage对其内部图像格式的解压发生在即将将图片交给GPU渲染的时候。从API上来看,一般我们调用[UIImage drawInRect]函数或者将UIView可见(放置于window上)的时候,UIImage会将其内部图像格式如PNG,JPEG进行解压。AsyncDisplayKit就是采用后面那个方式将UIView预先放置入一个frame为空得workingview中以达到将view内部的image进行预解压的目的。再说图像的处理。一般图像需要一些blend运算或者图像需要strech或crop,这些工作其实可以留在GPU中进行快速计算,但因为UIKit并没有提供此类的API,所以我们一般都是通过CoreGraphic来做,但CoreGraphic是CPU drawing比较费时。AsyncDisplayKit将这些图像处理放在工作线程中来处理,虽然也是CPU drawing,但不会影响到UI得顺滑响应。

布局:以AsyncDisplayKit的ASTableView为例,TableView的UI布局需要计算每行的高度,然后计算行内元素的布局,将行插入到TableView中,同时TableView又是scrollview,需要上下滑动。一旦行的生成和渲染比较慢,必然影响到滑动时的流畅体验。在这个过程中只有将行插入到TableView中需要在UI线程中执行。AsyncDisplayKit在子线程中分批次计算行(row)以及其子元素的布局,计算好后通过dispatch_group_notify通知UI线程将row插入到view中。AsyncDisplayKit有一个比较细腻的方式是考虑到设备的CPU核数,根据核数来分批次计算row的大小。

绘制:AsyncDisplayKit另一个强大之处在于将UI CALayer的backing image的绘制放入到工作线程。我们知道UIView的真正展现内容在于其CALayer的contents属性,而contents属性对应一个Backing image,我们可以将其理解成一个内存位图。默认情况下UIView一旦需要展现,其会自动创建一个Backing image,但我们也可以通过CALayer的delegate方式来定制这个Backing image。AsyncDisplayKit就是通过CALayer的delegate控制backing image的生成,并且通过Core Graphic的方式在工作线程上将View以及其子节点绘制到backing image上,这些绘制工作会根据UIView的层次构建一个绘制数组统一执行,绘制好之后在UI线程上将这个backing image传给CALayer的contents,最后将CALayer的渲染树交给GPU进行渲染。虽然这个过程中主要依赖于CoreGraphic来进行绘制,但因为都在后台,而且绘制以组的方式执行减少了graphic context的切换,对于UI性能和顺滑性没有什么影响。而且他通过transaction的方式管理dispatch_group之间的关系,并且只有在UI线程处于idle状态或将退出时才将transaction commit并将backing image赋给CALayer的contents。除了通过CAlayer的backing image绘制,AsyncDisplayKit还提供UIView的drawRect绘制以及UIView的rasterize。两者都会使用offscreen drawing,但后者会将UIView以及所有子节点都绘制在一个backing image上。

核心技术:Runloop 任务分发
iOS 的显示系统是由 VSync 信号驱动的,VSync 信号由硬件时钟生成,每秒钟发出 60 次(这个值取决设备硬件,比如 iPhone 真机上通常是 59.97)。iOS 图形服务接收到 VSync 信号后,会通过 IPC 通知到 App 内。App 的 Runloop 在启动后会注册对应的 CFRunLoopSource 通过 mach_port 接收传过来的时钟信号通知,随后 Source 的回调会驱动整个 App 的动画与显示。

Core Animation 在 RunLoop 中注册了一个 Observer,监听了 BeforeWaiting 和 Exit 事件。这个 Observer 的优先级是 2000000,低于常见的其他 Observer。当一个触摸事件到来时,RunLoop 被唤醒,App 中的代码会执行一些操作,比如创建和调整视图层级、设置 UIView 的 frame、修改 CALayer 的透明度、为视图添加一个动画;这些操作最终都会被 CALayer 捕获,并通过 CATransaction 提交到一个中间状态去(CATransaction 的文档略有提到这些内容,但并不完整)。当上面所有操作结束后,RunLoop 即将进入休眠(或者退出)时,关注该事件的 Observer 都会得到通知。这时 CA 注册的那个 Observer 就会在回调中,把所有的中间状态合并提交到 GPU 去显示;如果此处有动画,CA 会通过 DisplayLink 等机制多次触发相关流程。

ASDK 在此处模拟了 Core Animation 的这个机制:所有针对 ASNode 的修改和提交,总有些任务是必需放入主线程执行的。当出现这种任务时,ASNode 会把任务用 ASAsyncTransaction(Group) 封装并提交到一个全局的容器去。ASDK 也在 RunLoop 中注册了一个 Observer,监视的事件和 CA 一样,但优先级比 CA 要低。当 RunLoop 进入休眠前、CA 处理完事件后,ASDK 就会执行该 loop 内提交的所有任务。具体代码见这个文件:ASAsyncTransactionGroup。

以上是前两周看的一些原理性的东西,代码太多太复杂还没研究懂~,总之ASDK就是能实现把异步、并发的操作同步到主线程去,并且获得不错的性能。只是这个库是有点重,使用需谨慎,要用就得用一套node的东西。如果没有特别多图片处理或者富文本编辑的话也体现不出它的优势。

先来看看Symbol函数的定义:

1
Symbol(description?) → symbol

接收一个描述(描述可以为undefined),返回一个symbol类型的变量。
然后有意思的来了,首先传进去的参数只是个描述,给读代码的人看的,和返回值无关,Symbol函数每次都返回一个不一样的symbol类型变量,像这样:

1
2
3
4
5
6
7
8
9
> Symbol() === Symbol()
false

> Symbol("a") === Symbol("a")
false

> let a = Symbol("this is a")
> a === a
true

不过要注意的是,symbol类型虽然是一个基本类型,但是一般来说不被json格式支持,不要试图在json格式里使用它,像这样([k]的含义是用k的内容作为对象键-值对的键而不是用k这个字符串作为键):

1
2
3
4
> let k = Symbol("key")
> let obj = {[k]: 5}
> JSON.stringify(obj)
{}

这导致了nodejs的REPL打印对象的时候会漏掉以symbole作为键的内容,像这样:

1
2
3
4
> let k = Symbol("key")
> let obj = {[k]: 5, d: 6}
> obj
{ d: 6 }

不过直接访问是没有问题的:

1
2
3
4
> let k = Symbol("key")
> let obj = {[k]: 5, d: 6}
> obj[k]
5

所以Symbol最大的用处是用来做常量定义,相比:

1
const ERROR = "error"

使用Symbol

1
const ERROR = Symbol("error")

会更好一些,比如你有两个包log和print,它们的ERROR常量其实不是一回事,用Symbol就会很清晰:

1
2
> log.ERROR === print.ERROR
false

我在学习多线程编程的时候,得到的第一条关于性能忠告是锁的开销很大。由此引发了三个问题:有多大,为什么以及如何尽量避免。


在计算机里,“很大”实在是一个太过于模糊的概念。比如同样是函数调用,在一个移动APP的大部分地方,我们会鼓励尽量把大的函数拆分以便于阅读和理解代码,而如果这个APP包含一个视频codec,里边为了效率会尽量避免在循环里做函数调用,甚至会把循环展开减少跳转的次数来优化处理器流水线。所以笼统的说某个操作“开销很大”没太大的意义,只有比较精确的测量出实际的开销有多大,我们才能决定使用的方式和优化机制。

锁开销的测量

我们针对的是多线程环境下的锁机制,基于linux做测试。每种编程语言提供的锁机制都不太一样,不过无论如何,最终都会落实到两种机制上,一是处理器提供的原子操作指令(现在一般是CAS—compare and swap),处理器会用轮询的方式试图获得锁,在处理器(包括多核)架构里这是必不可少的机制;二是内核提供的锁系统调用,在被锁住的时候会把当前线程置于睡眠(阻塞)状态。

实际上我们在编程的时候并不会直接调用这两种机制,而是使用编程语言所带函数库里的锁方法,锁方法内部混合使用这两种机制。以pthread库(NPTL)的pthread_mutex来举例,一把锁本质上只是一个int类型的变量,占用4个字节内存并且内存边界按4字节对齐。加锁的时候先用trylock方法(内部使用的是CAS指令)来尝试获得锁,如果无法获得锁,则调用系统调用sys_futex来试图获得锁,这时候如果还不能获得锁,当前线程就会被阻塞。

java之类的语言会使用看起来和pthread_mutex很不一样的锁机制(比如synchronise),但是实际上底层还是通过pthread_mutex的方法来加锁,或者是混合使用CAS和sys_futex—和pthread_mutex差不多。

所以很容易得到一个结论,如果锁不存在冲突,每次获得锁和释放锁的处理器开销仅仅是CAS指令的开销,在x86-64处理器上,这个开销只比一次内存访问(无cache)高一点(大概是1.3倍)。以我现在在使用的MacBoolPro为例,内存的规格是DDR3-1600,实际的时钟频率为800MHz,每个时钟周期1.25ns;突发的内存访问存在8个时钟周期的延迟(这个延迟的缩写也是CAS,参考https://en.wikipedia.org/wiki/CAS_latency),也就是10ns的内存访问延迟,这样算下来,CAS指令的开销大改是十多个ns。

确定一件事情最好的方法是实际测试和观测它,让我们写一段代码来测试无冲突时锁的开销,核心代码大概是这样:

1
2
3
4
5
while(c < MAX) {
pthread_mutex_lock(&mutex);
c = c + 1;
pthread_mutex_unlock(&mutex);
}

其实就是循环的lock和unlock。运行五亿次以后,计算耗时为14.5s左右,平摊到每次加锁/解锁操作大改是14ns每次加锁/解锁(扣除很少的一点循环开销)。这个数值和CAS指令的理论开销吻合得很好。

在锁冲突的情况下,开销就没有这么小了。首先pthread_mutex_lock会真正的调用sys_futex来进入内核来试图加锁,被锁住以后线程会进入睡眠,这带来了上下文切换和线程调度的开销。可以写两个互相解锁的线程来测试这个过程的开销,
代码: https://github.com/tsuna/contextswitch/blob/master/timetctxsw.c
在单核和双核机器上分别测试(都是在一台2.5GHz的Core i5上用vmware建的虚拟机,运行ubuntu linux)。在单核机器上消耗大约1080ns而双核机器上消耗大约3400ns,双核机器上锁冲突的开销大概是单核的3倍。
另外一个c程序可以用来测试“纯上下文切换”的开销,线程只是使用sched_yield来放弃处理器,并不进入睡眠。
代码:https://github.com/tsuna/contextswitch/blob/master/timetctxsw2.c
结论很惊人,在单核处理器上,“纯上下文切换”只消耗了大概150ns。

锁开销的来源

这样我们大致可以把锁冲突的开销分成三部分,“纯上下文切换”开销,大概是150ns,调度器开销(把线程从睡眠变成就绪或者反过来)大概是900ns,在多核系统上,还存在跨处理器调度的开销,那部分开销很大,超过2000ns。在真实的应用场景里,还要考虑上下文切换带来的cache不命中和TLB不命中的开销,开销只会进一步加大。

锁的优化

锁的实现方式对我们的优化方向给予了很好的提示,让我们回想一下--先用一个快的操作(每秒7000万次)来测试锁是否冲突,再用一个慢的操作来实际加锁(每秒30万次)。所以真正消耗时间的不是上锁的次数,而是锁冲突的次数。减少锁冲突的次数才是提升性能的关键。
使用更细粒度的锁,可以减少锁冲突。这里说的粒度包括时间和空间,比如哈希表包含一系列哈希桶,为每个桶设置一把锁,空间粒度就会小很多--哈希值相互不冲突的访问不会导致锁冲突,这比为整个哈希表维护一把锁的冲突机率低很多。减少时间粒度也很容易理解,加锁的范围只包含必要的代码段,尽量缩短获得锁到释放锁之间的时间,最重要的是,绝对不要在锁中进行任何可能会阻塞的操作。使用读写锁也是一个很好的减少冲突的方式,读操作之间不互斥,大大减少了冲突。
锁本身的行为也存在进一步优化的可能性,sys_futex系统调用的作用在于让被锁住的当前线程睡眠,让出处理器供其它线程使用,既然这个过程的消耗高达几千ns,也就是说如果被锁定的时间不超过这个数值的话,根本没有必要进内核加锁--释放的处理器时间还不够消耗的。sys_futex的时间消耗够跑二百多次compare and swap的,也就是说,对于一个锁冲突比较频繁而且平均锁定时间比较短的系统,一个值得考虑的优化方式是先循环调用compare and swap来尝试获得锁(这个操作也被称作自旋锁),在若干次失败(一般是一百次左右)后再进入内核真正加锁。当然这个优化只能在多处理器的系统里起作用(得有另一个处理器来解锁,否则自旋锁无意义)。在glibc的pthread实现里,通过对pthread_mutex设置PTHREAD_MUTEX_ADAPTIVE_NP属性就可以使用这个机制;在Java世界的HotSpot虚拟机里,可以用-XX:PreBlockSpin来设置自选锁尝试的次数。

一个传统程序员是这么写的:

1
2
3
4
5
6
7
function length(array) {
var sum = 0
for(var i = 0; i < array.length; i++) {
sum++
}
return sum
}

一个读过SICP的程序员是这么写的:

1
2
3
4
5
6
7
function length(array) {
if(array.length === 0) {
return 0
} else {
return 1 + length(array.slice(1))
}
}

另一个读过SICP的程序员:

1
2
3
4
5
6
7
8
9
10
function length(array) {
function helper(array, num) {
if(array.length === 0) {
return num
} else {
return helper(array.slice(1), num + 1)
}
}
return helper(array, 0)
}

再函数式一点:

1
2
3
function length(array) {
return array.reduce(function(prev, cur) { return prev + 1 }, 0)
}

一个我面试过的人…

1
2
3
function length(array) {
return array.length
}

AMD = Asynchronous Module Definition

它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

1
require([module], callback);

第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。如果将前面的代码改写成AMD形式,就是下面这样:

1
2
3
4
require([‘jquery’,’math'], function ($, math) {
math.add(2, 3);
$(body).show();
});

CommonJS

那什么是CommonJS呢, 总是拿他和AMD来对比使用
2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。
node.js的模块系统,就是参照CommonJS规范实现的。在CommonJS中,有一个全局性方法require(),用于加载模块。假定有一个数学模块math.js,就可以像下面这样加载。

1
2
var math = require('math');
math.add(2,3); // 5

那么问题来了

有了服务器端模块以后,很自然地,大家就想要客户端模块。而且最好两者能够兼容,一个模块不用修改,在服务器和浏览器都可以运行。
但是,由于一个重大的局限,使得CommonJS规范不适用于浏览器环境。
服务器端的文件依赖只取决于磁盘IO,但是客户端会有网络的不确定性.取决于网速的快慢,可能要等很长时间,浏览器处于”假死”状态。

因此,浏览器端的模块,不能采用”同步加载”(synchronous),只能采用”异步加载”(asynchronous)。这就是AMD规范诞生的背景.

CommonJS 到底是个什么鬼

CommonJS 网站标题 : javascript: not just for browsers any more!

With CommonJS-compliant systems, you can use JavaScript to write:

  • Server-side JavaScript applications
  • Command line tools
  • Desktop GUI-based applications
  • Hybrid applications (Titanium, Adobe AIR)

感觉基本就是定义一些文档和API, 大家都遵循,自己去实现. 然后把JS写的到处都是,开枝散叶.

CommonJS是服务器端模块的规范,Node.js采用了这个规范。
根据CommonJS规范,一个单独的文件就是一个模块。 加载模块使用require方法,该方法读取一个文件并执行,最后返回文件内部的exports对象。

预告

接下来的几篇文章次会按照加载方式的不同,讲解一下 require.js 和 AngularJS 的加载机制 以及webwork 和 serverwork 的开发方式, 还有bower库和npm库的外壳的不同.