線程(thread)是進程(process)中的一個實體,一個進程至少包含一個線程。比如,對于視頻播放器,顯示視頻用一個線程,播放音頻用另一個線程。如果我們把進程看成一個容器,則線程是此容器的工作單位。
進程和線程的區(qū)別主要有:
在 Python 中,進行多線程編程的模塊有兩個:thread 和 threading。其中,thread 是低級模塊,threading 是高級模塊,對 thread 進行了封裝,一般來說,我們只需使用 threading 這個模塊。
下面,我們看一個簡單的例子:
from threading import Thread, current_thread
def thread_test(name):
print 'thread %s is running...' % current_thread().name
print 'hello', name
print 'thread %s ended.' % current_thread().name
if __name__ == "__main__":
print 'thread %s is running...' % current_thread().name
print 'hello world!'
t = Thread(target=thread_test, args=("test",), name="TestThread")
t.start()
t.join()
print 'thread %s ended.' % current_thread().name
可以看到,創(chuàng)建一個新的線程,就是把一個函數(shù)和函數(shù)參數(shù)傳給 Thread 實例,然后調(diào)用 start 方法開始執(zhí)行。代碼中的 current_thread 用于返回當前線程的實例。
執(zhí)行結(jié)果如下:
thread MainThread is running...
hello world!
thread TestThread is running...
hello test
thread TestThread ended.
thread MainThread ended.
由于同一個進程之間的線程是內(nèi)存共享的,所以當多個線程對同一個變量進行修改的時候,就會得到意想不到的結(jié)果。
讓我們先看一個簡單的例子:
from threading import Thread, current_thread
num = 0
def calc():
global num
print 'thread %s is running...' % current_thread().name
for _ in xrange(10000):
num += 1
print 'thread %s ended.' % current_thread().name
if __name__ == '__main__':
print 'thread %s is running...' % current_thread().name
threads = []
for i in range(5):
threads.append(Thread(target=calc))
threads[i].start()
for i in range(5):
threads[i].join()
print 'global num: %d' % num
print 'thread %s ended.' % current_thread().name
在上面的代碼中,我們創(chuàng)建了 5 個線程,每個線程對全局變量 num 進行 10000 次的 加 1 操作,這里之所以要循環(huán) 10000 次,是為了延長單個線程的執(zhí)行時間,使線程執(zhí)行時能出現(xiàn)中斷切換的情況。現(xiàn)在問題來了,當這 5 個線程執(zhí)行完畢時,全局變量的值是多少呢?是 50000 嗎?
讓我們看下執(zhí)行結(jié)果:
thread MainThread is running...
thread Thread-34 is running...
thread Thread-34 ended.
thread Thread-35 is running...
thread Thread-36 is running...
thread Thread-37 is running...
thread Thread-38 is running...
thread Thread-35 ended.
thread Thread-38 ended.
thread Thread-36 ended.
thread Thread-37 ended.
global num: 30668
thread MainThread ended.
我們發(fā)現(xiàn) num 的值是 30668,事實上,num 的值是不確定的,你再運行一遍,會發(fā)現(xiàn)結(jié)果變了。
原因是因為 num += 1
不是一個原子操作,也就是說它在執(zhí)行時被分成若干步:
由于線程是交替運行的,線程在執(zhí)行時可能中斷,就會導致其他線程讀到一個臟值。
為了保證計算的準確性,我們就需要給 num += 1
這個操作加上鎖
。當某個線程開始執(zhí)行這個操作時,由于該線程獲得了鎖,因此其他線程不能同時執(zhí)行該操作,只能等待,直到鎖被釋放,這樣就可以避免修改的沖突。創(chuàng)建一個鎖可以通過 threading.Lock()
來實現(xiàn),代碼如下:
from threading import Thread, current_thread, Lock
num = 0
lock = Lock()
def calc():
global num
print 'thread %s is running...' % current_thread().name
for _ in xrange(10000):
lock.acquire() # 獲取鎖
num += 1
lock.release() # 釋放鎖
print 'thread %s ended.' % current_thread().name
if __name__ == '__main__':
print 'thread %s is running...' % current_thread().name
threads = []
for i in range(5):
threads.append(Thread(target=calc))
threads[i].start()
for i in range(5):
threads[i].join()
print 'global num: %d' % num
print 'thread %s ended.' % current_thread().name
讓我們看下執(zhí)行結(jié)果:
thread MainThread is running...
thread Thread-44 is running...
thread Thread-45 is running...
thread Thread-46 is running...
thread Thread-47 is running...
thread Thread-48 is running...
thread Thread-45 ended.
thread Thread-47 ended.
thread Thread-48 ended.
thread Thread-46 ended.
thread Thread-44 ended.
global num: 50000
thread MainThread ended.
講到 Python 中的多線程,就不得不面對 GIL
鎖,GIL
鎖的存在導致 Python 不能有效地使用多線程實現(xiàn)多核任務,因為在同一時間,只能有一個線程在運行。
GIL
全稱是 Global Interpreter Lock,譯為全局解釋鎖。早期的 Python 為了支持多線程,引入了 GIL 鎖,用于解決多線程之間數(shù)據(jù)共享和同步的問題。但這種實現(xiàn)方式后來被發(fā)現(xiàn)是非常低效的,當大家試圖去除 GIL 的時候,卻發(fā)現(xiàn)大量庫代碼已重度依賴 GIL,由于各種各樣的歷史原因,GIL 鎖就一直保留到現(xiàn)在。