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

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

系列文章

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個線程,由於當前在執行的還有一個主線程。主線程會默認等待全部的子線程結束後再結束。ide

  • 咱們還有另一種建立線程的方式
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:判斷是不是守護線程;

線程非安全與鎖

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

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最好避免使用多線程,而用多進程代替多線程;

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

參考:

相關文章
相關標籤/搜索