1拌夏、線程和進程
計算機的核心是CPU,它承擔(dān)了所有的計算任務(wù)履因。它就像一座工廠障簿,時刻在運行。
假定工廠的電力有限栅迄,一次只能供給一個車間使用站故。也就是說,一個車間開工的時候毅舆,其他車間都必須停工西篓。背后的含義就是,單個CPU一次只能運行一個任務(wù)憋活。
進程就好比工廠的車間岂津,它代表CPU所能處理的單個任務(wù)。任一時刻悦即,CPU總是運行一個進程吮成,其他進程處于非運行狀態(tài)。
一個車間里辜梳,可以有很多工人粱甫。他們協(xié)同完成一個任務(wù)。
線程就好比車間里的工人作瞄。一個進程可以包括多個線程魔种。
車間的空間是工人們共享的,比如許多房間是每個工人都可以進出的粉洼。這象征一個進程的內(nèi)存空間是共享的节预,每個線程都可以使用這些共享內(nèi)存叶摄。
可是,每間房間的大小不同安拟,有些房間最多只能容納一個人蛤吓,比如廁所。里面有人的時候糠赦,其他人就不能進去了会傲。這代表一個線程使用某些共享內(nèi)存時,其他線程必須等它結(jié)束拙泽,才能使用這一塊內(nèi)存淌山。
一個防止他人進入的簡單方法,就是門口加一把鎖顾瞻。先到的人鎖上門泼疑,后到的人看到上鎖,就在門口排隊荷荤,等鎖打開再進去退渗。這就叫"互斥鎖"(Mutual exclusion,縮寫 Mutex)蕴纳,防止多個線程同時讀寫某一塊內(nèi)存區(qū)域会油。
還有些房間,可以同時容納n個人古毛,比如廚房翻翩。也就是說,如果人數(shù)大于n稻薇,多出來的人只能在外面等著体斩。這好比某些內(nèi)存區(qū)域,只能供給固定數(shù)目的線程使用颖低。
這時的解決方法,就是在門口掛n把鑰匙弧烤。進去的人就取一把鑰匙忱屑,出來時再把鑰匙掛回原處。后到的人發(fā)現(xiàn)鑰匙架空了暇昂,就知道必須在門口排隊等著了莺戒。這種做法叫做"信號量"(Semaphore),用來保證多個線程不會互相沖突急波。
不難看出从铲,mutex是semaphore的一種特殊情況(n=1時)。也就是說澄暮,完全可以用后者替代前者名段。但是,因為mutex較為簡單,且效率高咕痛,所以在必須保證資源獨占的情況下梅惯,還是采用這種設(shè)計。
2信夫、多線程與多進程
從上面關(guān)于線程和進程的的通俗解釋來看窃蹋,多線程和多進程的含義如下:
多進程:允許多個任務(wù)同時進行
多線程:允許單個任務(wù)分成不同的部分運行
3、Python多線程編程
3.1 單線程
在好些年前的MS-DOS時代静稻,操作系統(tǒng)處理問題都是單任務(wù)的警没,我想做聽音樂和看電影兩件事兒,那么一定要先排一下順序振湾。
from time import ctime,sleep
def music():
for i in range(2):
print "I was listening to music. %s" %ctime()
sleep(1)
def move():
for i in range(2):
print "I was at the movies! %s" %ctime()
sleep(5)
if __name__ == '__main__':
music()
move()
print "all over %s" %ctime()
我們先聽了一首音樂杀迹,通過for循環(huán)來控制音樂的播放了兩次,每首音樂播放需要1秒鐘恰梢,sleep()來控制音樂播放的時長佛南。接著我們又看了一場電影,
每一場電影需要5秒鐘嵌言,因為太好看了嗅回,所以我也通過for循環(huán)看兩遍。在整個休閑娛樂活動結(jié)束后摧茴,我通過
print "all over %s" %ctime()
看了一下當(dāng)前時間绵载,差不多該睡覺了。
運行結(jié)果:
I was listening to music. Thu Apr 17 10:47:08 2014
I was listening to music. Thu Apr 17 10:47:09 2014
I was at the movies! Thu Apr 17 10:47:10 2014
I was at the movies! Thu Apr 17 10:47:15 2014
all over Thu Apr 17 10:47:20 2014
其實苛白,music()和move()更應(yīng)該被看作是音樂和視頻播放器娃豹,至于要播放什么歌曲和視頻應(yīng)該由我們使用時決定。所以购裙,我們對上面代碼做了改造:
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 move(func):
for i in range(2):
print ("I was at the %s! %s" %(func,ctime()))
sleep(5)
if __name__ == '__main__':
music(u'愛情買賣')
move(u'阿凡達')
print ("all over %s" %ctime())
運行結(jié)果:
I was listening to 愛情買賣. Thu Apr 17 11:48:59 2014
I was listening to 愛情買賣. Thu Apr 17 11:49:00 2014
I was at the 阿凡達! Thu Apr 17 11:49:01 2014
I was at the 阿凡達! Thu Apr 17 11:49:06 2014
all over Thu Apr 17 11:49:11 2014
3.2 多線程
Python3 通過兩個標(biāo)準庫 _thread (python2中是thread模塊)和 threading 提供對線程的支持懂版。
_thread 提供了低級別的、原始的線程以及一個簡單的鎖躏率,它相比于 threading 模塊的功能還是比較有限的躯畴。
3.2.1使用_thread模塊
調(diào)用_thread模塊中的start_new_thread()函數(shù)來產(chǎn)生新線程。
先用一個實例感受一下:
import _thread
import time
# 為線程定義一個函數(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)建兩個線程
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: Thu Aug 10 16:35:47 2017
Thread-2: Thu Aug 10 16:35:49 2017
Thread-1: Thu Aug 10 16:35:49 2017
Thread-1: Thu Aug 10 16:35:51 2017
Thread-2: Thu Aug 10 16:35:53 2017
Thread-1: Thu Aug 10 16:35:53 2017
Thread-1: Thu Aug 10 16:35:55 2017
Thread-2: Thu Aug 10 16:35:57 2017
Thread-2: Thu Aug 10 16:36:01 2017
注意到薇芝,在主線程寫了:
while 1:
pass
這是讓主線程一直在等待.
如果去掉上面兩行蓬抄,那就直接輸出并結(jié)束程序執(zhí)行:
"Main Finished"
3.2.2使用threading模塊
threading 模塊除了包含 _thread 模塊中的所有方法外,還提供的其他方法:
threading.currentThread(): 返回當(dāng)前的線程變量夯到。
threading.enumerate(): 返回一個包含正在運行的線程的list嚷缭。正在運行指線程啟動后、結(jié)束前,不包括啟動前和終止后的線程阅爽。
threading.activeCount(): 返回正在運行的線程數(shù)量路幸,與len(threading.enumerate())有相同的結(jié)果。
除了使用方法外优床,線程模塊同樣提供了Thread類來處理線程劝赔,Thread類提供了以下方法:
run(): 用以表示線程活動的方法。
start():啟動線程活動胆敞。
join([time]): 等待至線程中止着帽。這阻塞調(diào)用線程直至線程的join() 方法被調(diào)用中止-正常退出或者拋出未處理的異常-或者是可選的超時發(fā)生。
isAlive(): 返回線程是否活動的移层。
getName(): 返回線程名仍翰。
setName(): 設(shè)置線程名。
直接創(chuàng)建線程
接上面的聽音樂和看電影的例子观话,我們可以直接使用threading.Thread 創(chuàng)建線程予借,并指定執(zhí)行的方法以及傳遞的參數(shù):
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 move(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=move,args=(u'阿凡達',))
threads.append(t2)
if __name__ == '__main__':
for t in threads:
t.start()
print ("all over %s" %ctime())
結(jié)果輸出為:
I was listening to 愛情買賣. Thu Aug 10 16:57:12 2017
I was at the 阿凡達! Thu Aug 10 16:57:12 2017
all over Thu Aug 10 16:57:12 2017
I was listening to 愛情買賣. Thu Aug 10 16:57:13 2017
I was at the 阿凡達! Thu Aug 10 16:57:17 2017
構(gòu)造線程類
我們也可以通過直接從 threading.Thread 繼承創(chuàng)建一個新的子類,并實例化后調(diào)用 start() 方法啟動新線程频蛔,即它調(diào)用了線程的 run() 方法:
#!/usr/bin/python3
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()
print ("退出主線程")
結(jié)果輸出為:
開始線程:Thread-1
開始線程:Thread-2
退出主線程
Thread-1: Thu Aug 10 16:48:41 2017
Thread-2: Thu Aug 10 16:48:42 2017
Thread-1: Thu Aug 10 16:48:42 2017
Thread-1: Thu Aug 10 16:48:43 2017
Thread-2: Thu Aug 10 16:48:44 2017
Thread-1: Thu Aug 10 16:48:44 2017
Thread-1: Thu Aug 10 16:48:45 2017
退出線程:Thread-1
Thread-2: Thu Aug 10 16:48:46 2017
Thread-2: Thu Aug 10 16:48:48 2017
Thread-2: Thu Aug 10 16:48:50 2017
退出線程:Thread-2
從結(jié)果可以看到灵迫,為什么我們開啟了兩個線程之后,主線程立即退出了晦溪?因為我們沒有使用join方法瀑粥,對于主線程來說,thread1和thread2是子線程三圆,使用join方法狞换,會讓主線程等待子線程執(zhí)行解說再繼續(xù)執(zhí)行。
join()方法
我們修改一下代碼:
#!/usr/bin/python3
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: Thu Aug 10 16:52:07 2017
Thread-2: Thu Aug 10 16:52:08 2017
Thread-1: Thu Aug 10 16:52:08 2017
Thread-1: Thu Aug 10 16:52:09 2017
Thread-2: Thu Aug 10 16:52:10 2017
Thread-1: Thu Aug 10 16:52:10 2017
Thread-1: Thu Aug 10 16:52:11 2017
退出線程:Thread-1
Thread-2: Thu Aug 10 16:52:12 2017
Thread-2: Thu Aug 10 16:52:14 2017
Thread-2: Thu Aug 10 16:52:16 2017
退出線程:Thread-2
退出主線程
可以看到 退出主線程 在最后才被打印出來舟肉。
setDaemon()方法
有一個方法常常拿來與join方法做比較修噪,那就是setDaemon()方法。我們首先來看一下setDaemon()方法的使用效果:
#!/usr/bin/python3
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é)束之后黄琼,程序就終止了,也就是說兩個子線程也被終止了整慎,這就是setDaemon方法的作用脏款。主線程A中,創(chuàng)建了子線程B院领,并且在主線程A中調(diào)用了B.setDaemon(),這個的意思是,把主線程A設(shè)置為守護線程够吩,這時候比然,要是主線程A執(zhí)行結(jié)束了,就不管子線程B是否完成,一并和主線程A退出.這就是setDaemon方法的含義周循,這基本和join是相反的强法。此外万俗,還有個要特別注意的:必須在start() 方法調(diào)用之前設(shè)置,如果不設(shè)置為守護線程饮怯,程序會被無限掛起闰歪。
兩個疑問
我們剛才介紹了兩種使用多線程的方式,一種是直接調(diào)用threading.Thread 創(chuàng)建線程蓖墅,另一種是從 threading.Thread 繼承創(chuàng)建一個新的子類库倘,并實例化后調(diào)用 start() 方法啟動進程。學(xué)到這里论矾,我就拋出了兩個疑問教翩,為什么第一種方法中我們可以為不同的線程指定運行的方法,而第二種我們都運行的是同一個方法贪壳,那么它內(nèi)部的實現(xiàn)機制是什么呢饱亿?第二個疑問是,第二種方法中闰靴,我們沒有實例化start()方法彪笼,那么run和start這兩個方法的聯(lián)系是什么呢?
首先蚂且,start方法和run方法的關(guān)系如下:用start方法來啟動線程配猫,真正實現(xiàn)了多線程運行,這時無需等待run方法體代碼執(zhí)行完畢而直接繼續(xù)執(zhí)行下面的代碼膘掰。通過調(diào)用Thread類的start()方法來啟動一個線程章姓,這時此線程處于就緒(可運行)狀態(tài),并沒有運行识埋,一旦得到cpu時間片凡伊,就開始執(zhí)行run()方法,這里方法 run()稱為線程體窒舟,它包含了要執(zhí)行的這個線程的內(nèi)容系忙,Run方法運行結(jié)束,此線程隨即終止惠豺。
而run()方法的源碼如下银还,可以看到,如果我們指定了target即線程執(zhí)行的函數(shù)的話洁墙,run方法可以轉(zhuǎn)而調(diào)用那個函數(shù)蛹疯,如果沒有的話,將不執(zhí)行热监,而我們在自定義的Thread類里面重寫了這個run 方法捺弦,所以程序會執(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
線程同步
如果多個線程共同對某個數(shù)據(jù)修改,則可能出現(xiàn)不可預(yù)料的結(jié)果列吼,為了保證數(shù)據(jù)的正確性幽崩,需要對多個線程進行同步。
使用 Thread 對象的 Lock 和 Rlock 可以實現(xiàn)簡單的線程同步寞钥,這兩個對象都有 acquire 方法和 release 方法慌申,對于那些需要每次只允許一個線程操作的數(shù)據(jù),可以將其操作放到 acquire 和 release 方法之間理郑。如下:
多線程的優(yōu)勢在于可以同時運行多個任務(wù)(至少感覺起來是這樣)蹄溉。但是當(dāng)線程需要共享數(shù)據(jù)時,可能存在數(shù)據(jù)不同步的問題香浩。
考慮這樣一種情況:一個列表里所有元素都是0类缤,線程"set"從后向前把所有元素改成1,而線程"print"負責(zé)從前往后讀取列表并打印邻吭。
那么餐弱,可能線程"set"開始改的時候,線程"print"便來打印列表了囱晴,輸出就成了一半0一半1膏蚓,這就是數(shù)據(jù)的不同步。為了避免這種情況畸写,引入了鎖的概念驮瞧。
鎖有兩種狀態(tài)——鎖定和未鎖定。每當(dāng)一個線程比如"set"要訪問共享數(shù)據(jù)時枯芬,必須先獲得鎖定论笔;如果已經(jīng)有別的線程比如"print"獲得鎖定了,那么就讓線程"set"暫停千所,也就是同步阻塞狂魔;等到線程"print"訪問完畢,釋放鎖以后淫痰,再讓線程"set"繼續(xù)最楷。
經(jīng)過這樣的處理,打印列表時要么全部輸出0待错,要么全部輸出1籽孙,不會再出現(xiàn)一半0一半1的尷尬場面。
實例:
#!/usr/bin/python3
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)
# 釋放鎖犯建,開啟下一個線程
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)建新線程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
# 開啟新線程
thread1.start()
thread2.start()
# 添加線程到線程列表
threads.append(thread1)
threads.append(thread2)
# 等待所有線程完成
for t in threads:
t.join()
print ("退出主線程")
輸出為:
開啟線程: Thread-1
開啟線程: Thread-2
Thread-1: Thu Aug 10 20:45:59 2017
Thread-1: Thu Aug 10 20:46:00 2017
Thread-1: Thu Aug 10 20:46:01 2017
Thread-2: Thu Aug 10 20:46:03 2017
Thread-2: Thu Aug 10 20:46:05 2017
Thread-2: Thu Aug 10 20:46:07 2017
退出主線程
線程優(yōu)先級隊列( Queue)
Python 的 Queue 模塊中提供了同步的、線程安全的隊列類瓜客,包括FIFO(先入先出)隊列Queue适瓦,LIFO(后入先出)隊列LifoQueue沟启,和優(yōu)先級隊列 PriorityQueue。
這些隊列都實現(xiàn)了鎖原語犹菇,能夠在多線程中直接使用,可以使用隊列來實現(xiàn)線程間的同步芽卿。
Queue 模塊中的常用方法:
Queue.qsize() 返回隊列的大小
Queue.empty() 如果隊列為空揭芍,返回True,反之False
Queue.full() 如果隊列滿了,返回True,反之False
Queue.full 與 maxsize 大小對應(yīng)
Queue.get([block[, timeout]])獲取隊列卸例,timeout等待時間
Queue.get_nowait() 相當(dāng)Queue.get(False)
Queue.put(item) 寫入隊列称杨,timeout等待時間
Queue.put_nowait(item) 相當(dāng)Queue.put(item, False)
Queue.task_done() 在完成一項工作之后,Queue.task_done()函數(shù)向任務(wù)已經(jīng)完成的隊列發(fā)送一個信號
Queue.join() 實際上意味著等到隊列為空筷转,再執(zhí)行別的操作
#!/usr/bin/python3
import queue
import threading
import time
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
# 填充隊列
queueLock.acquire()
for word in nameList:
workQueue.put(word)
queueLock.release()
# 等待隊列清空
while not workQueue.empty():
pass
# 通知線程是時候退出
exitFlag = 1
# 等待所有線程完成
for t in threads:
t.join()
print ("退出主線程")
上面的代碼每次執(zhí)行的結(jié)果是不一樣的姑原,取決于哪個進程先獲得鎖,一次運行的輸出如下:
開啟線程:Thread-1
開啟線程:Thread-2
開啟線程:Thread-3
Thread-2 processing One
Thread-3 processing Two
Thread-1 processing Three
Thread-3 processing Four
Thread-1 processing Five
退出線程:Thread-3
退出線程:Thread-2
退出線程:Thread-1
退出主線程
參考文章
進程與線程的一個簡單解釋:http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html
python 多線程就這么簡單:
http://www.cnblogs.com/fnng/p/3670789.html