1、線程和進(jìn)程
計(jì)算機(jī)的核心是CPU,它承擔(dān)了所有的計(jì)算任務(wù)。它就像一座工廠,時(shí)刻在運(yùn)行。
假定工廠的電力有限,一次只能供給一個(gè)車間使用。也就是說,一個(gè)車間開工的時(shí)候,其他車間都必須停工。背后的含義就是,單個(gè)CPU一次只能運(yùn)行一個(gè)任務(wù)。
進(jìn)程就好比工廠的車間,它代表CPU所能處理的單個(gè)任務(wù)。任一時(shí)刻,CPU總是運(yùn)行一個(gè)進(jìn)程,其他進(jìn)程處于非運(yùn)行狀態(tài)。
一個(gè)車間里,可以有很多工人。他們協(xié)同完成一個(gè)任務(wù)。
線程就好比車間里的工人。一個(gè)進(jìn)程可以包括多個(gè)線程。
車間的空間是工人們共享的,比如許多房間是每個(gè)工人都可以進(jìn)出的。這象征一個(gè)進(jìn)程的內(nèi)存空間是共享的,每個(gè)線程都可以使用這些共享內(nèi)存。
可是,每間房間的大小不同,有些房間最多只能容納一個(gè)人,比如廁所。里面有人的時(shí)候,其他人就不能進(jìn)去了。這代表一個(gè)線程使用某些共享內(nèi)存時(shí),其他線程必須等它結(jié)束,才能使用這一塊內(nèi)存。
一個(gè)防止他人進(jìn)入的簡(jiǎn)單方法,就是門口加一把鎖。先到的人鎖上門,后到的人看到上鎖,就在門口排隊(duì),等鎖打開再進(jìn)去。這就叫"互斥鎖"(Mutual exclusion,縮寫 Mutex),防止多個(gè)線程同時(shí)讀寫某一塊內(nèi)存區(qū)域。
還有些房間,可以同時(shí)容納n個(gè)人,比如廚房。也就是說,如果人數(shù)大于n,多出來的人只能在外面等著。這好比某些內(nèi)存區(qū)域,只能供給固定數(shù)目的線程使用。
這時(shí)的解決方法,就是在門口掛n把鑰匙。進(jìn)去的人就取一把鑰匙,出來時(shí)再把鑰匙掛回原處。后到的人發(fā)現(xiàn)鑰匙架空了,就知道必須在門口排隊(duì)等著了。這種做法叫做"信號(hào)量"(Semaphore),用來保證多個(gè)線程不會(huì)互相沖突。
不難看出,mutex是semaphore的一種特殊情況(n=1時(shí))。也就是說,完全可以用后者替代前者。但是,因?yàn)閙utex較為簡(jiǎn)單,且效率高,所以在必須保證資源獨(dú)占的情況下,還是采用這種設(shè)計(jì)。
2、多線程與多進(jìn)程
從上面關(guān)于線程和進(jìn)程的的通俗解釋來看,多線程和多進(jìn)程的含義如下:
多進(jìn)程:允許多個(gè)任務(wù)同時(shí)進(jìn)行
多線程:允許單個(gè)任務(wù)分成不同的部分運(yùn)行
3、Python多線程編程
3.1 單線程
在好些年前的MS-DOS時(shí)代,操作系統(tǒng)處理問題都是單任務(wù)的,我想做聽音樂和看電影兩件事兒,那么一定要先排一下順序。
# coding=UTF-8
from time import ctime,sleep
def music():
for i in range(2):
print 'I was listening to music. %s' %ctime()
sleep(1)
def movie():
for i in range(2):
print 'I was at the movies! %s' %ctime()
sleep(5)
if __name__ == '__main__':
music()
movie()
print 'all over %s' %ctime()
我們先聽了一首音樂,通過for循環(huán)來控制音樂的播放了兩次,每首音樂播放需要1秒鐘,sleep()來控制音樂播放的時(shí)長(zhǎng)。接著我們又看了一場(chǎng)電影,每一場(chǎng)電影需要5秒鐘,因?yàn)樘每戳耍晕乙餐ㄟ^for循環(huán)看兩遍。在整個(gè)休閑娛樂活動(dòng)結(jié)束后,我通過print "all over %s" %ctime() 看了一下當(dāng)前時(shí)間,差不多該睡覺了。
運(yùn)行結(jié)果:
I was listening to music. Mon Aug 21 15:32:18 2017
I was listening to music. Mon Aug 21 15:32:19 2017
I was at the movies! Mon Aug 21 15:32:20 2017
I was at the movies! Mon Aug 21 15:32:25 2017
all over Mon Aug 21 15:32:30 2017
其實(shí),music()和movie()更應(yīng)該被看作是音樂和視頻播放器,至于要播放什么歌曲和視頻應(yīng)該由我們使用時(shí)決定。所以,我們對(duì)上面代碼做了改造:
# coding=UTF-8
import threading
from time import ctime,sleep
def music(func):
for i in range(2):
print ("I was listening to %s. %s" %(func,ctime()))
sleep(1)
def movie(func):
for i in range(2):
print ("I was at the %s ! %s" %(func,ctime()))
sleep(5)
if __name__ == '__main__':
music(u'愛情買賣')
movie(u'阿凡達(dá)')
print("all over %s" %ctime())
運(yùn)行結(jié)果:
I was listening to 愛情買賣. Mon Aug 21 15:38:55 2017
I was listening to 愛情買賣. Mon Aug 21 15:38:56 2017
I was at the 阿凡達(dá) ! Mon Aug 21 15:38:57 2017
I was at the 阿凡達(dá) ! Mon Aug 21 15:39:02 2017
all over Mon Aug 21 15:39:07 2017
3.2 多線程
Python3 通過兩個(gè)標(biāo)準(zhǔn)庫(kù) _thread (python2中是thread模塊)和 threading 提供對(duì)線程的支持。
_thread 提供了低級(jí)別的、原始的線程以及一個(gè)簡(jiǎn)單的鎖,它相比于 threading 模塊的功能還是比較有限的。
3.2.1使用_thread模塊
調(diào)用_thread模塊中的start_new_thread()函數(shù)來產(chǎn)生新線程。
先用一個(gè)實(shí)例感受一下:
# coding=UTF-8
import thread
import time
# 為線程定義一個(gè)函數(shù)
def print_time(threadName, delay):
count = 0
while count < 5:
time.sleep(delay)
count += 1
print("%s: %s" % (threadName, time.ctime(time.time())))
# 創(chuàng)建兩個(gè)線程
try:
thread.start_new_thread(print_time, ("Thread-1", 2,))
thread.start_new_thread(print_time, ("Thread-2", 4,))
except:
print ("Error: unable to start thread")
while 1:
pass
print ("Main Finished")
代碼輸出為:
Thread-1: Mon Aug 21 15:49:00 2017
Thread-2: Mon Aug 21 15:49:02 2017
Thread-1: Mon Aug 21 15:49:02 2017
Thread-1: Mon Aug 21 15:49:04 2017
Thread-2: Mon Aug 21 15:49:06 2017
Thread-1: Mon Aug 21 15:49:06 2017
Thread-1: Mon Aug 21 15:49:08 2017
Thread-2: Mon Aug 21 15:49:10 2017
Thread-2: Mon Aug 21 15:49:14 2017
Thread-2: Mon Aug 21 15:49:18 2017
注意到,在主線程寫了:
while 1:
pass
這是讓主線程一直在等待.
如果去掉上面兩行,那就直接輸出并結(jié)束程序執(zhí)行:
"Main Finished"
3.2.2使用threading模塊
threading 模塊除了包含 thread 模塊中的所有方法外,還提供的其他方法:
threading.currentThread(): 返回當(dāng)前的線程變量。
threading.enumerate(): 返回一個(gè)包含正在運(yùn)行的線程的list。正在運(yùn)行指線程啟動(dòng)后、結(jié)束前,不包括啟動(dòng)前和終止后的線程。
threading.activeCount(): 返回正在運(yùn)行的線程數(shù)量,與len(threading.enumerate())有相同的結(jié)果。
除了使用方法外,線程模塊同樣提供了Thread類來處理線程,Thread類提供了以下方法:
run(): 用以表示線程活動(dòng)的方法。
start():啟動(dòng)線程活動(dòng)。
join([time]): 等待至線程中止。這阻塞調(diào)用線程直至線程的join() 方法被調(diào)用中止-正常退出或者拋出未處理的異常-或者是可選的超時(shí)發(fā)生。
isAlive(): 返回線程是否活動(dòng)的。
getName(): 返回線程名。
setName(): 設(shè)置線程名。
直接創(chuàng)建線程
接上面的聽音樂和看電影的例子,我們可以直接使用threading.Thread 創(chuàng)建線程,并指定執(zhí)行的方法以及傳遞的參數(shù):
# coding=UTF-8
import threading
from time import ctime, sleep
def music(func):
for i in range(2):
print ("I was listening to %s. %s" %(func,ctime()))
sleep(1)
def movie(func):
for i in range(2):
print ("I was at the %s! %s" %(func,ctime()))
sleep(5)
threads = []
t1 = threading.Thread(target=music,args=(u'愛情買賣',))
threads.append(t1)
t2 = threading.Thread(target=movie,args=(u'阿凡達(dá)',))
threads.append(t2)
if __name__ == '__main__':
for t in threads:
t.start()
print ("all over %s" % ctime())
結(jié)果輸出為:
I was listening to 愛情買賣. Tue Aug 22 10:41:33 2017
all over Tue Aug 22 10:41:33 2017
I was at the 阿凡達(dá)! Tue Aug 22 10:41:33 2017
I was listening to 愛情買賣. Tue Aug 22 10:41:34 2017
I was at the 阿凡達(dá)! Tue Aug 22 10:41:38 2017
構(gòu)造線程類
我們也可以通過直接從 threading.Thread 繼承創(chuàng)建一個(gè)新的子類,并實(shí)例化后調(diào)用 start() 方法啟動(dòng)新線程,即它調(diào)用了線程的 run() 方法:
# coding=UTF-8
import threading
import time
exitFlag = 0
class myThread(threading.Thread):
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self):
print ("開始線程:" + self.name)
print_time(self.name, self.counter, 5)
print ("結(jié)束線程:" + self.name)
def print_time(threadName, delay, counter):
while counter:
if exitFlag:
threadName.exit()
time.sleep(delay)
print ("%s: %s" % (threadName, time.ctime(time.time())))
counter -= 1
# 創(chuàng)建新線程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
# 開始新線程
thread1.start()
thread2.start()
print ("退出主線程")
結(jié)果輸出為:
開始線程:Thread-1
開始線程:Thread-2
退出主線程
Thread-1: Tue Aug 22 10:52:55 2017
Thread-2: Tue Aug 22 10:52:56 2017
Thread-1: Tue Aug 22 10:52:56 2017
Thread-1: Tue Aug 22 10:52:57 2017
Thread-2: Tue Aug 22 10:52:58 2017
Thread-1: Tue Aug 22 10:52:58 2017
Thread-1: Tue Aug 22 10:52:59 2017
結(jié)束線程:Thread-1
Thread-2: Tue Aug 22 10:53:00 2017
Thread-2: Tue Aug 22 10:53:02 2017
Thread-2: Tue Aug 22 10:53:04 2017
結(jié)束線程:Thread-2
從結(jié)果可以看到,為什么我們開啟了兩個(gè)線程之后,主線程立即退出了?因?yàn)槲覀儧]有使用join方法,對(duì)于主線程來說,thread1和thread2是子線程,使用join方法,會(huì)讓主線程等待子線程執(zhí)行解說再繼續(xù)執(zhí)行。
join()方法
我們修改一下代碼:
# coding=UTF-8
import threading
import time
exitFlag = 0
class myThread(threading.Thread):
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self):
print ("開始線程:" + self.name)
print_time(self.name, self.counter, 5)
print ("退出線程:" + self.name)
def print_time(threadName, delay, counter):
while counter:
if exitFlag:
threadName.exit()
time.sleep(delay)
print ("%s: %s" % (threadName, time.ctime(time.time())))
counter -= 1
# 創(chuàng)建新線程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
# 開啟新線程
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print("退出主線程")
結(jié)果就變?yōu)椋?/p>
開始線程:Thread-1
開始線程:Thread-2
Thread-1: Tue Aug 22 11:45:53 2017
Thread-2: Tue Aug 22 11:45:54 2017
Thread-1: Tue Aug 22 11:45:54 2017
Thread-1: Tue Aug 22 11:45:55 2017
Thread-2: Tue Aug 22 11:45:56 2017
Thread-1: Tue Aug 22 11:45:56 2017
Thread-1: Tue Aug 22 11:45:57 2017
退出線程:Thread-1
Thread-2: Tue Aug 22 11:45:58 2017
Thread-2: Tue Aug 22 11:46:00 2017
Thread-2: Tue Aug 22 11:46:02 2017
退出線程:Thread-2
退出主線程
可以看到 退出主線程 在最后才被打印出來。
setDaemon()方法
有一個(gè)方法常常拿來與join方法做比較,那就是setDaemon()方法。我們首先來看一下setDaemon()方法的使用效果:
# coding=UTF-8
import threading
import time
exitFlag = 0
class myThread(threading.Thread):
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self):
print ("開始線程:" + self.name)
print_time(self.name, self.counter, 5)
print ("退出線程:" + self.name)
def print_time(threadName, delay, counter):
while counter:
if exitFlag:
threadName.exit()
time.sleep(delay)
print ("%s: %s" % (threadName, time.ctime(time.time())))
counter -= 1
# 創(chuàng)建新線程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
# 開啟新線程
thread1.setDaemon(True)
thread2.setDaemon(True)
thread1.start()
thread2.start()
print ("退出主線程")
結(jié)果輸出為:
開始線程:Thread-1
開始線程:Thread-2
退出主線程
可以看到,在主線程結(jié)束之后,程序就終止了,也就是說兩個(gè)子線程也被終止了,這就是setDaemon方法的作用。主線程A中,創(chuàng)建了子線程B,并且在主線程A中調(diào)用了B.setDaemon(),這個(gè)的意思是,把主線程A設(shè)置為守護(hù)線程,這時(shí)候,要是主線程A執(zhí)行結(jié)束了,就不管子線程B是否完成,一并和主線程A退出.這就是setDaemon方法的含義,這基本和join是相反的。此外,還有個(gè)要特別注意的:必須在start() 方法調(diào)用之前設(shè)置,如果不設(shè)置為守護(hù)線程,程序會(huì)被無限掛起。
兩個(gè)疑問
我們剛才介紹了兩種使用多線程的方式,一種是直接調(diào)用threading.Thread 創(chuàng)建線程,另一種是從 threading.Thread 繼承創(chuàng)建一個(gè)新的子類,并實(shí)例化后調(diào)用 start() 方法啟動(dòng)進(jìn)程。學(xué)到這里,我就拋出了兩個(gè)疑問,為什么第一種方法中我們可以為不同的線程指定運(yùn)行的方法,而第二種我們都運(yùn)行的是同一個(gè)方法,那么它內(nèi)部的實(shí)現(xiàn)機(jī)制是什么呢?第二個(gè)疑問是,第二種方法中,我們沒有實(shí)例化start()方法,那么run和start這兩個(gè)方法的聯(lián)系是什么呢?
首先,start方法和run方法的關(guān)系如下:用start方法來啟動(dòng)線程,真正實(shí)現(xiàn)了多線程運(yùn)行,這時(shí)無需等待run方法體代碼執(zhí)行完畢而直接繼續(xù)執(zhí)行下面的代碼。通過調(diào)用Thread類的start()方法來啟動(dòng)一個(gè)線程,這時(shí)此線程處于就緒(可運(yùn)行)狀態(tài),并沒有運(yùn)行,一旦得到cpu時(shí)間片,就開始執(zhí)行run()方法,這里方法 run()稱為線程體,它包含了要執(zhí)行的這個(gè)線程的內(nèi)容,Run方法運(yùn)行結(jié)束,此線程隨即終止。
而run()方法的源碼如下,可以看到,如果我們指定了target即線程執(zhí)行的函數(shù)的話,run方法可以轉(zhuǎn)而調(diào)用那個(gè)函數(shù),如果沒有的話,將不執(zhí)行,而我們?cè)谧远x的Thread類里面重寫了這個(gè)run 方法,所以程序會(huì)執(zhí)行這一段。
def run(self):
"""Method representing the thread's activity.
You may override this method in a subclass. The standard run() method
invokes the callable object passed to the object's constructor as the
target argument, if any, with sequential and keyword arguments taken
from the args and kwargs arguments, respectively.
"""
try:
if self._target:
self._target(*self._args, **self._kwargs)
finally:
# Avoid a refcycle if the thread is running a function with
# an argument that has a member that points to the thread.
del self._target, self._args, self._kwargs
線程同步
如果多個(gè)線程共同對(duì)某個(gè)數(shù)據(jù)修改,則可能出現(xiàn)不可預(yù)料的結(jié)果,為了保證數(shù)據(jù)的正確性,需要對(duì)多個(gè)線程進(jìn)行同步。
使用 Thread 對(duì)象的 Lock 和 Rlock 可以實(shí)現(xiàn)簡(jiǎn)單的線程同步,這兩個(gè)對(duì)象都有 acquire 方法和 release 方法,對(duì)于那些需要每次只允許一個(gè)線程操作的數(shù)據(jù),可以將其操作放到 acquire 和 release 方法之間。如下:
多線程的優(yōu)勢(shì)在于可以同時(shí)運(yùn)行多個(gè)任務(wù)(至少感覺起來是這樣)。但是當(dāng)線程需要共享數(shù)據(jù)時(shí),可能存在數(shù)據(jù)不同步的問題。
考慮這樣一種情況:一個(gè)列表里所有元素都是0,線程"set"從后向前把所有元素改成1,而線程"print"負(fù)責(zé)從前往后讀取列表并打印。
那么,可能線程"set"開始改的時(shí)候,線程"print"便來打印列表了,輸出就成了一半0一半1,這就是數(shù)據(jù)的不同步。為了避免這種情況,引入了鎖的概念。
鎖有兩種狀態(tài)——鎖定和未鎖定。每當(dāng)一個(gè)線程比如"set"要訪問共享數(shù)據(jù)時(shí),必須先獲得鎖定;如果已經(jīng)有別的線程比如"print"獲得鎖定了,那么就讓線程"set"暫停,也就是同步阻塞;等到線程"print"訪問完畢,釋放鎖以后,再讓線程"set"繼續(xù)。
經(jīng)過這樣的處理,打印列表時(shí)要么全部輸出0,要么全部輸出1,不會(huì)再出現(xiàn)一半0一半1的尷尬場(chǎng)面。
實(shí)例:
# coding=UTF-8
import threading
import time
class myThread(threading.Thread):
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self):
print ("開啟線程:" + self.name)
# 獲取鎖,用于線程同步
threadLock.acquire()
print_time(self.name, self.counter, 3)
# 釋放鎖,開啟下一個(gè)線程
threadLock.release()
def print_time(threadName, delay, counter):
while counter:
time.sleep(delay)
print ("%s: %s" % (threadName, time.ctime(time.time())))
counter -= 1
threadLock = threading.Lock()
threads = []
# 創(chuàng)建新進(jìn)程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
# 開啟新進(jìn)程
thread1.start()
thread2.start()
# 添加線程到線程列表
threads.append(thread1)
threads.append(thread2)
# 等待所有線程完成
for t in threads:
t.join()
print("退出主線程")
輸出為:
開啟線程:Thread-1
開啟線程:Thread-2
Thread-1: Tue Aug 22 15:08:20 2017
Thread-1: Tue Aug 22 15:08:21 2017
Thread-1: Tue Aug 22 15:08:22 2017
Thread-2: Tue Aug 22 15:08:24 2017
Thread-2: Tue Aug 22 15:08:26 2017
Thread-2: Tue Aug 22 15:08:28 2017
退出主線程
線程優(yōu)先級(jí)隊(duì)列( Queue)
Python 的 Queue 模塊中提供了同步的、線程安全的隊(duì)列類,包括FIFO(先入先出)隊(duì)列Queue,LIFO(后入先出)隊(duì)列LifoQueue,和優(yōu)先級(jí)隊(duì)列 PriorityQueue。
這些隊(duì)列都實(shí)現(xiàn)了鎖原語,能夠在多線程中直接使用,可以使用隊(duì)列來實(shí)現(xiàn)線程間的同步。
Queue 模塊中的常用方法:
Queue.qsize() 返回隊(duì)列的大小
Queue.empty() 如果隊(duì)列為空,返回True,反之False
Queue.full() 如果隊(duì)列滿了,返回True,反之False
Queue.full 與 maxsize 大小對(duì)應(yīng)
Queue.get([block[, timeout]])獲取隊(duì)列,timeout等待時(shí)間
Queue.get_nowait() 相當(dāng)Queue.get(False)
Queue.put(item) 寫入隊(duì)列,timeout等待時(shí)間
Queue.put_nowait(item) 相當(dāng)Queue.put(item, False)
Queue.task_done() 在完成一項(xiàng)工作之后,Queue.task_done()函數(shù)向任務(wù)已經(jīng)完成的隊(duì)列發(fā)送一個(gè)信號(hào)
Queue.join() 實(shí)際上意味著等到隊(duì)列為空,再執(zhí)行別的操作
# coding=UTF-8
import threading
import time
import Queue
exitFlag = 0
class myThread(threading.Thread):
def __init__(self, threadID, name, q):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.q = q
def run(self):
print("開啟線程:" + self.name)
process_data(self.name, self.q)
print("退出線程:" + self.name)
def process_data(threadName, q):
while not exitFlag:
queueLock.acquire()
if not workQueue.empty():
data = q.get()
queueLock.release()
print ("%s processing %s" % (threadName, data))
else:
queueLock.release()
time.sleep(1)
threadList = ["Thread-1", "Thread-2", "Thread-3"]
nameList = ["One", "Two", "Three", "Four", "Five"]
queueLock = threading.Lock()
workQueue = Queue.Queue(10)
threads = []
threadID = 1
# 創(chuàng)建新線程
for tName in threadList:
thread = myThread(threadID, tName, workQueue)
thread.start()
threads.append(thread)
threadID += 1
# 填充隊(duì)列
queueLock.acquire()
for word in nameList:
workQueue.put(word)
queueLock.release()
# 等待隊(duì)列為空
while not workQueue.empty():
pass
# 通知線程是否退出
exitFlag = 1
# 等待所有線程完成
for t in threads:
t.join()
print("退出主線程")
上面的代碼每次執(zhí)行的結(jié)果是不一樣的,取決于哪個(gè)進(jìn)程先獲得鎖,一次運(yùn)行的輸出如下:
開啟線程:Thread-1
開啟線程:Thread-2
開啟線程:Thread-3
Thread-2 processing One
Thread-1 processing Two
Thread-3 processing Three
Thread-2 processing Four
Thread-1 processing Five
退出線程:Thread-3
退出線程:Thread-2
退出線程:Thread-1
退出主線程
參考文章
進(jìn)程與線程的一個(gè)簡(jiǎn)單解釋:
http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html
多線程就這么簡(jiǎn)單:
http://www.cnblogs.com/fnng/p/3670789.html