同步与异步,回调与协程

  本文主要介绍在网络请求中的同步与异步,以及异步的表现形式: 回调与协程,并经过python代码展现各自的优缺点。javascript

概念上下文:

  当提到同步与异步,你们难免会想到另外一组词语:阻塞与非阻塞。一般,同时提到这个这几个词语通常实在讨论network io的时候,在《unix network programming》中有详尽的解释,网络中也有许多讲解生动的文章。    
  本文所讨论的同步与异步, 是指对于请求的发起者,是否须要等到请求的结果(同步),仍是说请求完毕的时候以某种方式通知请求发起者(异步)。在这个语义环境下,阻塞与非阻塞, 是指请求的受理者在处理某个请求的状态,若是在处理这个请求的时候不能作其它事情(请求处理时间不肯定),那么称之为阻塞,不然为非阻塞。
  举个例子,我去柜台办理业务,那我是请求者,柜员时受理者。若是我在柜台一直等着柜员办理,直到办理完毕,那么对于我来讲,就是同步的;若是我只是在柜员那里登记,而后到一边歇着,等柜员办理完毕以后告诉我结果,那么就是异步的。对于柜员,办理业务的时候可能须要等待打印机打印,若是在这个时候柜员去处理其余人的业务,那么就是非阻塞的,若是必定获得把个人业务办完再接待下一位顾客,那么就是阻塞的。
 
  本文站在请求发起者的角度来思考同步与异步,在实际开发中,一个最简单的例子就是http请求。假设这么一个场景,程序须要访问两个网址(经过url),若是只有一个线程。那么同步与异步分别怎么处理呢

同步的方式:

   python的urllib2提供了http请求的功能,咱们来看看代码:
 1 def http_blockway(url1, url2):
 2     import urllib2 as urllib
 3     import time
 4     begin = time.time()
 5     data1 = urllib.urlopen(url1).read()
 6     data2 = urllib.urlopen(url2).read()
 7     print len(data1), len(data2)
 8     print 'http_blockway cost', time.time() - begin
 9 
10 url_list = [ 'http://xlambda.com/gevent-tutorial/','https://www.bing.com']
11 if __name__ == '__main__':
12     http_blockway(*url_list)
运行结果:

  45706 121483
  http_blockway cost 2.22000002861html

   注意:对代码运行的时间应该屡次执行求平均值,这里只是简单做为参考。下同。
  python的urllib是同步的,即当一个请求结束以后才能发起下一个请求,咱们知道http请求基于tcp,tcp又须要三次握手创建链接(https的握手会更加复杂),在这个过程当中,程序不少时候都在等待IO,CPU空闲,可是又不能作其余事情。但同步模式的优势是比较直观,符合人类的思惟习惯: 那就是一件一件的来,干完一件事再开始下一件事。在同步模式下,要想发挥duo多喝CPU的威力,可使用多进程或者多线程。
 

异步加回调的方式:

    若是发出了请求就当即返回,这个时候程序能够作其余事情,等请求完成了时候经过某种方式告知结果,而后请求者再继续再来处理请求结果,那么咱们称之为异步,最多见的就是回调(callback),在python中,tornado提供了异步的http请求。对于上面的场景,咱们来看看代码:
 1 import tornado
 2 from tornado.httpclient import AsyncHTTPClient
 3 import time, sys
 4 
 5 def http_callback_way(url1, url2):
 6     http_client = AsyncHTTPClient()
 7     begin = time.time()
 8     count = [0]
 9     def handle_result(response, url):
10         print('%s : handle_result with url %s' % (time.time(), url))
11         count[0] += 1
12         if count[0] == 2:
13             print 'http_callback_way cost', time.time() - begin
14             sys.exit(0)
15 
16     http_client.fetch(url1,lambda res, u = url1:handle_result(res, u))
17     print('%s here between to request' % time.time())
18     http_client.fetch(url2,lambda res, u = url2:handle_result(res, u))
19 
20 url_list = [ 'http://xlambda.com/gevent-tutorial/','https://www.bing.com']
21 if __name__ == '__main__':
22     http_callback_way(*url_list)
23     tornado.ioloop.IOLoop.instance().start() 

 

运行结果:

  1487292402.45 here between to request
  1487292403.09 : handle_result with url http://xlambda.com/gevent-tutorial/
  1487292403.21 : handle_result with url https://www.bing.com
  http_callback_way cost 0.759999990463java

    从代码能够看到,对请求的结果是放在一个额外的函数(handle_result)中进行的,这个处理函数在发出请求的时候注册的,这就是回调。从运行结果能够看到,在发出第一个请求以后就当即返回了(先于请求结果),并且运行时间大为缩短,这就是异步的优点所在: 不用在IO上等待,在单核CPU上就有更好的性能。可是callback这种形式,致使代码逻辑由于异步请求分离,不符合人类的思惟习惯,由于不直观。再来看一个很常见的例子:请求一个网址A,根据网页的内容来肯定是接着访问B仍是C,若是使用异步,那代码是这样的:
   
 1 import tornado
 2 from tornado.httpclient import AsyncHTTPClient
 3 http_client = AsyncHTTPClient()
 4 def handle_request_final(response):
 5     print('finally we got the result, do sth here')
 6 
 7 def handle_request_first(response, another1, another2):
 8     if response.error or 'some word' in response.body:
 9         target = another1
10     else:
11         target = another2
12     http_client.fetch(target, handle_request_final)
13 
14 def http_callback_way(url, another1, another2):
15     http_client.fetch(url, lambda res, u1 = another1, u2 = another2:handle_request_first(res, u1, u2))
16 
17 url_list = [ 'https://www.baidu.com', 'https://www.google.com','https://www.bing.com']
18 if __name__ == '__main__':
19     http_callback_way(*url_list)
20     tornado.ioloop.IOLoop.instance().start()
   代码表达的逻辑是连贯的,但代码的变现形式倒是割裂的,在这里分散到了三个函数里面。对于程序员来讲,这样的代码难以阅读,不容易看一眼就大概明白其意图,并且这样的代码是难以维护的,更糟糕的是,编程实现中还得保存或者传递上下文(好比函数中的参数 another1, another2)。
  在python中,因为lambda的功能仍是比较弱,因此回调通常都是另外命令一个函数。而在javascript,特别是以异步IO为基石的NodeJS中,因为匿名函数的强大,很轻易嵌套实现callback,这也就出现了让编码者沉默、维护者流泪的 callback hell。固然 promise对callback hell有必定的改善,但还有没有更好的办法呢?
 

异步协程方式:

  这个时候协程就出马了,原本是异步调用,可是程序上看上去变成了“同步”。关于协程,python中能够用原声的generator,也可使用更严格的greenlet。首先来看看tornado封装的协程:
  
 1 import tornado, sys, time
 2 from tornado.httpclient import AsyncHTTPClient
 3 from tornado import gen
 4 
 5 def http_generator_way(url1, url2):
 6     begin = time.time()
 7     count = [0]
 8     @gen.coroutine
 9     def do_fetch(url):
10         http_client = AsyncHTTPClient()
11         response = yield http_client.fetch(url, raise_error = False)
12         print url, response.error
13         count[0] += 1
14         if count[0] == 2:
15             print 'http_generator_way cost', time.time() - begin
16             sys.exit(0)
17 
18     do_fetch(url1)
19     do_fetch(url2)
20 
21 url_list = [ 'http://xlambda.com/gevent-tutorial/','https://www.bing.com']
22 if __name__ == '__main__':
23     http_generator_way(*url_list)
24     tornado.ioloop.IOLoop.instance().start()

 运行结果:python

  http://xlambda.com/gevent-tutorial/ None
  https://www.bing.com None
  http_generator_way cost 1.05999994278程序员

  从运行结果能够看到,效率仍是优于同步的,而代码看起来是顺序执行的,但事实上有某种意义上的并行。代码中使用了decorator 和 yield关键字,在看看使用gevent的代码:

 1 def http_coroutine_way(url1, url2):
 2     import gevent, time
 3     from gevent import monkey
 4     monkey.patch_all()
 5     begin = time.time()
 6 
 7     def looks_like_block(url):
 8         import urllib2 as urllib
 9         data = urllib.urlopen(url).read()
10         print url, len(data)
11 
12     gevent.joinall([gevent.spawn(looks_like_block, url1), gevent.spawn(looks_like_block, url2)])
13     print('http_coroutine_way cost', time.time() - begin)
14 
15 url_list = [ 'http://xlambda.com/gevent-tutorial/','https://www.bing.com']
16 if __name__ == '__main__':
17     http_coroutine_way(*url_list)
  代码中没有特殊的关键字,使用的API也是跟同步方式同样的,这就是gevent的牛逼之处,经过monkey_patch就是原来的同步API变成异步,gevent的介绍能够参见《 gevent调度流程解析》。
  协程与回调对比,优点一目了然:代码更清晰直观,更加复合思惟习惯。但协程也不是银弹,首先,协程是个新概念,须要花时间去理解;其次,程序员内心也得紧紧记住,在看似在一块儿的两句代码中间可能插入了很大其它逻辑(即便是在单线程)。但整体来讲,协程给出了人们解决问题的新思路,利大于弊,在其余语言(C# Lua golang)中也有支持,仍是值得程序员去了解和学习
   

总结:

  最后,简单总结一下,同步比较直观,编码和维护都比较容易,可是效率低,绝大多数时间都是不能忍的。异步效率高,对于callback这种形式逻辑容易被代码割裂,代码不直观,而异步协程的方式既看起来直观,又在效率上有保证。
 

referencesgolang

callbackhellweb

JavaScript异步编程的Promise模式
编程

gevent调度流程解析promise

tornado网络

相关文章
相关标签/搜索