python多線程詳解

python多線程詳解
目錄

python多線程詳解
一、線程介紹
什麼是線程
爲什麼要使用多線程
二、線程實現
threading模塊
自定義線程
守護線程
主線程等待子線程結束
多線程共享全局變量
互斥鎖
遞歸鎖
信號量(BoundedSemaphore類)
事件(Event類)
三、GIL(Global Interpreter Lock)全局解釋器鎖
python多線程詳解
一、線程介紹
什麼是線程
線程(Thread)也叫輕量級進程,是操作系統能夠進行運算調度的最小單位,它被包涵在進程之中,是進程中的實際運作單位。線程自己不擁有系統資源,只擁有一點兒在運行中必不可少的資源,但它可與同屬一個進程的其它線程共享進程所擁有的全部資源。一個線程可以創建和撤消另一個線程,同一進程中的多個線程之間可以併發執行。

爲什麼要使用多線程
線程在程序中是獨立的、併發的執行流。與分隔的進程相比,進程中線程之間的隔離程度要小,它們共享內存、文件句柄和其他進程應有的狀態。

因爲線程的劃分尺度小於進程,使得多線程程序的併發性高。進程在執行過程中擁有獨立的內存單元,而多個線程共享內存,從而極大地提高了程序的運行效率。

線程比進程具有更高的性能,這是由於同一個進程中的線程都有共性多個線程共享同一個進程的虛擬空間。線程共享的環境包括進程代碼段、進程的公有數據等,利用這些共享的數據,線程之間很容易實現通信。

操作系統在創建進程時,必須爲該進程分配獨立的內存空間,並分配大量的相關資源,但創建線程則簡單得多。因此,使用多線程來實現併發比使用多進程的性能要高得多。

總結起來,使用多線程編程具有如下幾個優點:

進程之間不能共享內存,但線程之間共享內存非常容易。

操作系統在創建進程時,需要爲該進程重新分配系統資源,但創建線程的代價則小得多。因此,使用多線程來實現多任務併發執行比使用多進程的效率高。

Python 語言內置了多線程功能支持,而不是單純地作爲底層操作系統的調度方式,從而簡化了 Python 的多線程編程。

二、線程實現
threading模塊
普通創建方式

import threading
import time

def run(n):

print("task", n)
time.sleep(1)
print('2s')
time.sleep(1)
print('1s')
time.sleep(1)
print('0s')
time.sleep(1)

if name == '__main__':

t1 = threading.Thread(target=run, args=("t1",))
t2 = threading.Thread(target=run, args=("t2",))
t1.start()
t2.start()

task t1
task t2
2s
2s
1s
1s
0s
0s

自定義線程
繼承threading.Thread來自定義線程類,其本質是重構Thread類中的run方法

import threading
import time

class MyThread(threading.Thread):

def __init__(self, n):
    super(MyThread, self).__init__()  # 重構run函數必須要寫
    self.n = n

def run(self):
    print("task", self.n)
    time.sleep(1)
    print('2s')
    time.sleep(1)
    print('1s')
    time.sleep(1)
    print('0s')
    time.sleep(1)

if name == "__main__":

t1 = MyThread("t1")
t2 = MyThread("t2")
t1.start()
t2.start()

task t1
task t2
2s
2s
1s
1s
0s
0s
守護線程

我們看下面這個例子,這裏使用setDaemon(True)把所有的子線程都變成了主線程的守護線程,因此當主進程結束後,子線程也會隨之結束。所以當主線程結束後,整個程序就退出了。

import threading
import time

def run(n):

print("task", n)
time.sleep(1)       #此時子線程停1s
print('3')
time.sleep(1)
print('2')
time.sleep(1)
print('1')

if name == '__main__':

t = threading.Thread(target=run, args=("t1",))
t.setDaemon(True)   #把子進程設置爲守護線程,必須在start()之前設置
t.start()
print("end")

task t1
end
我們可以發現,設置守護線程之後,當主線程結束時,子線程也將立即結束,不再執行。

主線程等待子線程結束
爲了讓守護線程執行結束之後,主線程再結束,我們可以使用join方法,讓主線程等待子線程執行。

import threading
import time

def run(n):

print("task", n)
time.sleep(1)       #此時子線程停1s
print('3')
time.sleep(1)
print('2')
time.sleep(1)
print('1')

if name == '__main__':

t = threading.Thread(target=run, args=("t1",))
t.setDaemon(True)   #把子進程設置爲守護線程,必須在start()之前設置
t.start()
t.join() # 設置主線程等待子線程結束
print("end")

task t1
3
2
1
end

多線程共享全局變量
線程是進程的執行單元,進程是系統分配資源的最小單位,所以在同一個進程中的多線程是共享資源的。

import threading
import time

g_num = 100

def work1():

global g_num
for i in range(3):
    g_num += 1
print("in work1 g_num is : %d" % g_num)

def work2():

global g_num
print("in work2 g_num is : %d" % g_num)

if name == '__main__':

t1 = threading.Thread(target=work1)
t1.start()
time.sleep(1)
t2 = threading.Thread(target=work2)
t2.start()

in work1 g_num is : 103
in work2 g_num is : 103

互斥鎖
由於線程之間是進行隨機調度,並且每個線程可能只執行n條執行之後,當多個線程同時修改同一條數據時可能會出現髒數據,所以,出現了線程鎖,即同一時刻允許一個線程執行操作。線程鎖用於鎖定資源,你可以定義多個鎖, 像下面的代碼, 當你需要獨佔某一資源時,任何一個鎖都可以鎖這個資源,就好比你用不同的鎖都可以把相同的一個門鎖住是一個道理。

由於線程之間是進行隨機調度,如果有多個線程同時操作一個對象,如果沒有很好地保護該對象,會造成程序結果的不可預期,我們也稱此爲“線程不安全”。

爲了方式上面情況的發生,就出現了互斥鎖(Lock)

from threading import Thread,Lock
import os,time
def work():

global n
lock.acquire()
temp=n
time.sleep(0.1)
n=temp-1
lock.release()

if name == '__main__':

lock=Lock()
n=100
l=[]
for i in range(100):
    p=Thread(target=work)
    l.append(p)
    p.start()
for p in l:
    p.join()

遞歸鎖
RLcok類的用法和Lock類一模一樣,但它支持嵌套,,在多個鎖沒有釋放的時候一般會使用使用RLcok類。

import threading
import time

def Func(lock):

global gl_num
lock.acquire()
gl_num += 1
time.sleep(1)
print(gl_num)
lock.release()

if name == '__main__':

gl_num = 0
lock = threading.RLock()
for i in range(10):
    t = threading.Thread(target=Func, args=(lock,))
    t.start()

信號量(BoundedSemaphore類)
互斥鎖同時只允許一個線程更改數據,而Semaphore是同時允許一定數量的線程更改數據 ,比如廁所有3個坑,那最多隻允許3個人上廁所,後面的人只能等裏面有人出來了才能再進去。

import threading
import time

def run(n, semaphore):

semaphore.acquire()   #加鎖
time.sleep(1)
print("run the thread:%s\n" % n)
semaphore.release()     #釋放

if name == '__main__':

num = 0
semaphore = threading.BoundedSemaphore(5)  # 最多允許5個線程同時運行
for i in range(22):
    t = threading.Thread(target=run, args=("t-%s" % i, semaphore))
    t.start()
while threading.active_count() != 1:
    pass  # print threading.active_count()
else:
    print('-----all threads done-----')

事件(Event類)
python線程的事件用於主線程控制其他線程的執行,事件是一個簡單的線程同步對象,其主要提供以下幾個方法:

clear 將flag設置爲“False”
set 將flag設置爲“True”
is_set 判斷是否設置了flag
wait 會一直監聽flag,如果沒有檢測到flag就一直處於阻塞狀態
事件處理的機制:全局定義了一個“Flag”,當flag值爲“False”,那麼event.wait()就會阻塞,當flag值爲“True”,那麼event.wait()便不再阻塞。

利用Event類模擬紅綠燈

import threading
import time

event = threading.Event()

def lighter():

count = 0
event.set()     #初始值爲綠燈
while True:
    if 5 < count <=10 :
        event.clear()  # 紅燈,清除標誌位
        print("\33[41;1mred light is on...\033[0m")
    elif count > 10:
        event.set()  # 綠燈,設置標誌位
        count = 0
    else:
        print("\33[42;1mgreen light is on...\033[0m")

    time.sleep(1)
    count += 1

def car(name):

while True:
    if event.is_set():      #判斷是否設置了標誌位
        print("[%s] running..."%name)
        time.sleep(1)
    else:
        print("[%s] sees red light,waiting..."%name)
        event.wait()
        print("[%s] green light is on,start going..."%name)

light = threading.Thread(target=lighter,)
light.start()

car = threading.Thread(target=car,args=("MINI",))
car.start()
三、GIL(Global Interpreter Lock)全局解釋器鎖
在非python環境中,單核情況下,同時只能有一個任務執行。多核時可以支持多個線程同時執行。但是在python中,無論有多少核,同時只能執行一個線程。究其原因,這就是由於GIL的存在導致的。

GIL的全稱是Global Interpreter Lock(全局解釋器鎖),來源是python設計之初的考慮,爲了數據安全所做的決定。某個線程想要執行,必須先拿到GIL,我們可以把GIL看作是“通行證”,並且在一個python進程中,GIL只有一個。拿不到通行證的線程,就不允許進入CPU執行。GIL只在cpython中才有,因爲cpython調用的是c語言的原生線程,所以他不能直接操作cpu,只能利用GIL保證同一時間只能有一個線程拿到數據。而在pypy和jpython中是沒有GIL的。

Python多線程的工作過程:
python在使用多線程的時候,調用的是c語言的原生線程。

拿到公共數據
申請gil
python解釋器調用os原生線程
os操作cpu執行運算
當該線程執行時間到後,無論運算是否已經執行完,gil都被要求釋放
進而由其他進程重複上面的過程
等其他進程執行完後,又會切換到之前的線程(從他記錄的上下文繼續執行),整個過程是每個線程執行自己的運算,當執行時間到就進行切換(context switch)。
python針對不同類型的代碼執行效率也是不同的:

1、CPU密集型代碼(各種循環處理、計算等等),在這種情況下,由於計算工作多,ticks計數很快就會達到閾值,然後觸發GIL的釋放與再競爭(多個線程來回切換當然是需要消耗資源的),所以python下的多線程對CPU密集型代碼並不友好。
2、IO密集型代碼(文件處理、網絡爬蟲等涉及文件讀寫的操作),多線程能夠有效提升效率(單線程下有IO操作會進行IO等待,造成不必要的時間浪費,而開啓多線程能在線程A等待時,自動切換到線程B,可以不浪費CPU的資源,從而能提升程序執行效率)。所以python的多線程對IO密集型代碼比較友好。

使用建議?

python下想要充分利用多核CPU,就用多進程。因爲每個進程有各自獨立的GIL,互不干擾,這樣就可以真正意義上的並行執行,在python中,多進程的執行效率優於多線程(僅僅針對多核CPU而言)。

GIL在python中的版本差異:

1、在python2.x裏,GIL的釋放邏輯是當前線程遇見IO操作或者ticks計數達到100時進行釋放。(ticks可以看作是python自身的一個計數器,專門做用於GIL,每次釋放後歸零,這個計數可以通過sys.setcheckinterval 來調整)。而每次釋放GIL鎖,線程進行鎖競爭、切換線程,會消耗資源。並且由於GIL鎖存在,python裏一個進程永遠只能同時執行一個線程(拿到GIL的線程才能執行),這就是爲什麼在多核CPU上,python的多線程效率並不高。
2、在python3.x中,GIL不使用ticks計數,改爲使用計時器(執行時間達到閾值後,當前線程釋放GIL),這樣對CPU密集型程序更加友好,但依然沒有解決GIL導致的同一時間只能執行一個線程的問題,所以效率依然不盡如人意。

原文地址https://www.cnblogs.com/luyuze95/p/11289143.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章