1. 程式人生 > >python多線程實現多任務

python多線程實現多任務

多個進程 怎麽 html 安全 函數的參數 任務 枚舉 控制 應該

1.什麽是線程?

進程是操作系統分配程序執行資源的單位,而線程是進程的一個實體,是CPU調度和分配的單位。一個進程肯定有一個主線程,我們可以在一個進程裏創建多個線程來實現多任務。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

2.一個程序實現多任務的方法

如上圖所示,實現多任務,我們可以用幾種方法。

(1)在主進程裏面開啟多個子進程,主進程和多個子進程一起處理任務。

有關多個進程實現多任務,可以參考我的博文:https://www.cnblogs.com/chichung/p/9532962.html

(2)在主進程裏開啟多個子線程,主線程和多個子線程一起處理任務。

(3)在主進程裏開啟多個協程,多個協程一起處理任務。

有關多個協程實現多任務,可以參考我的博文:https://www.cnblogs.com/chichung/p/9544566.html

註意:因為用多個線程一起處理任務,會產生線程安全問題,所以在開發中一般使用多進程+多協程來實現多任務。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

3.多線程的創建方式

import threading
p1 = threading.Thread(target=[函數名],args=([要傳入函數的參數]))
p1.start()  
# 啟動p1線程

我們來模擬一下多線程實現多任務。

假如你在用網易雲音樂一邊聽歌一邊下載。網易雲音樂就是一個進程。假設網易雲音樂內部程序是用多線程來實現多任務的,網易雲音樂開兩個子線程。一個用來緩存音樂,用於現在的播放。一個用來下載用戶要下載的音樂的。這時候的代碼框架是這樣的:

import threading
import time

def listen_music(name):
    while True:
        time.sleep(1)
        print(name,"正在播放音樂")


def download_music(name):
    while True:
        time.sleep(2)
        print(name,"正在下載音樂")


if __name__ == __main__:
    p1 = threading.Thread(target=listen_music,args=("網易雲音樂",))
    p2 = threading.Thread(target=download_music,args=("網易雲音樂",))
    p1.start()
    p2.start()

輸出:
網易雲音樂 正在播放音樂
網易雲音樂 正在下載音樂
網易雲音樂 正在播放音樂
網易雲音樂 正在播放音樂
網易雲音樂 正在下載音樂
網易雲音樂 正在播放音樂
網易雲音樂 正在播放音樂
網易雲音樂 正在下載音樂
網易雲音樂 正在播放音樂
網易雲音樂 正在播放音樂
網易雲音樂 正在播放音樂
......
......

觀察上面的輸出代碼可以知道:

1.CPU是按照時間片輪詢的方式來執行子線程的。cpu內部會合理分配時間片。時間片到a程序的時候,a程序如果在休眠,就會自動切換到b程序。

2.嚴謹來說,CPU在某個時間點,只在執行一個任務,但是由於CPU運行速度和切換速度快,因為看起來像多個任務在一起執行而已。

除了上面的方法創建線程,還有另一種方法。可以編寫一個類,繼承threaing.Thread類,然後重寫父類的run方法。

import threading
import time


class MyThread(threading.Thread):
    def run(self):
        for i in range(5):
            time.sleep(1)
            print(self.name,i)


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

輸出:
Thread-1 0
Thread-3 0
Thread-2 0
Thread-1 1
Thread-2 1
Thread-3 1
Thread-1 2
Thread-3 2
Thread-2 2
Thread-1 3
Thread-2 3
Thread-3 3
Thread-1 4
Thread-2 4
Thread-3 4

運行時無序的,說明已經啟用了多任務。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

4.線程何時開啟,何時結束

(1)子線程何時開啟,何時運行
   當調用thread.start()時 開啟線程,再運行線程的代碼
(2)子線程何時結束
   子線程把target指向的函數中的語句執行完畢後,或者線程中的run函數代碼執行完畢後,立即結束當前子線程
(3)查看當前線程數量
   通過threading.enumerate()可枚舉當前運行的所有線程
(4)主線程何時結束
   所有子線程執行完畢後,主線程才結束
import threading
import time


def run():
    for i in range(5):
        time.sleep(1)
        print(i)


t1 = threading.Thread(target=run)
t1.start()
print("我會在哪裏出現")

輸出:
我會在哪裏出現
0
1
2
3
4

為什麽主進程(主線程)的代碼會先出現呢?因為CPU采用時間片輪詢的方式,如果輪詢到子線程,發現他要休眠1s,他會先去運行主線程。所以說CPU的時間片輪詢方式可以保證CPU的最佳運行。

那如果我想主進程輸出的那句話運行在結尾呢?該怎麽辦呢?這時候就需要用到 join() 方法了。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

5.線程的 join() 方法

import threading
import time


def run():
    for i in range(5):
        time.sleep(1)
        print(i)


t1 = threading.Thread(target=run)
t1.start()
t1.join()  
print("我會在哪裏出現")

輸出:
0
1
2
3
4
我會在哪裏出現

join() 方法可以阻塞主進程(註意只能阻塞主進程,其他子進程是不能阻塞的),直到 t1 子線程執行完,再解阻塞。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

6.線程可以共享全局變量

這個稍微實驗下就可以知道了,所以這裏不廢話。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

7.多線程共享全局變量出現的問題

我們開兩個子線程,全局變量是0,我們每個線程對他自加1,每個線程加一百萬次,這時候就會出現問題了,來,看代碼:

 1 import threading
 2 import time
 3 
 4 num = 0
 5 
 6 def work1(loop):
 7     global num
 8     for i in range(loop):
 9         # 等價於 num += 1
10         temp = num
11         num = temp + 1
12     print(num)
13 
14 
15 def work2(loop):
16     global num
17     for i in range(loop):
18         # 等價於 num += 1
19         temp = num
20         num = temp + 1
21     print(num)
22 
23 
24 if __name__ == __main__:
25     t1 = threading.Thread(target=work1,args=(1000000,))
26     t2 = threading.Thread(target=work2, args=(1000000,))
27     t1.start()
28     t2.start()
29 
30     while len(threading.enumerate()) != 1:
31         time.sleep(1)
32     print(num)
33 
34 輸出:
35 1459526  # 第一個子線程結束後全局變量一共加到這個數
36 1588806  # 第二個子線程結束後全局變量一共加到這個數
37 1588806  # 兩個線程都結束後,全局變量一共加到這個數

奇怪了,我不是每個線程都自加一百萬次嗎?照理來說,應該最後的結果是200萬才對的呀。問題出在哪裏呢?

我們知道CPU是采用時間片輪詢的方式進行幾個線程的執行。

假設我CPU先輪詢到work1(),num此時為100,在我運行到第10行時,時間結束了!此時,賦值了,但是還沒有自加!即temp=100,num=100。

然後,時間片輪詢到了work2(),進行賦值自加。num=101了。

又回到work1()的斷點處,num=temp+1,temp=100,所以num=101。

就這樣!num少了一次自加!

在次數多了之後,這樣的錯誤積累在一起,結果只得到158806!

這就是線程安全問題

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

8.GIL鎖(互斥鎖)可以彌補部分線程安全問題。註意是部分!至於GIL鎖的弊端請關照我的這一篇博文

當多個線程幾乎同時修改某一個共享數據的時候,需要進行同步控制

線程同步能夠保證多個線程安全訪問競爭資源,最簡單的同步機制是引入互斥鎖。

互斥鎖為資源引入一個狀態:鎖定/非鎖定

某個線程要更改共享數據時,先將其鎖定,此時資源的狀態為“鎖定”,其他線程不能更改;直到該線程釋放資源,將資源的狀態變成“非鎖定”,其他的線程才能再次鎖定該資源。互斥鎖保證了每次只有一個線程進行寫入操作,從而保證了多線程情況下數據的正確性。

GIL鎖有三個常用步驟

lock = threading.Lock()  # 取得鎖
lock.acquire()  # 上鎖
lock.release()  # 解鎖

下面讓我們用GIL鎖來解決上面例子的線程安全問題。

import threading
import time

num = 0
lock = threading.Lock()  # 取得鎖
def work1(loop):
    global num
    for i in range(loop):
        # 等價於 num += 1
        lock.acquire()  # 上鎖
        temp = num
        num = temp + 1
        lock.release()  # 解鎖
    print(num)


def work2(loop):
    global num
    for i in range(loop):
        # 等價於 num += 1
        lock.acquire()  # 上鎖
        temp = num
        num = temp + 1
        lock.release()  # 解鎖
    print(num)


if __name__ == __main__:
    t1 = threading.Thread(target=work1,args=(1000000,))
    t2 = threading.Thread(target=work2, args=(1000000,))
    t1.start()
    t2.start()

    while len(threading.enumerate()) != 1:
        time.sleep(1)
    print(num)

輸出:
1945267  # 第一個子線程結束後全局變量一共加到這個數
2000000  # 第二個子線程結束後全局變量一共加到這個數
2000000  # 兩個線程都結束後,全局變量一共加到這個數

python多線程實現多任務