python中的多線程 threading

多線程

什麼是python多線程

多線程是加速程序計算的有效方式,Python的多線程模塊 threading 是挺容易學習的。

線程在執行過程中與進程還是有區別的。每個獨立的進程有一個程序運行的入口、順序執行序列和程序的出口。但是線程不能夠獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。

下面通過一個實例學習:

import threading
import time
import subprocess
def thread_job():
    print('T1 start\n')
    for i in range(10):
        time.sleep(0.1)
    print('T1 finish\n')

def T2_job():
    print('T2 start \n')
    print('T2 finish\n')
def main():
    # 通過 threading.Thread 創建一個線程,
    # 參數1 是線程函數;注意這裏只是將函數的索引賦值給 target , 沒有括號
    # 參數2 是該線程的名字
    added_thread = threading.Thread(target=thread_job, name='T1')
    thread2 = threading.Thread(target=T2_job, name='T2')
    # 啓動線程
    added_thread.start()
    thread2.start()    
    # 阻塞調用線程直至線程的join() 方法被調用中止,後面的程序纔會執行
    added_thread.join()
    thread2.join()
    print('all done\n')
    # 打印出當前的線程變量,打印正在運行的線程數量,打印正在運行的線程的list
    print(threading.current_thread())

    print(threading.active_count())
    print(threading.enumerate())

if __name__ == '__main__':
    main()

上述程序運行結果:

T1 start

T2 start 

T2 finish

T1 finish

all done

<_MainThread(MainThread, started 38104)>
1
[<_MainThread(MainThread, started 38104)>]

Process finished with exit code 0

可以看到當兩個線程的主函數執行完之後,後面的程序纔開始執行。打印 all done

常用的線程模塊及方法

Python通過兩個標準庫thread和threading提供對線程的支持。thread提供了低級別的、原始的線程以及一個簡單的鎖。

  • threading 模塊提供的其他方法:

  • threading.currentThread(): 返回當前的線程變量。

  • threading.enumerate(): 返回一個包含正在運行的線程的list。正在運行指線程啓動後、結束前,不包括啓動前和終止後的線程。

  • threading.activeCount(): 返回正在運行的線程數量,與len(threading.enumerate())有相同的結果。

除了使用方法外,線程模塊同樣提供了Thread類來處理線程,Thread類提供了以下方法:

  • run(): 用以表示線程活動的方法。
  • start():啓動線程活動。
  • join([time]): 等待至線程中止。這阻塞調用線程直至線程的join() 方法被調用中止-正常退出或者拋出未處理的異常-或者是可選的超時發生。
  • isAlive(): 返回線程是否活動的。
  • getName(): 返回線程名。
  • setName(): 設置線程名。

存儲進程結果 Queue

由於線程函數中不能使用 return 語句,於是我們考慮將線程函數中要保存的值放到隊列中。

繼續看一個實例:

import threading
import time
from queue import Queue
def job(l, q):
    for i in range(len(l)):
        l[i] = l[i]**2
    # return l
    # 調用線程不能用return,因此我們將它放入隊列中
    q.put(l)
def multithreading():
    q = Queue()
    # 使用一個list來存放創建的線程
    threads = []
    data = [[1,2,3],[3,4,5],[4,4,4],[5,5,5]]
    # 通過一個循環建立四個線程
    for i in range(4):
        t = threading.Thread(target=job, args=(data[i], q))
        t.start()
        threads.append(t)
    # 將循環中給的當前線程附加到主線程裏,即,執行完該線程才能往下執行
    for thread in threads:
        thread.join()
    result = []
    # 循壞四次取出隊列中的值
    for _ in range(4):
        result.append(q.get())
    print(result)

if __name__ == '__main__':
    multithreading()

多線程執行的效率?

在人們的常識中,多進程,多線程都是通過併發的方式充分利用硬件資源提高程序的運行效率,但是在python中卻不一定是這樣。相反,這個功能有點雞肋。

這是爲什麼呢?GIL的存在使得多行程的運行效率真不一定高。GIL待會說,先看多線程是不是雞肋,我們用代碼來說明。

import time
import threading
def decrement(n):
    while n > 0:
        n -= 1

start = time.time()
# 單線程
decrement(100000000)
cost = time.time() - start
print('single thread: ', cost)
# 多線程
start = time.time()

t1 = threading.Thread(target=decrement, args=[50000000])
t2 = threading.Thread(target=decrement, args=[50000000])

t1.start() # 啓動線程,執行任務
t2.start() # 同上

t1.join() # 主線程阻塞,直到t1執行完成,主線程繼續往後執行
t2.join() # 同上

cost = time.time() - start
print('multi thread:',cost)

運行結果;

single thread:  4.635588884353638
multi thread: 4.316659688949585

可以看到,使用多線程後效率並沒有提升,相反,有時效率反而會下降。

GIL

是什麼原因導致多線程不快反慢的呢?

原因就在於 GIL ,在 Cpython 解釋器(Python語言的主流解釋器)中,有一把全局解釋鎖(Global Interpreter Lock),在解釋器解釋執行 Python 代碼時,先要得到這把鎖,意味着,任何時候只可能有一個線程在執行代碼,其它線程要想獲得 CPU 執行代碼指令,就必須先獲得這把鎖,如果鎖被其它線程佔用了,那麼該線程就只能等待,直到佔有該鎖的線程釋放鎖纔有執行代碼指令的可能。

線程鎖 Lock

python解釋器在執行程序時,在任何時候只有一個線程在執行代碼。我們定義的多個線程是在不停的切換,只是速度快到我們以爲踏實多個線程並行,。那麼這種情況下可能會出現一個問題:如果我們定義一個全局變量,而我們的線程函數中也使用了該變量,那麼多個線程切換着執行可能會對該變量進行不同的運算。

這種情況下,就需要Python提供的進程鎖 lock了。在線程函數中調用鎖,執行完在釋放,使得整個過程不收其他進程干擾。

一個關於線程鎖的示例:

import threading
def job1():
    global A, lock
    lock.acquire()
    for i in range(5):
        A += 1
        print('job1 ', A)
    lock.release()
def job2():
    global A, lock
    lock.acquire()
    for i in range(10):
        A += 10
        print('job2 ', A)
    lock.release()
if __name__ == '__main__':
    lock = threading.Lock()
    A = 0
    t1 = threading.Thread(target=job1)
    t2 = threading.Thread(target=job2)
    t1.start()
    t2.start()
    # t2.join()
    # t1.join()

執行結果:

job1  1
job1  2
job1  3
job1  4
job1  5
job2  15
job2  25
job2  35
job2  45
job2  55
job2  65
job2  75
job2  85
job2  95
job2  105

我們將線程函數中的結果打印出來,可以看到首先對全局變量A 進行+1操作,循環打印5次。之後再線程函數job2中執行+10操作。

可以將線程鎖去掉,其他部分保持不變,執行原程序,觀察打印的結果是否還是如此整齊一致?

歡迎關注我的微信公衆號,談風月之餘談技術
在這裏插入圖片描述

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