同步與異步,回調與協程

  本文主要介紹在網絡請求中的同步與異步,以及異步的表現形式: 回調與協程,並經過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網絡

相關文章
相關標籤/搜索