python多線程比單線程效率低的原因及其解決方案
python多線程比單線程效率低的原因
Python語(yǔ)言的標(biāo)準(zhǔn)實(shí)現(xiàn)叫作CPython,它分兩步來(lái)運(yùn)行Python程序
步驟1:解析源代碼文本,并將其編譯成字節(jié)碼(bytecode)
- 字節(jié)碼是一種底層代碼,可以把程序表示成8位的指令
- 從Python 3.6開(kāi)始,這種底層代碼實(shí)際上已經(jīng)變成16位了
步驟2:CPython采用基于棧的解釋器來(lái)運(yùn)行字節(jié)碼。
- 字節(jié)碼解釋器在執(zhí)行Python程序的過(guò)程中,必須確保相關(guān)的狀態(tài)不受干擾,
- CPython會(huì)用一種叫作全局解釋器鎖(global interpreter lock,GIL)的機(jī)制來(lái)實(shí)現(xiàn)運(yùn)行的python程序的相關(guān)狀態(tài)不受干擾
GIL
GIL實(shí)際上就是一種互斥鎖(mutual-exclusion lock,mutex),用來(lái)防止CPython的狀態(tài)在搶占式的多線程環(huán)境(preemptive multithreading)之中受到干擾,因?yàn)樵谶@種環(huán)境下,一條線程有可能突然打斷另一條線程搶占程序的控制權(quán)。如果這種搶占行為來(lái)得不是時(shí)候,那么解釋器的狀態(tài)(例如為垃圾回收工作而設(shè)立的引用計(jì)數(shù)等)就會(huì)遭到破壞。
CPython要通過(guò)GIL阻止這樣的動(dòng)作,以確保它自身以及它的那些C擴(kuò)展模塊能夠正確地執(zhí)行每一條字節(jié)碼指令。
GIL會(huì)產(chǎn)生一個(gè)很不好的影響。在C++與Java這樣的語(yǔ)言里面,如果程序之中有多個(gè)線程能夠分頭執(zhí)行任務(wù),那么就可以把CPU的各個(gè)核心充分地利用起來(lái)。盡管Python也支持多線程,但這些線程受GIL約束,所以每次或許只能有一條線程向前推進(jìn),而無(wú)法實(shí)現(xiàn)多頭并進(jìn)。
所以,想通過(guò)多線程做并行計(jì)算或是給程序提速的開(kāi)發(fā)者,恐怕要失望了。
- 并發(fā) concurrency : 指計(jì)算機(jī)似乎能在同一時(shí)刻做許多不同的事情
- 并行 parallelism : 指計(jì)算機(jī)確實(shí)能夠在同一時(shí)刻做許多不同的事情
多線程下的線程執(zhí)行
- 獲取GIL
- 執(zhí)行代碼直到sleep或者是 python虛擬機(jī)將其掛起。
- 釋放 GIL
多線程效率低于單線程原因
如上我們可以知道,在 python中想要某個(gè)線程要執(zhí)行必須先拿到 GIL這把鎖,且 python只有一個(gè) GIL,拿到這個(gè) GIL才能進(jìn)入 CPU執(zhí)行, 在遇到 I/O操作時(shí)會(huì)釋放這把鎖。如果是純計(jì)算的程序,沒(méi)有 I/O 操作,解釋器會(huì)每隔 100次操作就釋放這把鎖,讓別的線程有機(jī)會(huì) 執(zhí)行(這個(gè)次數(shù)可以通sys.setcheckinterval來(lái)調(diào)整)。所以雖然 CPython 的線程庫(kù)直接封裝操作系統(tǒng)的原生線程,但 CPython 進(jìn)程做為一個(gè)整體,同一時(shí)間只會(huì)有一個(gè)獲得了 GIL 的線程在跑,其它的線程都處于等待狀態(tài)等著 GIL 的釋放。
而每次釋放 GIL鎖,線程進(jìn)行鎖競(jìng)爭(zhēng)、切換線程,會(huì)消耗資源。并且由于 GIL鎖存在,python里一個(gè)進(jìn)程永遠(yuǎn)只能同時(shí)執(zhí)行一個(gè)線程 (拿到 GIL的線程才能執(zhí)行 ),這就是為什么在多核 CPU上, python的多線程效率并不高
多線程效率低于或高于單線程原因
相同的代碼,為何有時(shí)候多線程會(huì)比單線程慢,有時(shí)又會(huì)比單線程快? 這主要跟運(yùn)行的代碼有關(guān):
CPU密集型代碼(各種循環(huán)處理、計(jì)數(shù)等等 ),在這種情況下,由于計(jì)算工作多, ticks計(jì)數(shù)很快就會(huì)達(dá)到 100閾值,然后觸發(fā) GIL的釋放與再競(jìng)爭(zhēng) (多個(gè)線程來(lái)回切換當(dāng)然是需要消耗資源的),所以 python下的多線程遇到 CPU密集型代碼時(shí),單線程比多線程效率高。
IO密集型代碼 (文件處理、網(wǎng)絡(luò)爬蟲(chóng)等 ),多線程能夠有效提升效率單線程下有 IO操作會(huì)進(jìn)行 IO等待,造成不必要的時(shí)間浪費(fèi)。開(kāi)啟多線程能在線程 A等待時(shí),自動(dòng)切換到線程 B,可以不浪費(fèi) CPU的資源,從而能提升程序執(zhí)行效率 。進(jìn)行IO密集型的時(shí)候可以進(jìn)行分時(shí)切換 所有這個(gè)時(shí)候多線程快過(guò)單線程
如果python想充分利用多核 CPU,可以采用多進(jìn)程
每個(gè)進(jìn)程有各自獨(dú)立的 GIL,互不干擾,這樣就可以真正意義上的并行執(zhí)行。
在 python中,多進(jìn)程的執(zhí)行效率優(yōu)于多線程 (僅僅針對(duì)多核 CPU而言 )。所以在多核 CPU下,想做并行提升效率,比較通用的方法是使用多進(jìn)程,能夠有效提高執(zhí)行效率
代碼示例:
# 多線程
# 最后完成的線程的耗時(shí)
# [TIME MEASURE] execute function: gene_1000_field took 3840.604ms
@time_measure
def mult_thread(rows):
# 總行數(shù)
rows = rows
# 線程數(shù)
batch_size = 4
cell = math.ceil(rows / batch_size)
# 處理數(shù)據(jù)生成
print('數(shù)據(jù)生成中,線程數(shù):' + str(batch_size))
threads = []
for i in range(batch_size):
starts = i * cell
ends = (i + 1) * cell
file = f"my_data_{str(i)}.csv"
# t = threading.Thread(target=gene_1000_field_test, args=(starts, ends, file))
t = threading.Thread(target=gene_1000_field, args=(starts, ends, file))
t.start()
threads.append(t)
# for t in threads:
# t.join()# 多進(jìn)程
# [TIME MEASURE] execute function: gene_1000_field took 1094.776ms
# 執(zhí)行時(shí)間和單個(gè)線程的執(zhí)行時(shí)間差不多,目的達(dá)到
@time_measure
def mult_process(rows):
# 總行數(shù)
rows = rows
# 線程數(shù)
batch_size = 4
cell = math.ceil(rows / batch_size)
# 處理數(shù)據(jù)生成
print('數(shù)據(jù)生成中,線程數(shù):' + str(batch_size))
process = []
for i in range(batch_size):
starts = i * cell
ends = (i + 1) * cell
file = f"my_data_{str(i)}.csv"
# p = Process(target=f, args=('bob',))
# p.start()
# p_lst.append(p)
# t = threading.Thread(target=gene_1000_field_test, args=(starts, ends, file))
p = Process(target=gene_1000_field, args=(starts, ends, file))
p.start()
process.append(p)python中多線程與單線程的對(duì)比
# 做一個(gè)簡(jiǎn)單的爬蟲(chóng):
import threading
import time
import functools
from urllib.request import urlopen
# 寫一個(gè)時(shí)間函數(shù)的裝飾器
def timeit(f):
@functools.wraps(f)
def wrapper(*args,**kwargs):
start_time=time.time()
res=f(*args,**kwargs)
end_time=time.time()
print("%s函數(shù)運(yùn)行時(shí)間:%.2f" % (f.__name__, end_time - start_time))
return res
return wrapper
def get_addr(ip):
url="http://ip-api.com/json/%s"%(ip)
urlobj=urlopen(url)
# 服務(wù)端返回的頁(yè)面信息, 此處為字符串類型
pagecontent=urlobj.read().decode('utf-8')
# 2. 處理Json數(shù)據(jù)
import json
# 解碼: 將json數(shù)據(jù)格式解碼為python可以識(shí)別的對(duì)象;
dict_data = json.loads(pagecontent)
print("""
ip : %s
所在城市: %s
所在國(guó)家: %s
""" % (ip, dict_data['city'], dict_data['country']))
#不使用多線程
@timeit
def main1():
ips = ['12.13.14.%s' % (i + 1) for i in range(10)]
for ip in ips:
get_addr(ip)
# 多線程的方法一
@timeit
def main2():
ips=['12.13.14.%s'%(i+1) for i in range(10)]
threads=[]
for ip in ips:
t=threading.Thread(target=get_addr,args=(ip,))
threads.append(t)
t.start()
[thread.join() for thread in threads]
# 多線程的方法二
class MyThread(threading.Thread):
def __init__(self, ip):
super(MyThread, self).__init__()
self.ip = ip
def run(self):
url = "http://ip-api.com/json/%s" % (self.ip)
urlObj = urlopen(url)
# 服務(wù)端返回的頁(yè)面信息, 此處為字符串類型
pageContent = urlObj.read().decode('utf-8')
# 2. 處理Json數(shù)據(jù)
import json
# 解碼: 將json數(shù)據(jù)格式解碼為python可以識(shí)別的對(duì)象;
dict_data = json.loads(pageContent)
print("""
%s
所在城市: %s
所在國(guó)家: %s
""" % (self.ip, dict_data['city'], dict_data['country']))
@timeit
def main3():
ips = ['12.13.14.%s' % (i + 1) for i in range(10)]
threads = []
for ip in ips:
t = MyThread(ip)
threads.append(t)
t.start()
[thread.join() for thread in threads]
if __name__ == '__main__':
main1()
main2()
main3()---->輸出:
# main1函數(shù)運(yùn)行時(shí)間:55.06
# main2函數(shù)運(yùn)行時(shí)間:5.64
# main3函數(shù)運(yùn)行時(shí)間:11.06
由次可以看出多線程確實(shí)速度快了很多,然而這只是適合I/O密集型,當(dāng)計(jì)算密集型中cpu一直在占用的時(shí)候,多線程反而更慢。
下面舉例
import threading
import time
def my_counter():
i = 1
for count in range(200000000):
i = i + 2*count
return True
# 采用單線程
@timeit
def main1():
thread_array = {}
for tid in range(2):
t = threading.Thread(target=my_counter)
t.start()
t.join()
# 采用多線程
@timeit
def main2():
thread_array = {}
for tid in range(2):
t = threading.Thread(target=my_counter)
t.start()
thread_array[tid] = t
for i in range(2):
thread_array[i].join()
if __name__ == '__main__':
main1()
main2()----->輸出:
main1函數(shù)運(yùn)行時(shí)間:27.57
main2函數(shù)運(yùn)行時(shí)間:28.19
這個(gè)時(shí)候就能體現(xiàn)出來(lái)多線程適應(yīng)的場(chǎng)景
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
python鏈接sqlite數(shù)據(jù)庫(kù)的詳細(xì)代碼實(shí)例
SQLite數(shù)據(jù)庫(kù)是一款非常小巧的嵌入式開(kāi)源數(shù)據(jù)庫(kù)軟件,也就是說(shuō)沒(méi)有獨(dú)立的維護(hù)進(jìn)程,所有的維護(hù)都來(lái)自于程序本身,它是遵守ACID的關(guān)聯(lián)式數(shù)據(jù)庫(kù)管理系統(tǒng),它的設(shè)計(jì)目標(biāo)是嵌入式的,而且目前已經(jīng)在很多嵌入式產(chǎn)品中使用了它,它占用資源非常的低2021-09-09
Python異步爬蟲(chóng)requests和aiohttp中代理IP的使用
本文主要介紹了Python異步爬蟲(chóng)requests和aiohttp中代理IP的使用,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
Python使用sigthief簽發(fā)證書(shū)的實(shí)現(xiàn)步驟
Windows 系統(tǒng)中的一些非常重要文件通常會(huì)被添加數(shù)字簽名,其目的是用來(lái)防止被篡改,能確保用戶通過(guò)互聯(lián)網(wǎng)下載時(shí)能確信此代碼沒(méi)有被非法篡改和來(lái)源可信,從而保護(hù)了代碼的完整性、保護(hù)了用戶不會(huì)被病毒、惡意代碼和間諜軟件所侵害,本章將演示證書(shū)的簽發(fā)與偽造2021-06-06
python 網(wǎng)絡(luò)編程要點(diǎn)總結(jié)
Python 提供了兩個(gè)級(jí)別訪問(wèn)的網(wǎng)絡(luò)服務(wù):低級(jí)別的網(wǎng)絡(luò)服務(wù)支持基本的 Socket,它提供了標(biāo)準(zhǔn)的 BSD Sockets API,可以訪問(wèn)底層操作系統(tǒng) Socket 接口的全部方法。高級(jí)別的網(wǎng)絡(luò)服務(wù)模塊SocketServer, 它提供了服務(wù)器中心類,可以簡(jiǎn)化網(wǎng)絡(luò)服務(wù)器的開(kāi)發(fā)。下面看下該如何使用2021-06-06
Python學(xué)習(xí)筆記之json模塊和pickle模塊
json和pickle模塊是將數(shù)據(jù)進(jìn)行序列化處理,并進(jìn)行網(wǎng)絡(luò)傳輸或存入硬盤,下面這篇文章主要給大家介紹了關(guān)于Python學(xué)習(xí)筆記之json模塊和pickle模塊的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-05-05
python實(shí)現(xiàn)PID溫控算法的示例代碼
PID算法是一種常用的控制算法,用于調(diào)節(jié)和穩(wěn)定控制系統(tǒng)的輸出,這篇文章主要為大家詳細(xì)介紹了如何使用Python實(shí)現(xiàn)pid溫控算法,需要的可以參考下2024-01-01

