Python中的多線程理解
我們將會看到一些在 Python 中使用線程的實例和如何避免線程之間的競爭。
你應當將下邊的例子運行多次,以便可以注意到線程是不可預測的和線程每次運行出的不同結果。聲明:從這里開始忘掉你聽到過的關于 GIL 的東西,因為 GIL 不會影響到我想要展示的東西。
示例1,我們將要請求五個不同的url:
1、單線程
import time import urllib2class GetUrlThread(Thread): def init(self, url): self.url = url super(GetUrlThread, self).init()
def run(self): resp = urllib2.urlopen(self.url) print self.url, resp.getcode()
define a global variable
some_var = 0
class IncrementThread(Thread): def run(self):
#we want to read a global variable #and then increment it global some_var read_value = some_var print "some_var in %s is %d" % (self.name, read_value) some_var = read_value + 1 print "some_var in %s after increment is %d" % (self.name, some_var)
def use_increment_thread(): threads = [] for i in range(50): t = IncrementThread() threads.append(t) t.start() for t in threads: t.join() print "After 50 modifications, some_var should have become 50" print "After 50 modifications, some_var is %d" % (some_var,)
use_increment_thread()</pre> 多次運行這個程序,你會看到多種不同的結果。
解釋:
有一個全局變量,所有的線程都想修改它。
所有的線程應該在這個全局變量上加 1 。
有50個線程,最后這個數值應該變成50,但是它卻沒有。
為什么沒有達到50?
在some_var是15的時候,線程t1讀取了some_var,這個時刻cpu將控制權給了另一個線程t2。
t2線程讀到的some_var也是15
t1和t2都把some_var加到16
當時我們期望的是t1 t2兩個線程使some_var + 2變成17
在這里就有了資源競爭。
相同的情況也可能發生在其它的線程間,所以出現了最后的結果小于50的情況。
2、解決資源競爭
from threading import Lock, Thread lock = Lock() some_var = 0class IncrementThread(Thread): def run(self):
#we want to read a global variable #and then increment it global some_var lock.acquire() read_value = some_var print "some_var in %s is %d" % (self.name, read_value) some_var = read_value + 1 print "some_var in %s after increment is %d" % (self.name, some_var) lock.release()
def use_increment_thread(): threads = [] for i in range(50): t = IncrementThread() threads.append(t) t.start() for t in threads: t.join() print "After 50 modifications, some_var should have become 50" print "After 50 modifications, some_var is %d" % (some_var,)
use_increment_thread()</pre> 再次運行這個程序,達到了我們預期的結果。
解釋:
Lock 用來防止競爭條件
如果在執行一些操作之前,線程t1獲得了鎖。其他的線程在t1釋放Lock之前,不會執行相同的操作
我們想要確定的是一旦線程t1已經讀取了some_var,直到t1完成了修改some_var,其他的線程才可以讀取some_var
這樣讀取和修改some_var成了邏輯上的原子操作。示例3,多線程環境下的原子操作
讓我們用一個例子來證明一個線程不能影響其他線程內的變量(非全局變量)。
time.sleep()可以使一個線程掛起,強制線程切換發生。
1、BUG 版
from threading import Thread import timeclass CreateListThread(Thread): def run(self): self.entries = [] for i in range(10): time.sleep(0.01) self.entries.append(i) print self.entries
def use_create_list_thread(): for i in range(3): t = CreateListThread() t.start()
use_create_list_thread()</pre>
運行幾次后發現并沒有打印出正確的結果。當一個線程正在打印的時候,cpu切換到了另一個線程,所以產生了不正確的結果。我們需要確保print self.entries是個邏輯上的原子操作,以防打印時被其他線程打斷。
2、加鎖保證操作的原子性
我們使用了Lock(),來看下邊的例子。
from threading import Thread, Lock import timelock = Lock()
class CreateListThread(Thread): def run(self): self.entries = [] for i in range(10): time.sleep(0.01) self.entries.append(i) lock.acquire() print self.entries lock.release()
def use_create_list_thread(): for i in range(3): t = CreateListThread() t.start()
use_create_list_thread()</pre>
這次我們看到了正確的結果。證明了一個線程不可以修改其他線程內部的變量(非全局變量)。
示例4,Python多線程簡易版:線程池 threadpool
上面的多線程代碼看起來有點繁瑣,下面我們用 treadpool 將案例 1 改寫下:
import threadpool import time import urllib2urls = [ '
def myRequest(url): resp = urllib2.urlopen(url) print url, resp.getcode()
def timeCost(request, n): print "Elapsed time: %s" % (time.time()-start)
start = time.time() pool = threadpool.ThreadPool(5) reqs = threadpool.makeRequests(myRequest, urls, timeCost) [ pool.putRequest(req) for req in reqs ] pool.wait()</pre> 解釋關鍵代碼:
- ThreadPool(poolsize)
表示最多可以創建poolsize這么多線程;
- makeRequests(some_callable, list_of_args, callback)
makeRequests創建了要開啟多線程的函數,以及函數相關參數和回調函數,其中回調函數可以不寫,default是無,也就是說makeRequests只需要2個參數就可以運行;
注意:threadpool 是非線程安全的。
5、REF:
http://agiliq.com/blog/2013/09/understanding-threads-in-python/
http://www.zhidaow.com/post/python-threadpool
6、推薦閱讀:
1、線程安全及Python中的GIL
http://www.cnblogs.com/mindsbook/archive/2009/10/15/thread-safety-and-GIL.html
2、Python 不能利用多核的問題以后能被解決嗎?
本文主要討論了 python 中的 單線程、多線程、多進程、異步、協程、多核、VM、GIL、GC、greenlet、Gevent、性能、IO 密集型、CPU 密集型、業務場景 等問題,以這些方面來判斷去除 GIL 實現多線程的優劣:
http://www.zhihu.com/question/21219976
注:協程可以認為是一種用戶態的線程,與系統提供的線程不同點是,它需要主動讓出CPU時間,而不是由系統進行調度,即控制權在程序員手上,用來執行協作式多任務非常合適。
3、GIL 與線程調度
——《Python源碼剖析--深度探索動態語言核心技術》第15章
大約在99年的時候,Greg Stein 和Mark Hammond 兩位老兄基于Python 1.5 創建了一份去除GIL 的branch,但是很不幸,這個分支在很多基準測試上,尤其是單線程操作的測試上,效率只有使用GIL 的Python 的一半左右。