python中最重要的併發模型:
- 多線程
- 多進程
- 異步編程
並行是併發概念的一個子集,你可以編寫有多個線程或進程的併發程序,但是如果不是在多核處理器上執行這個程序,那麼就不能以並行的方式來運行代碼。
多線程
什麼是多線程
程序員可以將他或她的工作分拆到線程中,這些線程同時運行並共享同一內存上下文。線程之間共享同樣的上下文,這意味着你必須保護數據,避免併發訪問這些數據。如果兩個線程更新相同的沒有任何保護的數據,則會發生競態條件,這被稱之爲競爭冒險。
Python如何處理多線程
與其他語言不同,Python使用多個內核級線程,每個線程可以運行任何解釋器級線程。但是CPython有一些主要的限制,渲染線程在多個上下文中不可用。所有訪問Python對象的線程都會被一個全局鎖串行化。這是由許多解釋器的內部結構完成的,和第三方代碼一樣,它們不是線程安全的,需要進程保護。這種機制被稱爲全局解釋器鎖。儘管有GIL限制,線程在某些情況下確實很有用,如:
- 構建響應式界面
- 委派工作
- 構建多用戶應用程序
多線程使用舉例
Python中使用多線程有兩種方式:實例化和類繼承
實例化threading.Thread類創建多線程
import sys,random,time
import threading
def loop(count):
n = 0
while n < count:
n = n + 1
output = threading.current_thread().name + ' : ' + str(n)
sys.stdout.write(output + '\n') # 使用sys.stdout.write代替print,是爲了避免輸出混亂,因爲print是非線程安全的。
#print(output)
time.sleep(random.random())
t1 = threading.Thread(target=loop, args=(3,)) # 創建新線程
t2 = threading.Thread(target=loop, args=(4,))
t1.start() # 啓動線程
t2.start()
t1.join() # 等待線程結束
t2.join()
輸出:
Thread-1 : 1
Thread-2 : 1
Thread-2 : 2
Thread-1 : 2
Thread-2 : 3
Thread-2 : 4
Thread-1 : 3
繼承threading.Thread類創建多線程
import sys,random,time
import threading
class myThread(threading.Thread):
def __init__(self, name, counter):
threading.Thread.__init__(self)
self.name = name
self.counter = counter
def run(self):
print('Starting ' + self.name)
n = 0
while n < self.counter:
n = n +1
output = self.name + ': ' + str(n)
sys.stdout.write(output + '\n')
time.sleep(random.random())
t1 = myThread('Thread-1', 3)
t2 = myThread('Thread-2', 4)
t1.start()
t2.start()
線程同步
如果多個線程對同一個數據進行修改,則會出現不可預料的結果,爲了保證數據的正確性,需要對多個線程進行同步。使用Thread對象的Lock和Rlock可以實現簡單的線程同步,這兩個對象都有acquire和release方法。
import sys
import threading
ticket_count = 100
threadLock = threading.Lock()
def loop():
global ticket_count
while True:
threadLock.acquire() # 獲得鎖
if ticket_count > 0:
output = threading.current_thread().name + ' get ticket : ' + str(ticket_count)
sys.stdout.write(output + '\n')
ticket_count = ticket_count - 1
else:
sys.stdout.write('Tickets sold out')
break
threadLock.release() # 釋放鎖
if __name__ == '__main__':
t1 = threading.Thread(target=loop)
t2 = threading.Thread(target=loop)
t1.start()
t2.start()
多進程
Unix/Linux操作系統提供了一個fork()系統調用,它非常特殊。普通函數調用一次返回一次,但是fork()調用一次,返回兩次,因爲操作系統自動把當前進程(稱爲父進程)複製了一份(稱爲子進程),然後,分別在父進程和子進程內返回。子進程永遠返回0,而父進程返回子進程的ID。這樣做的理由是,父進程可以fork出很多子進程,所以父進程要記下每個子進程的ID,而子進程只需要調用getppid()就可以拿到父進程的ID:
import os
def main():
print('Process (%s) start...' % os.getpid())
pid = os.fork()
if pid == 0:
print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
else:
print('I (%s) just created a child process (%s).' % (os.getpid(), pid))
if __name__ == '__main__':
main()
輸出:
Process (22597) start...
I (22597) just created a child process (22598).
I am child process (22598) and my parent is 22597.
由於windows沒有fork調用,上面的代碼在Windows上無法運行
multiprocessing模塊就是跨平臺的多進程模塊,它提供了一個Process類來代表一個進程對象,下面的演示啓動一個子進程並等待其結束:
import os
from multiprocessing import Process
def run_proc(name):
print('Run child process %s (%s)...' % (name, os.getpid()))
if __name__=='__main__':
print('Parent process %s.' % os.getpid())
p = Process(target=run_proc, args=('test',))
print('Process will start.')
p.start()
p.join()
print('Process end.')
輸出:
Parent process 13820.
Process will start.
Run child process test (8024)...
Process end.
進程池
如果要啓動大量的子進程,可以使用進程池的方式批量創建子進程:
import os, time, random
from multiprocessing import Pool
def long_time_task(name):
print('Run task %s (%s)...' % (name, os.getpid()))
start = time.time()
time.sleep(random.random() * 3)
end = time.time()
print('Task %s runs %0.2f seconds.' % (name, (end - start)))
if __name__=='__main__':
print('Parent process %s.' % os.getpid())
p = Pool() # 創建進程池
for i in range(5):
p.apply_async(long_time_task, args=(i,))
print('Waiting for all subprocesses done...')
p.close()
p.join()
print('All subprocesses done.')
對Pool對象調用join()方法會等待所有子進程執行完畢,調用join()之前必須先調用close(),調用close()之後就不能繼續添加新的Process了。
注意:Pool的默認大小是CPU的核數
進程間通信
multiprocessing模塊提供了Queue,Pipes等多種方式來交換數據。以Queue爲例,在父進程中創建兩個子進程,一個往Queue裏寫數據,一個從Queue裏讀數據:
import os, time, random
from multiprocessing import Process, Queue
def write(q):
for value in ['A', 'B', 'C']:
print('Put %s to queue...' % value)
q.put(value)
time.sleep(random.random())
def read(q):
while True:
value = q.get(True)
print('Get %s from queue.' % value)
if __name__=='__main__':
# 父進程創建Queue,並傳給各個子進程:
q = Queue()
pw = Process(target=write, args=(q,))
pr = Process(target=read, args=(q,))
pw.start()
pr.start()
pw.join()
# pr進程裏是死循環,無法等待其結束,只能強行終止:
pr.terminate()
輸出:
Put A to queue...
Get A from queue.
Put B to queue...
Get B from queue.
Put C to queue...
Get C from queue.