执行ruby脚本报错TimeoutError,请大家帮吗看下吧

Python011

执行ruby脚本报错TimeoutError,请大家帮吗看下吧,第1张

因为这个元素在60秒内一直没有展现出来吧,所以报了超时,贴下这个方法的源码:

#

# Waits until the element is present.

#

# @example

# browser.button(:id =>'foo').wait_until_present

#

# @param [Fixnum] timeout seconds to wait before timing out

#

# @see Watir::Wait

# @see Watir::Element#present?

#

def wait_until_present(timeout = 30)

message = "waiting for #{selector_string} to become present"

Watir::Wait.until(timeout, message) { present? }

end

def until(timeout = 30, message = nil, &block)

end_time = ::Time.now + timeout

until ::Time.now >end_time

result = yield(self)

return result if result

sleep INTERVAL

end

raise TimeoutError, message_for(timeout, message)

end

希望对你有帮助

这是javaeye上非常经典的关于线程的帖子,写的非常通俗易懂的,适合任何读计算机的同学.

线程同步

我们可以在计算机上运行各种计算机软件程序。每一个运行的程序可能包括多个独立运行的线程(Thread)。

线程(Thread)是一份独立运行的程序,有自己专用的运行栈。线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等。

当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团。

同步这个词是从英文synchronize(使同时发生)翻译过来的。我也不明白为什么要用这个很容易引起误解的词。既然大家都这么用,咱们也就只好这么将就。

线程同步的真实意思和字面意思恰好相反。线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。

因此,关于线程同步,需要牢牢记住的第一点是:线程同步就是线程排队。同步就是排队。线程同步的目的就是避免线程“同步”执行。这可真是个无聊的绕口令。

关于线程同步,需要牢牢记住的第二点是 “共享”这两个字。只有共享资源的读写访问才需要同步。如果不是共享资源,那么就根本没有同步的必要。

关于线程同步,需要牢牢记住的第三点是,只有“变量”才需要同步访问。如果共享的资源是固定不变的,那么就相当于“常量”,线程同时读取常量也不需要同步。至少一个线程修改共享资源,这样的情况下,线程之间就需要同步。

关于线程同步,需要牢牢记住的第四点是:多个线程访问共享资源的代码有可能是同一份代码,也有可能是不同的代码;无论是否执行同一份代码,只要这些线程的代码访问同一份可变的共享资源,这些线程之间就需要同步。

为了加深理解,下面举几个例子。

有两个采购员,他们的工作内容是相同的,都是遵循如下的步骤:

(1)到市场上去,寻找并购买有潜力的样品。

(2)回到公司,写报告。

这两个人的工作内容虽然一样,他们都需要购买样品,他们可能买到同样种类的样品,但是他们绝对不会购买到同一件样品,他们之间没有任何共享资源。所以,他们可以各自进行自己的工作,互不干扰。

这两个采购员就相当于两个线程;两个采购员遵循相同的工作步骤,相当于这两个线程执行同一段代码。

下面给这两个采购员增加一个工作步骤。采购员需要根据公司的“布告栏”上面公布的信息,安排自己的工作计划。

这两个采购员有可能同时走到布告栏的前面,同时观看布告栏上的信息。这一点问题都没有。因为布告栏是只读的,这两个采购员谁都不会去修改布告栏上写的信息。

下面增加一个角色。一个办公室行政人员这个时候,也走到了布告栏前面,准备修改布告栏上的信息。

如果行政人员先到达布告栏,并且正在修改布告栏的内容。两个采购员这个时候,恰好也到了。这两个采购员就必须等待行政人员完成修改之后,才能观看修改后的信息。

如果行政人员到达的时候,两个采购员已经在观看布告栏了。那么行政人员需要等待两个采购员把当前信息记录下来之后,才能够写上新的信息。

上述这两种情况,行政人员和采购员对布告栏的访问就需要进行同步。因为其中一个线程(行政人员)修改了共享资源(布告栏)。而且我们可以看到,行政人员的工作流程和采购员的工作流程(执行代码)完全不同,但是由于他们访问了同一份可变共享资源(布告栏),所以他们之间需要同步。

同步锁

前面讲了为什么要线程同步,下面我们就来看如何才能线程同步。

线程同步的基本实现思路还是比较容易理解的。我们可以给共享资源加一把锁,这把锁只有一把钥匙。哪个线程获取了这把钥匙,才有权利访问该共享资源。

生活中,我们也可能会遇到这样的例子。一些超市的外面提供了一些自动储物箱。每个储物箱都有一把锁,一把钥匙。人们可以使用那些带有钥匙的储物箱,把东西放到储物箱里面,把储物箱锁上,然后把钥匙拿走。这样,该储物箱就被锁住了,其他人不能再访问这个储物箱。(当然,真实的储物箱钥匙是可以被人拿走复制的,所以不要把贵重物品放在超市的储物箱里面。于是很多超市都采用了电子密码锁。)

线程同步锁这个模型看起来很直观。但是,还有一个严峻的问题没有解决,这个同步锁应该加在哪里?

当然是加在共享资源上了。反应快的读者一定会抢先回答。

没错,如果可能,我们当然尽量把同步锁加在共享资源上。一些比较完善的共享资源,比如,文件系统,数据库系统等,自身都提供了比较完善的同步锁机制。我们不用另外给这些资源加锁,这些资源自己就有锁。

但是,大部分情况下,我们在代码中访问的共享资源都是比较简单的共享对象。这些对象里面没有地方让我们加锁。

读者可能会提出建议:为什么不在每一个对象内部都增加一个新的区域,专门用来加锁呢?这种设计理论上当然也是可行的。问题在于,线程同步的情况并不是很普遍。如果因为这小概率事件,在所有对象内部都开辟一块锁空间,将会带来极大的空间浪费。得不偿失。

于是,现代的编程语言的设计思路都是把同步锁加在代码段上。确切的说,是把同步锁加在“访问共享资源的代码段”上。这一点一定要记住,同步锁是加在代码段上的。

同步锁加在代码段上,就很好地解决了上述的空间浪费问题。但是却增加了模型的复杂度,也增加了我们的理解难度。

现在我们就来仔细分析“同步锁加在代码段上”的线程同步模型。

首先,我们已经解决了同步锁加在哪里的问题。我们已经确定,同步锁不是加在共享资源上,而是加在访问共享资源的代码段上。

其次,我们要解决的问题是,我们应该在代码段上加什么样的锁。这个问题是重点中的重点。这是我们尤其要注意的问题:访问同一份共享资源的不同代码段,应该加上同一个同步锁;如果加的是不同的同步锁,那么根本就起不到同步的作用,没有任何意义。

这就是说,同步锁本身也一定是多个线程之间的共享对象。

Java语言的synchronized关键字

为了加深理解,举几个代码段同步的例子。

不同语言的同步锁模型都是一样的。只是表达方式有些不同。这里我们以当前最流行的Java语言为例。Java语言里面用synchronized关键字给代码段加锁。整个语法形式表现为

synchronized(同步锁) {

// 访问共享资源,需要同步的代码段

}

这里尤其要注意的就是,同步锁本身一定要是共享的对象。

… f1() {

Object lock1 = new Object()// 产生一个同步锁

synchronized(lock1){

// 代码段 A

// 访问共享资源 resource1

// 需要同步

}

}

上面这段代码没有任何意义。因为那个同步锁是在函数体内部产生的。每个线程调用这段代码的时候,都会产生一个新的同步锁。那么多个线程之间,使用的是不同的同步锁。根本达不到同步的目的。

同步代码一定要写成如下的形式,才有意义。

public static final Object lock1 = new Object()

… f1() {

synchronized(lock1){ // lock1 是公用同步锁

// 代码段 A

// 访问共享资源 resource1

// 需要同步

}

你不一定要把同步锁声明为static或者public,但是你一定要保证相关的同步代码之间,一定要使用同一个同步锁。

讲到这里,你一定会好奇,这个同步锁到底是个什么东西。为什么随便声明一个Object对象,就可以作为同步锁?

在Java里面,同步锁的概念就是这样的。任何一个Object Reference都可以作为同步锁。我们可以把Object Reference理解为对象在内存分配系统中的内存地址。因此,要保证同步代码段之间使用的是同一个同步锁,我们就要保证这些同步代码段的synchronized关键字使用的是同一个Object Reference,同一个内存地址。这也是为什么我在前面的代码中声明lock1的时候,使用了final关键字,这就是为了保证lock1的Object Reference在整个系统运行过程中都保持不变。

一些求知欲强的读者可能想要继续深入了解synchronzied(同步锁)的实际运行机制。Java虚拟机规范中(你可以在google用“JVM Spec”等关键字进行搜索),有对synchronized关键字的详细解释。synchronized会编译成 monitor enter, … monitor exit之类的指令对。Monitor就是实际上的同步锁。每一个Object Reference在概念上都对应一个monitor。

这些实现细节问题,并不是理解同步锁模型的关键。我们继续看几个例子,加深对同步锁模型的理解。

public static final Object lock1 = new Object()

… f1() {

synchronized(lock1){ // lock1 是公用同步锁

// 代码段 A

// 访问共享资源 resource1

// 需要同步

}

}

… f2() {

synchronized(lock1){ // lock1 是公用同步锁

// 代码段 B

// 访问共享资源 resource1

// 需要同步

}

}

上述的代码中,代码段A和代码段B就是同步的。因为它们使用的是同一个同步锁lock1。

如果有10个线程同时执行代码段A,同时还有20个线程同时执行代码段B,那么这30个线程之间都是要进行同步的。

这30个线程都要竞争一个同步锁lock1。同一时刻,只有一个线程能够获得lock1的所有权,只有一个线程可以执行代码段A或者代码段B。其他竞争失败的线程只能暂停运行,进入到该同步锁的就绪(Ready)队列。

每一个同步锁下面都挂了几个线程队列,包括就绪(Ready)队列,待召(Waiting)队列等。比如,lock1对应的就绪队列就可以叫做lock1 - ready queue。每个队列里面都可能有多个暂停运行的线程。

注意,竞争同步锁失败的线程进入的是该同步锁的就绪(Ready)队列,而不是后面要讲述的待召队列(Waiting Queue,也可以翻译为等待队列)。就绪队列里面的线程总是时刻准备着竞争同步锁,时刻准备着运行。而待召队列里面的线程则只能一直等待,直到等到某个信号的通知之后,才能够转移到就绪队列中,准备运行。

成功获取同步锁的线程,执行完同步代码段之后,会释放同步锁。该同步锁的就绪队列中的其他线程就继续下一轮同步锁的竞争。成功者就可以继续运行,失败者还是要乖乖地待在就绪队列中。

因此,线程同步是非常耗费资源的一种操作。我们要尽量控制线程同步的代码段范围。同步的代码段范围越小越好。我们用一个名词“同步粒度”来表示同步代码段的范围。

同步粒度

在Java语言里面,我们可以直接把synchronized关键字直接加在函数的定义上。

比如。

… synchronized … f1() {

// f1 代码段

}

这段代码就等价于

… f1() {

synchronized(this){ // 同步锁就是对象本身

// f1 代码段

}

}

同样的原则适用于静态(static)函数

比如。

… static synchronized … f1() {

// f1 代码段

}

这段代码就等价于

…static … f1() {

synchronized(Class.forName(…)){ // 同步锁是类定义本身

// f1 代码段

}

}

但是,我们要尽量避免这种直接把synchronized加在函数定义上的偷懒做法。因为我们要控制同步粒度。同步的代码段越小越好。synchronized控制的范围越小越好。

我们不仅要在缩小同步代码段的长度上下功夫,我们同时还要注意细分同步锁。

比如,下面的代码

public static final Object lock1 = new Object()

… f1() {

synchronized(lock1){ // lock1 是公用同步锁

// 代码段 A

// 访问共享资源 resource1

// 需要同步

}

}

… f2() {

synchronized(lock1){ // lock1 是公用同步锁

// 代码段 B

// 访问共享资源 resource1

// 需要同步

}

}

… f3() {

synchronized(lock1){ // lock1 是公用同步锁

// 代码段 C

// 访问共享资源 resource2

// 需要同步

}

}

… f4() {

synchronized(lock1){ // lock1 是公用同步锁

// 代码段 D

// 访问共享资源 resource2

// 需要同步

}

}

上述的4段同步代码,使用同一个同步锁lock1。所有调用4段代码中任何一段代码的线程,都需要竞争同一个同步锁lock1。

我们仔细分析一下,发现这是没有必要的。

因为f1()的代码段A和f2()的代码段B访问的共享资源是resource1,f3()的代码段C和f4()的代码段D访问的共享资源是resource2,它们没有必要都竞争同一个同步锁lock1。我们可以增加一个同步锁lock2。f3()和f4()的代码可以修改为:

public static final Object lock2 = new Object()

… f3() {

synchronized(lock2){ // lock2 是公用同步锁

// 代码段 C

// 访问共享资源 resource2

// 需要同步

}

}

… f4() {

synchronized(lock2){ // lock2 是公用同步锁

// 代码段 D

// 访问共享资源 resource2

// 需要同步

}

}

这样,f1()和f2()就会竞争lock1,而f3()和f4()就会竞争lock2。这样,分开来分别竞争两个锁,就可以大大较少同步锁竞争的概率,从而减少系统的开销。

信号量

同步锁模型只是最简单的同步模型。同一时刻,只有一个线程能够运行同步代码。

有的时候,我们希望处理更加复杂的同步模型,比如生产者/消费者模型、读写同步模型等。这种情况下,同步锁模型就不够用了。我们需要一个新的模型。这就是我们要讲述的信号量模型。

信号量模型的工作方式如下:线程在运行的过程中,可以主动停下来,等待某个信号量的通知;这时候,该线程就进入到该信号量的待召(Waiting)队列当中;等到通知之后,再继续运行。

很多语言里面,同步锁都由专门的对象表示,对象名通常叫Monitor。

同样,在很多语言中,信号量通常也有专门的对象名来表示,比如,Mutex,Semphore。

信号量模型要比同步锁模型复杂许多。一些系统中,信号量甚至可以跨进程进行同步。另外一些信号量甚至还有计数功能,能够控制同时运行的线程数。

我们没有必要考虑那么复杂的模型。所有那些复杂的模型,都是最基本的模型衍生出来的。只要掌握了最基本的信号量模型——“等待/通知”模型,复杂模型也就迎刃而解了。

我们还是以Java语言为例。Java语言里面的同步锁和信号量概念都非常模糊,没有专门的对象名词来表示同步锁和信号量,只有两个同步锁相关的关键字——volatile和synchronized。

这种模糊虽然导致概念不清,但同时也避免了Monitor、Mutex、Semphore等名词带来的种种误解。我们不必执着于名词之争,可以专注于理解实际的运行原理。

在Java语言里面,任何一个Object Reference都可以作为同步锁。同样的道理,任何一个Object Reference也可以作为信号量。

Object对象的wait()方法就是等待通知,Object对象的notify()方法就是发出通知。

具体调用方法为

(1)等待某个信号量的通知

public static final Object signal = new Object()

… f1() {

synchronized(singal) { // 首先我们要获取这个信号量。这个信号量同时也是一个同步锁

// 只有成功获取了signal这个信号量兼同步锁之后,我们才可能进入这段代码

signal.wait()// 这里要放弃信号量。本线程要进入signal信号量的待召(Waiting)队列

// 可怜。辛辛苦苦争取到手的信号量,就这么被放弃了

// 等到通知之后,从待召(Waiting)队列转到就绪(Ready)队列里面

// 转到了就绪队列中,离CPU核心近了一步,就有机会继续执行下面的代码了。

// 仍然需要把signal同步锁竞争到手,才能够真正继续执行下面的代码。命苦啊。

}

}

需要注意的是,上述代码中的signal.wait()的意思。signal.wait()很容易导致误解。signal.wait()的意思并不是说,signal开始wait,而是说,运行这段代码的当前线程开始wait这个signal对象,即进入signal对象的待召(Waiting)队列。

(2)发出某个信号量的通知

… f2() {

synchronized(singal) { // 首先,我们同样要获取这个信号量。同时也是一个同步锁。

// 只有成功获取了signal这个信号量兼同步锁之后,我们才可能进入这段代码

signal.notify()// 这里,我们通知signal的待召队列中的某个线程。

// 如果某个线程等到了这个通知,那个线程就会转到就绪队列中

// 但是本线程仍然继续拥有signal这个同步锁,本线程仍然继续执行

// 嘿嘿,虽然本线程好心通知其他线程,

// 但是,本线程可没有那么高风亮节,放弃到手的同步锁

// 本线程继续执行下面的代码

}

}

需要注意的是,signal.notify()的意思。signal.notify()并不是通知signal这个对象本身。而是通知正在等待signal信号量的其他线程。

以上就是Object的wait()和notify()的基本用法。

实际上,wait()还可以定义等待时间,当线程在某信号量的待召队列中,等到足够长的时间,就会等无可等,无需再等,自己就从待召队列转移到就绪队列中了。

另外,还有一个notifyAll()方法,表示通知待召队列里面的所有线程。

这些细节问题,并不对大局产生影响。

绿色线程

绿色线程(Green Thread)是一个相对于操作系统线程(Native Thread)的概念。

操作系统线程(Native Thread)的意思就是,程序里面的线程会真正映射到操作系统的线程,线程的运行和调度都是由操作系统控制的

绿色线程(Green Thread)的意思是,程序里面的线程不会真正映射到操作系统的线程,而是由语言运行平台自身来调度。

当前版本的Python语言的线程就可以映射到操作系统线程。当前版本的Ruby语言的线程就属于绿色线程,无法映射到操作系统的线程,因此Ruby语言的线程的运行速度比较慢。

难道说,绿色线程要比操作系统线程要慢吗?当然不是这样。事实上,情况可能正好相反。Ruby是一个特殊的例子。线程调度器并不是很成熟。

目前,线程的流行实现模型就是绿色线程。比如,stackless Python,就引入了更加轻量的绿色线程概念。在线程并发编程方面,无论是运行速度还是并发负载上,都优于Python。

另一个更著名的例子就是ErLang(爱立信公司开发的一种开源语言)。

ErLang的绿色线程概念非常彻底。ErLang的线程不叫Thread,而是叫做Process。这很容易和进程混淆起来。这里要注意区分一下。

ErLang Process之间根本就不需要同步。因为ErLang语言的所有变量都是final的,不允许变量的值发生任何变化。因此根本就不需要同步。

final变量的另一个好处就是,对象之间不可能出现交叉引用,不可能构成一种环状的关联,对象之间的关联都是单向的,树状的。因此,内存垃圾回收的算法效率也非常高。这就让ErLang能够达到Soft Real Time(软实时)的效果。这对于一门支持内存垃圾回收的语言来说,可不是一件容易的事情。

源起

小飞是一名刚入行前端不久的新人,因为进到了某个大公司,俨然成为了学弟学妹眼中'大神',大家遇到js问题都喜欢问他,这不,此时他的qq弹出了这样一条消息

"hi,大神在吗?我有个问题想问,现在我们的代码里面有这样的东西,可是得不到正确的返回结果

1234567function getDataByAjax () {return $.ajax(...postParam)}var data = getDataByAjax()if (data) {   console.log(data.info)}

"哦,你这里是异步调用,不能直接获得返回值,你要把if语句写到回调函数中",小飞不假思索的说到,对于一个‘专业’的fe来说,这根本不是一个问题。

“可是我希望只是改造getDataByAjax这个方法,让后面的代码成立。”

“研究这个没有意义,异步是js的精髓,同步的话会阻塞js调用,超级慢的,但是你要一再坚持的话,用async:true就好了”

“不愧是大神,我回去立刻试一试,么么哒”

两天后,她哭丧着脸登上了qq

“试了一下你的方法,但是根本行不通,哭~~”

“别急,我看看你这个postParam的参数行吗”

123456{...   dataType: 'jsonp',async: true...}

"这是一个jsonp请求啊,老掉牙的东西了,,jsonp请求是没有办法同步的"

“我知道jsonp请求的原理是通过script标签实现的,但是,你看,script也是支持同步的呀,你看tags/attscriptasync.asp”

“额,那可能是jquery没有实现吧,哈哈”

“大神,你能帮我实现一个jsonp的同步调用方式嘛,拜托了(星星眼)”

虽然他有点奇怪jquery为什么没有实现,但是既然w3school的标准摆在那里,码两行代码又没什么,

1234567891011121314export const loadJsonpSync = (url) =>{var result window.callback1 = (data) =>(result = data)let head = window.document.getElementsByTagName('head')[0]let js = window.document.createElement('script') js.setAttribute('type', 'text/javascript') js.setAttribute('async', 'sync')  // 这句显式声明强调src不是按照异步方式调用的 js.setAttribute('src', url) head.appendChild(js)return result}

额,运行起来结果竟然是undefined!w3cshool的文档竟然也不准,还权威呢,我看也不怎么着,小飞暗自想到。

“刚才试了一下,w3school文档上写的有问题,这个异步属性根本就是错的”

“可是我刚还试过一次这个,我确认是好的呀”

12<script src="loop50000 &&put('frist').js"></script><script src="put('second').js"></script>

(有兴趣的同学可以实现以下两个js,并且加上async的标签进行尝试。)

“这个,我就搞不清楚了”,小飞讪讪的说到

对方已离线

抽象

关于这个问题,相信不只是小飞,很多人都难以解答。为什么ajax可以做到同步,但jsonp不行,推广到nodejs上,为什么readFile也可以做到同步(readFileSync),但有的库却不行。

(至于script的async选项我们暂时避而不谈,是因为现在的知识维度暂时还不够,但是不要着急,下文中会给出明确的解释)

现在,让我们以计算机科学的角度抽象这个问题:

我们是否可以将异步代码转化为同步代码呢?(ASYNCCALL =>SYNCCALL)

既然是抽象问题,那么我们就可以不从工程角度/性能角度/实现语言等等等方面来看(同步比异步效率低下),每增加一个维度,复杂程度将以几何爆炸般增长下去。

首先,我们来明确一点,==在计算机科学领域==同步和异步的定义

同步(英语:Synchronization),指对在一个系统中所发生的事件(event)之间进行协调,在时间上出现一致性与统一化的现象。在系统中进行同步,也被称为及时(in time)、同步化的(synchronous、in sync)。--摘自百度百科

异步的概念和同步相对。即时间不一致,不统一

明确了这一点,我们可以借助甘特图来表示同步和异步

其中t1和t2是同步的,t1和t3是异步的。

答案就在操作系统原理的大学教材上,我们有自旋锁,信号量来解决问题,伪代码如下

1234567891011121314151617spinLock () {// 自旋锁  fork Wait 3000 unlock() //开启一个异步线程,等待三秒后执行解锁动作  loop until unlock // 不断进行空循环直到解锁动作Put ‘unlock’} //pv原语,当信号量为假时立即执行下一步,同时将信号量置真//反之将当前执行栈挂起,置入等待唤醒队列//uv原语,将信号量置为假,并从等待唤醒队列中唤醒一个执行栈Semaphore () {  pv()  fork Wait 3000 uv()  pv()  uv()Put 'unlock'}

很好,至此都可以在操作系统原理的教材上翻到答案。于是我们在此基础上添加约束条件

仅仅依赖于js本身,我们是否可以将异步代码转化为同步代码呢?(ASYNCCALL =>SYNCCALL)

论证

带着这个问题,我们翻看一下jquery的源码

src/ajax/xhr.js#L42

可以看出, ajax的同步机制本质上是由XMLHttpRequest实现的,而非js原生实现。

同样的道理,我们再翻看一下nodejs的源码

lib/fs.js#L550

从readFileSync->tryReadSync->readSync一路追下去,会追到一个c++ binding, node_file.cc#L1167

123456if (req->IsObject()) {   ASYNC_CALL(read, req, UTF8, fd, &uvbuf, 1, pos)} else {   SYNC_CALL(read, 0, fd, &uvbuf, 1, pos)   args.GetReturnValue().Set(SYNC_RESULT)}

同步的奥妙在于c++的宏定义上,这是一种借由c++来实现的底层同步方式。

观察了这两种最广泛的异步转同步式调用,我们发现均没有采用js来实现。

似乎从现象层面上来看js无法原生支持,但是这还不够,我们探究在js语义下上面的自旋锁/信号量的特性模拟实现(我知道你们一定会嗤之以鼻,==js本身就是单线程的,只是模拟了多线程的特性== 我无比赞同这句话,所以这里用的不是实现,而是特性模拟实现),另外,由于settimeout具有fork相似的异步执行特性,所以我们用setitmeout暂时代替fork

自旋锁

1.第一个实现版本

1234567var lock = truesetTimeout(function () {lock = false}, 5000) while(lock)console.log('unlock')

我们预期在5000ms后执行unlock语句,但是悲剧的是,整个chrome进程僵死掉了。

为了解释清楚这个问题,我们读一下阮一峰老师的event loop模型

event-loop.html

看样子咱们已经清楚的了解了event loop这个js运行顺序的本质(同步执行代码立即执行,异步代码入等待队列),那么,我们可以基于此给出js vm的调度实现(eventloop的一种实现),当然,咱们为了解释自旋锁失败只需要模拟异步操作, 同步操作,和循环就好

123456789101112131415161718192021222324//taskQueue:任务队列//runPart:当前正在执行的任务(同步指令集)//instruct: 正在执行的指令 function eventloop (taskQueue) {while(runPart = taskQueue.shift()) {while(instruct = runPart.shift()) {const { type, act, codePart } = instructswitch(type) {case 'SYNC':         console.log(act)if (act === 'loop')           runPart.unshift({             act: 'loop',             type: 'SYNC'})breakcase 'ASYNC':         taskQueue.push(codePart)break}}}}

然后转化我们的第一个版本自旋锁

1234567891011121314151617181920let taskQueue = [[{act: 'var lock = true', type: 'SYNC'}, //var lock = true{       act: 'setTimeout',       type: 'ASYNC',       codePart: [{act: 'lock = false', type: 'SYNC'}]}, // setTimeout(function () { lock = false }, 5000)/*{       act: 'loop',       type: 'SYNC'   },*/ // while(lock){       act: 'console.log(\'sync\')',       type: 'SYNC'} // console.log('unlock')]]<em id="__mceDel"> </em>

测试一下,符合evnet loop的定义,然后放开注释,我们成功的让loop block住了整个执行过程,lock = false永远也没有机会执行!!!

(真实的调度机制远比这个复杂的多得多的,有兴趣的可以看看webkit~~~的jscore的实现哈)

知道了原理,我们就来手动的改进这部分代码

2.改进的代码

12345678910111213141516var lock = truesetTimeout(function () {lock = false   console.log('unlock')}, 5000) function sleep() {var i = 5000while(i--)} var foo = () =>setTimeout(function () {   sleep()lock &&foo()})foo()

这个版本的改进我们对while(true)做了切块的动作,实际上这种技巧被广泛的应用到改善页面体验的方面,所以,有些人因为时序无法预知而抗拒使用settimeout这种想法是错误的!

6996528,

小测验1: 改写eventloop和taskQueue,使它支持改进后的代码

可是,如果把代码最后的foo() 变成 foo() &&console.log('wait5sdo'),

我们的代码依然没有成功,why

注意看我们标红的地方,如果你完成了小测验1,就会得到和这张图一致的顺序

==同步执行的代码片段必然在异步之前。==

所以,无论从理论还是实际出发,我们都不得不承认,在js中,把异步方法改成同步方法这个命题是水月镜花

哦对了,最后还需要解释一下最开始我们埋下的坑, 为什么jsonp中的async没有生效,现在解释起来真的是相当轻松,即document.appendChild的动作是交由dom渲染线程完成的,所谓的async阻塞的是dom的解析,而非js引擎的阻塞。实际上,在async获取资源后,与js引擎的交互依旧是push taskQueue的动作,也就是我们所说的async call

推荐阅读: 关于dom解析请大家参考webkit技术内幕第九章资源加载部分

峰回路转

相信很多新潮的同学已经开始运用切了async/await语法,在下面的语法中,getAjax1和console之间的具有同步的特性

1234async function () {var data = await getAjax1()   console.log(data)}

讲完了event loop和异步的本质,我们来重新审视一下async/await。

老天,这段代码亲手推翻了==同步执行的代码片段必然在异步之前。== 的黄金定律!

惊不惊喜,意不意外,这在我们的模型里如同三体里的质子一样的存在。我们重新审视了一遍上面的模型,实在找不到漏洞,找不到任何可以推翻的点,所以真的必须承认,async/await绝对是一个超级神奇的魔法。

到这里来看我们不得不暂时放弃前面的推论,从async/await本身来看这个问题

相信很多人都会说,async/await是CO的语法糖,CO又是generator/promise的语法糖,好的,那我们不妨去掉这层语法糖,来看看这种代码的本质, 关于CO,读的人太多了,我实在不好老生常谈,可以看看这篇文章,咱们就直接绕过去了,这里给出一个简易的实现

/5800210.html

1234567891011121314151617181920function wrap(wait) {var iter iter = wait()const f = () =>{const { value } = iter.next()   value &&value.then(f)} f()} function *wait() {var p = () => new Promise(resolve =>{     setTimeout(() =>resolve(), 3000)})yield p() console.log('unlock1')yield p() console.log('unlock2') console.log('it\'s sync!!')}

终于,我们发现了问题的关键,如果单纯的看wait生成器(注意,不是普通的函数),是不是觉得非常眼熟。这就是我们最开始提出的spinlock伪代码!!!

这个已经被我们完完全全的否定过了,js不可能存在自旋锁,事出反常必有妖,是的,yield和*就是表演async/await魔法的妖精。

generator和yield字面上含义。Gennerator叫做生成器,yield这块ruby,python,js等各种语言界争议很大,但是大多数人对于‘让权’这个概念是认同的(以前看到过maillist上面的争论,但是具体的内容已经找不到了)

扩展阅读---ruby元编程 闭包章节yield(ruby语义下的yield)

所谓让权,是指cpu在执行时让出使用权利,操作系统的角度来看就是‘挂起’原语,在eventloop的语义下,似乎是暂存起当时正在执行的代码块(在我们的eventloop里面对应runPart),然后顺序的执行下一个程序块。

我们可以修改eventloop来实现让权机制

小测验2 修改eventloop使之支持yield原语

至此,通过修改eventloop模型固然可以解决问题,但是,这并不能被称之为魔法。

和谐共存的世界

实际上通过babel,我们可以轻松的降级使用yield,(在es5的世界使用让权的概念!!)

看似不可能的事情,现在,让我们捡起曾经论证过的

==同步执行的代码片段必然在异步之前。== 这个定理,在此基础上进行进行逆否转化

==在异步代码执行之后的代码必然不是同步执行的(异步的)。==

这是一个圈子里人尽皆知的话,但直到现在他才变得有说服力(我们绕了一个好长的圈子)

现在,让我们允许使用callback,不使用generator/yield的情况下完成一个wait generator相同的功能!!!

1234567891011121314151617181920function wait() {const p = () =>({value: new Promise(resolve =>setTimeout(() =>resolve(), 3000))})let state = {next: () =>{       state.next = programPartreturn p()}}function programPart() {     console.log('unlocked1')     state.next = programPart2return p()}function programPart2() {     console.log('unlocked2')     console.log('it\'s sync!!')return {value: void 0}}return state}

太棒了,我们成功的完成了generator到function的转化(虽然成本高昂),同时,这段代码本身也解释清楚了generator的本质,高阶函数,片段生成器,或者直接叫做函数生成器!这和scip上的翻译完全一致,同时拥有自己的状态(有限状态机)

推荐阅读 计算机程序的构造和解释 第一章generator部分

小测验3 实际上我们提供的解决方式存在缺陷,请从作用域角度谈谈

其实,在不知不觉中,我们已经重新发明了计算机科学中大名鼎鼎的CPS变换

Continuation-passing_style

最后的最后,容我向大家介绍一下facebook的CPS自动变换工具--regenerator。他在我们的基础上修正了作用域的缺陷,让generator在es5的世界里自然优雅。我们向facebook脱帽致敬!!egenerator

后记

同步异步 可以说是整个圈子里面最喜欢谈论的问题,但是,谈来谈去,似乎绝大多数变成了所谓的‘约定俗称’,大家意味追求新技术的同时,却并不关心新技术是如何在老技术上传承发展的,知其然而不知其所以然,人云亦云的写着似是而非的js。

==技术,不应该浮躁==

PS: 最大的功劳不是CO,也不是babel。regenerator的出现比babel早几个月,而且最初的实现是基于esprima/recast的,关于resprima/recast,国内似乎了解的并不多,其实在babel刚刚诞生之际, esprima/esprima-fb/acron 以及recast/jstransfrom/babel-generator几大族系围绕着react产生过一场激烈的斗争,或许将来的某一天,我会再从实现细节上谈一谈为什么babel笑到了最后~~~~