關於我
程式設計界的一名小程式猿,目前在一個創業團隊任team lead,技術棧涉及Android、Python、Java和Go,這個也是我們團隊的主要技術棧。
0x00 前言
在前面的文章中對Python
協程的概念和實現做了簡單地介紹。為了對Python
併發程式設計有更加全面地認識,我也對Python
執行緒和程式的概念和相關技術的使用進行了學習,於是有了這篇文字。
0x01 執行緒與程式
當我們在手機或者PC
上開啟一個應用時,作業系統就會建立一個程式例項,並開始執行程式裡的主執行緒,它有獨立的記憶體空間和資料結構。執行緒是輕量級的程式。在同一個程式裡,多個執行緒共享記憶體和資料結構,執行緒間的通訊也更加容易。
0x02 使用執行緒實現併發
熟悉Java
程式設計的同學就會發現Python
中的執行緒模型與Java
非常類似。下文我們將主要使用Python
中的執行緒模組threading
包。(對於低階別的API
模組thread
不推薦初學者使用。本文所有程式碼將使用Python 3.7
的環境)
threading
要使用執行緒我們要匯入threading
包,這個包是在_thread
包(即上文提到的低階別thread
模組)的基礎上封裝了許多高階的API
,在開發中應當首選threading
包。
常見地,有兩種方式構建一個執行緒:通過Thread
的建構函式傳遞一個callable
物件,或繼承Thread
類並重寫run
方法。
import threading
import time
def do_in_thread(arg):
print('do in thread {}'.format(arg))
time.sleep(2)
if __name__ == '__main__':
start_time = time.time()
t1 = threading.Thread(target=do_in_thread, args=(1,), name='t1')
t2 = threading.Thread(target=do_in_thread, args=(2,), name='t2')
t1.start()
t2.start()
# join方法讓主執行緒等待子執行緒執行完畢
t1.join()
t2.join()
print("\nduration {} ".format(time.time() - start_time))
# do in thread 1
# do in thread 2
# duration 2.001628875732422
複製程式碼
還可以通過繼承threading.Thread
類定義執行緒
import threading
import time
def do_in_thread(arg):
print('do in thread {}'.format(arg))
time.sleep(2)
class MyThread(threading.Thread):
def __init__(self, arg):
super().__init__()
self.arg = arg
def run(self):
start_time = time.time()
do_in_thread(self.arg)
print("duration {} ".format(time.time() - start_time))
def start_thread_2():
start_time = time.time()
print("duration {} ".format(time.time() - start_time))
if __name__ == '__main__':
mt1 = MyThread(3)
mt2 = MyThread(4)
mt1.start()
mt2.start()
# join方法讓主執行緒等待子執行緒執行完畢
mt1.join()
mt2.join()
# do in thread 3
# do in thread 4
# duration 2.004937171936035
複製程式碼
join
方法的作用是讓呼叫它的執行緒等待其執行完畢。
class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
複製程式碼
定義執行緒時可以通過指定構造方法的name
引數設定執行緒名稱。
target
用於指定callable
物件,將在run
方法中被呼叫。
args
設定target
物件被呼叫時的引數,型別是元組()
,例如上文中的do_in_thread(arg)
方法的引數。
kwargs
是一個字典型別的引數,也用於target
物件的引數。
daemon
設定守護執行緒的標識,如果設定為True
那麼這個執行緒就是守護執行緒,此時如果主執行緒結束了,那麼守護執行緒也會立即被殺死。所以當有在守護執行緒中開啟檔案、資料庫等資源操作時,釋放資源就有可能出錯。
執行緒池
程式中若有大量的執行緒建立和銷燬,則對效能影響較大。我們可以使用執行緒池。同樣地,它的API
與Java
極為相似。
Executor
concurrent.futures.Executor
複製程式碼
這是一個抽象類,定義了執行緒池的介面。
submit(fn, *args, **kwargs)
執行fn(args,kwargs) 並會返回一個future
物件,通過future
可獲取到執行結果map(func, *iterables, timeout=None, chunksize=1)
這個方法與map(func,*iterables)
類似shutdown(wait=True)
關閉執行緒池
from concurrent.futures import ThreadPoolExecutor
# 使用max_workers引數指定執行緒池中執行緒的最大數量為2
with ThreadPoolExecutor(max_workers=2) as executor:
# 提交任務到執行緒池
future = executor.submit(pow, 2, 31) # 計算2^31
future2 = executor.submit(pow, 1024, 2)
# 使用future 獲取執行結果
print(future.result())
print(future2.result())
# 執行結果
# 2147483648
# 1048576
複製程式碼
同步
若有多個執行緒對同一個資源或記憶體進行訪問或操作就有會產生競爭條件。
Python
提供了鎖、訊號量、條件和事件等同步原語可幫忙我們實現執行緒的同步機制。
Lock
Lock
有兩種狀態:locked
和unlocked
。它有兩個基本方法:acquire()
和release()
,且都是原子操作的。
一個執行緒通過acquire()
獲取到鎖,Lock
的狀態變成locked
,而其它執行緒呼叫acquire()
時只能等待鎖被釋放。 當執行緒呼叫了release()
時Lock
的狀態就變成了unlocked
,此時其它等待執行緒中只有一個執行緒將獲得鎖。
import threading
share_mem_lock = 0
share_mem = 0
count = 1000000
locker = threading.Lock()
def add_in_thread_with_lock():
global share_mem_lock
for i in range(count):
locker.acquire()
share_mem_lock += 1
locker.release()
def minus_in_thread_with_lock():
global share_mem_lock
for i in range(count):
locker.acquire()
share_mem_lock -= 1
locker.release()
def add_in_thread():
global share_mem
for i in range(count):
share_mem += 1
def minus_in_thread():
global share_mem
for i in range(count):
share_mem -= 1
if __name__ == '__main__':
t1 = threading.Thread(target=add_in_thread_with_lock)
t2 = threading.Thread(target=minus_in_thread_with_lock)
t3 = threading.Thread(target=add_in_thread)
t4 = threading.Thread(target=minus_in_thread)
t1.start()
t2.start()
t3.start()
t4.start()
t1.join()
t2.join()
t3.join()
t4.join()
print("share_mem_lock : ", share_mem_lock)
print("share_mem : ", share_mem)
# 執行結果
# share_mem_lock : 0
# share_mem : 51306
複製程式碼
沒有使用鎖機制的程式碼執行後最後的值很有可能就不為0。而有鎖的程式碼則可以保證同步。
RLock
RLock
即Reentrant Lock
,就是可以重複進入的鎖,也叫遞迴鎖。它有3個特點:
- 誰獲取鎖誰釋放。如A執行緒獲取了鎖,那麼只有A執行緒才能釋放該鎖
- 同一執行緒可重複多次獲取該鎖。即可以呼叫
acquire
多次 acquire
多少次,對應release
就多少次,且最後一次release
才會釋放鎖。
Condition
條件是另一種同步原語機制。其實它的內部是封裝了RLock
,它的acquire()
和release()
方法就是RLock
的方法。
Condition
常用的API
還有wait()
、notify()
和notify_all()
方法。 wait()
方法會釋放鎖,然後進入阻塞狀態直到其它執行緒通過notify()
或notify_all()
喚醒自己。wait()
方法重新獲取到鎖就會返回。
notify()
會喚醒其中一個等待的執行緒,而notify_all()
會喚醒所有等待的執行緒。
需要注意的是notify()
或notify_all()
執行後並不會釋放鎖,只有呼叫了release()
方法後鎖才會釋放。
讓我們看一個來自於《Python並行程式設計手冊》中的一個生產者與消費者例子
from threading import Thread, Condition
import time
items = []
condition = Condition()
class consumer(Thread):
def __init__(self):
Thread.__init__(self)
def consume(self):
global condition
global items
# 獲取鎖
condition.acquire()
if len(items) == 0:
# 當items為空時,釋放了鎖,並等待生產者notify
condition.wait()
print("Consumer notify : no item to consume")
# 開始消費
items.pop()
print("Consumer notify : consumed 1 item")
print("Consumer notify : items to consume are " + str(len(items)))
# 消費之後notify喚醒生產者,因為notify不會釋放鎖,所以還要呼叫release釋放鎖
condition.notify()
condition.release()
def run(self):
for i in range(0, 10):
time.sleep(2)
self.consume()
class producer(Thread):
def __init__(self):
Thread.__init__(self)
def produce(self):
global condition
global items
condition.acquire()
if len(items) == 5:
# 若items時滿的,則執行wait,釋放鎖,並等待消費者notify
condition.wait()
print("Producer notify : items producted are " + str(len(items)))
print("Producer notify : stop the production!!")
# 開始生產
items.append(1)
print("Producer notify : total items producted " + str(len(items)))
# 生產後notify消費者,因為notify不會釋放鎖,所以還執行了release釋放鎖。
condition.notify()
condition.release()
def run(self):
for i in range(0, 10):
time.sleep(1)
self.produce()
if __name__ == "__main__":
producer = producer()
consumer = consumer()
producer.start()
consumer.start()
producer.join()
consumer.join()
複製程式碼
Semaphore
訊號量內部維護一個計數器。acquire()
會減少這個計數,release()
會增加這個計數,這個計數器永遠不會小於0。當計數器等於0時,acquire()
方法就會等待其它執行緒呼叫release()
。
還是藉助一個生產者與消費者的例子來理解
# -*- coding: utf-8 -*-
"""Using a Semaphore to synchronize threads"""
import threading
import time
import random
# 預設情況內部計數為1,這裡設定為0。
# 若設定為負數則會丟擲ValueError
semaphore = threading.Semaphore(0)
def consumer():
print("consumer is waiting.")
# 獲取一個訊號量,因為初始化時內部計數設定為0,所以這裡一開始時是處於等待狀態
semaphore.acquire()
# 開始消費
print("Consumer notify : consumed item number %s " % item)
def producer():
global item
time.sleep(2)
# create a random item
item = random.randint(0, 1000)
# 開始生產
print("producer notify : produced item number %s" % item)
# 釋放訊號量, 內部計數器+1。當有等待的執行緒發現計數器大於0時,就會喚醒並從acquire方法中返回
semaphore.release()
if __name__ == '__main__':
for i in range(0, 5):
t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer)
t1.start()
t2.start()
t1.join()
t2.join()
print("program terminated")
複製程式碼
訊號量經常會用於資源容量確定的場景,比如資料庫連線池等。
Event
事件線上程間的通訊方式非常簡單。一個執行緒傳送事件另一個執行緒等待接收。
Event
物件內部維護了一個bool
變數flag
。通過set()
方法設定該變數為True
,clear()
方法設定flag
為False
。wait()
方法會一直等待直到flag
變成True
結合例子
# -*- coding: utf-8 -*-
import time
from threading import Thread, Event
import random
items = []
event = Event()
class consumer(Thread):
def __init__(self, items, event):
Thread.__init__(self)
self.items = items
self.event = event
def run(self):
while True:
time.sleep(2)
# 等待事件
self.event.wait()
# 開始消費
item = self.items.pop()
print('Consumer notify : %d popped from list by %s' % (item, self.name))
class producer(Thread):
def __init__(self, integers, event):
Thread.__init__(self)
self.items = items
self.event = event
def run(self):
global item
while True:
time.sleep(2)
# 開始生產
item = random.randint(0, 256)
self.items.append(item)
print('Producer notify : item N° %d appended to list by %s' % (item, self.name))
print('Producer notify : event set by %s' % self.name)
# 傳送事件通知消費者消費
self.event.set()
print('Produce notify : event cleared by %s ' % self.name)
# 設定事件內部變數為False,隨後消費者執行緒呼叫wait()方法時,進入阻塞狀態
self.event.clear()
if __name__ == '__main__':
t1 = producer(items, event)
t2 = consumer(items, event)
t1.start()
t2.start()
t1.join()
t2.join()
複製程式碼
Timer
定時器Timer
是Thread
的子類。用於處理定時執行的任務。啟動定時器使用start()
,取消定時器使用cancel()
。
from threading import Timer
def hello():
print("hello, world")
t = Timer(3.0, hello)
t.start() # 3秒後 列印 "hello, world"
複製程式碼
with語法
Lock
、RLock
、Condition
和Semaphore
可以使用with
語法。
這幾個物件都實現擁有acquire()
和release()
方法,且都實現了上下文管理協議。
with some_lock:
# do something...
複製程式碼
等價於
some_lock.acquire()
try:
# do something...
finally:
some_lock.release()
複製程式碼
0x03 小結
本文主要介紹Python
中執行緒的使用,主要是對threading
模組中Thread
物件、執行緒池Executor
常見用法的展示。還了解了執行緒的同步原語Lock
、RLock
、Condition
、Semaphore
、Event
以及Timer
等API
的使用。