關(guān)于如何使用鎖
【介紹】關(guān)于如何加鎖,獲取鑰匙,釋放鎖。
- lock = threading.Lock():生成鎖對(duì)象,全局唯一;
- lock.acquire():獲取鎖。未獲取到會(huì)阻塞程序,直到獲取到鎖才會(huì)往下執(zhí)行;
- lock.release():釋放鎖,歸回后,其他人也可以調(diào)用;
【注意事項(xiàng)】:lock.acquire() 和 lock.release()必須成對(duì)出現(xiàn),否則就有可能造成死鎖。
為了規(guī)避這個(gè)問(wèn)題,可以使用使用上下文管理器來(lái)加鎖。如下所示:
import threading
lock = threading.Lock()
with lock:
# 這里寫(xiě)想要實(shí)現(xiàn)的代碼
pass
【解釋】with 語(yǔ)句會(huì)在這個(gè)代碼塊執(zhí)行前自動(dòng)獲取鎖,在執(zhí)行結(jié)束后自動(dòng)釋放鎖
為何要“上”鎖 ?
import threading
import time
g_num = 0
def test1(num):
global g_num
for i in range(num):
mutex.acquire() # 上鎖
g_num += 1
mutex.release() # 解鎖
print("---test1---g_num=%d"%g_num)
def test2(num):
global g_num
for i in range(num):
mutex.acquire() # 上鎖
g_num += 1
mutex.release() # 解鎖
print("---test2---g_num=%d"%g_num)
# 創(chuàng)建一個(gè)互斥鎖
# 默認(rèn)是未上鎖的狀態(tài)
mutex = threading.Lock()
# 創(chuàng)建2個(gè)線(xiàn)程,讓他們各自對(duì)g_num加1000000次
p1 = threading.Thread(target=test1, args=(1000000,))
p1.start()
p2 = threading.Thread(target=test2, args=(1000000,))
p2.start()
# 等待計(jì)算完成
while len(threading.enumerate()) != 1:
time.sleep(1)
print("2個(gè)線(xiàn)程對(duì)同一個(gè)全局變量操作之后的最終結(jié)果是:%s" % g_num)
輸出:
---test1---g_num=1909909
---test2---g_num=2000000
2個(gè)線(xiàn)程對(duì)同一個(gè)全局變量操作之后的最終結(jié)果是:2000000
【總結(jié)】入互斥鎖后,其結(jié)果與預(yù)期相符。
關(guān)于死鎖
【解釋】在線(xiàn)程間共享多個(gè)資源的時(shí)候,如果兩個(gè)線(xiàn)程分別占有一部分資源并且同時(shí)等待對(duì)方的資源,就會(huì)造成死鎖。
來(lái)看個(gè)實(shí)例:
import threading
import time
class MyThread1(threading.Thread):
def run(self):
# 對(duì)mutexA上鎖
mutexA.acquire()
# mutexA上鎖后,延時(shí)1秒,等待另外那個(gè)線(xiàn)程 把mutexB上鎖
print(self.name+'----do1---up----')
time.sleep(1)
# 此時(shí)會(huì)堵塞,因?yàn)檫@個(gè)mutexB已經(jīng)被另外的線(xiàn)程搶先上鎖了
mutexB.acquire()
print(self.name+'----do1---down----')
mutexB.release()
# 對(duì)mutexA解鎖
mutexA.release()
class MyThread2(threading.Thread):
def run(self):
# 對(duì)mutexB上鎖
mutexB.acquire()
# mutexB上鎖后,延時(shí)1秒,等待另外那個(gè)線(xiàn)程 把mutexA上鎖
print(self.name+'----do2---up----')
time.sleep(1)
# 此時(shí)會(huì)堵塞,因?yàn)檫@個(gè)mutexA已經(jīng)被另外的線(xiàn)程搶先上鎖了
mutexA.acquire()
print(self.name+'----do2---down----')
mutexA.release()
# 對(duì)mutexB解鎖
mutexB.release()
mutexA = threading.Lock()
mutexB = threading.Lock()
if __name__ == '__main__':
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()
【重點(diǎn)】標(biāo)準(zhǔn)的鎖對(duì)象(threading.Lock)并不關(guān)心當(dāng)前是哪個(gè)線(xiàn)程占有了該鎖;如果該鎖已經(jīng)被占有了,那么任何其它嘗試獲取該鎖的線(xiàn)程都會(huì)被阻塞,包括已經(jīng)占有該鎖的線(xiàn)程也會(huì)被阻塞。
【 獲取鎖和釋放鎖的語(yǔ)句也可以用Python的with來(lái)實(shí)現(xiàn)】
【知識(shí)提升】如有某個(gè)線(xiàn)程在兩個(gè)函數(shù)調(diào)用之間修改了共享資源,那么我們最終會(huì)得到不一致的數(shù)據(jù)。【最直接的解決辦法】是在這個(gè)函數(shù)中也使用lock。然而,這是不可行的。里面的兩個(gè)訪(fǎng)問(wèn)函數(shù)將會(huì)阻塞,因?yàn)橥鈱诱Z(yǔ)句已經(jīng)占有了該鎖。
飽受爭(zhēng)議的GIL(全局鎖)
什么是GIL呢?
【解釋】任何Python線(xiàn)程執(zhí)行前,必須先獲得GIL鎖,然后,每執(zhí)行100條字節(jié)碼,解釋器就自動(dòng)釋放GIL鎖,讓別的線(xiàn)程有機(jī)會(huì)執(zhí)行。這個(gè)GIL全局鎖實(shí)際上把所有線(xiàn)程的執(zhí)行代碼都給上了鎖,所以,多線(xiàn)程在Python中只能交替執(zhí)行,
即使100個(gè)線(xiàn)程跑在100核CPU上,也只能用到1個(gè)核。
GIL執(zhí)行過(guò)程
- 1). 設(shè)置一個(gè)GIL;
- 2). 切換線(xiàn)程去準(zhǔn)備執(zhí)行任務(wù)(Runnale就緒狀態(tài));
- 3). 運(yùn)行;
- 4). 可能出現(xiàn)的狀態(tài):
- 線(xiàn)程任務(wù)執(zhí)行結(jié)束;
- time.sleep()
- 需要獲取其他的信息才能繼續(xù)執(zhí)行(eg: 讀取文件, 需要從網(wǎng)絡(luò)下載html網(wǎng)頁(yè))
5). 將線(xiàn)程設(shè)置為睡眠狀態(tài);
6). 解GIL的鎖;
【重點(diǎn)】python解釋器中任意時(shí)刻都只有一個(gè)線(xiàn)程在執(zhí)行;
I/O密集型(input, output):
計(jì)算密集型(cpu一直占用):
那么如何避免受到GIL的影響?
- 使用多進(jìn)程代替多線(xiàn)程。
- 更換Python解釋器,不使用CPython
Queue隊(duì)列
談及多線(xiàn)程,就不得不說(shuō)Queue隊(duì)列,這是從一個(gè)線(xiàn)程向另一個(gè)線(xiàn)程發(fā)送數(shù)據(jù)最安全的方式。創(chuàng)建一個(gè)被多個(gè)線(xiàn)程共享的 Queue 對(duì)象,這些線(xiàn)程通過(guò)使用put() 和 get() 操作來(lái)向隊(duì)列中添加或者刪除元素。
關(guān)于Queue隊(duì)列的重要的函數(shù)
from queue import Queue
# maxsize默認(rèn)為0,不受限
# 一旦>0,而消息數(shù)又達(dá)到限制,q.put()也將阻塞
q = Queue(maxsize=0)
# 阻塞程序,等待隊(duì)列消息。
q.get()
# 獲取消息,設(shè)置超時(shí)時(shí)間
q.get(timeout=5.0)
# 發(fā)送消息
q.put()
# 等待所有的消息都被消費(fèi)完
q.join()
# 以下三個(gè)方法,知道就好,代碼中不要使用
# 查詢(xún)當(dāng)前隊(duì)列的消息個(gè)數(shù)
q.qsize()
# 隊(duì)列消息是否都被消費(fèi)完,True/False
q.empty()
# 檢測(cè)隊(duì)列里消息是否已滿(mǎn)
q.full()
生產(chǎn)者-消費(fèi)者模型(繼承實(shí)現(xiàn))
什么是生產(chǎn)者-消費(fèi)者模型?
某個(gè)模塊專(zhuān)門(mén)負(fù)責(zé)生產(chǎn)+數(shù)據(jù), 可以認(rèn)為是生產(chǎn)者;
另外一個(gè)模塊負(fù)責(zé)對(duì)生產(chǎn)的數(shù)據(jù)進(jìn)行處理的, 可以認(rèn)為是消費(fèi)者.
在生產(chǎn)者和消費(fèi)者之間加個(gè)緩沖區(qū)(隊(duì)列queue實(shí)現(xiàn)), 可以認(rèn)為是商店。
【生產(chǎn)者】 ===》【緩沖區(qū)】 ===》【 消費(fèi)者】
生產(chǎn)者-消費(fèi)者模型的優(yōu)點(diǎn)
- 1). 生產(chǎn)者和消費(fèi)者的依賴(lài)關(guān)系減少,邏輯聯(lián)系少了,簡(jiǎn)化代碼;
- 2). 生產(chǎn)者和消費(fèi)者是兩個(gè)獨(dú)立的個(gè)體, 可并發(fā)執(zhí)行;
關(guān)于線(xiàn)程池
在Python3中,創(chuàng)建線(xiàn)程池是通過(guò)concurrent.futures函數(shù)庫(kù)中的ThreadPoolExecutor類(lèi)來(lái)實(shí)現(xiàn)的。
future對(duì)象:在未來(lái)的某一時(shí)刻完成操作的對(duì)象. submit方法可以返回一個(gè)future對(duì)象.
先看實(shí)例:簡(jiǎn)單線(xiàn)程池實(shí)現(xiàn)
#線(xiàn)程執(zhí)行的函數(shù)
def add(n1,n2):
v = n1 + n2
print('add :', v , ', tid:',threading.currentThread().ident)
time.sleep(n1)
return v
#通過(guò)submit把需要執(zhí)行的函數(shù)扔進(jìn)線(xiàn)程池中.
#submit 直接返回一個(gè)future對(duì)象
ex = ThreadPoolExecutor(max_workers=3) #制定最多運(yùn)行N個(gè)線(xiàn)程
f1 = ex.submit(add,2,3)
f2 = ex.submit(add,2,2)
print('main thread running')
print(f1.done()) #done 看看任務(wù)結(jié)束了沒(méi)
print(f1.result()) #獲取結(jié)果 ,阻塞方法
簡(jiǎn)單線(xiàn)程池實(shí)現(xiàn)
import Queue
import threading
import time
'''
這個(gè)簡(jiǎn)單的例子的想法是通過(guò):
1、利用Queue特性,在Queue里創(chuàng)建多個(gè)線(xiàn)程對(duì)象
2、那我執(zhí)行代碼的時(shí)候,去queue里去拿線(xiàn)程!
如果線(xiàn)程池里有可用的,直接拿。
如果線(xiàn)程池里沒(méi)有可用,那就等。
3、線(xiàn)程執(zhí)行完畢,歸還給線(xiàn)程池
'''
class ThreadPool(object): #創(chuàng)建線(xiàn)程池類(lèi)
def __init__(self,max_thread=20):#構(gòu)造方法,設(shè)置最大的線(xiàn)程數(shù)為20
self.queue = Queue.Queue(max_thread) #創(chuàng)建一個(gè)隊(duì)列
for i in xrange(max_thread):#循環(huán)把線(xiàn)程對(duì)象加入到隊(duì)列中
self.queue.put(threading.Thread)
#把線(xiàn)程的類(lèi)名放進(jìn)去,執(zhí)行完這個(gè)Queue
def get_thread(self):#定義方法從隊(duì)列里獲取線(xiàn)程
return self.queue.get()
def add_thread(self):#定義方法在隊(duì)列里添加線(xiàn)程
self.queue.put(threading.Thread)
pool = ThreadPool(10)
def func(arg,p):
print arg
time.sleep(2)
p.add_thread() #當(dāng)前線(xiàn)程執(zhí)行完了,我在隊(duì)列里加一個(gè)線(xiàn)程!
for i in xrange(300):
thread = pool.get_thread() #線(xiàn)程池10個(gè)線(xiàn)程,每一次循環(huán)拿走一個(gè)!默認(rèn)queue.get(),如果隊(duì)列里沒(méi)有數(shù)據(jù)就會(huì)等待。
t = thread(target=func,args=(i,pool))
t.start()
'''
self.queue.put(threading.Thread) 添加的是類(lèi)不是對(duì)象,在內(nèi)存中如果相同的類(lèi)只占一份內(nèi)存空間
并且如果這里存儲(chǔ)的是對(duì)象的話(huà)每次都的新增都得在內(nèi)存中開(kāi)辟一段內(nèi)存空間
還有如果是對(duì)象的話(huà):下面的這個(gè)語(yǔ)句就不能這么調(diào)用了!
for i in xrange(300):
thread = pool.get_thread()
t = thread(target=func,args=(i,pool))
t.start()
通過(guò)查看源碼可以知道,在thread的構(gòu)造函數(shù)中:self.__args = args self.__target = target 都是私有字段那么調(diào)用就應(yīng)該這么寫(xiě)
for i in xrange(300):
ret = pool.get_thread()
ret._Thread__target = func
ret._Thread__args = (i,pool)
ret.start()
【map 方法】
返回值和提交的序列是一致的. 即是有序的
。
#下面是map 方法的簡(jiǎn)單使用.
#注意:map 返回是一個(gè)生成器 ,并且是有序的
URLS = ['http://www.baidu.com', 'http://www.qq.com', 'http://www.sina.com.cn']
def get_html(url):
print('thread id:',threading.currentThread().ident,' 訪(fǎng)問(wèn)了:',url)
#這里使用了requests 模塊
return requests.get(url)
ex = ThreadPoolExecutor(max_workers=3)
#內(nèi)部迭代中, 每個(gè)url 開(kāi)啟一個(gè)線(xiàn)程
res_iter = ex.map(get_html,URLS)
for res in res_iter:
#此時(shí)將阻塞 , 直到線(xiàn)程完成或異常
print('url:%s ,len: %d'%(res.url,len(res.text)))
【 as_completed】
用于解決submit什么時(shí)候完成,避免一次次調(diào)用future.done 或者是使用 future.result 。
concurrent.futures.as_completed(fs, timeout=None)
:返回一個(gè)生成器,在迭代過(guò)程中會(huì)阻塞。
【關(guān)聯(lián)】map方法返回是有序的, as_completed 是那個(gè)線(xiàn)程先完成/失敗 就返回。
【舉個(gè)栗子】
#as_completed 返回一個(gè)生成器,用于迭代, 一旦一個(gè)線(xiàn)程完成(或失敗) 就返回
URLS = ['http://www.baidu.com', 'http://www.qq.com', 'http://www.sina.com.cn']
def get_html(url):
time.sleep(1)
print('thread id:',threading.currentThread().ident,' 訪(fǎng)問(wèn)了:',url)
return requests.get(url) #這里使用了requests 模塊
ex = ThreadPoolExecutor(max_workers=3) #最多3個(gè)線(xiàn)程
future_tasks = [ex.submit(get_html,url) for url in URLS] #創(chuàng)建3個(gè)future對(duì)象
for future in as_completed(future_tasks): #迭代生成器
try:
resp = future.result()
except Exception as e:
print('%s'%e)
else:
print('%s has %d bytes!'%(resp.url, len(resp.text)))
輸出:
"""
thread id: 5160 訪(fǎng)問(wèn)了: http://www.baidu.com
thread id: 7752 訪(fǎng)問(wèn)了: http://www.sina.com.cn
thread id: 5928 訪(fǎng)問(wèn)了: http://www.qq.com
http://www.qq.com/ has 240668 bytes!
http://www.baidu.com/ has 2381 bytes!
https://www.sina.com.cn/ has 577244 bytes!
"""
【強(qiáng)調(diào)】關(guān)于回調(diào)函數(shù)add_done_callback(fn)
回調(diào)函數(shù)是在調(diào)用線(xiàn)程完成后再調(diào)用的,在同一個(gè)線(xiàn)程中.
import os,sys,time,requests,threading
from concurrent import futures
URLS = [
'http://baidu.com',
'http://www.qq.com',
'http://www.sina.com.cn'
]
def load_url(url):
print('tid:',threading.currentThread().ident,',url:',url)
with requests.get(url) as resp:
return resp.content
def call_back(obj):
print('->>>>>>>>>call_back , tid:',threading.currentThread().ident, ',obj:',obj)
with futures.ThreadPoolExecutor(max_workers=3) as ex:
# mp = {ex.submit(load_url,url) : url for url in URLS}
mp = dict()
for url in URLS:
f = ex.submit(load_url,url)
mp[f] = url
f.add_done_callback(call_back)
for f in futures.as_completed(mp):
url = mp[f]
try:
data = f.result()
except Exception as exc:
print(exc, ',url:',url)
else:
print('url:', url, ',len:',len(data),',data[:20]:',data[:20])
"""
tid: 7128 ,url: http://baidu.com
tid: 7892 ,url: http://www.qq.com
tid: 3712 ,url: http://www.sina.com.cn
->>>>>>>>>call_back , tid: 7892 ,obj: <Future at 0x2dd64b0 state=finished returned bytes>
url: http://www.qq.com ,len: 251215 ,data[:20]: b'<!DOCTYPE html>\n<htm'
->>>>>>>>>call_back , tid: 3712 ,obj: <Future at 0x2de07b0 state=finished returned bytes>
url: http://www.sina.com.cn ,len: 577333 ,data[:20]: b'<!DOCTYPE html>\n<!--'
->>>>>>>>>call_back , tid: 7128 ,obj: <Future at 0x2d533d0 state=finished returned bytes>
url: http://baidu.com ,len: 81 ,data[:20]: b'<html>\n<meta http-eq'
"""
最后的最后,來(lái)兩個(gè)栗子總結(jié)一下關(guān)于多線(xiàn)程的應(yīng)用。
- 【多線(xiàn)程實(shí)現(xiàn)文件復(fù)制】
import concurrent.futures as fu
import os
ex_pools = fu.ThreadPoolExecutor(max_workers = 3)
def copy(org_file,dest_file):
"""
復(fù)制文件
"""
print("開(kāi)始從%s復(fù)制文件到%s" % (org_file,dest_file))
with open(org_file,'rb+') as f:
content = f.read()
with open(dest_file,'wb+') as f:
f.write(content)
print("從%s復(fù)制文件到%s,完成!" % (org_file, dest_file))
def copy_dir(base,dest):
"""
復(fù)制目錄
"""
if not os.path.exists(dest):
print("創(chuàng)建文件夾:%s" %dest)
os.mkdir(dest)
org_dir_files = os.listdir(base)
for file_name in org_dir_files:
file = os.path.join(base,file_name)
dest_file = os.path.join(dest,file_name)
if os.path.isfile(file):
ex_pools.submit(copy,file,dest_file)
if os.path.isdir(file):
ex_pools.submit(copy_dir, file, dest_file)
# 要復(fù)制的目標(biāo)文件路徑
base = r"C:\Users\42072\Desktop\python"
# 復(fù)制到該文件路徑
dest = r"C:\Users\42072\Desktop\python123"
copy_dir(base,dest)
- 【實(shí)現(xiàn)網(wǎng)絡(luò)上圖片的下載】
(初衷想下音樂(lè)的,不過(guò)好像都收費(fèi)了,還是尊重版權(quán)吧)
import requests
import os
import random
import concurrent.futures as futures
def download_img(url):
resp = requests.get(url)
filename = os.path.split(url)[1] # 獲取文件名
with open(filename,'wb+') as f:
f.write(resp.content)
num = random.randint(2,5)
print(filename + "generate:",num)
time.sleep(num)
return filename
urls = ["http://pic27.nipic.com/20130320/8952533_092547846000_2.jpg",
"http://pic19.nipic.com/20120212/9337475_104548381000_2.jpg",]
ex = futures.ThreadPoolExecutor(max_workers = 3)
res_iter = ex.map(download_img,urls)
type(res_iter)
for res in res_iter:
print(res)
def cf(rs):
print(rs.result())
# [ex.submit(download_img,url).add_done_callback(cf) for url in urls]
# for future in futures.as_completed(fu_tasks):
for url in urls:
f = ex.submit(download_img,url)
f.add_done_callback(cf)
【輸出】:
9337475_104548381000_2.jpggenerate: 3
8952533_092547846000_2.jpggenerate: 3
8952533_092547846000_2.jpg
9337475_104548381000_2.jpg
9337475_104548381000_2.jpggenerate: 3
8952533_092547846000_2.jpggenerate: 4
9337475_104548381000_2.jpg
8952533_092547846000_2.jpg