Python程式VS執行緒

一隻寫程式的猿發表於2019-02-19

#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.解決執行緒不安全的方法
可以通過執行緒同步來解決

  1. 系統呼叫t1,然後獲取到num的值為0,此時上一把鎖,即不允許其他現在操作num
  2. 對num的值進行+1
  3. 解鎖,此時num的值為1,其他的執行緒就可以使用num了,而且是num的值不是0而是1
  4. 同理其他執行緒在對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解決了引數在一個執行緒中各個函式之間互相傳遞的問題

相關文章