新聞中心
本篇內(nèi)容介紹了“Python多線程是什么及怎么用”的有關(guān)知識(shí),在實(shí)際案例的操作過(guò)程中,不少人都會(huì)遇到這樣的困境,接下來(lái)就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!
創(chuàng)新互聯(lián)服務(wù)項(xiàng)目包括覃塘網(wǎng)站建設(shè)、覃塘網(wǎng)站制作、覃塘網(wǎng)頁(yè)制作以及覃塘網(wǎng)絡(luò)營(yíng)銷策劃等。多年來(lái),我們專注于互聯(lián)網(wǎng)行業(yè),利用自身積累的技術(shù)優(yōu)勢(shì)、行業(yè)經(jīng)驗(yàn)、深度合作伙伴關(guān)系等,向廣大中小型企業(yè)、政府機(jī)構(gòu)等提供互聯(lián)網(wǎng)行業(yè)的解決方案,覃塘網(wǎng)站推廣取得了明顯的社會(huì)效益與經(jīng)濟(jì)效益。目前,我們服務(wù)的客戶以成都為中心已經(jīng)輻射到覃塘省份的部分城市,未來(lái)相信會(huì)繼續(xù)擴(kuò)大服務(wù)區(qū)域并繼續(xù)獲得客戶的支持與信任!
什么是線程?你為什么想要它?
從本質(zhì)上講,Python是一種線性語(yǔ)言,但當(dāng)你需要更多的處理能力時(shí),線程模塊非常方便。雖然Python中的線程不能用于并行CPU計(jì)算,但它非常適合I/O操作,如web抓取,因?yàn)樘幚砥魈幱诳臻e狀態(tài),等待數(shù)據(jù)。
線程正在改變游戲規(guī)則,因?yàn)樵S多與網(wǎng)絡(luò)/數(shù)據(jù)I/O相關(guān)的腳本將大部分時(shí)間用于等待來(lái)自遠(yuǎn)程源的數(shù)據(jù)。由于下載可能沒(méi)有鏈接(即,抓取單獨(dú)的網(wǎng)站),處理器可以并行地從不同的數(shù)據(jù)源下載,并在最后合并結(jié)果。對(duì)于CPU密集型進(jìn)程,使用線程模塊沒(méi)有什么好處。

幸運(yùn)的是,線程包含在標(biāo)準(zhǔn)庫(kù)中:
import threading from queue import Queue import time
你可以使用target作為可調(diào)用對(duì)象,使用args將參數(shù)傳遞給函數(shù),并start啟動(dòng)線程。
def testThread(num): print num if __name__ == '__main__': for i in range(5): t = threading.Thread(target=testThread, arg=(i,)) t.start()
如果你以前從未見(jiàn)過(guò)if __name__ == '__main__':,這基本上是一種確保嵌套在其中的代碼僅在腳本直接運(yùn)行(而不是導(dǎo)入)時(shí)運(yùn)行的方法。
鎖
同一操作系統(tǒng)進(jìn)程的線程將計(jì)算工作負(fù)載分布到多個(gè)內(nèi)核中,如C++和Java等編程語(yǔ)言所示。通常,python只使用一個(gè)進(jìn)程,從該進(jìn)程生成一個(gè)主線程來(lái)執(zhí)行運(yùn)行時(shí)。由于一種稱為全局解釋器鎖(global interpreter lock)的鎖定機(jī)制,它保持在單個(gè)核上,而不管計(jì)算機(jī)有多少核,也不管產(chǎn)生了多少新線程,這種機(jī)制是為了防止所謂的競(jìng)爭(zhēng)條件。

提到競(jìng)爭(zhēng),我想到了想到 NASCAR 和一級(jí)方程式賽車。讓我們用這個(gè)類比,想象所有一級(jí)方程式賽車手都試圖同時(shí)在一輛賽車上比賽。聽(tīng)起來(lái)很荒謬,對(duì)吧?,這只有在每個(gè)司機(jī)都可以使用自己的車的情況下才有可能,或者最好還是一次跑一圈,每次把車交給下一個(gè)司機(jī)。
這與線程中發(fā)生的情況非常相似。線程是從“主”線程“派生”的,每個(gè)后續(xù)線程都是前一個(gè)線程的副本。這些線程都存在于同一進(jìn)程“上下文”(事件或競(jìng)爭(zhēng))中,因此分配給該進(jìn)程的所有資源(如內(nèi)存)都是共享的。例如,在典型的python解釋器會(huì)話中:
>>> a = 8
在這里,a 通過(guò)讓內(nèi)存中的某個(gè)任意位置暫時(shí)保持值 8 來(lái)消耗很少的內(nèi)存 (RAM)。
到目前為止一切順利,讓我們啟動(dòng)一些線程并觀察它們的行為,當(dāng)添加兩個(gè)數(shù)字x時(shí)y:
import time
import threading
from threading import Thread
a = 8
def threaded_add(x, y):
# simulation of a more complex task by asking
# python to sleep, since adding happens so quick!
for i in range(2):
global a
print("computing task in a different thread!")
time.sleep(1)
#this is not okay! but python will force sync, more on that later!
a = 10
print(a)
# the current thread will be a subset fork!
if __name__ != "__main__":
current_thread = threading.current_thread()
# here we tell python from the main
# thread of execution make others
if __name__ == "__main__":
thread = Thread(target = threaded_add, args = (1, 2))
thread.start()
thread.join()
print(a)
print("main thread finished...exiting")>>> computing task in a different thread! >>> 10 >>> computing task in a different thread! >>> 10 >>> 10 >>> main thread finished...exiting
兩個(gè)線程當(dāng)前正在運(yùn)行。讓我們把它們稱為thread_one和thread_two。如果thread_one想要用值10修改a,而thread_two同時(shí)嘗試更新同一變量,我們就有問(wèn)題了!將出現(xiàn)稱為數(shù)據(jù)競(jìng)爭(zhēng)的情況,并且a的結(jié)果值將不一致。
一場(chǎng)你沒(méi)有看的賽車比賽,但從你的兩個(gè)朋友那里聽(tīng)到了兩個(gè)相互矛盾的結(jié)果!thread_one告訴你一件事,thread two反駁了這一點(diǎn)!這里有一個(gè)偽代碼片段說(shuō)明:
a = 8 # spawns two different threads 1 and 2 # thread_one updates the value of a to 10 if (a == 10): # a check #thread_two updates the value of a to 15 a = 15 b = a * 2 # if thread_one finished first the result will be 20 # if thread_two finished first the result will be 30 # who is right?
到底是怎么回事?
Python是一種解釋語(yǔ)言,這意味著它帶有一個(gè)解釋器——一個(gè)從另一種語(yǔ)言解析其源代碼的程序!python中的一些此類解釋器包括cpython、pypypy、Jpython和IronPython,其中,cpython是python的原始實(shí)現(xiàn)。
CPython是一個(gè)解釋器,它提供與C以及其他編程語(yǔ)言的外部函數(shù)接口,它將python源代碼編譯成中間字節(jié)碼,由CPython虛擬機(jī)進(jìn)行解釋。迄今為止和未來(lái)的討論都是關(guān)于CPython和理解環(huán)境中的行為。
內(nèi)存模型和鎖定機(jī)制
編程語(yǔ)言使用程序中的對(duì)象來(lái)執(zhí)行操作。這些對(duì)象由基本數(shù)據(jù)類型組成,如string、integer或boolean。它們還包括更復(fù)雜的數(shù)據(jù)結(jié)構(gòu),如list或classes/objects。程序?qū)ο蟮闹荡鎯?chǔ)在內(nèi)存中,以便快速訪問(wèn)。在程序中使用變量時(shí),進(jìn)程將從內(nèi)存中讀取值并對(duì)其進(jìn)行操作。在早期的編程語(yǔ)言中,大多數(shù)開(kāi)發(fā)人員負(fù)責(zé)他們程序中的所有內(nèi)存管理。這意味著在創(chuàng)建列表或?qū)ο笾?,首先必須為變量分配?nèi)存。在這樣做時(shí),你可以繼續(xù)釋放以“釋放”內(nèi)存。
在python中,對(duì)象通過(guò)引用存儲(chǔ)在內(nèi)存中。引用是對(duì)象的一種標(biāo)簽,因此一個(gè)對(duì)象可以有許多名稱,比如你如何擁有給定的名稱和昵稱。引用是對(duì)象的精確內(nèi)存位置。引用計(jì)數(shù)器用于python中的垃圾收集,這是一種自動(dòng)內(nèi)存管理過(guò)程。
在引用計(jì)數(shù)器的幫助下,python通過(guò)在創(chuàng)建或引用對(duì)象時(shí)遞增引用計(jì)數(shù)器和在取消引用對(duì)象時(shí)遞減來(lái)跟蹤每個(gè)對(duì)象。當(dāng)引用計(jì)數(shù)為0時(shí),對(duì)象的內(nèi)存將被釋放。
import sys import gc hello = "world" #reference to 'world' is 2 print (sys.getrefcount(hello)) bye = "world" other_bye = bye print(sys.getrefcount(bye)) print(gc.get_referrers(other_bye))
>>> 4
>>> 6
>>> [['sys', 'gc', 'hello', 'world', 'print', 'sys', 'getrefcount', 'hello', 'bye', 'world', 'other_bye', 'bye', 'print', 'sys', 'getrefcount', 'bye', 'print', 'gc', 'get_referrers', 'other_bye'], (0, None, 'world'), {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x0138ADF0>, '__spec__': None, '__annotations__': {}, '__builtins__': , '__file__': 'test.py', '__cached__': None, 'sys': , 'gc': , 'hello': 'world', 'bye': 'world', 'other_bye': 'world'}] 需要保護(hù)這些參考計(jì)數(shù)器變量,防止競(jìng)爭(zhēng)條件或內(nèi)存泄漏。以保護(hù)這些變量;可以將鎖添加到跨線程共享的所有數(shù)據(jù)結(jié)構(gòu)中。
CPython 的 GIL 通過(guò)一次允許一個(gè)線程控制解釋器來(lái)控制 Python 解釋器。它為單線程程序提供了性能提升,因?yàn)橹恍枰芾硪粋€(gè)鎖,但代價(jià)是它阻止了多線程 CPython 程序在某些情況下充分利用多處理器系統(tǒng)。
當(dāng)用戶編寫python程序時(shí),性能受CPU限制的程序和受I/O限制的程序之間存在差異。CPU通過(guò)同時(shí)執(zhí)行許多操作將程序推到極限,而I/O程序必須花費(fèi)時(shí)間等待I/O。
因此,只有多線程程序在GIL中花費(fèi)大量時(shí)間來(lái)解釋CPython字節(jié)碼;GIL成為瓶頸。即使沒(méi)有嚴(yán)格必要,GIL也會(huì)降低性能。例如,一個(gè)用python編寫的同時(shí)處理IO和CPU任務(wù)的程序:
import time, os
from threading import Thread, current_thread
from multiprocessing import current_process
COUNT = 200000000
SLEEP = 10
def io_bound(sec):
pid = os.getpid()
threadName = current_thread().name
processName = current_process().name
print(f"{pid} * {processName} * {threadName} \
---> Start sleeping...")
time.sleep(sec)
print(f"{pid} * {processName} * {threadName} \
---> Finished sleeping...")
def cpu_bound(n):
pid = os.getpid()
threadName = current_thread().name
processName = current_process().name
print(f"{pid} * {processName} * {threadName} \
---> Start counting...")
while n>0:
n -= 1
print(f"{pid} * {processName} * {threadName} \
---> Finished counting...")
def timeit(function,args,threaded=False):
start = time.time()
if threaded:
t1 = Thread(target = function, args =(args, ))
t2 = Thread(target = function, args =(args, ))
t1.start()
t2.start()
t1.join()
t2.join()
else:
function(args)
end = time.time()
print('Time taken in seconds for running {} on Argument {} is {}s -{}'.format(function,args,end - start,"Threaded" if threaded else "None Threaded"))
if __name__=="__main__":
#Running io_bound task
print("IO BOUND TASK NON THREADED")
timeit(io_bound,SLEEP)
print("IO BOUND TASK THREADED")
#Running io_bound task in Thread
timeit(io_bound,SLEEP,threaded=True)
print("CPU BOUND TASK NON THREADED")
#Running cpu_bound task
timeit(cpu_bound,COUNT)
print("CPU BOUND TASK THREADED")
#Running cpu_bound task in Thread
timeit(cpu_bound,COUNT,threaded=True)>>> IO BOUND TASK NON THREADED >>> 17244 * MainProcess * MainThread ---> Start sleeping... >>> 17244 * MainProcess * MainThread ---> Finished sleeping... >>> 17244 * MainProcess * MainThread ---> Start sleeping... >>> 17244 * MainProcess * MainThread ---> Finished sleeping... >>> Time taken in seconds for runningon Argument 10 is 20.036664724349976s -None Threaded >>> IO BOUND TASK THREADED >>> 10180 * MainProcess * Thread-1 ---> Start sleeping... >>> 10180 * MainProcess * Thread-2 ---> Start sleeping... >>> 10180 * MainProcess * Thread-1 ---> Finished sleeping... >>> 10180 * MainProcess * Thread-2 ---> Finished sleeping... >>> Time taken in seconds for running on Argument 10 is 10.01464056968689s -Threaded >>> CPU BOUND TASK NON THREADED >>> 14172 * MainProcess * MainThread ---> Start counting... >>> 14172 * MainProcess * MainThread ---> Finished counting... >>> 14172 * MainProcess * MainThread ---> Start counting... >>> 14172 * MainProcess * MainThread ---> Finished counting... >>> Time taken in seconds for running on Argument 200000000 is 44.90199875831604s -None Threaded >>> CPU BOUND TASK THEADED >>> 15616 * MainProcess * Thread-1 ---> Start counting... >>> 15616 * MainProcess * Thread-2 ---> Start counting... >>> 15616 * MainProcess * Thread-1 ---> Finished counting... >>> 15616 * MainProcess * Thread-2 ---> Finished counting... >>> Time taken in seconds for running on Argument 200000000 is 106.09711360931396s -Threaded
從結(jié)果中我們注意到,multithreading在多個(gè)IO綁定任務(wù)中表現(xiàn)出色,執(zhí)行時(shí)間為10秒,而非線程方法執(zhí)行時(shí)間為20秒。我們使用相同的方法執(zhí)行CPU密集型任務(wù)。好吧,最初它確實(shí)同時(shí)啟動(dòng)了我們的線程,但最后,我們看到整個(gè)程序的執(zhí)行需要大約106秒!然后發(fā)生了什么?這是因?yàn)楫?dāng)Thread-1啟動(dòng)時(shí),它獲取全局解釋器鎖(GIL),這防止Thread-2使用CPU。因此,Thread-2必須等待Thread-1完成其任務(wù)并釋放鎖,以便它可以獲取鎖并執(zhí)行其任務(wù)。鎖的獲取和釋放增加了總執(zhí)行時(shí)間的開(kāi)銷。因此,可以肯定地說(shuō),線程不是依賴CPU執(zhí)行任務(wù)的理想解決方案。
這種特性使并發(fā)編程變得困難。如果GIL在并發(fā)性方面阻礙了我們,我們是不是應(yīng)該擺脫它,還是能夠關(guān)閉它?。嗯,這并不容易。其他功能、庫(kù)和包都依賴于GIL,因此必須有一些東西來(lái)取代它,否則整個(gè)生態(tài)系統(tǒng)將崩潰。這是一個(gè)很難解決的問(wèn)題。
多進(jìn)程
我們已經(jīng)證實(shí),CPython使用鎖來(lái)保護(hù)數(shù)據(jù)不受競(jìng)速的影響,盡管這種鎖存在,但程序員已經(jīng)找到了一種顯式實(shí)現(xiàn)并發(fā)的方法。當(dāng)涉及到GIL時(shí),我們可以使用multiprocessing庫(kù)來(lái)繞過(guò)全局鎖。多處理實(shí)現(xiàn)了真正意義上的并發(fā),因?yàn)樗诓煌珻PU核上跨不同進(jìn)程執(zhí)行代碼。它創(chuàng)建了一個(gè)新的Python解釋器實(shí)例,在每個(gè)內(nèi)核上運(yùn)行。不同的進(jìn)程位于不同的內(nèi)存位置,因此它們之間的對(duì)象共享并不容易。在這個(gè)實(shí)現(xiàn)中,python為每個(gè)要運(yùn)行的進(jìn)程提供了不同的解釋器;因此在這種情況下,為多處理中的每個(gè)進(jìn)程提供單個(gè)線程。
import os
import time
from multiprocessing import Process, current_process
SLEEP = 10
COUNT = 200000000
def count_down(cnt):
pid = os.getpid()
processName = current_process().name
print(f"{pid} * {processName} \
---> Start counting...")
while cnt > 0:
cnt -= 1
def io_bound(sec):
pid = os.getpid()
threadName = current_thread().name
processName = current_process().name
print(f"{pid} * {processName} * {threadName} \
---> Start sleeping...")
time.sleep(sec)
print(f"{pid} * {processName} * {threadName} \
---> Finished sleeping...")
if __name__ == '__main__':
# creating processes
start = time.time()
#CPU BOUND
p1 = Process(target=count_down, args=(COUNT, ))
p2 = Process(target=count_down, args=(COUNT, ))
#IO BOUND
#p1 = Process(target=, args=(SLEEP, ))
#p2 = Process(target=count_down, args=(SLEEP, ))
# starting process_thread
p1.start()
p2.start()
# wait until finished
p1.join()
p2.join()
stop = time.time()
elapsed = stop - start
print ("The time taken in seconds is :", elapsed)>>> 1660 * Process-2 ---> Start counting... >>> 10184 * Process-1 ---> Start counting... >>> The time taken in seconds is : 12.815475225448608
可以看出,對(duì)于cpu和io綁定任務(wù),multiprocessing性能異常出色。MainProcess啟動(dòng)了兩個(gè)子進(jìn)程,Process-1和Process-2,它們具有不同的PIDs,每個(gè)都執(zhí)行將COUNT減少到零的任務(wù)。每個(gè)進(jìn)程并行運(yùn)行,使用單獨(dú)的CPU內(nèi)核和自己的Python解釋器實(shí)例,因此整個(gè)程序執(zhí)行只需12秒。
請(qǐng)注意,輸出可能以無(wú)序的方式打印,因?yàn)檫^(guò)程彼此獨(dú)立。這是因?yàn)槊總€(gè)進(jìn)程都在自己的默認(rèn)主線程中執(zhí)行函數(shù)。
我們還可以使用asyncio庫(kù)(上一節(jié)我已經(jīng)講過(guò)了,沒(méi)看的可以返回到上一節(jié)去學(xué)習(xí))繞過(guò)GIL鎖。asyncio的基本概念是,一個(gè)稱為事件循環(huán)的python對(duì)象控制每個(gè)任務(wù)的運(yùn)行方式和時(shí)間。事件循環(huán)知道每個(gè)任務(wù)及其狀態(tài)。就緒狀態(tài)表示任務(wù)已準(zhǔn)備好運(yùn)行,等待階段表示任務(wù)正在等待某個(gè)外部任務(wù)完成。在異步IO中,任務(wù)永遠(yuǎn)不會(huì)放棄控制,也不會(huì)在執(zhí)行過(guò)程中被中斷,因此對(duì)象共享是線程安全的。
import time
import asyncio
COUNT = 200000000
# asynchronous function defination
async def func_name(cnt):
while cnt > 0:
cnt -= 1
#asynchronous main function defination
async def main ():
# Creating 2 tasks.....You could create as many tasks (n tasks)
task1 = loop.create_task(func_name(COUNT))
task2 = loop.create_task(func_name(COUNT))
# await each task to execute before handing control back to the program
await asyncio.wait([task1, task2])
if __name__ =='__main__':
# get the event loop
start_time = time.time()
loop = asyncio.get_event_loop()
# run all tasks in the event loop until completion
loop.run_until_complete(main())
loop.close()
print("--- %s seconds ---" % (time.time() - start_time))>>> --- 41.74118399620056 seconds ---
我們可以看到,asyncio需要41秒來(lái)完成倒計(jì)時(shí),這比multithreading的106秒要好,但對(duì)于cpu受限的任務(wù),不如multiprocessing的12秒。Asyncio創(chuàng)建一個(gè)eventloop和兩個(gè)任務(wù)task1和task2,然后將這些任務(wù)放在eventloop上。然后,程序await任務(wù)的執(zhí)行,因?yàn)槭录h(huán)執(zhí)行所有任務(wù)直至完成。
為了充分利用python中并發(fā)的全部功能,我們還可以使用不同的解釋器。JPython和IronPython沒(méi)有GIL,這意味著用戶可以充分利用多處理器系統(tǒng)。
與線程一樣,多進(jìn)程仍然存在缺點(diǎn):
數(shù)據(jù)在進(jìn)程之間混洗會(huì)產(chǎn)生 I/O 開(kāi)銷
整個(gè)內(nèi)存被復(fù)制到每個(gè)子進(jìn)程中,這對(duì)于更重要的程序來(lái)說(shuō)可能是很多開(kāi)銷
“Python多線程是什么及怎么用”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!
網(wǎng)頁(yè)名稱:Python多線程是什么及怎么用
瀏覽地址:http://www.dlmjj.cn/article/jsedsg.html


咨詢
建站咨詢
