版權(quán)聲明:本文為作者原創(chuàng)文章,可以隨意轉(zhuǎn)載,但必須在明確位置標(biāo)明出處!??!
tips:本基礎(chǔ)系列旨在以爬蟲帶大家入門Python語(yǔ)言
本章并不是要教你如何去打群架,本篇文章將主要介紹如何使用多線程來幫我們的提高程序的執(zhí)行效率,「群架」只是為了更加生動(dòng)的去描述多線程的現(xiàn)象,相信大部分讀者都看過《葉問》,葉問為了在日本軍那里拿到糧食它決定一次要打10個(gè),這種模式就是單線程模式,他必須把10個(gè)人一個(gè)個(gè)打敗了才能贏得糧食,這種模式也是我們前面章節(jié)用到的模式,雖然最終能夠贏得勝利,但用時(shí)就比較長(zhǎng)了。假設(shè)葉問有多只手,每次最多只能用兩只手,那么他在和人對(duì)戰(zhàn)的時(shí)候就可以出其不意的將對(duì)手干翻掉,這肯定不兩只手解決敵人的速度要塊。這就是多線程模式,由于Python解釋器GIL(Global Interpreter Lock)的存在,同一時(shí)刻只能有一個(gè)線程在執(zhí)行,所以對(duì)于現(xiàn)在的多核CPU而言,Python中的多線程并沒提高程序的處理能力,反而有可能降低程序的處理能力,如果你開啟的線程足夠多的話,線程的上下文切換將會(huì)造成程序的執(zhí)行效率變低。
那么有的讀者就會(huì)問既然Python的多線程設(shè)計(jì)并不能有效的提高程序的處理能力,那為什么還要有這個(gè)模塊呢,說到這里就需要給讀者普及一下概念了,I/O密集型、CPU密集型
I/O密集型
什么是I/O密集型呢,I/O的是輸入/輸出(input/output)的意思,就是我們程序大部分的時(shí)間都是用來等待I/O操作,想網(wǎng)絡(luò)的請(qǐng)求、文件的讀寫、數(shù)據(jù)庫(kù)的讀寫都屬于I/O操作,前面的章節(jié)說過I/O操作是很費(fèi)時(shí)的。所以Python中的多線程適用于I/O密集型的任務(wù),因?yàn)樗恍枰玫紺PU的計(jì)算能力。
CPU密集型
CPU密集型的意思就是計(jì)算密集型,如果你的程序中有大量復(fù)雜的計(jì)算邏輯那么就選擇CPU密集型,因?yàn)閺?fù)雜計(jì)算是十分耗CPU資源的,想圓周率的計(jì)算,高清視頻數(shù)據(jù)的處理等等。那么Python中怎么設(shè)計(jì)CPU密集型呢,當(dāng)然是多進(jìn)程的使用,什么意思呢,就拿上面葉問的例子,若是葉問有10個(gè)分身,二期10個(gè)分身都是獨(dú)立有思想的,那么打10個(gè)還不是分分鐘的事嗎。
threading模塊
多線程有兩個(gè)模塊一個(gè)是thread模塊、一個(gè)就是threading模塊,這里推薦大家使用threading模塊,不推薦使用thread模塊有兩個(gè)原因,一個(gè)是它對(duì)于進(jìn)程何時(shí)退出沒有控制,當(dāng)主線程結(jié)束時(shí)子線程也會(huì)被強(qiáng)制性的結(jié)束也不會(huì)發(fā)出警告或者進(jìn)行適當(dāng)?shù)那謇砗歪尫殴ぷ鳌A硪粋€(gè)原因是該模塊不支持守護(hù)線程這個(gè)概念。這里普及一個(gè)概念,線程是不能單獨(dú)執(zhí)行的,它必須依賴進(jìn)程才能存活。進(jìn)程至少包含一個(gè)線程也就是上面說道的主線程,只要進(jìn)程啟動(dòng),那么主線程也就啟動(dòng)了。
threading模塊對(duì)象
threading模塊提供了如下對(duì)象:

這些對(duì)象都包含在threading模塊里。沒個(gè)類對(duì)象都提供了哪些方法接口都可以在開放這文檔查看到。

創(chuàng)建線程
使用Thread類來創(chuàng)建線程,創(chuàng)建線程有有幾種方式,你可以選擇你最舒服最中意的方式去使用。
- 創(chuàng)建Thread的實(shí)例,傳給它一個(gè)參數(shù)
import threading
def fun():
count = 0
for index in range(10000000):
count = count + 1
th = threading.Thread(target=fun, args=())
th.start()
th.join()
- 創(chuàng)建Thread 的實(shí)例,傳給它一個(gè)可調(diào)用的類實(shí)例
import threading
class ThreadFunc(object):
def __init__(self, func, args):
self.func = func
self.args = args
def __call__(self):
self.res = self.func(*self.args)
def fun():
count = 0
for index in range(10000000):
count = count + 1
th = threading.Thread(target = ThreadFunc(fun, ())
th.start()
th.join()
- 派生Thread 的子類,并創(chuàng)建子類的實(shí)例
import threading
class myThread(threading.Thread):
def __init__(self, func, args):
super(myThread, self).__init__(self)
self.args = args
self.func = func
def run(self):
self.res = self.func(*self.args)
def fun():
count = 0
for index in range(10000000):
count = count + 1
th= myThread(fun, ())
th.start()
th.join()
推薦使用第三種方式,第三種方式對(duì)于靈活行和未來的擴(kuò)展性更好。
同步
涉及到多線程編程必定要涉及到一個(gè)主題就是數(shù)據(jù)同步,這里普及一下多線程為什么能提高程序的處理能力,這個(gè)跟CPU的工作原理有關(guān),只有拿到CPU分給線程執(zhí)行的時(shí)間片,我們的代碼才能夠被CPU執(zhí)行,所以如果我們有多個(gè)線程去執(zhí)行同一個(gè)任務(wù)那么每個(gè)線程有是有機(jī)會(huì)得到CPU時(shí)間片,那對(duì)于處理同一個(gè)人才得到的時(shí)間片總和就比單個(gè)線程大太多了,所以多線程能夠提高程序的處理能力。但是多線程的處理會(huì)遇到數(shù)據(jù)同步的問題,為什么會(huì)出現(xiàn)這種問題呢,我們知道了線程只有得到CPU時(shí)間片才能執(zhí)行,那么當(dāng)A線程正在執(zhí)行還沒有執(zhí)行完,這個(gè)時(shí)候CPU時(shí)間片到了,那么A線程就會(huì)暫停在此刻并進(jìn)入「休眠」?fàn)顟B(tài),B線程拿到時(shí)間片也執(zhí)行同樣的動(dòng)作,這個(gè)時(shí)候B就有可能把A、B線程共同擁有的數(shù)據(jù)給破壞掉,舉個(gè)例子,張三、李四都喜歡吃熱狗,他們一起走進(jìn)店里買熱狗,張三剛剛想把熱狗拿起來,這個(gè)時(shí)候由于CPU時(shí)間到了,張三就進(jìn)入休眠狀態(tài)了,李四剛好拿到CPU時(shí)間片他就把熱狗拿走了,等到下一次張三拿到CPU時(shí)間片從休眠狀態(tài)中喚醒,他認(rèn)為自已已經(jīng)拿到熱狗了,但真實(shí)的情況是這根熱狗已經(jīng)被李四拿走了所以這就造成了數(shù)據(jù)的不同步了。講了這么多我們來看一個(gè)賣票的例子,代碼如下:
import threading
tickets = 10000000
listdata = []
def sale_ticket():
global tickets
while tickets:
tickets = tickets - 1
# print(tickets)
listdata.append(tickets)
threads = []
for index in range(10):
thread = threading.Thread(target=sale_ticket, args=())
thread.start()
threads.append(thread)
for t in threads:
t.join()
print(len(listdata))
print('thread exit')
#執(zhí)行結(jié)果:33934146
從結(jié)果中我們可以看出這個(gè)結(jié)構(gòu)肯定是不對(duì)的,因?yàn)槲覀冎挥?0000000這么多張票,而結(jié)果是我們總票數(shù)的3倍還多,這就是因?yàn)橛泻芏嗳硕寄玫搅讼嗤钠?。那么如何解決數(shù)據(jù)同步呢,解決數(shù)據(jù)同步的很多總方法,章節(jié)前面threading模塊對(duì)象已經(jīng)列出來了,Lock、RLock等,下面我們主要講一下Lock的用法,
Lock類
Lock類提供了兩個(gè)方法
- acquire(blocking=True, timeout=-1)
獲取一個(gè)鎖 blocking參數(shù)默認(rèn)為True,即阻塞模式,什么意思呢,就是電話亭一樣,如果電話亭里有人在打電話那么下一個(gè)人只有等電話亭里的人打完電話你才能打,如果設(shè)置為False,既不阻塞,如果電話亭里有人打電話那我就干其它事去了,等會(huì)兒再來看看,若是電話亭里這個(gè)時(shí)候沒有人我就把電話亭外掛個(gè)請(qǐng)勿打擾的牌子,timeout超時(shí)時(shí)間,只有blocking為True有用。 - released()
該函數(shù)沒有參數(shù),釋放一個(gè)鎖
加鎖改寫后的代碼
import threading
tickets = 10000000
listdata = []
mutex = threading.Lock()
def sale_ticket():
global tickets
while tickets:
if mutex.acquire():
tickets = tickets - 1
# print(tickets)
listdata.append(tickets)
mutex.release()
threads = []
for index in range(10):
thread = threading.Thread(target=sale_ticket, args=())
thread.start()
threads.append(thread)
for t in threads:
t.join()
print(len(listdata))
print('thread exit')
# 執(zhí)行結(jié)果:10000000
從結(jié)果可以看出我們保證了數(shù)據(jù)的同步,也就是保證了每個(gè)人都買到的票都是唯一的。
okay,本章就講到這里就結(jié)束了, 因?yàn)槲覀兊呐老x程序正好是一個(gè)I/O密集型程序,所以使用多線程設(shè)計(jì)正好合適,如何設(shè)計(jì)多線程我將放到后面的章節(jié)將,讀者有興趣的可以去我的git查看源碼,地址https://github.com/Gavinxyj/Python/tree/master/python_study/Scrapy/modules歡迎大家fork、star。
PS:成為牛人你只需要保持每天進(jìn)步一點(diǎn)點(diǎn)