Python并發(fā)編程實(shí)例教程之線程的玩法
一、線程基礎(chǔ)以及守護(hù)進(jìn)程
線程是CPU調(diào)度的最小單位
全局解釋器鎖
全局解釋器鎖GIL(global interpreter lock)
全局解釋器鎖的出現(xiàn)主要是為了完成垃圾回收機(jī)制的回收機(jī)制,對(duì)不同線程的引用計(jì)數(shù)的變化記錄的更加精準(zhǔn)。
全局解釋器鎖導(dǎo)致了同一個(gè)進(jìn)程中的多個(gè)線程只能有一個(gè)線程真正被CPU執(zhí)行。
GIL鎖每執(zhí)行700條指令才會(huì)進(jìn)行一次(輪轉(zhuǎn))切換(從一個(gè)線程切換到另外一個(gè)線程)
節(jié)省的是IO操作(不占用CPU)的時(shí)間,而不是CPU計(jì)算的時(shí)間,因?yàn)镃PU的計(jì)算速度非???,大多數(shù)情況下,我們沒(méi)有辦法把一條進(jìn)程中所有的IO操作都規(guī)避掉。
threading模塊
import time from threading import Thread, current_thread, enumerate, active_count def func(i): print('start%s' % i, current_thread().ident) # 函數(shù)中獲取當(dāng)前線程id time.sleep(1) print('end%s' % i) if __name__ == '__main__': t1 = [] for i in range(3): t = Thread(target=func, args=(i,)) t.start() print(t.ident) # 查看當(dāng)前線程id t1.append(t) print(enumerate(), active_count()) for t in t1: t.join() print('所有線程執(zhí)行完畢')
線程是不能從外部強(qiáng)制終止(terminate),所有的子線程只能是自己執(zhí)行完代碼之后就關(guān)閉。
current_thread 獲取當(dāng)前的線程對(duì)象
current_thread().ident 或者 線程對(duì)象.ident 獲取當(dāng)前線程id。
enumerate返回一個(gè)列表,存儲(chǔ)了所有活著的線程對(duì)象,包括主線程。
active_count返回一個(gè)數(shù)字,存儲(chǔ)了所有活著的線程個(gè)數(shù)。
【注意】enumerate導(dǎo)入之后,會(huì)和內(nèi)置函數(shù)enumerate重名,需要做特殊的處理
- from threading import enumerate as en
- import threading
threading.enumerate()
面向?qū)ο蠓绞介_(kāi)啟一個(gè)線程
from threading import Thread class MyThread(Thread): def __init__(self, a, b): super(MyThread, self).__init__() self.a = a self.b = b def run(self): print(self.ident) t = MyThread(1, 3) t.start() # 開(kāi)啟線程,才在線程中執(zhí)行run方法 print(t.ident)
線程之間的數(shù)據(jù)是共享的
from threading import Thread n = 100 def func(): global n n -= 1 t_li = [] for i in range(100): t = Thread(target=func) t.start() t_li.append(t) for t in t_li: t.join() print(n)
結(jié)果是:0
守護(hù)線程
- 主線程會(huì)等待子線程結(jié)束之后才結(jié)束,為什么?
因?yàn)橹骶€程結(jié)束,進(jìn)程就會(huì)結(jié)束。
- 守護(hù)線程隨著主線程的結(jié)束而結(jié)束
- 守護(hù)進(jìn)程會(huì)隨著主進(jìn)程的代碼結(jié)束而結(jié)束,如果主進(jìn)程代碼之后還有其他子進(jìn)程在運(yùn)行,守護(hù)進(jìn)程不守護(hù)。
- 守護(hù)線程會(huì)隨著主線程的結(jié)束而結(jié)束,如果主線程代碼結(jié)束之后還有其他子線程在運(yùn)行,守護(hù)線程也守護(hù)。
import time from threading import Thread def son(): while True: print('in son') time.sleep(1) def son2(): for i in range(3): print('in son2...') time.sleep(1) # flag a t = Thread(target=son) t.daemon = True t.start() # flag b a-->b用時(shí)0s Thread(target=son2).start()
為什么守護(hù)線程會(huì)在主線程的代碼結(jié)束之后繼續(xù)守護(hù)其他子線程?
答:因?yàn)槭刈o(hù)進(jìn)程和守護(hù)線程的結(jié)束原理不同。守護(hù)進(jìn)程需要主進(jìn)程來(lái)回收資源,守護(hù)線程是隨著主線程的結(jié)束而結(jié)束,其他子線程–>主線程結(jié)束–>主進(jìn)程結(jié)束–>整個(gè)進(jìn)程中所有的資源都被回收,守護(hù)線程也會(huì)被回收。
二、線程鎖(互斥鎖)
線程之間也存在數(shù)據(jù)不安全
import dis a = 0 def func(): global a a += 1 dis.dis(func) # 得到func方法中的代碼翻譯成CPU指令 """ 結(jié)果 0 LOAD_GLOBAL 0 (a) 2 LOAD_CONST 1 (1) 4 INPLACE_ADD 6 STORE_GLOBAL 0 (a) 8 LOAD_CONST 0 (None) 10 RETURN_VALUE """
+=、-=、*=、/=、while、if、帶返回值的方法(都是先計(jì)算后賦值,前提要涉及到全局變量或靜態(tài)變量) 等都是數(shù)據(jù)不安全的,append、pop、queue、logging模塊等都是數(shù)據(jù)安全的。
列表中的方法或者字典中的方法去操作全局變量的時(shí)候,數(shù)據(jù)是安全的。
只有一個(gè)線程,永遠(yuǎn)不會(huì)出現(xiàn)線程不安全現(xiàn)象。
采用加鎖的方式來(lái)保證數(shù)據(jù)安全。
from threading import Thread, Lock n = 0 def add(lock): for i in range(500000): global n with lock: n += 1 def sub(lock): for i in range(500000): global n with lock: n -= 1 t_li = [] lock = Lock() for i in range(2): t1 = Thread(target=add, args=(lock,)) t1.start() t2 = Thread(target=sub, args=(lock,)) t2.start() t_li.append(t1) t_li.append(t2) for t in t_li: t.join() print(n)
線程安全的單例模式
import time from threading import Thread, Lock class A: __instance = None lock = Lock() def __new__(cls, *args, **kwargs): with cls.lock: if not cls.__instance: time.sleep(0.00001) cls.__instance = super().__new__(cls) return cls.__instance def func(): a = A() print(a) for i in range(10): Thread(target=func).start()
不用考慮加鎖的小技巧
- 不要操作全局變量
- 不要在類(lèi)中操作靜態(tài)變量
因?yàn)槎鄠€(gè)線程同時(shí)操作全局變量/靜態(tài)變量,會(huì)產(chǎn)生數(shù)據(jù)不安全現(xiàn)象。
三、線程鎖(遞歸鎖)
from threading import Lock, RLock # Lock 互斥鎖 # RLock 遞歸(recursion)鎖 l = Lock() l.acquire() print('希望被鎖住的代碼') l.release() rl = RLock() # 在同一個(gè)線程中可以被acquire多次 rl.acquire() rl.acquire() rl.acquire() print('希望被鎖住的代碼') rl.release()
from threading import Thread, RLock def func(i, lock): lock.acquire() lock.acquire() print(i, ':start') lock.release() lock.release() print(i, ':end') lock = RLock() for i in range(5): Thread(target=func, args=(i, lock)).start()
互斥鎖與遞歸鎖
遞歸鎖在同一個(gè)線程中可以被acquire多次,而互斥鎖不行
互斥鎖效率高,遞歸鎖效率相對(duì)低
多把互斥鎖容易產(chǎn)生死鎖現(xiàn)象,遞歸鎖可以快速解決死鎖
四、死鎖
死鎖:指兩個(gè)或兩個(gè)以上的進(jìn)程或線程在執(zhí)行過(guò)程中,因爭(zhēng)奪資源而造成的一種互相等待的現(xiàn)象。
死鎖現(xiàn)象是怎么產(chǎn)生的?
答:有多把鎖,并且在多個(gè)線程中交叉使用。與互斥鎖、遞歸鎖無(wú)關(guān),都會(huì)發(fā)生死鎖。如果是互斥鎖,出現(xiàn)了死鎖現(xiàn)象,最快速的解決方案是把所有的互斥鎖都改成一把遞歸鎖(noodle_lock = fork_lock = RLock()),程序的效率會(huì)降低。
from threading import Thread, Lock import time noodle_lock = Lock() fork_lock = Lock() def eat1(name): noodle_lock.acquire() print(name, '搶到面了') fork_lock.acquire() print(name, '搶到叉子了') print(name, '吃面') time.sleep(0.0001) fork_lock.release() print(name, '放下叉子了') noodle_lock.release() print(name, '放下面了') def eat2(name): fork_lock.acquire() print(name, '搶到叉子了') noodle_lock.acquire() print(name, '搶到面了') print(name, '吃面') noodle_lock.release() print(name, '放下面了') fork_lock.release() print(name, '放下叉子了') Thread(target=eat1, args=('lucy',)).start() Thread(target=eat2, args=('jack',)).start() Thread(target=eat1, args=('rose',)).start() Thread(target=eat2, args=('disen',)).start()
五、隊(duì)列
隊(duì)列:線程之間數(shù)據(jù)安全的容器
線程隊(duì)列:數(shù)據(jù)安全,先進(jìn)先出
原理:加鎖 + 鏈表
Queue
fifo 先進(jìn)先出的隊(duì)列
get和put
import queue q = queue.Queue(3) # fifo 先進(jìn)先出的隊(duì)列 q.put(1) q.put(2) print(q.get()) print(q.get())
1
2
get_nowait
import queue # from queue import Empty # 不是內(nèi)置的錯(cuò)誤類(lèi)型,而是queue模塊中的錯(cuò)誤 q = queue.Queue() # fifo 先進(jìn)先出的隊(duì)列 try: q.get_nowait() except queue.Empty: pass print('隊(duì)列為空,繼續(xù)執(zhí)行其他代碼')
put_nowait
用的很少,因?yàn)殛?duì)列滿(mǎn)時(shí),拋異常,數(shù)據(jù)放不進(jìn)去,丟失了。
LifoQueue
后進(jìn)先出的隊(duì)列,也就是棧。last in first out
from queue import LifoQueue lq = LifoQueue() lq.put(1) lq.put(2) print(lq.get()) print(lq.get())
2
1
PriorityQueue
優(yōu)先級(jí)隊(duì)列,按照放入數(shù)據(jù)的第一位數(shù)值從小到大輸出
from queue import PriorityQueue priq = PriorityQueue() priq.put((2, 'lucy')) priq.put((0, 'rose')) priq.put((1, 'jack')) print(priq.get()) print(priq.get()) print(priq.get())
(0, 'rose')
(1, 'jack')
(2, 'lucy')
三種隊(duì)列使用場(chǎng)景
先進(jìn)先出:用于處理服務(wù)類(lèi)任務(wù)(買(mǎi)票任務(wù))
后進(jìn)先出:算法中用的比較多
優(yōu)先級(jí)隊(duì)列:比如,VIP制度,VIP用戶(hù)優(yōu)先;
六、相關(guān)面試題
請(qǐng)聊聊進(jìn)程隊(duì)列的特點(diǎn)和實(shí)現(xiàn)原理
特點(diǎn):實(shí)現(xiàn)進(jìn)程之間的通信;數(shù)據(jù)安全;先進(jìn)先出。
實(shí)現(xiàn)原理:基于管道 + 鎖 實(shí)現(xiàn)的,管道是基于文件級(jí)別的socket + pickle 實(shí)現(xiàn)的。
你了解生產(chǎn)者消費(fèi)者模型嗎,如何實(shí)現(xiàn)
了解
為什么了解?工作經(jīng)驗(yàn)
采集圖片/爬取音樂(lè):由于要爬取大量的數(shù)據(jù),想提高爬取效率
有用過(guò)一個(gè)生產(chǎn)者消費(fèi)者模型,這個(gè)模型是我自己寫(xiě)的,消息中間件,用的是xxx(redis),我獲取網(wǎng)頁(yè)的過(guò)程作為生產(chǎn)者,分析網(wǎng)頁(yè),獲取所有歌曲歌曲鏈接的過(guò)程作為消費(fèi)者。
自己寫(xiě)監(jiān)控,或者是自己寫(xiě)郵件報(bào)警系統(tǒng),監(jiān)控程序作為生產(chǎn)者,一旦發(fā)現(xiàn)有問(wèn)題的程序,就需要把要發(fā)送的郵件信息交給消息中間件redis,消費(fèi)者就從中間件中取值,然后來(lái)處理發(fā)郵件的邏輯。
什么時(shí)候用過(guò)?
項(xiàng)目 或者 例子,結(jié)合上面一起
在python中實(shí)現(xiàn)生產(chǎn)者消費(fèi)者模型可以用哪些機(jī)制
消息中間件
celery(分布式框架):定時(shí)發(fā)短信的任務(wù)
從你的角度說(shuō)說(shuō)進(jìn)程在計(jì)算機(jī)中扮演什么角色
進(jìn)程用來(lái)管理一個(gè)運(yùn)行中的程序的資源,是資源分配的最小單位
進(jìn)程與進(jìn)程之間內(nèi)存是隔離的
進(jìn)程是由操作系統(tǒng)負(fù)責(zé)調(diào)度的,并且多個(gè)進(jìn)程之間是一種競(jìng)爭(zhēng)關(guān)系,所以我們應(yīng)該對(duì)進(jìn)程的三狀態(tài)時(shí)刻關(guān)注,盡量減少進(jìn)程中的IO操作,或者在進(jìn)程里面開(kāi)線程來(lái)規(guī)避IO,讓我們寫(xiě)的程序在運(yùn)行的時(shí)候能夠更多的占用CPU資源。
為什么線程之間的數(shù)據(jù)不安全
線程之間數(shù)據(jù)共享
多線程的情況下,
如果在計(jì)算某一個(gè)變量的時(shí)候,還要進(jìn)行賦值操作,這個(gè)過(guò)程不是由一條完整的CPU指令完成的;
如果在判斷某個(gè)bool表達(dá)式之后,再做某些操作,這個(gè)過(guò)程也不是由一條完整的CPU指令完成的;
在中間發(fā)生了GIL鎖的切換(時(shí)間片的輪轉(zhuǎn)),可能會(huì)導(dǎo)致數(shù)據(jù)不安全。
讀程序,請(qǐng)確認(rèn)執(zhí)行到最后number的長(zhǎng)度是否一定為 1
import threading import time # loop = 1E7 # 10000000. loop = int(1E7) # 10000000 def _add(loop: int = 1): global numbers for _ in range(loop): numbers.append(0) def _sub(loop: int = 1): global numbers for _ in range(loop): while not numbers: time.sleep(1E-8) numbers.pop() numbers = [0] ta = threading.Thread(target=_add, args=(loop,)) ts = threading.Thread(target=_sub, args=(loop,)) # ts1 = threading.Thread(target=_sub, args=(loop,)) ta.start() ts.start() # ts1.start() ta.join() ts.join() # ts1.join()
因?yàn)橹婚_(kāi)啟了一個(gè)進(jìn)行pop操作的線程,如果開(kāi)啟多個(gè)pop操作的線程,必須在while前面加鎖,因?yàn)榭赡苡袃蓚€(gè)線程,一個(gè)執(zhí)行了while not numbers,發(fā)生了GIL的切換,另外一個(gè)線程執(zhí)行完了代碼,numbers剛好沒(méi)有了數(shù)據(jù),導(dǎo)致結(jié)果一個(gè)pop成功,一個(gè)pop不成功。
所以number長(zhǎng)度一定為1,如果把注釋去了,不一定為1
讀程序,請(qǐng)確認(rèn)執(zhí)行到最后number的長(zhǎng)度是否一定為 1
import threading import time loop = int(1E7) def _add(loop: int = 1): global numbers for _ in range(loop): numbers.append(0) def _sub(loop: int = 1): global numbers for _ in range(loop): while not numbers: time.sleep(1E-8) numbers.pop() numbers = [0] ta = threading.Thread(target=_add, args=(loop,)) ts = threading.Thread(target=_sub, args=(loop,)) ta.start() ta.join() ts.start() ts.join()
一定為1,因?yàn)槭峭降摹?/p>
讀程序,請(qǐng)確認(rèn)執(zhí)行到最后number是否一定為 0
import threading loop = int(1E7) def _add(loop: int = 1): global numbers for _ in range(loop): numbers += 1 def _sub(loop: int = 1): global numbers for _ in range(loop): numbers -= 1 numbers = 0 ta = threading.Thread(target=_add, args=(loop,)) ts = threading.Thread(target=_sub, args=(loop,)) ta.start() ta.join() ts.start() ts.join()
一定等于0,因?yàn)槭峭降摹?/p>
讀程序,請(qǐng)確認(rèn)執(zhí)行到最后number是否一定為 0
import threading loop = int(1E7) def _add(loop: int = 1): global numbers for _ in range(loop): numbers += 1 def _sub(loop: int = 1): global numbers for _ in range(loop): numbers -= 1 numbers = 0 ta = threading.Thread(target=_add, args=(loop,)) ts = threading.Thread(target=_sub, args=(loop,)) ta.start() ts.start() ta.join() ts.join()
不一定為0,因?yàn)槭钱惒降那掖嬖?+= 操作
七、判斷數(shù)據(jù)是否安全
是否數(shù)據(jù)共享,是同步還是異步(數(shù)據(jù)共享并且異步的情況下)
- +=、-=、*=、/=、a = 計(jì)算之后賦值給變量
- if、while 條件,這兩個(gè)判斷是由多個(gè)線程完成的
這兩種情況下,數(shù)據(jù)不安全。
八、進(jìn)程池 & 線程池
以前,有多少個(gè)任務(wù)就開(kāi)多少個(gè)進(jìn)程或線程。
什么是池
要在程序開(kāi)始的時(shí)候,還沒(méi)有提交任務(wù),先創(chuàng)建幾個(gè)線程或者進(jìn)程,放在一個(gè)池子里,這就是池
為什么要用池
如果先開(kāi)好進(jìn)程/線程,那么有任務(wù)之后就可以直接使用這個(gè)池中的數(shù)據(jù)了;并且開(kāi)好的進(jìn)程/線程會(huì)一直存在在池中,可以被多個(gè)任務(wù)反復(fù)利用,這樣極大的減少了開(kāi)啟/關(guān)閉/調(diào)度進(jìn)程/調(diào)度線程的時(shí)間開(kāi)銷(xiāo)。
池中的線程/進(jìn)程個(gè)數(shù)控制了操作系統(tǒng)需要調(diào)用的任務(wù)個(gè)數(shù),控制池中的單位,有利于提高操作系統(tǒng)的效率,減輕操作系統(tǒng)的負(fù)擔(dān)。
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor # threading模塊 沒(méi)有提供池 # multiprocessing模塊 仿照threading增加了Pool(逐漸被淘汰) # concurrent.futures模塊 線程池,進(jìn)程池都能夠用相似的方式開(kāi)啟/使用 ThreadPoolExecutor() # 參數(shù)代表開(kāi)啟多少個(gè)線程,線程的個(gè)數(shù)一般起cpu個(gè)數(shù)*4(或者*5) ProcessPoolExecutor() # 參數(shù)代表開(kāi)啟多少個(gè)進(jìn)程,進(jìn)程的個(gè)數(shù)一般起cpu個(gè)數(shù)+1
創(chuàng)建線程池并提交任務(wù)
from concurrent.futures import ThreadPoolExecutor from threading import current_thread import time def func(a, b): print(current_thread().ident, a, b) time.sleep(1) tp = ThreadPoolExecutor(4) # 創(chuàng)建線程池對(duì)象 for i in range(20): # tp.submit(func, i, i + 1) # 向池中提交任務(wù) tp.submit(func, a=i, b=i + 1) # 位置傳參,關(guān)鍵字傳參都可以
創(chuàng)建進(jìn)程池并提交任務(wù)
from concurrent.futures import ProcessPoolExecutor import os import time def func(a, b): print(os.getpid(), 'start', a, b) time.sleep(1) print(os.getpid(), 'end', a, b) if __name__ == '__main__': tp = ProcessPoolExecutor(4) # 創(chuàng)建進(jìn)程池對(duì)象 for i in range(20): # tp.submit(func, i, i + 1) # 向池中提交任務(wù) tp.submit(func, a=i, b=i + 1) # 位置傳參,關(guān)鍵字傳參都可以
獲取任務(wù)結(jié)果
from concurrent.futures import ProcessPoolExecutor import os import time def func(a, b): print(os.getpid(), 'start', a, b) time.sleep(1) print(os.getpid(), 'end', a, b) return a * b if __name__ == '__main__': tp = ProcessPoolExecutor(4) # 創(chuàng)建進(jìn)程池對(duì)象 future_d = {} for i in range(20): # 異步非阻塞的 ret = tp.submit(func, a=i, b=i + 1) # future未來(lái)對(duì)象 # print(ret) # <Future at 0x1ad918e1148 state=running> # print(ret.result()) # 這樣需要等待,同步的 future_d[i] = ret for key in future_d: # 同步阻塞的 print(key, future_d[key].result())
tp對(duì)象的map
map 只適合傳遞簡(jiǎn)單的參數(shù),并且必須是一個(gè)可迭代的類(lèi)型
from concurrent.futures import ProcessPoolExecutor import os import time def func(a): print(os.getpid(), 'start', a[0], a[1]) time.sleep(1) print(os.getpid(), 'end', a[0], a[1]) return a[0] * a[1] if __name__ == '__main__': tp = ProcessPoolExecutor(4) ret = tp.map(func, ((i, i + 1) for i in range(20))) # 一般函數(shù)只接收一個(gè)參數(shù),要想傳入多個(gè),使用元組方式 for r in ret: print(r)
回調(diào)函數(shù)
當(dāng)有一個(gè)結(jié)果需要進(jìn)行處理時(shí),都會(huì)綁定一個(gè)回調(diào)函數(shù)來(lái)處理,除非是得到所有結(jié)果之后才做處理,我們使用 把結(jié)果存入列表 遍歷列表 的方式。
回調(diào)函數(shù)效率最高的。
from concurrent.futures import ThreadPoolExecutor from threading import current_thread import time def func(a, b): print(current_thread().ident, 'start', a, b) time.sleep(1) print(current_thread().ident, 'end', a) return a * b if __name__ == '__main__': tp = ThreadPoolExecutor(4) future_d = {} for i in range(20): # 異步非阻塞的 ret = tp.submit(func, a=i, b=i + 1) future_d[i] = ret for key in future_d: # 同步阻塞的 print(key, future_d[key].result())
上述代碼,打印結(jié)果是按照順序(0,1,2,3……),并不是誰(shuí)先結(jié)束就打印誰(shuí)。
使用回調(diào)函數(shù)以后,誰(shuí)先執(zhí)行完就打印誰(shuí),代碼如下:
from concurrent.futures import ThreadPoolExecutor from threading import current_thread import time def func(a, b): print(current_thread().ident, 'start', a, b) time.sleep(1) print(current_thread().ident, 'end', a) return a, a * b def print_func(ret): # 異步阻塞 每個(gè)任務(wù)都是各自阻塞各自,誰(shuí)先執(zhí)行完誰(shuí)先打印 print(ret.result()) if __name__ == '__main__': tp = ThreadPoolExecutor(4) for i in range(20): # 異步非阻塞的 ret = tp.submit(func, a=i, b=i + 1) # [ret0, ret1, ..., ret19] ret.add_done_callback(print_func) # 異步阻塞 [print_func, print_func,...,print_func] # 回調(diào)機(jī)制 # 回調(diào)函數(shù) 給ret對(duì)象綁定一個(gè)回調(diào)函數(shù),等待ret對(duì)應(yīng)的任務(wù)有了結(jié)果之后立即調(diào)用print_func函數(shù) # 就可以對(duì)結(jié)果立即進(jìn)行處理,而不用按照順序接收結(jié)果處理結(jié)果
ret這個(gè)任務(wù)會(huì)在執(zhí)行完畢的瞬間立即觸發(fā)print_func函數(shù),并且把任務(wù)的返回值對(duì)象傳遞到print_func做參數(shù)。
回調(diào)函數(shù)的例子
from concurrent.futures import ThreadPoolExecutor import requests def get_page(url): # 訪問(wèn)網(wǎng)頁(yè),獲取網(wǎng)頁(yè)源代碼,用線程池中的線程來(lái)操作 respone = requests.get(url) if respone.status_code == 200: return {'url': url, 'text': respone.text} def parse_page(res): # 獲取到字典結(jié)果之后,計(jì)算網(wǎng)頁(yè)源代碼的長(zhǎng)度,把'https://www.baidu.com : 長(zhǎng)度值'寫(xiě)到文件里,線程任務(wù)執(zhí)行完畢之后綁定回調(diào)函數(shù) res = res.result() parse_res = 'url:<%s> size:[%s]\n' % (res['url'], len(res['text'])) with open('db.txt', 'a') as f: f.write(parse_res) if __name__ == '__main__': urls = [ 'https://www.baidu.com', 'https://www.python.org', 'https://www.openstack.org', 'https://www.tencent.com/zh-cn', 'http://www.sina.com.cn/' ] tp = ThreadPoolExecutor(4) for url in urls: ret = tp.submit(get_page, url) ret.add_done_callback(parse_page) # 誰(shuí)先回來(lái)誰(shuí)就先把結(jié)果寫(xiě)進(jìn)文件 # 不用回調(diào)函數(shù): # 按照順序獲取網(wǎng)頁(yè),baidu python openstack tencent sina # 也只能按照順序?qū)? # 用上了回調(diào)函數(shù) # 按照順序獲取網(wǎng)頁(yè),baidu python openstack tencent sina # 哪一個(gè)網(wǎng)頁(yè)先返回結(jié)果,就先執(zhí)行哪個(gè)網(wǎng)頁(yè)對(duì)應(yīng)的回調(diào)函數(shù)(parse_page)
進(jìn)程池線程池的應(yīng)用場(chǎng)景
進(jìn)程池:
場(chǎng)景:高計(jì)算的場(chǎng)景,沒(méi)有IO操作(沒(méi)有文件操作,沒(méi)有數(shù)據(jù)庫(kù)操作,沒(méi)有網(wǎng)絡(luò)操作,沒(méi)有input);
進(jìn)程的個(gè)數(shù):[cpu_count*1, cpu_count*2]
線程池:
場(chǎng)景:爬蟲(chóng)
線程的個(gè)數(shù):一般根據(jù)IO的比例定制,cpu_count*5
總結(jié)
到此這篇關(guān)于Python并發(fā)編程實(shí)例教程之線程的文章就介紹到這了,更多相關(guān)Python并發(fā)編程線程內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決tensorflow打印tensor有省略號(hào)的問(wèn)題
今天小編就為大家分享一篇解決tensorflow打印tensor有省略號(hào)的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-02-02Python pydotplus安裝及可視化圖形創(chuàng)建教程
這篇文章主要為大家介紹了Python pydotplus安裝及可視化圖形創(chuàng)建教程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10Python 實(shí)現(xiàn)RSA加解密文本文件
這篇文章主要介紹了Python 實(shí)現(xiàn)RSA加解密文本文件的方法,幫助大家更好的理解和使用python,感興趣的朋友可以了解下2020-12-12PyCharm安裝配置Qt Designer+PyUIC圖文教程
這篇文章主要介紹了PyCharm安裝配置Qt Designer+PyUIC圖文教程,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-05-05django 框架實(shí)現(xiàn)的用戶(hù)注冊(cè)、登錄、退出功能示例
這篇文章主要介紹了django 框架實(shí)現(xiàn)的用戶(hù)注冊(cè)、登錄、退出功能,結(jié)合實(shí)例形式詳細(xì)分析了Django框架用戶(hù)注冊(cè)、登陸、退出等功能具體實(shí)現(xiàn)方法及操作注意事項(xiàng),需要的朋友可以參考下2019-11-11Python實(shí)現(xiàn)蟻群優(yōu)化算法的示例代碼
蟻群算法是一種源于大自然生物世界的新的仿生進(jìn)化算法,本文主要介紹了Python如何實(shí)現(xiàn)蟻群算法,文中通過(guò)示例代碼具有一定的參考價(jià)值,感興趣的小伙伴們可以了解一下2023-08-08python 爬取免費(fèi)簡(jiǎn)歷模板網(wǎng)站的示例
這篇文章主要介紹了python 爬取免費(fèi)簡(jiǎn)歷模板網(wǎng)站的示例,幫助大家更好的理解和使用python 爬蟲(chóng),感興趣的朋友可以了解下2020-09-09