聚沙成塔--爬蟲系列(十四)(群架要怎么打)

版權(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ì)象:


hreading模塊對(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)


歡迎關(guān)注我:「愛做飯的老謝」,老謝一直在努力...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 目錄 一、開啟線程的兩種方式 在python中開啟線程要導(dǎo)入threading,它與開啟進(jìn)程所需要導(dǎo)入的模塊mul...
    CaiGuangyin閱讀 2,468評(píng)論 1 16
  • 引言&動(dòng)機(jī) 考慮一下這個(gè)場(chǎng)景,我們有10000條數(shù)據(jù)需要處理,處理每條數(shù)據(jù)需要花費(fèi)1秒,但讀取數(shù)據(jù)只需要0.1秒,...
    chen_000閱讀 584評(píng)論 0 0
  • 線程 引言&動(dòng)機(jī) 考慮一下這個(gè)場(chǎng)景,我們有10000條數(shù)據(jù)需要處理,處理每條數(shù)據(jù)需要花費(fèi)1秒,但讀取數(shù)據(jù)只需要0....
    不浪漫的浪漫_ea03閱讀 415評(píng)論 0 0
  • 6年前,我離開這里, 6年后,又回來這里。 生活還真是如戲。 但我知道,這是因?yàn)槟恪?6年前,我哭著離開, 6年后...
    小A雜貨鋪閱讀 402評(píng)論 0 0
  • 以低落的心情寫下這篇文字,也許想法會(huì)更實(shí)際、堅(jiān)定。 計(jì)劃分段: (階段1)生產(chǎn)前1個(gè)月-產(chǎn)后3個(gè)月 (階段2)...
    午安小姐閱讀 156評(píng)論 0 0

友情鏈接更多精彩內(nèi)容