線程是最小的執行單元,而進程由至少一個線程組成。如何調度進程和線程,完全由操作系統決定,程序自己不能決定什麼時候執行,執行多長時間。
在 Python 中我們要同時執行多個任務怎麼辦?
有兩種解決方案:
- 啓動多個進程,每個進程雖然只有一個線程,但多個進程可以一塊執行多個任務。
- 啓動一個進程,在一個進程內啓動多個線程,這樣,多個線程也可以一塊執行多個任務。
- 啓動多個進程,每個進程再啓動多個線程,這樣同時執行的任務就更多了,當然這種模型更復雜,實際很少採用。
總結一下就是,多任務的實現有 3 種方式: - 多進程模式;
- 多線程模式;
- 多進程+多線程模式。
多線程
Python 的標準庫提供了兩個模塊:_thread 和 threading,_thread 是低級模塊, threading 是高級模塊,對 _thread 進行了封裝。絕大多數情況下,我們只需要使用 threading 這個高級模塊。
import time, threading
# 新線程執行的代碼:
def loop():
print('thread %s is running...' % threading.current_thread().name)
n = 0
while n < 5:
n = n + 1
print('thread %s >>> %s' % (threading.current_thread().name, n))
time.sleep(1)
print('thread %s ended.' % threading.current_thread().name)
print('thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print('thread %s ended.' % threading.current_thread().name)
結果:
thread MainThread is running...
thread LoopThread is running...
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread LoopThread ended.
thread MainThread ended.
Python 的 threading 模塊有個 current_thread() 函數,它永遠返回當前線程的實例。主線程實例的名字叫 MainThread ,子線程的名字在創建時指定,我們用 LoopThread 命名子線程。名字僅僅在打印時用來顯示,完全沒有其他意義,如果不起名字 Python 就自動給線程命名爲 Thread-1,Thread-2……
線程同步與互斥鎖
使用線程加載獲取數據,通常都會造成數據不同步的情況。當然,這時候我們可以給資源進行加鎖,也就是訪問資源的線程需要獲得鎖才能訪問。
其中 threading 模塊給我們提供了一個 Lock 功能。
lock = threading.Lock()
在線程中獲取鎖
lock.acquire()
使用完成後,我們肯定需要釋放鎖
lock.release()
Condition 條件變量
Python 提供了 Condition 對象。
使用 Condition 對象可以在某些事件觸發或者達到特定的條件後才處理數據,Condition 除了具有 Lock 對象的 acquire 方法和 release 方法外,還提供了 wait 和 notify 方法。
線程首先 acquire 一個條件變量鎖。如果條件不足,則該線程 wait,如果滿足就執行線程,甚至可以 notify 其他線程。其他處於 wait 狀態的線程接到通知後會重新判斷條件。
其中條件變量可以看成不同的線程先後 acquire 獲得鎖,如果不滿足條件,可以理解爲被扔到一個( Lock 或 RLock )的 waiting 池。直到其他線程 notify 之後再重新判斷條件。不斷的重複這一過程,從而解決複雜的同步問題。
線程間通信
從一個線程向另一個線程發送數據最安全的方式可能就是使用 queue 庫中的隊列了。創建一個被多個線程共享的 Queue 對象,這些線程通過使用 put() 和 get() 操作來向隊列中添加或者刪除元素。
# -*- coding: UTF-8 -*-
from queue import Queue
from threading import Thread
isRead = True
def write(q):
# 寫數據進程
for value in ['兩點水', '三點水', '四點水']:
print('寫進 Queue 的值爲:{0}'.format(value))
q.put(value)
def read(q):
# 讀取數據進程
while isRead:
value = q.get(True)
print('從 Queue 讀取的值爲:{0}'.format(value))
if __name__ == '__main__':
q = Queue()
t1 = Thread(target=write, args=(q,))
t2 = Thread(target=read, args=(q,))
t1.start()
t2.start()
結果:
寫進 Queue 的值爲:兩點水
寫進 Queue 的值爲:三點水
從 Queue 讀取的值爲:兩點水
寫進 Queue 的值爲:四點水
從 Queue 讀取的值爲:三點水
從 Queue 讀取的值爲:四點水
Python 還提供了 Event 對象用於線程間通信,它是由線程設置的信號標誌,如果信號標誌位真,則其他線程等待直到信號接觸。
Event 對象實現了簡單的線程通信機制,它提供了設置信號,清楚信號,等待等用於實現線程間的通信。
-
設置信號
使用 Event 的 set() 方法可以設置 Event 對象內部的信號標誌爲真。Event 對象提供了 isSe() 方法來判斷其內部信號標誌的狀態。當使用 event 對象的 set() 方法後,isSet() 方法返回真 -
清除信號
使用 Event 對象的 clear() 方法可以清除 Event 對象內部的信號標誌,即將其設爲假,當使用 Event 的 clear 方法後,isSet() 方法返回假 -
等待
Event 對象 wait 的方法只有在內部信號爲真的時候纔會很快的執行並完成返回。當 Event 對象的內部信號標誌位假時,則 wait 方法一直等待到其爲真時才返回。
實例
# -*- coding: UTF-8 -*-
import threading
class mThread(threading.Thread):
def __init__(self, threadname):
threading.Thread.__init__(self, name=threadname)
def run(self):
# 使用全局Event對象
global event
# 判斷Event對象內部信號標誌
if event.isSet():
event.clear()
event.wait()
print(self.getName())
else:
print(self.getName())
# 設置Event對象內部信號標誌
event.set()
# 生成Event對象
event = threading.Event()
# 設置Event對象內部信號標誌
event.set()
t1 = []
for i in range(10):
t = mThread(str(i))
# 生成線程列表
t1.append(t)
for i in t1:
# 運行線程
i.start()
結果:
1
0
3
2
5
4
7
6
9
8
小結
多線程編程,模型複雜,容易發生衝突,必須用鎖加以隔離,同時,又要小心死鎖的發生。
Python解釋器由於設計時有GIL全局鎖,導致了多線程無法利用多核。多線程的併發在Python中就是一個美麗的夢。