1.進程和線程
隊列:
1、進程之間的通信: q = multiprocessing.Queue()
2、進程池之間的通信: q = multiprocessing.Manager().Queue()
3、線程之間的通信: q = queue.Queue()
1.功能
- 進程,能夠完成多任務,比如 在一臺電腦上能夠同時運行多個QQ
- 線程,能夠完成多任務,比如 一個QQ中的多個聊天窗口
2.定義的不同
- 進程是系統進行資源分配和調度的一個獨立單位.
- 線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位.線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源.
3.區別
- 一個程序至少有一個進程,一個進程至少有一個線程.
- 線程的劃分尺度小于進程(資源比進程少),使得多線程程序的并發性高。
- 進程在執行過程中擁有獨立的內存單元,而多個線程共享內存,從而極大地提高了程序的運行效率
- 線程不能夠獨立執行,必須依存在進程中
4.優缺點
線程和進程在使用上各有優缺點:線程執行開銷小,但不利于資源的管理和保護;而進程正相反。
2.同步的概念
1.多線程開發可能遇到的問題
假設兩個線程t1和t2都要對num=0進行增1運算,t1和t2都各對num修改10次,num的最終的結果應該為20。
但是由于是多線程訪問,有可能出現下面情況:
在num=0時,t1取得num=0。此時系統把t1調度為”sleeping”狀態,把t2轉換為”running”狀態,t2也獲得num=0。然后t2對得到的值進行加1并賦給num,使得num=1。然后系統又把t2調度為”sleeping”,把t1轉為”running”。線程t1又把它之前得到的0加1后賦值給num。這樣,明明t1和t2都完成了1次加1工作,但結果仍然是num=1。
from threading import Thread
import time
g_num = 0
def test1():
global g_num
for i in range(1000000):
g_num += 1
print("---test1---g_num=%d"%g_num)
def test2():
global g_num
for i in range(1000000):
g_num += 1
print("---test2---g_num=%d"%g_num)
p1 = Thread(target=test1)
p1.start()
# time.sleep(3) #取消屏蔽之后 再次運行程序,結果的不同
p2 = Thread(target=test2)
p2.start()
print("---g_num=%d---"%g_num)
運行結果卻不是2000000:
---g_num=129699---
---test2---g_num=1126024
---test1---g_num=1135562
取消屏蔽之后,再次運行結果如下:
---test1---g_num=1000000
---g_num=1025553---
---test2---g_num=2000000
問題產生的原因就是沒有控制多個線程對同一資源的訪問,對數據造成破壞,使得線程運行的結果不可預期。這種現象稱為“線程不安全”。
2.同步
- 同步就是協同步調,按預定的先后次序進行運行。
- 如進程、線程同步,可理解為進程或線程A和B一塊配合,A執行到一定程度時要依靠B的某個結果,于是停下來,示意B運行;B依言執行,再將結果給A;A再繼續操作。
3.解決線程不安全的方法
可以通過線程同步來解決
- 系統調用t1,然后獲取到num的值為0,此時上一把鎖,即不允許其他現在操作num
- 對num的值進行+1
- 解鎖,此時num的值為1,其他的線程就可以使用num了,而且是num的值不是0而是1
- 同理其他線程在對num進行修改時,都要先上鎖,處理完后再解鎖,在上鎖的整個過程中不允許其他線程訪問,就保證了數據的正確性
3.互斥鎖
- 當多個線程幾乎同時修改某一個共享數據的時候,需要進行同步控制
線程同步能夠保證多個線程安全訪問競爭資源,最簡單的同步機制是引入互斥鎖。 - 互斥鎖為資源引入一個狀態:鎖定/非鎖定。
- 某個線程要更改共享數據時,先將其鎖定,此時資源的狀態為“鎖定”,其他線程不能更改;直到該線程釋放資源,將資源的狀態變成“非鎖定”,其他的線程才能再次鎖定該資源。互斥鎖保證了每次只有一個線程進行寫入操作,從而保證了多線程情況下數據的正確性。
threading模塊中定義了Lock類,可以方便的處理鎖定:
#創建鎖
mutex = threading.Lock()
#鎖定
mutex.acquire([blocking])
#釋放
mutex.release()
其中,鎖定方法acquire可以有一個blocking參數。
- 如果設定blocking為True,則當前線程會堵塞,直到獲取到這個鎖為止(如果沒有指定,那么默認為True)
- 如果設定blocking為False,則當前線程不會堵塞
from threading import Thread, Lock
import time
g_num = 0
def test1():
global g_num
for i in range(1000000):
#True表示堵塞 即如果這個鎖在上鎖之前已經被上鎖了,那么這個線程會在這里一直等待到解鎖為止
#False表示非堵塞,即不管本次調用能夠成功上鎖,都不會卡在這,而是繼續執行下面的代碼
mutexFlag = mutex.acquire(True)
if mutexFlag:
g_num += 1
mutex.release()
print("---test1---g_num=%d"%g_num)
def test2():
global g_num
for i in range(1000000):
mutexFlag = mutex.acquire(True) #True表示堵塞
if mutexFlag:
g_num += 1
mutex.release()
print("---test2---g_num=%d"%g_num)
#創建一個互斥鎖
#這個鎖默認是未上鎖的狀態
mutex = Lock()
p1 = Thread(target=test1)
p1.start()
p2 = Thread(target=test2)
p2.start()
print("---g_num=%d---"%g_num)
運行結果:
---g_num=19446---
---test1---g_num=1699950
---test2---g_num=2000000
加入互斥鎖后,運行結果與預期相符。
我們可以模擬一下賣票的程序:
# Python主要通過標準庫中的threading包來實現多線程
import threading
import time
import os
def doChore(): # 作為間隔 每次調用間隔0.5s
time.sleep(0.5)
def booth(tid):
global i
global lock
while True:
lock.acquire() # 得到一個鎖,鎖定
if i != 0:
i = i - 1 # 售票 售出一張減少一張
print(tid, ':now left:', i) # 剩下的票數
doChore()
else:
print("Thread_id", tid, " No more tickets")
os._exit(0) # 票售完 退出程序
lock.release() # 釋放鎖
doChore()
#全局變量
i = 15 # 初始化票數
lock = threading.Lock() # 創建鎖
def main():
# 總共設置了3個線程
for k in range(3):
# 創建線程; Python使用threading.Thread對象來代表線程
new_thread = threading.Thread(target=booth, args=(k,))
# 調用start()方法啟動線程
new_thread.start()
if __name__ == '__main__':
main()
運行結果:
0 :now left: 14
1 :now left: 13
0 :now left: 12
2 :now left: 11
1 :now left: 10
0 :now left: 9
1 :now left: 8
2 :now left: 7
0 :now left: 6
1 :now left: 5
2 :now left: 4
0 :now left: 3
2 :now left: 2
0 :now left: 1
1 :now left: 0
Thread_id 2 No more tickets
- 上鎖解鎖過程
當一個線程調用鎖的acquire()方法獲得鎖時,鎖就進入“locked”狀態。
每次只有一個線程可以獲得鎖。如果此時另一個線程試圖獲得這個鎖,該線程就會變為“blocked”狀態,稱為“阻塞”,直到擁有鎖的線程調用鎖的release()方法釋放鎖之后,鎖進入“unlocked”狀態。
線程調度程序從處于同步阻塞狀態的線程中選擇一個來獲得鎖,并使得該線程進入運行(running)狀態。
鎖的好處: - 確保了某段關鍵代碼只能由一個線程從頭到尾完整地執行
鎖的壞處: - 阻止了多線程并發執行,包含鎖的某段代碼實際上只能以單線程模式執行,效率就大大地下降了
- 由于可以存在多個鎖,不同的線程持有不同的鎖,并試圖獲取對方持有的鎖時,可能會造成死鎖
4.多線程-非共享數據
對于多線程中全局變量和局部變量是否共享
- 多線程局部變量
#coding=utf-8
import threading
import time
class MyThread(threading.Thread):
# 重寫 構造方法
def __init__(self,num,sleepTime):
threading.Thread.__init__(self)
self.num = num
self.sleepTime = sleepTime
def run(self):
self.num += 1
time.sleep(self.sleepTime)
print('線程(%s),num=%d'%(self.name, self.num))
if __name__ == '__main__':
mutex = threading.Lock()
t1 = MyThread(100,5)
t1.start()
t2 = MyThread(200,1)
t2.start()
運行結果:
線程(Thread-2),num=201
線程(Thread-1),num=101
- 多線程全局變量
import threading
from time import sleep
def test(sleepTime):
num = 1
sleep(sleepTime)
num+=1
print('---(%s)--num=%d'%(threading.current_thread(), num))
if __name__ == '__main__':
t1 = threading.Thread(target = test,args=(5,))
t2 = threading.Thread(target = test,args=(1,))
t1.start()
t2.start()
運行結果:
---(<Thread(Thread-2, started 10876)>)--num=2
---(<Thread(Thread-1, started 7484)>)--num=2
- 在多線程開發中,全局變量是多個線程都共享的數據,而局部變量等是各自線程的,是非共享的
5.同步應用
- 多個線程有序執行
from threading import Thread,Lock
from time import sleep
class Task1(Thread):
def run(self):
while True:
if lock1.acquire():
print("------Task 1 -----")
sleep(0.5)
lock2.release()
class Task2(Thread):
def run(self):
while True:
if lock2.acquire():
print("------Task 2 -----")
sleep(0.5)
lock3.release()
class Task3(Thread):
def run(self):
while True:
if lock3.acquire():
print("------Task 3 -----")
sleep(0.5)
lock1.release()
#使用Lock創建出的鎖默認沒有“鎖上”
lock1 = Lock()
#創建另外一把鎖,并且“鎖上”
lock2 = Lock()
lock2.acquire()
#創建另外一把鎖,并且“鎖上”
lock3 = Lock()
lock3.acquire()
t1 = Task1()
t2 = Task2()
t3 = Task3()
t1.start()
t2.start()
t3.start()
運行結果:
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
...........`
- 可以使用互斥鎖完成多個任務,有序的進程工作,這就是線程的同步
6.生產者與消費者模式
- Python的Queue模塊中提供了同步的、線程安全的隊列類,包括FIFO(先入先出)隊列Queue,LIFO(后入先出)隊列LifoQueue,和優先級隊列PriorityQueue。這些隊列都實現了鎖原語(可以理解為原子操作,即要么不做,要么就做完),能夠在多線程中直接使用。可以使用隊列來實現線程間的同步。
- 用FIFO隊列實現上述生產者與消費者問題的代碼如下:
import threading,time
from queue import Queue
class Producer(threading.Thread):
def run(self):
global queue
count = 0
while True:
if queue.qsize() < 1000:
for i in range(100):
count = count +1
msg = '生成產品'+str(count)
queue.put(msg)
print(msg)
time.sleep(0.5)
class Consumer(threading.Thread):
def run(self):
global queue
while True:
if queue.qsize() > 100:
for i in range(3):
msg = self.name + '消費了 '+queue.get()
print(msg)
time.sleep(1)
if __name__ == '__main__':
queue = Queue()
for i in range(500):
queue.put('初始產品'+str(i))
for i in range(2):
p = Producer()
p.start()
for i in range(5):
c = Consumer()
c.start()
運行結果:
生成產品1
生成產品2
生成產品1
生成產品3
生成產品2
生成產品4
Thread-3消費了 初始產品0
生成產品3
生成產品5
Thread-3消費了 初始產品1
生成產品4
生成產品6
Thread-4消費了 初始產品2
Thread-3消費了 初始產品3
生成產品5
生成產品7
Thread-4消費了 初始產品4
生成產品6
生成產品8
Thread-5消費了 初始產品5
Thread-4消費了 初始產品6
............
此時就出現生產者與消費者的問題
1.Queue的說明
1.對于Queue,在多線程通信之間扮演重要的角色
2.添加數據到隊列中,使用put()方法
3.從隊列中取數據,使用get()方法
4.判斷隊列中是否還有數據,使用qsize()方法
2.生產者消費者模式的說明
-
使用生產者和消費者模式的原因
在線程世界里,生產者就是生產數據的線程,消費者就是消費數據的線程。在多線程開發當中,如果生產者處理速度很快,而消費者處理速度很慢,那么生產者就必須等待消費者處理完,才能繼續生產數據。同樣的道理,如果消費者的處理能力大于生產者,那么消費者就必須等待生產者。為了解決這個問題于是引入了生產者和消費者模式。 -
生產者消費者模式
生產者消費者模式是通過一個容器來解決生產者和消費者的強耦合問題。生產者和消費者彼此之間不直接通訊,而通過阻塞隊列來進行通訊,所以生產者生產完數據之后不用等待消費者處理,直接扔給阻塞隊列,消費者不找生產者要數據,而是直接從阻塞隊列里取,阻塞隊列就相當于一個緩沖區,平衡了生產者和消費者的處理能力。這個阻塞隊列就是用來給生產者和消費者解耦的。
3.ThreadLocal
在多線程環境下,每個線程都有自己的數據。一個線程使用自己的局部變量比使用全局變量好,因為局部變量只有線程自己能看見,不會影響其他線程,而全局變量的修改必須加鎖。
1.使用函數傳參的方法
def process_student(name):
std = Student(name)
# std是局部變量,但是每個函數都要用它,因此必須傳進去:
do_task_1(std)
do_task_2(std)
def do_task_1(std):
do_subtask_1(std)
do_subtask_2(std)
def do_task_2(std):
do_subtask_2(std)
do_subtask_2(std)
說明:用局部變量也有問題,因為每個線程處理不同的Student對象,不能共享。
2.使用全局字典的方法
import threading
# 創建字典對象:
myDict={}
def process_student():
# 獲取當前線程關聯的student:
std = myDict[threading.current_thread()]
print('Hello, %s (in %s)' % (std, threading.current_thread().name))
def process_thread(name):
# 綁定ThreadLocal的student:
myDict[threading.current_thread()] = name
process_student()
t1 = threading.Thread(target=process_thread, args=('yongGe',), name='Thread-A')
t2 = threading.Thread(target=process_thread, args=('老王',), name='Thread-B')
t1.start()
t2.start()
運行結果;
Hello, yongGe (in Thread-A)
Hello, 老王 (in Thread-B)
這種方式理論上是可行的,它最大的優點是消除了std對象在每層函數中的傳遞問題,但是,每個函數獲取std的代碼有點low。
3.使用ThreadLocal的方法
import threading
# 創建全局ThreadLocal對象:
local_school = threading.local()
def process_student():
# 獲取當前線程關聯的student:
std = local_school.student
print('Hello, %s (in %s)' % (std, threading.current_thread().name))
def process_thread(name):
# 綁定ThreadLocal的student:
local_school.student = name
process_student()
t1 = threading.Thread(target=process_thread, args=('erererbai',), name='Thread-A')
t2 = threading.Thread(target=process_thread, args=('老王',), name='Thread-B')
t1.start()
t2.start()
運行結果:
Hello, erererbai (in Thread-A)
Hello, 老王 (in Thread-B)
說明:
全局變量local_school就是一個ThreadLocal對象,每個Thread對它都可以讀寫student屬性,但互不影響。你可以把local_school看成全局變量,但每個屬性如local_school.student都是線程的局部變量,可以任意讀寫而互不干擾,也不用管理鎖的問題,ThreadLocal內部會處理。
可以理解為全局變量local_school是一個dict,不但可以用local_school.student,還可以綁定其他變量,如local_school.teacher等等。
ThreadLocal最常用的地方就是為每個線程綁定一個數據庫連接,HTTP請求,用戶身份信息等,這樣一個線程的所有調用到的處理函數都可以非常方便地訪問這些資源。
- 一個ThreadLocal變量雖然是全局變量,但每個線程都只能讀寫自己線程的獨立副本,互不干擾。ThreadLocal解決了參數在一個線程中各個函數之間互相傳遞的問題