1、線程和進程
計算機的核心是CPU搬设,它承擔了所有的計算任務穴店。它就像一座工廠,時刻在運行拿穴。
假定工廠的電力有限泣洞,一次只能供給一個車間使用。也就是說默色,一個車間開工的時候球凰,其他車間都必須停工。背后的含義就是腿宰,單個CPU一次只能運行一個任務呕诉。
進程就好比工廠的車間,它代表CPU所能處理的單個任務吃度。任一時刻甩挫,CPU總是運行一個進程,其他進程處于非運行狀態(tài)椿每。
一個車間里伊者,可以有很多工人。他們協(xié)同完成一個任務间护。
線程就好比車間里的工人删壮。一個進程可以包括多個線程。
車間的空間是工人們共享的兑牡,比如許多房間是每個工人都可以進出的。這象征一個進程的內(nèi)存空間是共享的税灌,每個線程都可以使用這些共享內(nèi)存均函。
可是亿虽,每間房間的大小不同,有些房間最多只能容納一個人苞也,比如廁所洛勉。里面有人的時候,其他人就不能進去了如迟。這代表一個線程使用某些共享內(nèi)存時收毫,其他線程必須等它結束,才能使用這一塊內(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較為簡單刽射,且效率高军拟,所以在必須保證資源獨占的情況下,還是采用這種設計誓禁。
2懈息、多線程與多進程
從上面關于線程和進程的的通俗解釋來看,多線程和多進程的含義如下:
多進程:允許多個任務同時進行
多線程:允許單個任務分成不同的部分運行
3摹恰、Python多線程編程
3.1 單線程
在好些年前的MS-DOS時代辫继,操作系統(tǒng)處理問題都是單任務的,我想做聽音樂和看電影兩件事兒俗慈,那么一定要先排一下順序姑宽。
# 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()來控制音樂播放的時長。接著我們又看了一場電影馏颂,每一場電影需要5秒鐘示血,因為太好看了,所以我也通過for循環(huán)看兩遍救拉。在整個休閑娛樂活動結束后难审,我通過print "all over %s" %ctime() 看了一下當前時間,差不多該睡覺了亿絮。
運行結果:
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
其實告喊,music()和movie()更應該被看作是音樂和視頻播放器,至于要播放什么歌曲和視頻應該由我們使用時決定派昧。所以黔姜,我們對上面代碼做了改造:
# 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'阿凡達')
print("all over %s" %ctime())
運行結果:
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 阿凡達 ! Mon Aug 21 15:38:57 2017
I was at the 阿凡達 ! Mon Aug 21 15:39:02 2017
all over Mon Aug 21 15:39:07 2017
3.2 多線程
Python3 通過兩個標準庫 _thread (python2中是thread模塊)和 threading 提供對線程的支持。
_thread 提供了低級別的蒂萎、原始的線程以及一個簡單的鎖秆吵,它相比于 threading 模塊的功能還是比較有限的。
3.2.1使用_thread模塊
調(diào)用_thread模塊中的start_new_thread()函數(shù)來產(chǎn)生新線程五慈。
先用一個實例感受一下:
# coding=UTF-8
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: 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
這是讓主線程一直在等待.
如果去掉上面兩行,那就直接輸出并結束程序執(zhí)行:
"Main Finished"
3.2.2使用threading模塊
threading 模塊除了包含 thread 模塊中的所有方法外泻拦,還提供的其他方法:
threading.currentThread(): 返回當前的線程變量毙芜。
threading.enumerate(): 返回一個包含正在運行的線程的list。正在運行指線程啟動后争拐、結束前腋粥,不包括啟動前和終止后的線程。
threading.activeCount(): 返回正在運行的線程數(shù)量,與len(threading.enumerate())有相同的結果灯抛。
除了使用方法外金赦,線程模塊同樣提供了Thread類來處理線程,Thread類提供了以下方法:
run(): 用以表示線程活動的方法对嚼。
start():啟動線程活動。
join([time]): 等待至線程中止绳慎。這阻塞調(diào)用線程直至線程的join() 方法被調(diào)用中止-正常退出或者拋出未處理的異常-或者是可選的超時發(fā)生纵竖。
isAlive(): 返回線程是否活動的。
getName(): 返回線程名杏愤。
setName(): 設置線程名靡砌。
直接創(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'阿凡達',))
threads.append(t2)
if __name__ == '__main__':
for t in threads:
t.start()
print ("all over %s" % ctime())
結果輸出為:
I was listening to 愛情買賣. Tue Aug 22 10:41:33 2017
all over Tue Aug 22 10:41:33 2017
I was at the 阿凡達! Tue Aug 22 10:41:33 2017
I was listening to 愛情買賣. Tue Aug 22 10:41:34 2017
I was at the 阿凡達! Tue Aug 22 10:41:38 2017
構造線程類
我們也可以通過直接從 threading.Thread 繼承創(chuàng)建一個新的子類通殃,并實例化后調(diào)用 start() 方法啟動新線程,即它調(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 ("結束線程:" + 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 ("退出主線程")
結果輸出為:
開始線程: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
結束線程: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
結束線程:Thread-2
從結果可以看到厕宗,為什么我們開啟了兩個線程之后画舌,主線程立即退出了?因為我們沒有使用join方法已慢,對于主線程來說曲聂,thread1和thread2是子線程,使用join方法佑惠,會讓主線程等待子線程執(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("退出主線程")
結果就變?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()方法
有一個方法常常拿來與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 ("退出主線程")
結果輸出為:
開始線程:Thread-1
開始線程:Thread-2
退出主線程
可以看到,在主線程結束之后赌厅,程序就終止了穷绵,也就是說兩個子線程也被終止了,這就是setDaemon方法的作用察蹲。主線程A中请垛,創(chuàng)建了子線程B,并且在主線程A中調(diào)用了B.setDaemon(),這個的意思是洽议,把主線程A設置為守護線程宗收,這時候,要是主線程A執(zhí)行結束了亚兄,就不管子線程B是否完成,一并和主線程A退出.這就是setDaemon方法的含義混稽,這基本和join是相反的。此外,還有個要特別注意的:必須在start() 方法調(diào)用之前設置匈勋,如果不設置為守護線程礼旅,程序會被無限掛起。
兩個疑問
我們剛才介紹了兩種使用多線程的方式洽洁,一種是直接調(diào)用threading.Thread 創(chuàng)建線程痘系,另一種是從 threading.Thread 繼承創(chuàng)建一個新的子類,并實例化后調(diào)用 start() 方法啟動進程饿自。學到這里汰翠,我就拋出了兩個疑問,為什么第一種方法中我們可以為不同的線程指定運行的方法昭雌,而第二種我們都運行的是同一個方法复唤,那么它內(nèi)部的實現(xiàn)機制是什么呢?第二個疑問是烛卧,第二種方法中佛纫,我們沒有實例化start()方法,那么run和start這兩個方法的聯(lián)系是什么呢总放?
首先呈宇,start方法和run方法的關系如下:用start方法來啟動線程,真正實現(xiàn)了多線程運行间聊,這時無需等待run方法體代碼執(zhí)行完畢而直接繼續(xù)執(zhí)行下面的代碼攒盈。通過調(diào)用Thread類的start()方法來啟動一個線程,這時此線程處于就緒(可運行)狀態(tài)哎榴,并沒有運行型豁,一旦得到cpu時間片,就開始執(zhí)行run()方法尚蝌,這里方法 run()稱為線程體迎变,它包含了要執(zhí)行的這個線程的內(nèi)容,Run方法運行結束飘言,此線程隨即終止衣形。
而run()方法的源碼如下,可以看到姿鸿,如果我們指定了target即線程執(zhí)行的函數(shù)的話谆吴,run方法可以轉而調(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)不可預料的結果胳螟,為了保證數(shù)據(jù)的正確性,需要對多個線程進行同步筹吐。
使用 Thread 對象的 Lock 和 Rlock 可以實現(xiàn)簡單的線程同步糖耸,這兩個對象都有 acquire 方法和 release 方法,對于那些需要每次只允許一個線程操作的數(shù)據(jù)丘薛,可以將其操作放到 acquire 和 release 方法之間嘉竟。如下:
多線程的優(yōu)勢在于可以同時運行多個任務(至少感覺起來是這樣)。但是當線程需要共享數(shù)據(jù)時洋侨,可能存在數(shù)據(jù)不同步的問題周拐。
考慮這樣一種情況:一個列表里所有元素都是0,線程"set"從后向前把所有元素改成1凰兑,而線程"print"負責從前往后讀取列表并打印。
那么审丘,可能線程"set"開始改的時候吏够,線程"print"便來打印列表了,輸出就成了一半0一半1滩报,這就是數(shù)據(jù)的不同步锅知。為了避免這種情況,引入了鎖的概念脓钾。
鎖有兩種狀態(tài)——鎖定和未鎖定售睹。每當一個線程比如"set"要訪問共享數(shù)據(jù)時,必須先獲得鎖定可训;如果已經(jīng)有別的線程比如"print"獲得鎖定了昌妹,那么就讓線程"set"暫停,也就是同步阻塞握截;等到線程"print"訪問完畢飞崖,釋放鎖以后,再讓線程"set"繼續(xù)谨胞。
經(jīng)過這樣的處理固歪,打印列表時要么全部輸出0,要么全部輸出1胯努,不會再出現(xiàn)一半0一半1的尷尬場面牢裳。
實例:
# 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)
# 釋放鎖叶沛,開啟下一個線程
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: 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)先級隊列( 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 大小對應
Queue.get([block[, timeout]])獲取隊列旱捧,timeout等待時間
Queue.get_nowait() 相當Queue.get(False)
Queue.put(item) 寫入隊列,timeout等待時間
Queue.put_nowait(item) 相當Queue.put(item, False)
Queue.task_done() 在完成一項工作之后踩麦,Queue.task_done()函數(shù)向任務已經(jīng)完成的隊列發(fā)送一個信號
Queue.join() 實際上意味著等到隊列為空枚赡,再執(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
# 填充隊列
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í)行的結果是不一樣的,取決于哪個進程先獲得鎖谓谦,一次運行的輸出如下:
開啟線程: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
退出主線程
參考文章
進程與線程的一個簡單解釋:
http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html
多線程就這么簡單:
http://www.cnblogs.com/fnng/p/3670789.html