多任務----線程、同步、互斥鎖、死鎖

多任務

        簡單來說,就是同時進行多個任務,比如一邊上班一邊刷手機,咳咳。。。一邊唱歌,一邊跳舞,一邊偷瞄旁邊的妹子,嗯,這都是多任務。

羅賓的技能,以及千手觀音,哪吒三頭六臂。

通常的程序是這樣式兒的:

# 順序執行(非多任務)
from time import sleep

def work():
    for i in range(3):
        print('我在工作...%d'%i)
        sleep(1)

def play():
    for i in range(3):
        print('我在玩手機...%d'%i)
        sleep(1)

if __name__ == '__main__':
    work()
    play()

        多核CPU已經非常普及了,即使過去的單核CPU,也可以執行多任務。CPU執行代碼都是順序執行的,單核CPU是怎麼執行多任務的呢?

        時間片輪轉----操作系統輪流讓各個任務交替執行,任務1執行0.01秒,切換到任務2,任務2執行0.01秒,再切換到任務3,執行0.01秒……這樣反覆執行下去。表面上看,每個任務都是交替執行的,但是,由於CPU的執行速度實在是太快了,我們感覺就像所有任務都在同時執行一樣。

        真正的並行執行多任務只能在多核CPU上實現,但是,由於任務數量遠遠多於CPU的核心數量,所以,操作系統也會自動把很多任務輪流調度到每個核心上執行。

  • 併發:指的是任務數多餘cpu核數,通過操作系統的各種任務調度算法,實現用多個任務“一起”執行(實際上總有一些任務不在執行,因爲切換任務的速度相當快,看上去一起執行而已)
  • 並行:指的是任務數小於等於cpu核數,即任務真的是一起執行的

 

 

線程

Python的thread模塊是比較底層的模塊,python的threading模塊是對thread做了一些包裝的,可以更加方便的被使用。

Python中threading的使用

1.單線程執行

import time

def sayLove():
    print('親愛的,我愛你!')
    time.sleep(1)

if __name__ == '__main__':
    for i in range(5):
        sayLove()

運行結果:

# 1s一條
親愛的,我愛你!
親愛的,我愛你!
親愛的,我愛你!
親愛的,我愛你!
親愛的,我愛你!

 

2.多線程執行

import time
import threading

def sayLove():
    print("親愛的,我愛你!")
    time.sleep(1)

if __name__ == "__main__":
    for i in range(5):
        t = threading.Thread(target=sayLove)
        t.start() # 啓動線程,讓線程開始執行。
    

 運行結果:

# 1s 全出
親愛的,我愛你!
親愛的,我愛你!
親愛的,我愛你!
親愛的,我愛你!
親愛的,我愛你!

注:

  1. 主線程會等待所有子線程結束後才結束。
  2. 通過使用threading模塊能完成多任務的程序開發,爲了讓每個線程的封裝性更完美,所以使用threading模塊時,往往會定義一個新的子類class,只要繼承threading.Thread就可以了,然後重寫run方法。
  3. 多線程程序的執行順序是不確定的。當執行到sleep語句時,線程將被阻塞(Blocked),到sleep結束後,線程進入就緒(Runnable)狀態,等待調度。而線程調度將自行選擇一個線程執行。上面的代碼中只能保證每個線程都運行完整個run函數,但是線程的啓動順序、run函數中每次循環的執行順序都不能確定。
"""
重寫run方法
Python的threading.Thread類有一個run方法,用於定義線程的功能函數,可以在自己的線程類中覆蓋該方法。
創建自己的線程實例後,通過Thread類的start方法,可以啓動該線程,交給python虛擬機進行調度,
當該線程獲得執行的機會時,就會調用run方法執行線程。
"""
import threading
import time

class MyThread(threading.Thread): # 繼承threading.Thread
    def run(self):
        for i in range(3):
            time.sleep(1)
            msg = "I'm "+self.name+' @ '+str(i) #name屬性中保存的是當前線程的名字
            print(msg)


if __name__ == '__main__':
    t = MyThread()
    t.start()

 

 

多線程共享全局變量

雖然大家都能騎,不過最好一次一個哦~不然翻車警告!
from threading import Thread
import time

g_num = 100

def work1():
    global g_num
    for i in range(3):
        g_num += 1

    print("----in work1, g_num is %d---"%g_num)


def work2():
    global g_num
    print("----in work2, g_num is %d---"%g_num)


print("---線程創建之前g_num is %d---"%g_num)

t1 = Thread(target=work1)
t1.start()

#延時一會,保證t1線程中的事情做完
time.sleep(1)

t2 = Thread(target=work2)
t2.start()

運行結果:

---線程創建之前g_num is 100---
----in work1, g_num is 103---
----in work2, g_num is 103---

 

列表做實參傳遞到線程中

from threading import Thread
import time

def work1(nums):
    nums.append(44)
    print("----in work1---",nums)


def work2(nums):
    #延時一會,保證t1線程中的事情做完
    time.sleep(1)
    print("----in work2---",nums)

g_nums = [11,22,33]

t1 = Thread(target=work1, args=(g_nums,))
t1.start()

t2 = Thread(target=work2, args=(g_nums,))
t2.start()

運行結果:

----in work1--- [11, 22, 33, 44]
----in work2--- [11, 22, 33, 44]
  • 在一個進程內的所有線程共享全局變量,很方便在多個線程間共享數據
  • 缺點就是,線程是對全局變量隨意遂改可能造成多線程之間對全局變量的混亂(即線程非安全)

 

線程安全----多線程共享全局變量可能遇到的問題

假設兩個線程t1和t2都要對全局變量g_num(默認是0)進行加1運算,t1和t2都各對g_num加10次,g_num的最終的結果應該爲20。

但是由於是多線程同時操作,有可能出現下面情況:

  1. 在g_num=0時,t1取得g_num=0。此時系統把t1調度爲”sleeping”狀態,把t2轉換爲”running”狀態,t2也獲得g_num=0
  2. 然後t2對得到的值進行加1並賦給g_num,使得g_num=1
  3. 然後系統又把t2調度爲”sleeping”,把t1轉爲”running”。線程t1又把它之前得到的0加1後賦值給g_num。
  4. 這樣導致雖然t1和t2都對g_num加1,但結果仍然是g_num=1

即多線程狀態下,加了兩次結果只加了一次。

import threading
import time

g_num = 0

def work1(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("----in work1, g_num is %d---"%g_num)


def work2(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("----in work2, g_num is %d---"%g_num)


print("---線程創建之前g_num is %d---"%g_num)

t1 = threading.Thread(target=work1, args=(1000000,))
t1.start()  # 啓動線程

t2 = threading.Thread(target=work2, args=(1000000,))
t2.start()

while len(threading.enumerate()) != 1:
    time.sleep(1)

print("2個線程對同一個全局變量操作之後的最終結果是:%s" % g_num)

運行結果:

---線程創建之前g_num is 0---
----in work1, g_num is 1088005---
----in work2, g_num is 1286202---
2個線程對同一個全局變量操作之後的最終結果是:1286202

---線程創建之前g_num is 0---
----in work1, g_num is 1240520---
----in work2, g_num is 1347813---
2個線程對同一個全局變量操作之後的最終結果是:1347813

---線程創建之前g_num is 0---
----in work2, g_num is 1328364---
----in work1, g_num is 1342582---
2個線程對同一個全局變量操作之後的最終結果是:1342582

 多線程同時對同一個全局變量操作,會出現資源競爭問題,從而數據結果會不正確。

 

 

同步

協同同步,按照預定的先後順序依次運行,你先bb, 然後我再說。

如上所示的多線程競爭資源問題,就可以通過線程同步來解決。

思路,如下:

  1. 系統調用t1,然後獲取到g_num的值爲0,此時上一把鎖,即不允許其他線程操作g_num
  2. t1對g_num的值進行+1
  3. t1解鎖,此時g_num的值爲1,其他的線程就可以使用g_num了,而且是g_num的值不是0而是1
  4. 同理其他線程在對g_num進行修改時,都要先上鎖,處理完後再解鎖,在上鎖的整個過程中不允許其他線程訪問,就保證了數據的正確性

總結:同一時刻只允許一個線程對共享資源擁有處置權。

 

互斥鎖

        當多個線程幾乎同時修改某一個共享數據的時候,需要進行同步控制。線程同步能夠保證多個線程安全訪問競爭資源,最簡單的同步機制是引入互斥鎖。

互斥鎖爲資源引入一個狀態:鎖定/非鎖定

        某個線程要更改共享數據時,先將其鎖定,此時資源的狀態爲“鎖定”,其他線程不能更改;直到該線程釋放資源,將資源的狀態變成“非鎖定”,其他的線程才能再次鎖定該資源。互斥鎖保證了每次只有一個線程進行寫入操作,從而保證了多線程情況下數據的正確性。

        比如生活中的各種排隊場景,售票處(共享資源)每次只能爲有限位顧客服務,服務時,就是上鎖,未在服務狀態時,便可用。

Python 的 threading模塊中定義了Lock類,可以方便的處理鎖定:

# 創建鎖
mutex = threading.Lock()

# 鎖定
mutex.acquire()

# 釋放
mutex.release()

注意:

  • 如果這個鎖之前是沒有上鎖的,那麼acquire不會堵塞
  • 如果在調用acquire對這個鎖上鎖之前 它已經被 其他線程上了鎖,那麼此時acquire會堵塞,直到這個鎖被解鎖爲止
import threading
import time

g_num = 0

def work1(num):
    global g_num
    for i in range(num):
        mutex.acquire()
        g_num += 1
        mutex.release()
    print('---work1----g_num=%d'%g_num)

def work2(num):
    global g_num
    for i in range(num):
        mutex.acquire()
        g_num += 1
        mutex.release()
    print('----work2----g_num=%d'%g_num)

# 創建互斥鎖,默認未上鎖
mutex = threading.Lock()


# 創建兩個線程,讓他們各自對g_num 加10000次
p1 = threading.Thread(target=work1, args=(10000,))
p1.start()

p2 = threading.Thread(target=work2, args=(10000,))
p2.start()

while len(threading.enumerate()) != 1:
    time.sleep(1)
print('2個線程對同一個全局變量操作之後的結果是%s'%g_num)

運行結果:

---work1----g_num=10000
----work2----g_num=20000
2個線程對同一個全局變量操作之後的結果是20000

 

上鎖解鎖過程

        當一個線程調用鎖的acquire()方法獲得鎖時,鎖就進入“locked”狀態。

        每次只有一個線程可以獲得鎖。如果此時另一個線程試圖獲得這個鎖,該線程就會變爲“blocked”狀態,稱爲“阻塞”,直到擁有鎖的線程調用鎖的release()方法釋放鎖之後,鎖進入“unlocked”狀態。

        線程調度程序從處於同步阻塞狀態的線程中選擇一個來獲得鎖,並使得該線程進入運行(running)狀態。

 

上鎖的優缺點:

優點:

  • 上鎖可以保證一個線程從頭到尾完整的執行。

缺點:

  • 阻止了多線程併發執行,包含鎖的某段代碼實際上只能以單線程執行,效率會大大下降。
  • 鎖可以存在多個,不同線程持有不同的鎖,並試圖獲取對方持有的鎖的時候,可能會造成死鎖。

 

死鎖

        一轉身就是一輩子,AB互生情愫,但都不確信對方喜歡自己,都在等對方的態度,就生生錯過了,這種都等待對方的狀態就是死鎖。人‘死鎖'會錯過,程序死鎖會卡在那兒。

在線程間共享多個資源的時候,如果兩個線程分別佔有一部分資源並且同時等待對方的資源,就會造成死鎖。

儘管死鎖很少發生,但一旦發生就會造成應用的停止響應。

下面看一個死鎖的例子:

import threading
import time

class MyThread1(threading.Thread):
    def run(self):
        # 對mutexA上鎖
        mutexA.acquire()

        # mutexA上鎖後,延時1秒,等待另外那個線程 把mutexB上鎖
        print(self.name+'----do1---up----')
        time.sleep(1)

        # 此時會堵塞,因爲這個mutexB已經被另外的線程搶先上鎖了
        mutexB.acquire()
        print(self.name+'----do1---down----')
        mutexB.release()

        # 對mutexA解鎖
        mutexA.release()

class MyThread2(threading.Thread):
    def run(self):
        # 對mutexB上鎖
        mutexB.acquire()

        # mutexB上鎖後,延時1秒,等待另外那個線程 把mutexA上鎖
        print(self.name+'----do2---up----')
        time.sleep(1)

        # 此時會堵塞,因爲這個mutexA已經被另外的線程搶先上鎖了
        mutexA.acquire()
        print(self.name+'----do2---down----')
        mutexA.release()

        # 對mutexB解鎖
        mutexB.release()

mutexA = threading.Lock()
mutexB = threading.Lock()

if __name__ == '__main__':
    t1 = MyThread1()
    t2 = MyThread2()
    t1.start()
    t2.start()

避免死鎖

  • 程序設計時要儘量避免(銀行家算法)
  • 添加超時時間等

 

 

附錄-銀行家算法

[背景知識]

        一個銀行家如何將一定數目的資金安全地借給若干個客戶,使這些客戶既能借到錢完成要乾的事,同時銀行家又能收回全部資金而不至於破產,這就是銀行家問題。這個問題同操作系統中資源分配問題十分相似:銀行家就像一個操作系統,客戶就像運行的進程,銀行家的資金就是系統的資源。

[問題的描述]

        一個銀行家擁有一定數量的資金,有若干個客戶要貸款。每個客戶須在一開始就聲明他所需貸款的總額。若該客戶貸款總額不超過銀行家的資金總數,銀行家可以接收客戶的要求。客戶貸款是以每次一個資金單位(如1萬RMB等)的方式進行的,客戶在借滿所需的全部單位款額之前可能會等待,但銀行家須保證這種等待是有限的,可完成的。

例如:有三個客戶C1,C2,C3,向銀行家借款,該銀行家的資金總額爲10個資金單位,其中C1客戶要借9各資金單位,C2客戶要借3個資金單位,C3客戶要借8個資金單位,總計20個資金單位。某一時刻的狀態如圖所示。

        對於a圖的狀態,按照安全序列的要求,我們選的第一個客戶應滿足該客戶所需的貸款小於等於銀行家當前所剩餘的錢款,可以看出只有C2客戶能被滿足:C2客戶需1個資金單位,小銀行家手中的2個資金單位,於是銀行家把1個資金單位借給C2客戶,使之完成工作並歸還所借的3個資金單位的錢,進入b圖。同理,銀行家把4個資金單位借給C3客戶,使其完成工作,在c圖中,只剩一個客戶C1,它需7個資金單位,這時銀行家有8個資金單位,所以C1也能順利借到錢並完成工作。最後(見圖d)銀行家收回全部10個資金單位,保證不賠本。那麼客戶序列{C1,C2,C3}就是個安全序列,按照這個序列貸款,銀行家纔是安全的。否則的話,若在圖b狀態時,銀行家把手中的4個資金單位借給了C1,則出現不安全狀態:這時C1,C3均不能完成工作,而銀行家手中又沒有錢了,系統陷入僵持局面,銀行家也不能收回投資。

        綜上所述,銀行家算法是從當前狀態出發,逐個按安全序列檢查各客戶誰能完成其工作,然後假定其完成工作且歸還全部貸款,再進而檢查下一個能完成工作的客戶,......。如果所有客戶都能完成工作,則找到一個安全序列,銀行家纔是安全的。

 

 Scrat 一個熱愛堅果的松鼠 ~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章