国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開(kāi)APP
userphoto
未登錄

開(kāi)通VIP,暢享免費(fèi)電子書等14項(xiàng)超值服

開(kāi)通VIP
熬了兩個(gè)通宵寫的!終于把多線程和多進(jìn)程徹底講明白了!

我們知道,在一臺(tái)計(jì)算機(jī)中,我們可以同時(shí)打開(kāi)許多軟件,比如同時(shí)瀏覽網(wǎng)頁(yè)、聽(tīng)音樂(lè)、打字等等,看似非常正常。但仔細(xì)想想,為什么計(jì)算機(jī)可以做到這么多軟件同時(shí)運(yùn)行呢?這就涉及到計(jì)算機(jī)中的兩個(gè)重要概念:多進(jìn)程和多線程了。(PS:萬(wàn)字長(zhǎng)文,講得很詳細(xì),建議先收藏再好好看!

同樣,在編寫爬蟲程序的時(shí)候,為了提高爬取效率,我們可能想同時(shí)運(yùn)行多個(gè)爬蟲任務(wù)。這里同樣需要涉及多進(jìn)程和多線程的知識(shí)。

本文,我們就先來(lái)了解一下多線程的基本原理,以及在 Python 中如何實(shí)現(xiàn)多線程。

1. 全局解釋器鎖

全局解釋器鎖 (英語(yǔ):Global Interpreter Lock,縮寫 GIL

計(jì)算機(jī)程序設(shè)計(jì)語(yǔ)言解釋器 用于 同步線程 的一種機(jī)制,它使得任何時(shí)刻僅有 一個(gè)線程 在執(zhí)行,即便在 多核心處理器 上,使用 GIL 的解釋器也只允許同一時(shí)間執(zhí)行一個(gè)線程。常見(jiàn)的使用 GIL 的解釋器有 CPythonRuby MRI。

如果,你對(duì)上面的不理解,也沒(méi)有問(wèn)題。通俗的解釋就是:你電腦是 一核或者多核 ,還是你的代碼寫了了多個(gè)線程,但因?yàn)?GIL 鎖的存在你也就只能運(yùn)行一個(gè)線程,無(wú)法同時(shí)運(yùn)行多個(gè)線程。

接下來(lái),我們來(lái)用個(gè)圖片來(lái)解釋一下:

比如圖中,假如你開(kāi)了兩個(gè)線程(Py thread1 、Py tread2),

  1. 當(dāng)我們線程一(Py thread1)開(kāi)始執(zhí)行時(shí),這個(gè)線程會(huì)去我們的解釋器中申請(qǐng)到一個(gè)鎖。也就是我們的 GIL 鎖;
  2. 然后,解釋器接收到一個(gè)請(qǐng)求的時(shí)候呢,它就會(huì)到我們的 OS 里面,申請(qǐng)我們的系統(tǒng)線程;
  3. 系統(tǒng)統(tǒng)一你的線程執(zhí)行的時(shí)候,就會(huì)在你的 CPU 上面執(zhí)行。(假設(shè)你現(xiàn)在是四核CPU);
  4. 而我們的另一個(gè)線程二(py thread2)也在同步運(yùn)行。
  5. 而線程二在向這個(gè)解釋器申請(qǐng) GIL 的時(shí)候線程二會(huì)卡在這里(Python 解釋器),因?yàn)樗?GIL 鎖已經(jīng)被線程一給拿走了(也就是說(shuō):他要進(jìn)去執(zhí)行,必須拿到這把鎖);
  6. 線程二要運(yùn)行的話,就必須等我們的線程一運(yùn)行完成之后(也就是把我們的 GIL 釋放之后(圖片中的第5步)線程二才能拿到這把鎖);
  7. 當(dāng)線程二拿到這把鎖之后就和線程一的運(yùn)行過(guò)程一樣。
① Create > ② GIL > ③ 申請(qǐng)?jiān)€程(OS) > ④ CPU 執(zhí)行(如果有其他線程,都會(huì)卡在 Python 解釋器的外邊)
這個(gè)鎖其實(shí)是 Python 之父想一勞永逸解決線程的安全問(wèn)題(也就是禁止多線程同時(shí)運(yùn)行)2. 多線程的含義

說(shuō)起多線程,就不得不先說(shuō)什么是線程。然而想要弄明白什么是線程,又不得不先說(shuō)什么是進(jìn)程。

進(jìn)程我們可以理解為是一個(gè)可以獨(dú)立運(yùn)行的程序單位。

比如:

  1. 打開(kāi)一個(gè)瀏覽器,這就開(kāi)啟了一個(gè)瀏覽器進(jìn)程;
  2. 打開(kāi)一個(gè)文本編輯器,這就開(kāi)啟了一個(gè)文本編輯器進(jìn)程。

但一個(gè)進(jìn)程中是可以同時(shí)處理很多事情的。

比如:在瀏覽器中,我們可以在多個(gè)選項(xiàng)卡中打開(kāi)多個(gè)頁(yè)面。

  • 有的頁(yè)面在播放音樂(lè),
  • 有的頁(yè)面在播放視頻,
  • 有的網(wǎng)頁(yè)在播放動(dòng)畫,它們可以同時(shí)運(yùn)行,互不干擾。

為什么能同時(shí)做到同時(shí)運(yùn)行這么多的任務(wù)呢?

這里就需要引出線程的概念了,其實(shí)這一個(gè)個(gè)任務(wù),實(shí)際上就對(duì)應(yīng)著一個(gè)個(gè)線程的執(zhí)行。

而進(jìn)程呢?

它就是線程的集合,進(jìn)程就是由一個(gè)或多個(gè)線程構(gòu)成的,線程是操作系統(tǒng)進(jìn)行運(yùn)算調(diào)度的最小單位,是進(jìn)程中的一個(gè)最小運(yùn)行單元。

比如:

上面所說(shuō)的瀏覽器進(jìn)程,其中的播放音樂(lè)就是一個(gè)線程,播放視頻也是一個(gè)線程,當(dāng)然其中還有很多其他的線程在同時(shí)運(yùn)行,這些線程的并發(fā)或并行執(zhí)行最后使得整個(gè)瀏覽器可以同時(shí)運(yùn)行這么多的任務(wù)。

了解了線程的概念,多線程就很容易理解了,多線程就是一個(gè)進(jìn)程中同時(shí)執(zhí)行多個(gè)線程,前面所說(shuō)的瀏覽器的情景就是典型的多線程執(zhí)行。

3. 并發(fā)和并行

說(shuō)到多進(jìn)程和多線程,這里就需要再講解兩個(gè)概念,那就是并發(fā)和并行。我們知道,一個(gè)程序在計(jì)算機(jī)中運(yùn)行,其底層是處理器通過(guò)運(yùn)行一條條的指令來(lái)實(shí)現(xiàn)的。

3.1 并發(fā)

英文叫作 concurrency。它是指同一時(shí)刻只能有一條指令執(zhí)行,但是多個(gè)線程的對(duì)應(yīng)的指令被快速輪換地執(zhí)行。比如:

一個(gè)處理器,它先執(zhí)行線程 A 的指令一段時(shí)間,再執(zhí)行線程 B 的指令一段時(shí)間,再切回到線程 A 執(zhí)行一段時(shí)間。

由于處理器執(zhí)行指令的速度和切換的速度非常非??欤送耆兄坏接?jì)算機(jī)在這個(gè)過(guò)程中有多個(gè)線程切換上下文執(zhí)行的操作,這就使得宏觀上看起來(lái)多個(gè)線程在同時(shí)運(yùn)行。但微觀上只是這個(gè)處理器在連續(xù)不斷地在多個(gè)線程之間切換和執(zhí)行,每個(gè)線程的執(zhí)行一定會(huì)占用這個(gè)處理器一個(gè)時(shí)間片段,同一時(shí)刻,其實(shí)只有一個(gè)線程在執(zhí)行。

3.2 并行

英文叫作 parallel。它是指同一時(shí)刻,有多條指令在多個(gè)處理器上同時(shí)執(zhí)行,并行必須要依賴于多個(gè)處理器。不論是從宏觀上還是微觀上,多個(gè)線程都是在同一時(shí)刻一起執(zhí)行的。

并行只能在多處理器系統(tǒng)中存在,如果我們的計(jì)算機(jī)處理器只有一個(gè)核,那就不可能實(shí)現(xiàn)并行。

而并發(fā)在單處理器和多處理器系統(tǒng)中都是可以存在的,因?yàn)閮H靠一個(gè)核,就可以實(shí)現(xiàn)并發(fā)。

舉個(gè)例子

比如系統(tǒng)處理器需要同時(shí)運(yùn)行多個(gè)線程。如果系統(tǒng)處理器只有一個(gè)核,那它只能通過(guò)并發(fā)的方式來(lái)運(yùn)行這些線程。如果系統(tǒng)處理器有多個(gè)核,當(dāng)一個(gè)核在執(zhí)行一個(gè)線程時(shí),另一個(gè)核可以執(zhí)行另一個(gè)線程,這樣這兩個(gè)線程就實(shí)現(xiàn)了并行執(zhí)行,當(dāng)然其他的線程也可能和另外的線程處在同一個(gè)核上執(zhí)行,它們之間就是并發(fā)執(zhí)行。具體的執(zhí)行方式,就取決于操作系統(tǒng)的調(diào)度了。

4. 多線程適用場(chǎng)景

在一個(gè)程序進(jìn)程中,有一些操作是比較耗時(shí)或者需要等待的,比如等待數(shù)據(jù)庫(kù)的查詢結(jié)果的返回,等待網(wǎng)頁(yè)結(jié)果的響應(yīng)。如果使用單線程,處理器必須要等到這些操作完成之后才能繼續(xù)往下執(zhí)行其他操作,而這個(gè)線程在等待的過(guò)程中,處理器明顯是可以來(lái)執(zhí)行其他的操作的。如果使用多線程,處理器就可以在某個(gè)線程等待的時(shí)候,去執(zhí)行其他的線程,從而從整體上提高執(zhí)行效率。

像上述場(chǎng)景,線程在執(zhí)行過(guò)程中很多情況下是需要等待的。

比如

網(wǎng)絡(luò)爬蟲就是一個(gè)非常典型的例子,爬蟲在向服務(wù)器發(fā)起請(qǐng)求之后,有一段時(shí)間必須要等待服務(wù)器的響應(yīng)返回,這種任務(wù)就屬于 IO 密集型任務(wù)。對(duì)于這種任務(wù),如果我們啟用多線程,處理器就可以在某個(gè)線程等待的過(guò)程中去處理其他的任務(wù),從而提高整體的爬取效率。

但并不是所有的任務(wù)都是 IO 密集型任務(wù),還有一種任務(wù)叫作計(jì)算密集型任務(wù),也可以稱之為 CPU 密集型任務(wù)。顧名思義,就是任務(wù)的運(yùn)行一直需要處理器的參與。此時(shí)如果我們開(kāi)啟了多線程,一個(gè)處理器從一個(gè)計(jì)算密集型任務(wù)切換到切換到另一個(gè)計(jì)算密集型任務(wù)上去,處理器依然不會(huì)停下來(lái),始終會(huì)忙于計(jì)算,這樣并不會(huì)節(jié)省總體的時(shí)間,因?yàn)樾枰幚淼娜蝿?wù)的計(jì)算總量是不變的。如果線程數(shù)目過(guò)多,反而還會(huì)在線程切換的過(guò)程中多耗費(fèi)一些時(shí)間,整體效率會(huì)變低。

所以,如果任務(wù)不全是計(jì)算密集型任務(wù),我們可以使用多線程來(lái)提高程序整體的執(zhí)行效率。尤其對(duì)于網(wǎng)絡(luò)爬蟲這種 IO 密集型任務(wù)來(lái)說(shuō),使用多線程會(huì)大大提高程序整體的爬取效率。

5. Python 實(shí)現(xiàn)多線程

在 Python 中,實(shí)現(xiàn)多線程的模塊叫作 threading,是 Python 自帶的模塊。下面我們來(lái)了解下使用 threading 實(shí)現(xiàn)多線程的方法。

在具體實(shí)現(xiàn)之前,我們先來(lái)測(cè)試一下多線程與當(dāng)線程裸奔的速度對(duì)比,為了更加直觀,我這里使用把每種線程代碼單獨(dú)寫出來(lái)并做對(duì)比:

單線程裸奔:(這也是一個(gè)主線程(main thread))

import timedef start():    for i in range(1000000):        i += i    return# 不使用任何線程(裸著來(lái))def main():    start_time = time.time()    for i in range(10):        start()    print(time.time()-start_time)if __name__ == '__main__':    main()

輸出:

6.553307056427002

注意:因?yàn)槊颗_(tái)電腦的性能不一樣,所運(yùn)行的結(jié)果也相對(duì)不同(請(qǐng)按實(shí)際情況分析)


接下來(lái)我們寫一個(gè)多線程

我們先創(chuàng)建個(gè)字典 (thread_name_time) 來(lái)存儲(chǔ)我們每個(gè)線程的名稱與對(duì)應(yīng)的時(shí)間

import threading,timedef start():    for i in range(1000000):        i += i    return# # 不使用任何線程(裸著來(lái))# def main():#     start_time = time.time()#     for i in range(10):#         start()#     print(time.time()-start_time)# if __name__ == '__main__':#     main()def main():    start_time = time.time()    thread_name_time = {}# 我們先創(chuàng)建個(gè)字典 (thread_name_time) 用來(lái)來(lái)存儲(chǔ)我們每個(gè)線程的名稱與對(duì)應(yīng)的時(shí)間    for i in range(10):        # 也就是說(shuō),每個(gè)線程順序執(zhí)行        thread = threading.Thread(target=start)# target=寫你要多線程運(yùn)行的函數(shù),不需要加括號(hào)        thread.start()# 上一行開(kāi)啟了線程,這一行是開(kāi)始運(yùn)行(也就是開(kāi)啟個(gè) run)        thread_name_time[i] = thread # 添加數(shù)據(jù)到我們的字典當(dāng)中,這里為什么要用i做key?這是因?yàn)檫@樣方便我們join    for i in range(10):        thread_name_time[i].join()    #     join() 等待線程執(zhí)行完畢(也就是說(shuō)卡在這里,這個(gè)線程執(zhí)行完才會(huì)執(zhí)行下一步)    print(time.time()-start_time)if __name__ == '__main__':    main()

輸出

6.2037984102630615
# 6.553307056427002 裸奔# 6.2037984102630615 單線程順序執(zhí)行# 6.429047107696533 線程并發(fā)

我們可以看到,速度上的區(qū)別不大。

多線程并發(fā)不如單線程順序執(zhí)行快

這是得不償失的

造成這種情況的原因就是 GIL

這里是計(jì)算密集型,所以不適用

在我們執(zhí)行加減乘除或者圖像處理的時(shí)候,都是在從 CPU 上面執(zhí)行才可以。Python 因?yàn)?GIL 存在,同一時(shí)期肯定只有一個(gè)線程在執(zhí)行,這樣這樣就是造成我們開(kāi)是個(gè)線程和一個(gè)線程沒(méi)有太大區(qū)別的原因。

而我們的網(wǎng)絡(luò)爬蟲大多時(shí)候是屬于 IO 密集與計(jì)算機(jī)密集

BIOS:B:Base、I:Input、O:Output、S:System

也就是你電腦一開(kāi)機(jī)的時(shí)候就會(huì)啟動(dòng)。

1. 計(jì)算密集型

在上面的時(shí)候,我們開(kāi)啟了兩個(gè)線程,如果這兩個(gè)線程要同時(shí)執(zhí)行,那同一時(shí)期 CPU 上只有一個(gè)線程在執(zhí)行。

那從上圖可知,那這兩個(gè)線程就需要頻繁的在上下文切換。

Ps:我們這個(gè)綠色表示我們這個(gè)線程正在執(zhí)行,紅色代表阻塞。

所以,我們可以明顯的觀察到,線程的上下文切換也是需要消耗資源的(時(shí)間-ms)不斷的歸還和拿取 GIL 等,切換上下文。明顯造成很大的資源浪費(fèi)。

2. IO 密集型

我們現(xiàn)在假設(shè),有個(gè)服務(wù)器程序(Socket)也就是我們新開(kāi)的一個(gè)程序(也就是我們網(wǎng)絡(luò)爬蟲的最底層)開(kāi)始爬取目標(biāo)網(wǎng)頁(yè)了,我們那個(gè)網(wǎng)頁(yè)呢,有兩個(gè)線程同時(shí)運(yùn)行,我們線程二已經(jīng)請(qǐng)求成功開(kāi)始運(yùn)行了,也就是上圖的 (Thread 2)綠色一條路過(guò)去。

而我們的線程一(Thread 1)- Datagram(這里它開(kāi)啟了一個(gè) UDP),然后等待數(shù)據(jù)建立(也就是等待哪些 HTML、CSS 等數(shù)據(jù)返回)也就是說(shuō),在 Ready to receive(recvfrom)之間都是準(zhǔn)備階段。這樣就是有一段時(shí)間一直阻塞,而我們的線程二可以一直無(wú)停歇也不用切換上下文就一直在運(yùn)行。這樣的 IO 密集型就有很大的好處。

IO 密集型,這樣就把我們等待的時(shí)間計(jì)算進(jìn)去了,節(jié)省了大部分時(shí)間。這里我們需要注意的是,我們的多線程是運(yùn)行在 IO 密集型上的,我們得區(qū)分清楚。

還有就是,資源等待,比如有時(shí)候我們使用瀏覽器發(fā)起了一個(gè) Get 請(qǐng)求,那瀏覽器圖標(biāo)上面在轉(zhuǎn)圈圈的時(shí)候就是我們請(qǐng)求資源等待的時(shí)間,(也就是圖上面的 Datagram 到 Ready to receive )數(shù)據(jù)建立到數(shù)據(jù)接收(就是轉(zhuǎn)圈圈的時(shí)間)。我們完全就不需要執(zhí)行它,就讓它等待就好。這個(gè)時(shí)候讓另一個(gè)線程去執(zhí)行就好

換言之就是:第一個(gè)線程,我們爬取那個(gè)網(wǎng)頁(yè)轉(zhuǎn)圈圈的時(shí)候讓另一個(gè)線程繼續(xù)爬取。這樣就避免了資源浪費(fèi)。(把時(shí)間都利用起來(lái))

注意: 請(qǐng)求資源是不需要 CPU 進(jìn)行計(jì)算的,CPU 參與是很少的,而我們第一個(gè)例子,計(jì)算數(shù)字的 for 循環(huán)中,是需要 CPU 進(jìn)行計(jì)算的。


5.1 Thread 直接創(chuàng)建子線程5.1.1 非守護(hù)線程

復(fù)雜的操作之前需要一個(gè)簡(jiǎn)單的示例開(kāi)始:

import threading, timedef start(): time.sleep(1) print(threading.current_thread().name) # 當(dāng)前線程名稱 print(threading.current_thread().is_alive()) # 當(dāng)前線程狀態(tài) print(threading.current_thread().ident) # 當(dāng)前線程的編號(hào)print('start')# 要使用多線程哪個(gè)函數(shù)>>>target=函數(shù),name=給這個(gè)多線程取個(gè)名字# 如果你不起一個(gè)名字的話,那那它會(huì)自己去起一個(gè)名字的(pid)也就是個(gè) ident# 類似聲明thread = threading.Thread(target=start,name='my first thread')# 每個(gè)線程寫完你不start()的話,就類似只是聲明thread.start()print('stop')# 輸出startstopmy first threadTrue2968

如果有參數(shù)的話,我們就對(duì)多線程參數(shù)進(jìn)行傳參數(shù)。代碼示例:

import threading, timedef start(num):    time.sleep(num)    print(threading.current_thread().name)    print(threading.current_thread().isAlive())    print(threading.current_thread().ident)print('start')thread = threading.Thread(target=start,name='my first thread', args=(1,))thread.start()print('stop')

解析:

我認(rèn)認(rèn)真看一下我們的運(yùn)行結(jié)果,

startstopmy first threadTrue2968

我們會(huì)發(fā)現(xiàn)并不是按我們正常的邏輯執(zhí)行這一系列的代碼。

而是,先執(zhí)行完 start 然后就直接 stop 然后才會(huì)執(zhí)行我們函數(shù)的其他三項(xiàng)。

一個(gè)線程它就直接貫穿到底了。也就是先把我們主線程里面的代碼運(yùn)行完,然后才會(huì)運(yùn)行它里面的代碼。

我們的代碼并不是當(dāng)代碼執(zhí)行到 thread.start() 等它執(zhí)行完再執(zhí)行 print(‘stop’) 。而是,我們線程執(zhí)行到thread.start() 繼續(xù)向下執(zhí)行,同時(shí)再執(zhí)行里面的代碼(也就是start()函數(shù)里面的代碼)。(不會(huì)卡在 thread.start() 那里) 也不會(huì)隨著主線程結(jié)束而結(jié)束

因?yàn)?,程序在?zhí)行到 print(‘stop’) 之后就是主線程結(jié)束,而里面開(kāi)的線程是我們自己開(kāi)的。當(dāng)我們主線程執(zhí)行這個(gè) stop 就已經(jīng)結(jié)束了。

這種不會(huì)隨著主線程結(jié)束而銷毀的,這種線程它叫做:非守護(hù)線程

  1. 主線程會(huì)跳過(guò)創(chuàng)建的線程繼續(xù)執(zhí)行;
  2. 直到創(chuàng)建線程運(yùn)行完畢;
  3. 程序結(jié)束;

既然,有非守護(hù)線程。那就還有守護(hù)線程。不要急,我再舉個(gè)非守護(hù)線程的例子。

首先,我們可以使用 Thread 類來(lái)創(chuàng)建一個(gè)線程,創(chuàng)建時(shí)需要指定 target 參數(shù)為運(yùn)行的方法名稱,如果被調(diào)用的方法需要傳入額外的參數(shù),則可以通過(guò) Thread的 args參數(shù)來(lái)指定。示例如下:

import threading, timedef target(second): print(f'Threading {threading.current_thread().name} is runing') print(f'Threading {threading.current_thread().name} sleep {second}s') time.sleep(second) print(f'Threading {threading.current_thread().name} ended')print(f'Threading {threading.current_thread().name} is runing')for i in [1, 5]: t = threading.Thread(target=target, args=[i]) # t = threading.Thread(target=target, args=(i,)) t.start()print(f'Threading {threading.current_thread().name} is ended')# 輸出Threading MainThread is runingThreading Thread-1 is runingThreading Thread-1 sleep 1sThreading Thread-2 is runingThreading Thread-2 sleep 5sThreading MainThread is endedThreading Thread-1 endedThreading Thread-2 ended

在這里我們首先聲明了一個(gè)方法,叫作 target,它接收一個(gè)參數(shù)為 second,通過(guò)方法的實(shí)現(xiàn)可以發(fā)現(xiàn),這個(gè)方法其實(shí)就是執(zhí)行了一個(gè) time.sleep 休眠操作,second參數(shù)就是休眠秒數(shù),其前后都 print了一些內(nèi)容,其中線程的名字我們通過(guò) threading.current_thread().name 來(lái)獲取出來(lái),如果是主線程的話,其值就是 MainThread,如果是子線程的話,其值就是 Thread-*。

然后我們通過(guò) Thead類新建了兩個(gè)線程,target參數(shù)就是剛才我們所定義的方法名,args以列表的形式傳遞。兩次循環(huán)中,這里 i 分別就是 1 和 5,這樣兩個(gè)線程就分別休眠 1 秒和 5 秒,聲明完成之后,我們調(diào)用 start 方法即可開(kāi)始線程的運(yùn)行。

觀察結(jié)果我們可以發(fā)現(xiàn),這里一共產(chǎn)生了三個(gè)線程,分別是主線程 MainThread和兩個(gè)子線程 Thread-1、Thread-2。另外我們觀察到,主線程首先運(yùn)行結(jié)束,緊接著 Thread-1、Thread-2 才接連運(yùn)行結(jié)束,分別間隔了 1 秒和 4 秒。這說(shuō)明主線程并沒(méi)有等待子線程運(yùn)行完畢才結(jié)束運(yùn)行,而是直接退出了,有點(diǎn)不符合常理。

如果我們想要主線程等待子線程運(yùn)行完畢之后才退出,可以讓每個(gè)子線程對(duì)象都調(diào)用下 join方法,實(shí)現(xiàn)如下:

for i in [1, 5]:    t = threading.Thread(target=target, args=[i])    t.start()    t.join()# 輸出Threading MainThread is runingThreading Thread-1 is runingThreading Thread-1 sleep 1sThreading Thread-1 endedThreading Thread-2 is runingThreading Thread-2 sleep 5sThreading Thread-2 endedThreading MainThread is ended

這樣,主線程必須等待子線程都運(yùn)行結(jié)束,主線程才繼續(xù)運(yùn)行并結(jié)束。

5.2 繼承 Thread 類創(chuàng)建子線程

另外,我們也可以通過(guò)繼承 Thread 類的方式創(chuàng)建一個(gè)線程,該線程需要執(zhí)行的方法寫在類的 run 方法里面即可。上面的例子的等價(jià)改寫為:

import threading, timeclass MyThread(threading.Thread): def __init__(self, second): threading.Thread.__init__(self) self.second = second def run(self): print(f'Threading {threading.current_thread().name} is runing') print(f'Threading {threading.current_thread().name} sleep {self.second}s') time.sleep(self.second) print(f'Threading {threading.current_thread().name} is ended')print(f'Threading {threading.current_thread().name} is runing')for i in [1, 5]: t = MyThread(i) t.start() t.join()print(f'Threading {threading.current_thread().name} is ended')# 輸出Threading MainThread is runingThreading Thread-1 is runingThreading Thread-1 sleep 1sThreading Thread-1 is endedThreading Thread-2 is runingThreading Thread-2 sleep 5sThreading Thread-2 is endedThreading MainThread is ended

可以看到,兩種實(shí)現(xiàn)方式,其運(yùn)行效果是相同的。

5.3 守護(hù)線程

在線程中有一個(gè)叫作守護(hù)線程的概念,如果一個(gè)線程被設(shè)置為守護(hù)線程,那么意味著這個(gè)線程是“不重要”的,這意味著,如果主線程結(jié)束了而該守護(hù)線程還沒(méi)有運(yùn)行完,那么它將會(huì)被強(qiáng)制結(jié)束。在 Python 中我們可以通過(guò) setDaemon方法來(lái)將某個(gè)線程設(shè)置為守護(hù)線程。

如果要修改成守護(hù)線程,那你就得在 thread.start()前面加一個(gè):

需要在我們啟動(dòng)之前設(shè)置。

示例一如下:

添加之前:

import threading, timedef start(num):    time.sleep(num)    print(threading.current_thread().name) # 當(dāng)前線程的名字    print(threading.current_thread().isAlive())    print(threading.current_thread().ident)print('start') # 主線程開(kāi)始thread = threading.Thread(target=start,name='my first thread', args=(1,))# 可以使用 for 循環(huán)來(lái)添加多個(gè)thread.start()print('stop') # 主線程結(jié)束# 運(yùn)行結(jié)果startstopmy first threadTrue15816

添加之后:

import threading, timedef start(num): time.sleep(num) print(threading.current_thread().name) # 當(dāng)前線程的名字 print(threading.current_thread().isAlive()) print(threading.current_thread().ident)print('start') # 主線程開(kāi)始thread = threading.Thread(target=start,name='my first thread', args=(1,))# 可以使用 for 循環(huán)來(lái)添加多個(gè)thread.setDaemon(True) # 在 start 開(kāi)始之前設(shè)置thread.start()print('stop') # 主線程結(jié)束# 運(yùn)行結(jié)果startstop

我們可以看見(jiàn),程序直接運(yùn)行:start、stop,執(zhí)行到 print(‘stop’) 它就結(jié)束了。也就隨著我們的主線程結(jié)束而結(jié)束。并不管它里面還有什么沒(méi)有執(zhí)行完。(也不會(huì)管他里面的 time.sleep())我們的主線程一結(jié)束,我們的守護(hù)線程就會(huì)隨著主線程一起銷毀。

我們?nèi)粘?dòng)的是非守護(hù)線程,守護(hù)線程用的較少。

守護(hù)線程會(huì)伴隨主線程一起結(jié)束,setDaemon設(shè)置為 True 即可。

示例二如下:

添加之前:

import threading, timedef target(second):    print(f'Threading {threading.current_thread().name} is runing')    print(f'Threading {threading.current_thread().name} sleep {second}s')    time.sleep(second)    print(f'Threading {threading.current_thread().name} is ended')print(f'Threading {threading.current_thread().name} is runing')t1 = threading.Thread(target=target, args=[2])t1.start()t2 = threading.Thread(target=target, args=[5])t2.start()print(f'Threading {threading.current_thread().name} is ended')# 運(yùn)行結(jié)果Threading MainThread is runingThreading Thread-1 is runingThreading Thread-1 sleep 2sThreading Thread-2 is runingThreading Thread-2 sleep 5sThreading MainThread is endedThreading Thread-1 is endedThreading Thread-2 is ended

添加之后:

import threading, timedef target(second): print(f'Threading {threading.current_thread().name} is runing') print(f'Threading {threading.current_thread().name} sleep {second}s') time.sleep(second) print(f'Threading {threading.current_thread().name} is ended')print(f'Threading {threading.current_thread().name} is runing')t1 = threading.Thread(target=target, args=[2])t1.start()t2 = threading.Thread(target=target, args=[5])t2.setDaemon(True)t2.start()print(f'Threading {threading.current_thread().name} is ended')# 運(yùn)行結(jié)果Threading MainThread is runingThreading Thread-1 is runingThreading Thread-1 sleep 2sThreading Thread-2 is runingThreading Thread-2 sleep 5sThreading MainThread is endedThreading Thread-1 is ended

在這里我們通過(guò) setDaemon方法將 t2設(shè)置為了守護(hù)線程,這樣主線程在運(yùn)行完畢時(shí),t2線程會(huì)隨著線程的結(jié)束而結(jié)束。

運(yùn)行結(jié)果:

Threading MainThread is runingThreading Thread-1 is runingThreading Thread-1 sleep 2sThreading Thread-2 is runingThreading Thread-2 sleep 5sThreading MainThread is endedThreading Thread-1 is ended

可以看到,我們沒(méi)有看到 Thread-2打印退出的消息,Thread-2 隨著主線程的退出而退出了。

不過(guò)細(xì)心的你可能會(huì)發(fā)現(xiàn),這里并沒(méi)有調(diào)用 join方法,如果我們讓 t1和 t2都調(diào)用 join方法,主線程就會(huì)仍然等待各個(gè)子線程執(zhí)行完畢再退出,不論其是否是守護(hù)線程。

5.4 互斥鎖

接下來(lái)是比較難的知識(shí)點(diǎn),還是從簡(jiǎn)單的知識(shí)點(diǎn)開(kāi)始。

比方說(shuō)我們現(xiàn)在有兩個(gè)線程,一個(gè)是求加一千萬(wàn)次,另一個(gè)是減一千萬(wàn)次。按原本得計(jì)劃來(lái)說(shuō),一個(gè)加一千萬(wàn)一個(gè)減一千萬(wàn)結(jié)果應(yīng)該還是零??墒亲罱K得結(jié)果并不是等于零,我們多運(yùn)行幾次會(huì)發(fā)現(xiàn)幾次得出來(lái)得結(jié)果并不相同。多線程代碼如下:

import threadingimport timenumber = 0def addNumber(i): time.sleep(i) global number for i in range(1000000): number += 1 print('加',number)def downNumber(i): time.sleep(i) global number for i in range(1000000): number -= 1 print('減',number)print('start') # 輸出一個(gè)開(kāi)始thread = threading.Thread(target = addNumber, args=(2,)) #開(kāi)啟一個(gè)線程(聲明)thread2 = threading.Thread(target = downNumber, args=(2,)) # 開(kāi)啟第二個(gè)線程(聲明)thread.start() # 開(kāi)始thread2.start() # 開(kāi)始thread.join()thread2.join()# join 阻塞在這里,直到我們得阻塞線程執(zhí)行完畢才會(huì)向下執(zhí)行print('外', number)print('stop')

就算單線程也會(huì)出現(xiàn)兩個(gè)值:1000000 與 -1000000,兩個(gè)函數(shù)誰(shuí)先運(yùn)行就是輸出誰(shuí)的結(jié)果,為什么呢?因?yàn)閮蓚€(gè)函數(shù)調(diào)用的是全局變量 number 所以,如果先運(yùn)行加法函數(shù),加法得到的結(jié)果是 1000000 ,那全局下的 number 的值也會(huì)變成:1000000 ,那減法的操作亦然就是 0。反過(guò)來(lái)也是一個(gè)意思。代碼如下:

import threadingimport timenumber = 0def addNumber(i = None):    # time.sleep(i)    global number    for i in range(1000000):        number += 1    print('加',number)def downNumber(i = None):    # time.sleep(i)    global number    for i in range(1000000):        number -= 1    print('減',number)addNumber()downNumber()print(number)# 運(yùn)行結(jié)果加 1000000減 00# 反過(guò)來(lái)運(yùn)行downNumber()addNumber()print(number)# 運(yùn)行結(jié)果減 -1000000加 00# 再來(lái)一個(gè)差不多的例子:import threadingimport timenumber = 0def addNumber():    global number    for i in range(1000000):        number += 1    print('加',number)    return numberdef downNumber():    global number    for i in range(1000000):        number -= 1    print('減',number)    return numbersum_num = downNumber() + addNumber()print('Result', sum_num)# 輸出減 -1000000加 0Result -1000000# 修改以下代碼,其他不變:sum_num = addNumber() + downNumber()# 輸出加 1000000減 0Result 1000000

由上面的多線程代碼,我可以發(fā)現(xiàn)結(jié)果:兩個(gè)線程操作同一個(gè)數(shù)字,最后得到的數(shù)字是混亂的。為什么說(shuō)是混亂的呢?

我們現(xiàn)在所要做的是一個(gè)賦值,number += 1 其實(shí)也就是 number = number + 1,的這個(gè)操作。而在我們的 Python 當(dāng)中,我們是先:計(jì)算右邊的,然后賦值給左邊的,一共兩步。

我先來(lái)看一下正確的運(yùn)行流程:

# 我們的 number = 0# 第一步是先運(yùn)行我們的代碼:a = number + 1 # 等價(jià)于 0+1=1 # 也就是先運(yùn)行右邊的,然后賦值給 anumber = a # 然后,再把 a 的結(jié)果賦值個(gè) number# 上面運(yùn)行完加法之后,我們加下來(lái)運(yùn)行減肥的操作。b = number - 1 # 等價(jià)于 1-1 = 0# 然后,賦值個(gè) number# 最后 number 等于 0number = 0

上面的過(guò)成是正確的流程,可在多線程里面呢?

number = 0 # 開(kāi)始初始值 0a = number+1 # 等價(jià)于 0+1=1# 這個(gè)地方要注意?。?!# 在運(yùn)行完上面一步的時(shí)候,還沒(méi)來(lái)得急把結(jié)果賦值給 number# 就開(kāi)始運(yùn)行減法操作:b = number-1 # 等價(jià)于 0-1=-1# 然后,這兩個(gè)運(yùn)行結(jié)束之后就被賦值:number=b # b = -1number=a # a = 1# 最終得結(jié)果為:number = 1

上面就是我們剛才結(jié)果錯(cuò)亂得原因,也就是說(shuō):我們計(jì)算和賦值是兩部分,但是該多線程它沒(méi)有順序執(zhí)行,這也就是我們所說(shuō)的線程不安全。

因?yàn)?,?zhí)行太快了,兩個(gè)線程交互交織在一起,最終得到我們這個(gè)錯(cuò)誤結(jié)果。以上就是線程不安全的問(wèn)題。

這就是需要 Lock 鎖,給它上一把鎖,來(lái)達(dá)到我們 number 的效果,這個(gè)時(shí)候?yàn)榱吮苊忮e(cuò)誤,我們要給他上一把鎖了。再給你講解上鎖之前呢,接下來(lái),我們來(lái)講一點(diǎn)復(fù)雜的例子:

在一個(gè)進(jìn)程中的多個(gè)線程是共享資源的

比如

在一個(gè)進(jìn)程中,有一個(gè)全局變量 count 用來(lái)計(jì)數(shù),現(xiàn)在我們聲明多個(gè)線程,每個(gè)線程運(yùn)行時(shí)都給 count加 1,讓我們來(lái)看看效果如何,代碼實(shí)現(xiàn)如下:

import threading, timecount = 0class MyThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): global count temp = count + 1 time.sleep(0.001) count = tempthreads = []for _ in range(1000): thread = MyThread() thread.start() threads.append(thread)for thread in threads: thread.join()# print(len(threads))print(f'Final count: {count}')

在這里,我們聲明了 1000 個(gè)線程,每個(gè)線程都是現(xiàn)取到當(dāng)前的全局變量 count 值,然后休眠一小段時(shí)間,然后對(duì) count 賦予新的值。

那這樣,按照常理來(lái)說(shuō),最終的 count 值應(yīng)該為 1000。但其實(shí)不然,我們來(lái)運(yùn)行一下看看。

運(yùn)行結(jié)果如下:

Final count: 69

最后的結(jié)果居然只有 69,而且多次運(yùn)行或者換個(gè)環(huán)境運(yùn)行結(jié)果是不同的。

這是為什么呢?

因?yàn)?count 這個(gè)值是共享的,每個(gè)線程都可以在執(zhí)行 temp = count 這行代碼時(shí)拿到當(dāng)前 count 的值,但是這些線程中的一些線程可能是并發(fā)或者并行執(zhí)行的,這就導(dǎo)致不同的線程拿到的可能是同一個(gè) count 值,最后導(dǎo)致有些線程的 count 的加 1 操作并沒(méi)有生效,導(dǎo)致最后的結(jié)果偏小。

所以,如果多個(gè)線程同時(shí)對(duì)某個(gè)數(shù)據(jù)進(jìn)行讀取或修改,就會(huì)出現(xiàn)不可預(yù)料的結(jié)果。為了避免這種情況,我們需要對(duì)多個(gè)線程進(jìn)行同步,要實(shí)現(xiàn)同步,我們可以對(duì)需要操作的數(shù)據(jù)進(jìn)行加鎖保護(hù),這里就需要用到 threading.Lock了。

加鎖保護(hù)是什么意思呢?

就是說(shuō),某個(gè)線程在對(duì)數(shù)據(jù)進(jìn)行操作前,需要先加鎖,這樣其他的線程發(fā)現(xiàn)被加鎖了之后,就無(wú)法繼續(xù)向下執(zhí)行,會(huì)一直等待鎖被釋放,只有加鎖的線程把鎖釋放了,其他的線程才能繼續(xù)加鎖并對(duì)數(shù)據(jù)做修改,修改完了再釋放鎖。這樣可以確保同一時(shí)間只有一個(gè)線程操作數(shù)據(jù),多個(gè)線程不會(huì)再同時(shí)讀取和修改同一個(gè)數(shù)據(jù),這樣最后的運(yùn)行結(jié)果就是對(duì)的了。

我們可以將代碼修改為如下內(nèi)容:

示例一的修改:

import threadingimport timelock = threading.Lock() # 創(chuàng)建一個(gè)最簡(jiǎn)單的 讀寫鎖number = 0def addNumber(): global number for i in range(1000000): lock.acquire() # 先獲取 number += 1 # 中間的這個(gè)過(guò)程讓他強(qiáng)制有這個(gè)計(jì)算和賦值的過(guò)程,也就是讓他執(zhí)行完這兩個(gè)操作,后再切換。 # 這樣就不會(huì)完成計(jì)算后,還沒(méi)來(lái)的及賦值就跑到下一個(gè)去了。 # 這樣也就防止了線程不安全的情況 lock.release() # 再釋放def downNumber(): global number for i in range(1000000): lock.acquire() number -= 1 lock.release()print('start') # 輸出一個(gè)開(kāi)始thread = threading.Thread(target = addNumber) #開(kāi)啟一個(gè)線程(聲明)thread2 = threading.Thread(target = downNumber) # 開(kāi)啟第二個(gè)線程(聲明)thread.start() # 開(kāi)始thread2.start() # 開(kāi)始thread.join()thread2.join()# join 阻塞在這里,直到我們得阻塞線程執(zhí)行完畢才會(huì)向下執(zhí)行print('外', number)print('stop')# 輸出start外 0stop

在代碼:lock.acquire() 與 lock.release() 中間的這個(gè)過(guò)程讓它強(qiáng)制有這個(gè)計(jì)算和賦值的過(guò)程,也就是讓他執(zhí)行完這兩個(gè)操作,后再切換。這樣就不會(huì)完成計(jì)算后,還沒(méi)來(lái)的及賦值就跑到下一個(gè)去了。這樣也就防止了線程不安全的情況。

然后,就是我們第一個(gè)線程拿到這把鎖的 lock.acquire() 了,那另一個(gè)線程就會(huì)在 lock.acquire() 阻塞了,直到我們另一個(gè)線程把 lock.release() 鎖釋放,然后拿到鎖執(zhí)行,就這樣不斷地切換拿鎖執(zhí)行。

死鎖:就是前面的線程拿到鎖之后,運(yùn)行完卻不釋放鎖,下一個(gè)線程在等待前一個(gè)線程釋放鎖,這種就是死鎖。說(shuō)的直白一點(diǎn)就是,相互等待。就像照鏡子一樣,你中有我,我中有你。也就是在沒(méi)有 release 的這種情況。(你等我表白,我等你表白)

示例二的加鎖

import threading, timecount = 0class MyThread(threading.Thread):    def __init__(self):        threading.Thread.__init__(self)    def run(self):        global count        lock.acquire() # 獲取鎖        temp = count + 1        time.sleep(0.001)        count = temp        lock.release() # 釋放鎖lock = threading.Lock()threads = []for _ in range(1000):    thread = MyThread()    thread.start()    threads.append(thread)for thread in threads:    thread.join()print(f'Final count: {count}')

在這里我們聲明了一個(gè) lock 對(duì)象,其實(shí)就是 threading.Lock 的一個(gè)實(shí)例,然后在 run 方法里面,獲取 count 前先加鎖,修改完 count 之后再釋放鎖,這樣多個(gè)線程就不會(huì)同時(shí)獲取和修改 count 的值了。

運(yùn)行結(jié)果如下:

Final count: 1000

這樣運(yùn)行結(jié)果就正常了。

關(guān)于 Python 多線程的內(nèi)容,這里暫且先介紹這些,關(guān)于 theading更多的使用方法,如信號(hào)量、隊(duì)列等,可以參考官方文檔:https://docs.python.org/zh-cn/3.7/library/threading.html#module-threading。

5.5 遞歸鎖 RLOCK

再次復(fù)用,一個(gè)鎖可以再嵌套一個(gè)鎖。向我們上面的普通鎖,一個(gè)線程里面,你只能獲取一次。如果獲取第二次就會(huì)報(bào)錯(cuò)。

遞歸鎖什么時(shí)候用呢?需要更低精度的,力度更小,為了更小的力度。

import threadingimport timeclass Test:    rlock = threading.RLock()    def __init__(self):        self.number = 0    def execute(self, n):        # 原本是獲取鎖和釋放鎖,那如果有時(shí)候你忘記了寫 lock.release() 那就變成了死鎖。        # 而 with 可以解決這個(gè)問(wèn)題。        with Test.rlock:            # with 內(nèi)部有個(gè)資源釋放的機(jī)制            self.number += n    def add(self):        with Test.rlock:            self.execute(1)    def down(self):        with Test.rlock:            self.execute(-1)def add(test):    for i in range(1000000):        test.add()def down(test):    for i in range(1000000):        test.down()if __name__ == '__main__':    thread = Test() # 實(shí)例化    t1 = threading.Thread(target=add, args=(thread,))    t2 = threading.Thread(target=down, args=(thread,))    t1.start()    t2.start()    t1.join()    t2.join()    print(t.number)

我們會(huì)發(fā)現(xiàn)這個(gè)遞歸鎖是比較耗費(fèi)時(shí)間的,也就死我們獲取鎖與釋放鎖都是進(jìn)行上下文切換導(dǎo)致資源消耗的,所以說(shuō)開(kāi)啟的鎖越多,所耗費(fèi)的資源也就越多,程序的運(yùn)行速度也就越慢。一些大的工程很少上這么多的鎖,因?yàn)檫@個(gè)鎖的速度會(huì)拖慢你整個(gè)程序的運(yùn)行速度。所以得思考好,用不用這些東西。

5.6 Python 多線程的問(wèn)題

由于 Python 中 GIL 的限制,導(dǎo)致不論是在單核還是多核條件下,在同一時(shí)刻只能運(yùn)行一個(gè)線程,導(dǎo)致 Python 多線程無(wú)法發(fā)揮多核并行的優(yōu)勢(shì)。

GIL 全稱為 Global Interpreter Lock,中文翻譯為全局解釋器鎖,其最初設(shè)計(jì)是出于數(shù)據(jù)安全而考慮的。

在 Python 多線程下,每個(gè)線程的執(zhí)行方式如下:

  • 獲取 GIL
  • 執(zhí)行對(duì)應(yīng)線程的代碼
  • 釋放 GIL

可見(jiàn),某個(gè)線程想要執(zhí)行,必須先拿到 GIL,我們可以把 GIL 看作是通行證,并且在一個(gè) Python 進(jìn)程中,GIL 只有一個(gè)。拿不到通行證的線程,就不允許執(zhí)行。這樣就會(huì)導(dǎo)致,即使是多核條件下,一個(gè) Python 進(jìn)程下的多個(gè)線程,同一時(shí)刻也只能執(zhí)行一個(gè)線程。

不過(guò)對(duì)于爬蟲這種 IO 密集型任務(wù)來(lái)說(shuō),這個(gè)問(wèn)題影響并不大。而對(duì)于計(jì)算密集型任務(wù)來(lái)說(shuō),由于 GIL 的存在,多線程總體的運(yùn)行效率相比可能反而比單線程更低。

5.7 避免 GIL

前面開(kāi)頭已經(jīng)提到:因?yàn)?GIL 的存在,所以不管我們開(kāi)了多少線程,同一時(shí)間始終只有一個(gè)線程在執(zhí)行。那我們?cè)撊绾伪苊?GIL 呢?

那這樣的話,我們不開(kāi)線程不就行,(它的的存在已經(jīng)無(wú)法避免,那我們選擇不使用它不就相當(dāng)于不存在嘛)。那這是,你會(huì)想:那不開(kāi)線程我們開(kāi)啥呢?

問(wèn)的好!

我們來(lái)開(kāi):進(jìn)程,那怎么說(shuō)?別急!請(qǐng)聽(tīng)我細(xì)細(xì)道來(lái)。

比方你有 3 個(gè) CPU(當(dāng)然,你可能有更多,這里就按 3 個(gè) CPU來(lái)為例子),那我們就開(kāi) 3 個(gè)進(jìn)程就好。一個(gè) CPU 上運(yùn)行就好。

Ps:我們的進(jìn)程是可以同時(shí)運(yùn)行的。

我們可以看一下下面的圖片:

任務(wù)管理器

我們 任務(wù)管理 上的每一項(xiàng)都是一個(gè)進(jìn)程。

多進(jìn)程比多線程不好的地方是什么呢?

多進(jìn)程的創(chuàng)建和銷毀開(kāi)銷也會(huì)更大,成本高。

你可能線程可以開(kāi)許多的線程,但你的進(jìn)程就是看你的 CPU 數(shù)量。

進(jìn)程間無(wú)法看到對(duì)方數(shù)據(jù),需要使用?;蛘哧?duì)列進(jìn)行獲取。

每個(gè)進(jìn)程之間都是獨(dú)立的。

就好像我們上面的谷歌瀏覽器和我們的 Pycharm 是沒(méi)有任何關(guān)系的,谷歌瀏覽器上面的數(shù)據(jù)肯定不可能讓 Pycharm 看到。這就是我們所說(shuō)的進(jìn)程之間的獨(dú)立性。

如果你想要一個(gè)進(jìn)行抓取數(shù)據(jù),一個(gè)進(jìn)行調(diào)用數(shù)據(jù),那這時(shí)是不能直接調(diào)用的,需要你自己定義個(gè)結(jié)構(gòu)才能使用。>>> 編程復(fù)雜度提升。


學(xué)員問(wèn)題:任務(wù)管理器上面超過(guò)五六個(gè)進(jìn)程。都是進(jìn)程的話,怎么能開(kāi)那么多呢?

答:我們一個(gè) CPU 不止能執(zhí)行一個(gè)進(jìn)程,就比如我的一個(gè) CPU 里面密麻麻有許多進(jìn)程。(比方我現(xiàn)在開(kāi)六個(gè)進(jìn)程)并發(fā)執(zhí)行的。只不過(guò)計(jì)算機(jī)執(zhí)行的速度非常快,這里我簡(jiǎn)單講一下哈。這是計(jì)算機(jī)原理的課。

不管是任何操作系統(tǒng),現(xiàn)在就拿單核操作系統(tǒng)來(lái)說(shuō):我們假設(shè)現(xiàn)在只有一個(gè) CPU ,一個(gè) CPU 里面六個(gè)進(jìn)程,同一時(shí)間它只有一個(gè)進(jìn)程在運(yùn)行。不過(guò)我們計(jì)算執(zhí)行速度非??欤@個(gè)程序執(zhí)行完,它就會(huì)執(zhí)行一個(gè)上下文切換,執(zhí)行下一個(gè)。(因?yàn)?,它?zhí)行的速度非???,你就會(huì)感覺(jué)是并發(fā)執(zhí)行一樣。)

實(shí)際上,一個(gè) CPU 同一時(shí)間只有一個(gè)進(jìn)程在執(zhí)行,一個(gè)進(jìn)程里面它只有一個(gè)線程在執(zhí)行。(當(dāng)然,這個(gè)單核是五六年前了?,F(xiàn)在肯定至少有雙核。

那就說(shuō)有第二個(gè) CPU 了。

而第二個(gè)和 CPU 上面又有許多個(gè) 進(jìn)程,兩個(gè) CPU 是互不相干。

那這時(shí)候,第一個(gè) CPU 上面運(yùn)行一個(gè)進(jìn)程,而我們的第二個(gè) CPU 上面也有一個(gè)進(jìn)程,兩個(gè)是互補(bǔ)相干。(就相當(dāng)于你開(kāi)了兩臺(tái)電腦。)

但是同一個(gè) CPU 在同一時(shí)間只有一個(gè)就進(jìn)程。(不管你(電腦)速度多么快,實(shí)際上本質(zhì)上(在那一秒)只有一個(gè)進(jìn)程在執(zhí)行。如果你是雙核,那就有兩個(gè)進(jìn)程。(四核就有四個(gè)進(jìn)程)

Python 有個(gè)不好的地方,剛剛上面講到,如果我們有兩個(gè) CPU 那就有兩個(gè)進(jìn)程在執(zhí)行(那四個(gè) CPU 就是四個(gè)進(jìn)程在執(zhí)行),但是因?yàn)?Python 當(dāng)中存在著 GIL,它即使有四個(gè) CPU 每次也只有一個(gè)線程能進(jìn)去,也就是說(shuō):同一時(shí)間當(dāng)中,一個(gè) CPU 上的一個(gè)進(jìn)程中的一個(gè)線程在執(zhí)行。剩下的都不能運(yùn)行,我們的 Python 不能利用多核。

如果,大家用的是 C、Java、Go 這種的就沒(méi)有這個(gè)說(shuō)法了。

本站僅提供存儲(chǔ)服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊舉報(bào)。
打開(kāi)APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
python多線程詳解
Python中的多進(jìn)程與多線程:新手簡(jiǎn)介
python之線程、進(jìn)程和協(xié)程
Python學(xué)習(xí)教程_Python學(xué)習(xí)路線:Day13-進(jìn)程和線程
【一分鐘知識(shí)】進(jìn)程與線程,Thread的sleep與wait
python線程筆記
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長(zhǎng)圖 關(guān)注 下載文章
綁定賬號(hào)成功
后續(xù)可登錄賬號(hào)暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服