python並發編程之threading線程(一)


進程是系統進行資源分配最小單元,線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位.進程在執行過程中擁有獨立的內存單元,而多個線程共享內存等資源。

系列文章

threading模塊創建線程

import threading
from threading import Thread

def test(x):
    print('this is {}'.format(x))
    time.sleep(2)

def get_thread(number=5):
    l_thread = (Thread(target=test, args=(i,)) for i in range(number))
    for t in l_thread:
        print(t)
        t.start() # 啟動線程開始執行
    print(len(threading.enumerate()))
if __name__ == '__main__':
    get_thread(5)

# 結果
<Thread(Thread-1, initial)>
this is 0
<Thread(Thread-2, initial)>
this is 1
<Thread(Thread-3, initial)>
this is 2
<Thread(Thread-4, initial)>
this is 3
<Thread(Thread-5, initial)>
this is 4
6

通過以上可知,我們只需要創建一個Thread對象,並運行start方法,解釋器就會創建一個子進程執行我們的target,我們創建了5個線程,但是使用threading.enumerate查看線程的數量發現有6個線程,因為當前在執行的還有一個主線程。主線程會默認等待所有的子線程結束后再結束。

  • 我們還有另外一種創建線程的方式
import threading
from threading import Thread
class MyThread(Thread):

    def __init__(self, x):
        super().__init__()
        self.x = x

    def run(self):
        print('this is {}'.format(self.x))
        time.sleep(2)

def get_thread1(number=5):
    l_thread = (MyThread(i) for i in range(number))
    for t in l_thread:
        print(t.name)
        t.start()
    print(len(threading.enumerate()))

if __name__ == '__main__':
    get_thread1(5)

Thread對象有一個run方法,它就是我們需要執行的目標函數,所以我們可以通過繼承Thread對象,重寫run方法,將我們的目標代碼放置在run方法中。

Thread對象分析

class Thread:
    def __init__(self, group=None, target=None, name=None,
                 args=(), kwargs=None, *, daemon=None):
        pass

# Thread類是python用來創建線程的類,
group:擴展保留字段;
target:目標代碼,一般是我們需要創建線程執行的目標函數。
name:線程的名字,如果不指定會自動分配一個;
args:目標函數的普通參數;
kwargs:目標函數的鍵值對參數;
daemon:設置線程是否為守護線程,即是前台執行還是后台執行,默認是非守護線程,當daemon=True時,子線程為守護線程,此時主線程不會等待子線程,如果主線程完成會強制殺死所有的子線程然后退出。

# 方法
start():創建一個子線程並執行,該方法一個Thread實例只能執行一次,其會創建一個線程執行該類的run方法。
run():子線程需要執行的代碼;
join():主線程阻塞等待子線程直到子線程結束才繼續執行,可以設置等待超時時間timeout.
ident():線程標識符,線程未啟動之前為None,啟動后為一個int;
is_alive():查看子線程是否還活着你返回一個布爾值。
daemon:判斷是否是守護線程;

線程非安全與鎖

多個線程之間可以共享內存等資源,使得多個線程操作同一份資源的時候可能導致資源發生破壞,即線程非安全。

number = 100
class MyThread(Thread):

    def run(self):
        for i in range(1000000):
            global number
            number += 1
        print(number)

def get_thread1(number=5):
    l_thread = (MyThread() for i in range(number))
    for t in l_thread:
        t.start()

if __name__ == '__main__':
    get_thread1(5)

# 結果
1439426
1378835
2241060
2533150
3533150

上例可知,如果是同步運算的話,最終number的結果應該為5000100,但顯然不是。原因是如果線程1取得number=100時,線程切換到線程2,又取得number=100,加1賦值給number=101;如果,又切換回線程1,number加1也是101;相當於執行了兩次加1的操作,然而number=101.這就是多線程的線程非安全!

怎么解決這個問題呢?我們看到上述代碼中number += 1是核心代碼,這個地方隨意切換線程就會造成數據破壞,因此只要我們能夠設置代碼每次執行到這里的時候不允許切換線程就行了。這就是鎖的由來。

用鎖加入上述代碼:

number = 100
mutex = threading.Lock() # 創建鎖對象

class MyThread(Thread):

    def run(self):
        global number
        for i in range(1000000):
            y = mutex.acquire() # 獲取鎖
            if y: # 拿到鎖就執行下面
                number += 1
                mutex.release() # 釋放鎖
        print(number)

def get_thread1(number=5):
    l_thread = (MyThread() for i in range(number))
    for t in l_thread:
        t.start()

if __name__ == '__main__':
    get_thread1(5)

# 結果:
4481177
4742053
4869413
4973771
5000100

可知最后的結果符合預期,threading模塊中定義了Lock類,可以很方便實現鎖機制,每次執行核心代碼之前先去獲取鎖,拿到了才能執行,拿不到默認阻塞等待。

#創建鎖
mutex = threading.Lock()
#鎖定
mutex.acquire(blocking=True) # blocking=True,默認線程阻塞等待;如果blocking=False,線程不會等待,即上例中y會返回False,繼續執行下面的代碼,最后的結果不會符合預期
#釋放
mutex.release()
  • 小結
  1. 加鎖之后,鎖住的那段代碼變成了單線程,阻止了多線程並發執行,效率下降了;

  2. 鎖可以有多個,如果不同的線程持有不同的鎖並相互等待的話,就會造成死鎖;

  3. python的多線程問題遠不止如此,還有一個歷史遺留問題-全局鎖。

死鎖

如果一段代碼存在兩個鎖的話,可能會出現死鎖現象,一旦出現死鎖,系統就會卡死。

number = 100
mutex1 = threading.Lock() # 創建鎖對象
mutex2 = threading.Lock()

class MyThread1(Thread):

    def run(self):
        global number
        for i in range(1000):
            if mutex1.acquire(): # 拿到鎖就執行下面
                number += 1
                if mutex2.acquire():
                    print('this is mutex2')
                    mutex2.release()
                mutex1.release() # 釋放鎖
        print(number)
class MyThread2(Thread):

    def run(self):
        global number
        for i in range(1000):
            if mutex2.acquire(): # 拿到鎖就執行下面
                number += 1
                if mutex1.acquire():
                    print('this is mutex2')
                    mutex1.release()
                mutex2.release() # 釋放鎖
        print(number)

def get_thread1():
    l_thread = (MyThread1(), MyThread2())
    for t in l_thread:
        t.start()

if __name__ == '__main__':
    get_thread1()

一般解決死鎖的辦法是盡量不使用多個鎖,或設計程序時避免死鎖,或為鎖添加超時等待。

全局鎖(GIL)

全局鎖的前世今生不是一兩句話能講完的。可參考:Python全局解釋器鎖

總結一下就是:

  1. 全局鎖的存在是為了保護多線程對數據的安全訪問;
  2. 對於任何Python程序,不管有多少的處理器內核,任何時候都總是只有一個線程在執行;
  3. 全局鎖的存在使得一般情況下多線程比單線程的執行速度慢;
  4. python程序只有在io密集時多線程代碼效率有所提高,所以不推薦使用多線程而是多進程;更好的替代方案為協程;
number = 100
number1 = 100
mutex = threading.Lock()
class MyThread(Thread):

    def run(self):
        global number
        t1 = time.time()
        for i in range(1000000):
            y = mutex.acquire() # 獲取鎖
            if y: # 拿到鎖就執行下面
                number += 1
                mutex.release() # 釋放鎖
        t2 = time.time()
        print(t2-t1)

def get_thread1(number=5):
    l_thread = (MyThread() for i in range(number))
    for t in l_thread:
        t.start()

def get_thread2(n=5):
    global number1
    for i in range(1000000*n):
        number1 += 1
    print(number1)

if __name__ == '__main__':
    get_thread1()
    t2 = time.time()
    get_thread2()
    t3 = time.time()
    print(t3-t2)

可知多線程的執行時間遠遠大於單線程。

結論

  • python最好避免使用多線程,而用多進程代替多線程;

  • 協程是多線程的很好的替代方案。

參考:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM