新人求问,异步如何转同步,还是我的思路不对?
发布于 5 年前 作者 timerainv7 16724 次浏览 最后一次编辑是 4 年前 来自 问答

才开始学node没多久,遇到了好多问题,感觉最让我困扰的就是异步转同步

  1. 有一次遇到的问题大概代码是这样的: for(var i=arr.length; i>0; i–){ fun(); if(i==0){ console.log(“ok”) } } 但是我发现每次fun()还没执行完就先输出“OK”。我知道大概是fun()被放进线程池没有立即执行所以要后执行,但是我不想这样。。
  2. 还有一次在使用sqlLite模块时遇到的,大概是这样: var db = new sqlite3.Database(‘xxx’); … for(var i=0; i<arr.length; i++){ … db.all(sql1, function(){ … console.log( arr[i] ); db.all(sql2, function(){ … console.log( arr[i] ); }) }) } 具体忘记了,但是问题我记得,i的值可以传递到第一层的db.all中,但是传递不到第二层的db.all中,导致arr[i]值是undefined。
  3. 还有就是写代码的时候不知不觉就嵌套了很多层。。。 求大神帮忙,万分感谢~~
37 回复

代码不能手敲么。。。怎么没排版了、、

var db = new sqlite3.Database(‘xxx’); … for(var i=0; i<arr.length; i++){ … db.all(sql1, function(){ … console.log( arr[i] ); db.all(sql2, function(){ … console.log( arr[i] ); }) }) }

@timerainv7 代码部分可以使用 markdown标记。 请参考 在线markdown编辑器 ,里面有示例。 效果是酱紫:

console.log(output);

终极建议,不要用nodejs。nodejs的异步模式非常初级,粗暴,简单,不利于和谐,异步终极的目标是没有异步,没有显式异步的异步才是好异步。

没有显式异步的异步其实就是同步,如果你觉得nodejs的异步模式不好理解,那么可以试一试fibjs的形式同步。

曾经我对nodejs的异步模式深信不移,后来才发现,什么回调,什么promise,什么generator都特么弱爆了,有这个时间去搞这些,还不如coroutine来的清爽实在,协程才是异步的未来,才是将异步代码转为同步代码的正途。大牛们告诉我们,这个世界是并行的,这个世界是异步的,并行,异步才是符合人类思维模式的。但请注意这个观点仅仅适用于大牛。我不属于大牛,我脑子比较简单,我的思维方式是顺序是同步的,我怕耗费无谓的时间在流程控制上,怕花时间在理解generator上,所以我选择形式同步,抛弃了nodejs。

nodejs不改变,不管它版本号变得怎么快(刚出了4.0,立马跑去看,这样的更新能搞一个大版本号?难不成这个版本号是庆祝社区合并?),都不能代表未来,因为我坚信,这个世界普通人还是大多数,大牛毕竟是少数。

如果不像抛弃nodejs,请把回调模式下的异步彻底搞明白了再来写代码,不然死多活少。教你一个理解回调模式下的异步的秘诀,你必须要搞清楚,所有javascript的代码除了回调函数外都是同步执行的,所有带回调的函数只是发出异步命令和注册回调函数,命令发布完毕,就会顺序往下执行,异步命令执行完成之后,调用回调函数。回调函数调用是异步的,但一旦被调用,除非某异步函数发出另一个异步命令,否则回调函数内都是同步执行的。回调函数的作用其实就是保证逻辑顺序,是为了保证同步,从这点上来看,那些宣称世界是并行的异步的大牛其实都是骗子,我们的脑子事实上还没那么发达,我们的思考模式其实还是顺序的,是同步的。回调模式只是比多线程的锁同步要好理解得多,仅此而已(当然性能上也是好得多)。

	var fs = require('fs');
	var a = 0;
	fs.readFile('test.txt',  'utf-8', function(err, data){
		a++;
		console.log('a in:' + a);
	});
	a++;
	console.log('a out:' + a);

下面是我用lua协程的实现,假设代码已被包含在某个协程中。

	local fs = require('fs')
	local a = 0
	data,  err = fs.readFile('test.ext',  'utf-8')
	a = a + 1
	print(a)

fs.readFile实实在在是异步的,基本的原理就是发出异步请求之后将当前协程挂起yield,异步请求完成后将结果resume。 当然在这么简单的运用看不出谁好谁坏的,当有层层回调的时候,协程的威力才会显现出来,一个要时刻考虑回调的执行顺序,参数传递,错误处理等问题,一个就跟写同步代码一样。

async 或者 bluebird ,看看这两个

老生常谈, promise 化就是了,用async, q, bluebird 等做异步处理

如果无法转变编程思想,那还是用Java .net 等编译类的吧

加标识,或者用async.foreach

@pangguoming 然而 Node 也是 JIT 编译。。。

@coordcn 我就习惯了js现在的方式,习惯了事件驱动回调。

@coordcn 按 lua 的写法,es2015 现在能这么写

const fs = require('fs')
let a = 0
let = yield fs.readFile('test.ext',  'utf-8')
a = a + 1
console.log(a)

es2016之后能这么写

const fs = require('fs')
let a = 0
let = await fs.readFile('test.ext',  'utf-8')
a = a + 1
console.log(a)

如果想從語法層面解決這個問題,而且不用等待es新語法實現,我推薦 iced-coffee-script 。現成的 defer await 已經可以用了,編譯出的js兼容現在的node和瀏覽器引擎

https://maxtaco.github.io/coffee-script/

@coordcn

local fs = require('fs')
    local a = 0
    data,  err = fs.readFile('test.ext',  'utf-8')
    a = a + 1
    print(data)

想知道这个打印出什么

@fengmk2

写你这样的代码,如果只是作为普通用户,不需要理解原理,照着做就行了,如果稍微有点追求的,要探求个所以然来,这就要下功夫了。首先必须要透彻了理解回调模式下的异步,因为这些代码背后首先做的就是对回调的包装和转换,这个转换本身理解成本就不小,为什么要这么转换,理解generator是如何将回调异步转为形式同步的成本也不小,这中间就造成了大神与普通用户的区别,当然有的人是醉心于这种不平等的。而我的方案是平的,不需要附加任何额外的代价,不需要死额外的脑细胞,但对眼里只有javascript一种语言的人是无解的。

这里还有人说lua是很low的语言,我只能表示无语,一个优美小巧的学院派语言竟然被称为low,lua和javascript我都很喜欢,但javascript是工程应用的产物,是很多无奈的妥协的产物,虽然很成功,但却是远不及lua简单干净的。

形式同步大家已没有争议,这是大势所趋,javascript也在朝这个方向发展,但历史包袱过重,进化得过于缓慢,协程的引入也搞成了半调子,es6既然已经这样,下个版本干脆直接一步到位得了,回调异步转形式同步,协程是绕不过去的,不管表面上用什么关键字,yield也好,await也罢,都要在异步的时候主动让出CPU,让其他协程来继续。

已经习惯了回调异步的同学,建议你们立即向形式同步转吧,这个趋势不会因为自己已经习惯了而改变,你不改变就会被淘汰。永远要记住,我们要的是异步带来的好处,而不是复杂难懂的异步代码。

@dayuoba

这个打印的就是文件内容

@coordcn 我并不觉得有什么好难理解的 我node的callback tj的co 包括go的goruntine都用的蛮好 不过js的异步确实比较费劲 要多理解理解 go的goruntine和channel基本一次就上手了 也没有报错 自豪地采用 CNodeJS ionic

@wenshiqi0

go的模型比javascirpt的要好。

异步本身不难理解,难的是复杂的逻辑和错误处理,回调模式下的异步这个问题是很头疼的。

co如果仅仅是应用,不需要理解原理, 那的确没什么难的,但能把回调模式下的异步转成形式同步的原理完全搞明白了还是要废点脑子的。田永强的这篇文章不错http://www.infoq.com/cn/articles/generator-and-asynchronous-programming/

我的方法是不需要做这么多转换的,lua天生支持协程,所有的包装在c层面做好了,在lua层面就是同步代码,完全不需要进行任何转换。c层面也很容易理解,我的做法跟openresty是一样的。

@coordcn 我看过那个了 co的我也理解基本要我自己写也没有问题 不过没必要还是用co吧 我现在比较有兴趣自己实现一个协程 自豪地采用 CNodeJS ionic

@wenshiqi0

为什么要实现协程?在javascript里么?javascirpt不是已经提供了generator这个半调子协程了么?

generator里的yield就相当于lua的yield,next相当于resume。

@coordcn 实现来玩而已 没有特别的目的 是c语言和汇编层面的 可能在协程切换上面要用汇编 自豪地采用 CNodeJS ionic

@coordcn 我对任何东西都没有排斥 纯粹的即学即用 兴趣来了就弄 做完这个我准备就去自己实现一个rust的http库~~ 自豪地采用 CNodeJS ionic

@wenshiqi0

c实现协程主要有两种方式,一种是是lua用的longjmp,还有一种是ucontext,fibjs就是自己用汇编实现的ucontext(为了兼容windows平台)

@wenshiqi0

我也喜欢弄新东西,折腾自己,这样不管结果如何,自我提升还是很快的,做http库的话可以交流啊。

@coordcn 我想上下文也自己实现 实现一个最轻量的 自豪地采用 CNodeJS ionic

@coordcn 没办法 rust没有http库 也当熟悉一下rust语法 自豪地采用 CNodeJS ionic

你可以使用nimble, 实现异步逻辑顺序化

嵌套问题,你可以将回调函数独立成一个函数,维护的时候旧舒服多了

以上内容来自node in action, 吴海星译

@coordcnreadFile是异步的嘛?阻塞的异步?

@coordcn 哈哈,我接触后也觉得,nodejs的异步模式用起来非常不爽(因为人的思维也好,事物的发展也好,问题的解决也好,都是过程式的,只在必要时异步就好了)

@dayuoba

当然是异步的,如果是同步的话就没有意义了。readFile调用的同时当前运行的协程会让出CPU给其他协程运行,等readFile异步运行完成后,会将结果压入协程堆栈,协程唤醒继续向下执行。

es6的generator可以模拟这个过程,首先要将原来的异步函数都包装成一个可以传入真实回调函数的函数,

	  function readFile(file, options){
	  	var pass;
		
		fs.readFile(file, options, function(err, data){
			if(pass){
				pass(err, data);
			}
		})
	
	   	return function (fn){
			pass = fn;
		};
	}

再通过generator.next()来触发执行异步函数,这个过程只是巧妙的利用generator的特性伪装了回调,其实整个异步过程还是通过回调函数来控制的。generator用这种方法只是伪装了协程特点,但不能算是协程,还是主要利用回调来实现的。当然javascript有javascript的难处,船大难掉头,他必须兼容之前的回调代码。

我用lua实现的是不需要兜这么大圈子的,直接了当当成同步函数用就行了,我们只是要异步带来的好处,还管什么异步的形式?只要能痛快的写代码就行了。

@tim1020

nodejs开了个好头,但能不能笑到最后就要看后续的发展了,nodejs是成也javascript败也javascript,javascript编程模型在接下来几个版本可能会剧烈变动,人心如果散了,队伍就不好带了。。。

我对nodejs的评价是,可以学思想,但千万不能被他的形式束缚。nodejs最初的作者那么激烈的反对协程,认为回调才是异步的未来,现在大家被回调折磨几年之后,其实都在思变,不管是generator还是async,显式的异步终将慢慢退出舞台,后面的历史必然会由形式同步来书写。

我们要的是异步的好处,而不是复杂难懂的异步代码。

其实异步完全是协作的结果。。如果一个人干一件事,完全不依赖外界,是没有异步的。。但是要等别热的结果,结果回来前啥都不干就是同步,等结果的时候干干其他事情就是异步了。。其实等结果的时候干点别的事情就是提高时间的利用率。。本身没啥难理解的,只是事情太复杂就麻烦了。。就像杂技演员两只手可以抛起好多个球。。普通人就不行。。这个就是专业水平的问题了。。

看了很多@coordcn 的回复, 虽然是在极力推荐lua, 不过思路上我是非常支持的. 我们都是普通人, 我们要写复杂的业务逻辑, 不希望把时间浪费在眼花缭乱的洋葱回调或者是那个名不达意的yield上. 但是我认为相比lua, node还是有优势的, 也就是来源于javascript. 而且前后端都用js, 存在一个非常大的优势就是, 模板渲染可以用js引擎解决. 而真正的后端–核心业务逻辑, java也好, node也好, python也好, 都是可以的. 扯远了, 说回来, node里有fibers这个包从底层实现了协程的概念. 我的日常开发中也广泛使用这个包, 并且在此基础上封装了下, api更友好, 错误处理更完善, 便是nbs. 本来fibers这种非主流存在, 我就已经很满意了, 不期待node有什么大的改进, 可没想到, 4.0后竟然把domain也标为deprecated, 这让异步错误处理怎么搞? 好忧伤~~~

1.感觉代码本身就有问题,压根不可能i=0。。。还有你想在fun后输出ok直接把输出写在fun函数里不就好啦~ 因为nodejs本身是单进程,单线程的,但是他的很多io是异步的。。。 2.= =。第二题,感觉还是一样的道理传参数i

同是新手,刚学nodejs一个多月~,感觉你JS不太行,建议看一下js方面的书籍

@kiliwak

我不是极力推荐lua,我选择语言是很现实的,自己熟悉,能够解决问题,lua天生就提供了异步转同步的基础设施,很多人也做了很多探索,有可以参考的代码,lua可以很轻松的做到形式同步,实质异步。很多人对lua有误解,没有深入的了解,其实lua和javascript是非常类似的,语法虽然不是类c的,但核心的东西都是可以类比的,熟悉javascript的人可以很快上手。

lua与c绑定接口比较简单,我有大量的计算要做,这部分原来用javascript写的,node运行会有明显的阻塞,严重的会客户端假死,超时。用c改写后好很多,v8很快,那是跟其他脚本语言比,跟原生的还是有很大差距的,尤其是数学计算。v8的绑定还是稍麻烦,没有lua这么清爽简单。lua这个特性对库的增加是非常有利的,像node-canvas这些库,都可以移植成lua版本的。

如果有一天javscript也提供了像lua一样的异步转同步的基础设施,我可能还会转到node,但现阶段,javascript语言进化不确定性太多了,这两三年内版本估计变动得可能比较剧烈,我认为lua更符合我现在的需求。

很多人其实并没有真正的理解异步,也没有理解异步回调的意义,就觉得用异步了,性能就提升了,好牛逼的。很多人还在大谈人的思维方式,异步符合人类思维方式云云,都是自欺欺人,人类的思维方式要真是异步,还要层层回调做什么?回调干的就是同步的活,就是保证执行顺序的,一边被回调折磨的很爽,一边自我安慰说这才是异步的正途,但讽刺的是,回调特么就是为了保证同步的。人就是顺序思维的。

你的方案我看过了,还是比较复杂的,刚上手的话必须要有个学习适应的过程,这其实就是javascript无奈的地方,必须要兼容原有的回调代码。我追求的是完全没有异步的异步,没有协程的显式创建(每个链接就是一个协程,所有异步操作都围绕这个协程进行切换),没有协程的显式运行,没有yield,没有resume,这些都是在c层面对用户隐式调用的,用户看到的代码就是简单的同步代码,用户只要熟悉下文档,没有异步基础也能堆代码。我的方案跟openresty类似,openresty与nginx结合的过于紧密,我选择的是lua+libuv组合,linuv是非常优秀的异步库,libuv可以说是nodejs的核心,理解了libuv,就基本理解了nodejs,这样的话,我将来如果要转回nodejs也是很容易的。

@coordcn nodejs已经船大难掉头了, 核心库和第三方库, 基本都是按回调来写, 不会默认是在一个协程栈里. 不过, 你想要的没有协程的显式创建, …, 没有yield,没有resume其实在node中也可以实现.

  1. 在入口文件里, 一开始就创建一个协程, 作为全局默认协程. 所有后续的代码都运行在这个协程里
  2. 对于http server, 每个request的处理都额外创建一个协程.
  3. 改造核心库, 如setTimeout改为sleep, readFile用协程包装一下. 如下示例:
function sleep(ms){
	setTimeout(resume, ms);
	wait();
}

function readFile(path){
	fs.readFile(path. resume);
	return wait()[1];
}

通过这3步, 一个基本兼容现有nodejs的环境就出来了. 这部分的复杂度, 可以通过写一个框架来隐藏. 所有的后续包开发者, 基本都不需要考虑异步/回调/run/resume/wait了, 只要调用sleep/readFile即可 这也是我自己目前的一个产品的开发思路. 一个示例伪代码

@kiliwak

每个request一个协程代价是否大了点,我认为每个连接一个协程可能更好,不过nodejs可能要动到底层库。

这也是nodejs无奈的地方,从一开始就排斥了协程,现在其实又要回头,不管是es6的generator还是es7的await都是类似协程方式实现的。要想从回调异步转为形式同步,协程是绕不过去的,nodejs必须要兼容回调代码,这个时候,很多人引以为傲库的数量反而是个累赘。转换完成后的同步代码大家都会写,但最关键的就是如何转换。举个例子,如果用户不明白到底是如何转换的,不明白整个框架,转换或框架过程出了任何问题,用户就束手无策了,这中间就有了所谓高手和普通人的区别,其实这种区别完全是没有必要的。generator和你的方案都是存在这种问题的,很多人都觉得没必要理解,会用就行,但我认为,如果不能理解的话,整个数据的流向不清晰,碰到问题,极有可能要吃亏的。再者稍微有点追求的程序员,都不会让自己做一个白痴,都会对自己的使用技术的原理探个明白。

在大方向上,其实大家已经很明白了,回调必死(之前还有人跟我争论,我反问了一句,既然回调那么好,那要pormise和generator做什么?什么东西,有缺陷就是有缺陷,回调异步老早就有了,为什么没有大规模使用,就是用起来麻烦,在c里写个两层以上就恶心的不行了。),但就是具体的技术实现上,我个人追求的比较极端,我追求完全没有显式异步的异步,我也追求极致的性能,所以现阶段,javascript对我而言肯定不是好的选择。

arr = [1,2,3];
function fun(){
        console.log("This is fun");
}

function forEach(a, callback, index) {
      index = index || 0;
      if (index < a.length) {
        callback(a[index]);
        forEach(a, callback, index + 1);
      }else{
        console.log("ok");
      }
  }
forEach(arr,fun);

@timerainv7 这段代码可以达到你需要的效果,在异步代码中,有几种方法来处理for循环,用递归。另外的办法是用callback函数。

回到顶部