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