写点什么

同步与异步,回调与协程

发布于: 2020 年 12 月 02 日

目录

  • 概念上下文:

  • 同步的方式:

  • 异步加回调的方式:

  • 异步协程方式:

  • 总结:

这里分享一个 协程原理到实现,全局分析丨协程的切换与调度视频点击查看:「链接」

正文



概念上下文:

回到顶部

  当提到同步与异步,大家不免会想到另一组词语:阻塞与非阻塞。通常,同时提到这个这几个词语一般实在讨论network io的时候,在《unix network programming》中有详尽的解释,网络中也有许多讲解生动的文章。

  本文所讨论的同步与异步,是指对于请求的发起者,是否需要等到请求的结果(同步),还是说请求完毕的时候以某种方式通知请求发起者(异步)。在这个语义环境下,阻塞与非阻塞,是指请求的受理者在处理某个请求的状态,如果在处理这个请求的时候不能做其它事情(请求处理时间不确定),那么称之为阻塞,否则为非阻塞。

  举个例子,我去柜台办理业务,那我是请求者,柜员时受理者。如果我在柜台一直等着柜员办理,直到办理完毕,那么对于我来说,就是同步的;如果我只是在柜员那里登记,然后到一边歇着,等柜员办理完毕之后告诉我结果,那么就是异步的。对于柜员,办理业务的时候可能需要等待打印机打印,如果在这个时候柜员去处理其他人的业务,那么就是非阻塞的,如果一定得到把我的业务办完再接待下一位顾客,那么就是阻塞的。



同步的方式:

回到顶部

  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.22000002861

注意:对代码运行的时间应该多次执行求平均值,这里只是简单作为参考。下同。



异步加回调的方式:

回到顶部

如果发出了请求就立即返回,这个时候程序可以做其他事情,等请求完成了时候通过某种方式告知结果,然后请求者再继续再来处理请求结果,那么我们称之为异步,最常见的就是回调(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()



运行结果:



  http_callback_way cost 0.759999990463

从代码可以看到,对请求的结果是放在一个额外的函数(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中可以用原声的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()



运行结果:



  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调度流程解析》。



总结:

回到顶部



看完的都是真爱,点个赞再走呗?您的「三连」就是我的最大动力!

  1. 关注公众号「Linux C后台服务器开发」,第一时间获取最新Linux干货!





  1. +qun720209036 获取精选一线大厂面试、自我提升、简历等资料。





点击并拖拽以移动

​​





点击并拖拽以移动



用户头像

Linux服务器开发qun720209036,欢迎来交流 2020.11.26 加入

专注C/C++ Linux后台服务器开发。

评论

发布
暂无评论
同步与异步,回调与协程