本篇內(nèi)容介紹了“Python多線程是什么及怎么用”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!
創(chuàng)新互聯(lián)服務(wù)項目包括覃塘網(wǎng)站建設(shè)、覃塘網(wǎng)站制作、覃塘網(wǎng)頁制作以及覃塘網(wǎng)絡(luò)營銷策劃等。多年來,我們專注于互聯(lián)網(wǎng)行業(yè),利用自身積累的技術(shù)優(yōu)勢、行業(yè)經(jīng)驗、深度合作伙伴關(guān)系等,向廣大中小型企業(yè)、政府機構(gòu)等提供互聯(lián)網(wǎng)行業(yè)的解決方案,覃塘網(wǎng)站推廣取得了明顯的社會效益與經(jīng)濟(jì)效益。目前,我們服務(wù)的客戶以成都為中心已經(jīng)輻射到覃塘省份的部分城市,未來相信會繼續(xù)擴大服務(wù)區(qū)域并繼續(xù)獲得客戶的支持與信任!
從本質(zhì)上講,Python是一種線性語言,但當(dāng)你需要更多的處理能力時,線程模塊非常方便。雖然Python中的線程不能用于并行CPU計算,但它非常適合I/O操作,如web抓取,因為處理器處于空閑狀態(tài),等待數(shù)據(jù)。
線程正在改變游戲規(guī)則,因為許多與網(wǎng)絡(luò)/數(shù)據(jù)I/O相關(guān)的腳本將大部分時間用于等待來自遠(yuǎn)程源的數(shù)據(jù)。由于下載可能沒有鏈接(即,抓取單獨的網(wǎng)站),處理器可以并行地從不同的數(shù)據(jù)源下載,并在最后合并結(jié)果。對于CPU密集型進(jìn)程,使用線程模塊沒有什么好處。
幸運的是,線程包含在標(biāo)準(zhǔn)庫中:
import threading from queue import Queue import time
你可以使用target
作為可調(diào)用對象,使用args
將參數(shù)傳遞給函數(shù),并start
啟動線程。
def testThread(num): print num if __name__ == '__main__': for i in range(5): t = threading.Thread(target=testThread, arg=(i,)) t.start()
如果你以前從未見過if __name__ == '__main__':
,這基本上是一種確保嵌套在其中的代碼僅在腳本直接運行(而不是導(dǎo)入)時運行的方法。
同一操作系統(tǒng)進(jìn)程的線程將計算工作負(fù)載分布到多個內(nèi)核中,如C++和Java等編程語言所示。通常,python只使用一個進(jìn)程,從該進(jìn)程生成一個主線程來執(zhí)行運行時。由于一種稱為全局解釋器鎖(global interpreter lock)的鎖定機制,它保持在單個核上,而不管計算機有多少核,也不管產(chǎn)生了多少新線程,這種機制是為了防止所謂的競爭條件。
提到競爭,我想到了想到 NASCAR 和一級方程式賽車。讓我們用這個類比,想象所有一級方程式賽車手都試圖同時在一輛賽車上比賽。聽起來很荒謬,對吧?,這只有在每個司機都可以使用自己的車的情況下才有可能,或者最好還是一次跑一圈,每次把車交給下一個司機。
這與線程中發(fā)生的情況非常相似。線程是從“主”線程“派生”的,每個后續(xù)線程都是前一個線程的副本。這些線程都存在于同一進(jìn)程“上下文”(事件或競爭)中,因此分配給該進(jìn)程的所有資源(如內(nèi)存)都是共享的。例如,在典型的python解釋器會話中:
>>> a = 8
在這里,a
通過讓內(nèi)存中的某個任意位置暫時保持值 8 來消耗很少的內(nèi)存 (RAM)。
到目前為止一切順利,讓我們啟動一些線程并觀察它們的行為,當(dāng)添加兩個數(shù)字x
時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
兩個線程當(dāng)前正在運行。讓我們把它們稱為thread_one
和thread_two
。如果thread_one
想要用值10修改a
,而thread_two
同時嘗試更新同一變量,我們就有問題了!將出現(xiàn)稱為數(shù)據(jù)競爭的情況,并且a
的結(jié)果值將不一致。
一場你沒有看的賽車比賽,但從你的兩個朋友那里聽到了兩個相互矛盾的結(jié)果!thread_one
告訴你一件事,thread two
反駁了這一點!這里有一個偽代碼片段說明:
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是一種解釋語言,這意味著它帶有一個解釋器——一個從另一種語言解析其源代碼的程序!python中的一些此類解釋器包括cpython、pypypy、Jpython和IronPython,其中,cpython是python的原始實現(xiàn)。
CPython是一個解釋器,它提供與C以及其他編程語言的外部函數(shù)接口,它將python源代碼編譯成中間字節(jié)碼,由CPython虛擬機進(jìn)行解釋。迄今為止和未來的討論都是關(guān)于CPython和理解環(huán)境中的行為。
編程語言使用程序中的對象來執(zhí)行操作。這些對象由基本數(shù)據(jù)類型組成,如string
、integer
或boolean
。它們還包括更復(fù)雜的數(shù)據(jù)結(jié)構(gòu),如list
或classes/objects
。程序?qū)ο蟮闹荡鎯υ趦?nèi)存中,以便快速訪問。在程序中使用變量時,進(jìn)程將從內(nèi)存中讀取值并對其進(jìn)行操作。在早期的編程語言中,大多數(shù)開發(fā)人員負(fù)責(zé)他們程序中的所有內(nèi)存管理。這意味著在創(chuàng)建列表或?qū)ο笾埃紫缺仨殲樽兞糠峙鋬?nèi)存。在這樣做時,你可以繼續(xù)釋放以“釋放”內(nèi)存。
在python中,對象通過引用存儲在內(nèi)存中。引用是對象的一種標(biāo)簽,因此一個對象可以有許多名稱,比如你如何擁有給定的名稱和昵稱。引用是對象的精確內(nèi)存位置。引用計數(shù)器用于python中的垃圾收集,這是一種自動內(nèi)存管理過程。
在引用計數(shù)器的幫助下,python通過在創(chuàng)建或引用對象時遞增引用計數(shù)器和在取消引用對象時遞減來跟蹤每個對象。當(dāng)引用計數(shù)為0時,對象的內(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ù)這些參考計數(shù)器變量,防止競爭條件或內(nèi)存泄漏。以保護(hù)這些變量;可以將鎖添加到跨線程共享的所有數(shù)據(jù)結(jié)構(gòu)中。
CPython 的 GIL 通過一次允許一個線程控制解釋器來控制 Python 解釋器。它為單線程程序提供了性能提升,因為只需要管理一個鎖,但代價是它阻止了多線程 CPython 程序在某些情況下充分利用多處理器系統(tǒng)。
當(dāng)用戶編寫python程序時,性能受CPU限制的程序和受I/O限制的程序之間存在差異。CPU通過同時執(zhí)行許多操作將程序推到極限,而I/O程序必須花費時間等待I/O。
因此,只有多線程程序在GIL中花費大量時間來解釋CPython字節(jié)碼;GIL成為瓶頸。即使沒有嚴(yán)格必要,GIL也會降低性能。例如,一個用python編寫的同時處理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
在多個IO綁定任務(wù)中表現(xiàn)出色,執(zhí)行時間為10秒,而非線程方法執(zhí)行時間為20秒。我們使用相同的方法執(zhí)行CPU密集型任務(wù)。好吧,最初它確實同時啟動了我們的線程,但最后,我們看到整個程序的執(zhí)行需要大約106秒!然后發(fā)生了什么?這是因為當(dāng)Thread-1
啟動時,它獲取全局解釋器鎖(GIL),這防止Thread-2
使用CPU。因此,Thread-2
必須等待Thread-1
完成其任務(wù)并釋放鎖,以便它可以獲取鎖并執(zhí)行其任務(wù)。鎖的獲取和釋放增加了總執(zhí)行時間的開銷。因此,可以肯定地說,線程不是依賴CPU執(zhí)行任務(wù)的理想解決方案。
這種特性使并發(fā)編程變得困難。如果GIL在并發(fā)性方面阻礙了我們,我們是不是應(yīng)該擺脫它,還是能夠關(guān)閉它?。嗯,這并不容易。其他功能、庫和包都依賴于GIL,因此必須有一些東西來取代它,否則整個生態(tài)系統(tǒng)將崩潰。這是一個很難解決的問題。
我們已經(jīng)證實,CPython使用鎖來保護(hù)數(shù)據(jù)不受競速的影響,盡管這種鎖存在,但程序員已經(jīng)找到了一種顯式實現(xiàn)并發(fā)的方法。當(dāng)涉及到GIL時,我們可以使用multiprocessing
庫來繞過全局鎖。多處理實現(xiàn)了真正意義上的并發(fā),因為它在不同CPU核上跨不同進(jìn)程執(zhí)行代碼。它創(chuàng)建了一個新的Python解釋器實例,在每個內(nèi)核上運行。不同的進(jìn)程位于不同的內(nèi)存位置,因此它們之間的對象共享并不容易。在這個實現(xiàn)中,python為每個要運行的進(jìn)程提供了不同的解釋器;因此在這種情況下,為多處理中的每個進(jìn)程提供單個線程。
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
可以看出,對于cpu和io綁定任務(wù),multiprocessing
性能異常出色。MainProcess
啟動了兩個子進(jìn)程,Process-1
和Process-2
,它們具有不同的PIDs
,每個都執(zhí)行將COUNT
減少到零的任務(wù)。每個進(jìn)程并行運行,使用單獨的CPU內(nèi)核和自己的Python解釋器實例,因此整個程序執(zhí)行只需12秒。
請注意,輸出可能以無序的方式打印,因為過程彼此獨立。這是因為每個進(jìn)程都在自己的默認(rèn)主線程中執(zhí)行函數(shù)。
我們還可以使用asyncio
庫(上一節(jié)我已經(jīng)講過了,沒看的可以返回到上一節(jié)去學(xué)習(xí))繞過GIL鎖。asyncio
的基本概念是,一個稱為事件循環(huán)的python對象控制每個任務(wù)的運行方式和時間。事件循環(huán)知道每個任務(wù)及其狀態(tài)。就緒狀態(tài)表示任務(wù)已準(zhǔn)備好運行,等待階段表示任務(wù)正在等待某個外部任務(wù)完成。在異步IO中,任務(wù)永遠(yuǎn)不會放棄控制,也不會在執(zhí)行過程中被中斷,因此對象共享是線程安全的。
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秒來完成倒計時,這比multithreading
的106秒要好,但對于cpu受限的任務(wù),不如multiprocessing
的12秒。Asyncio創(chuàng)建一個eventloop
和兩個任務(wù)task1
和task2
,然后將這些任務(wù)放在eventloop
上。然后,程序await
任務(wù)的執(zhí)行,因為事件循環(huán)執(zhí)行所有任務(wù)直至完成。
為了充分利用python中并發(fā)的全部功能,我們還可以使用不同的解釋器。JPython和IronPython沒有GIL,這意味著用戶可以充分利用多處理器系統(tǒng)。
與線程一樣,多進(jìn)程仍然存在缺點:
數(shù)據(jù)在進(jìn)程之間混洗會產(chǎn)生 I/O 開銷
整個內(nèi)存被復(fù)制到每個子進(jìn)程中,這對于更重要的程序來說可能是很多開銷
“Python多線程是什么及怎么用”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!