併發編程(三)—— 線程編程、同步互斥(線程鎖)、GIL

線程編程(Thread)

線程基本概念

  1. 什麼是線程
    【1】線程被稱爲輕量級的進程
    【2】線程也可以使用計算機多核資源,是多任務編程方式
    【3】線程是系統分配內核的最小單元(有可能多個線程佔用多個內核)
    【4】線程可以理解爲進程的分支任務
    在這裏插入圖片描述
  2. 線程特徵
    【1】一個進程中可以包含多個線程
    【2】線程也是一個運行行爲,消耗計算機資源
    【3】一個進程中的所有線程共享這個進程的資源
    【4】多個線程之間的運行互不影響各自運行
    【5】線程的創建和銷燬消耗資源遠小於進程
    【6】各個線程也有自己的ID等特徵

threading模塊創建線程

【1】 創建線程對象

from threading import Thread 

t = Thread()
功能:創建線程對象
參數:target 綁定線程函數
   args 元組 給線程函數位置傳參
   kwargs 字典 給線程函數鍵值傳參

【2】 啓動線程

t.start()

【3】 回收線程

t.join([timeout])

單線程(無參數):

from threading import Thread
from time import sleep
import os

a=1
def music():
	global a
	print("a=",a)# 1
    a = 10000
    for i in range(5):
        sleep(2)
        print(os.getpid(),"播放心如止水")

# 創建線程對象
t = Thread(target=music)
t.start()

# 主線程任務
for i in range(3):
    sleep(3)
    print(os.getpid(),"播放跳舞吧")

t.join()  # 回收進程

# 同一進程,分支線程將變量值更改,主線程變量值也隨之更改(區分多進程,兩個進程間互不影響)
print("Main thread a:",a)#10000

多線程(有參數):

from threading import Thread
from time import sleep

# 含有參數的線程函數
def fun(sec, name):
    print("線程函數傳參")
    sleep(sec)
    print("%s 線程執行完畢" % name)

# 創建多個線程
jobs = []
for i in range(5):
    t = Thread(target=fun, args=(2,),
               kwargs={'name': 'T%d' % i})
    jobs.append(t)
    t.start()

for i in jobs:
    i.join()

線程對象屬性

t.name 線程名稱
t.setName() 設置線程名稱
t.getName() 獲取線程名稱

t.is_alive() 查看線程是否在生命週期

t.daemon 設置主線程和分支線程的退出關係
t.setDaemon() 設置daemon屬性值
t.isDaemon() 查看daemon屬性值
*daemon爲True時主線程退出分支線程也退出。要在start前設置,通常不和join一起使用

from threading import Thread
from time import sleep

def fun():
    sleep(3)
    print("線程屬性測試")

t = Thread(target=fun, name="Tarena")

# 主線程退出分支線程也退出
t.setDaemon(True)

t.start()

t.setName("Tedu")
print("Name:", t.getName())  # 線程名稱(默認Thread-1)
print("Alive:", t.is_alive())  # 線程生命週期
print("is Daemon", t.isDaemon())

自定義線程類

  1. 創建步驟
    【1】繼承Thread類
    【2】重寫__init__方法添加自己的屬性,使用super加載父類屬性
    【3】重寫run方法
  2. 使用方法
    【1】 實例化對象
    【2】 調用start自動執行run方法
    【3】 調用join回收線程

重點代碼:

from threading import Thread

class ThreadClass(Thread):
    def __init__(self, attr):
        self.attr = attr
        super().__init__()  # 調用父類的init方法

    def f1(self):
        print("步驟1", self.attr)

    def f2(self):
        print("步驟2")

    # 父類方法重寫
    def run(self):
        self.f1()
        self.f2()

t = ThreadClass('XXXXX')
t.start()  # 自動運行run方法
t.join()

小練習:

from threading import Thread
from time import sleep, ctime

class MyThread(Thread):
	# pass
    # 解答如下:
    def __init__(self, target=None, args=(),
                 kwargs={}, name="AAA"):
        super().__init__()
        self.target = target
        self.args = args
        self.kwargs = kwargs
        self.name = name

    def run(self):
        self.target(*self.args, **self.kwargs)

# **********************************
# 通過完成上面的MyThread類讓整個程序可以正常執行
# 當調用start時player作爲一個線程功能函數運行
# 注意:函數名稱和參數並不確定,player只是測試函數
# **********************************

def player(sec, song):
    for i in range(2):
        print("Playing %s:%s" % (song, ctime()))
        sleep(sec)

t = MyThread(target=player, args=(3,),
             kwargs={'song': '涼涼'}, name='happy')

t.start()
t.join()

同步互斥

線程間通信方法

  1. 通信方法
    線程間使用全局變量進行通信。
  2. 共享資源爭奪
  • 共享資源:多個進程或者線程都可以操作的資源稱爲共享資源。對共享資源的操作代碼段稱爲臨界區。
  • 影響 : 對共享資源的無序操作可能會帶來數據的混亂,或者操作錯誤。此時往往需要同步互斥機制協調操作順序。
  1. 同步互斥機制
    同步 : 同步是一種協作關係,爲完成操作,多進程或者線程間形成一種協調,按照必要的步驟有序執行操作。
    在這裏插入圖片描述
    互斥 : 互斥是一種制約關係,當一個進程或者線程佔有資源時會進行加鎖處理,此時其他進程線程就無法操作該資源,直到解鎖後才能操作。
    在這裏插入圖片描述

線程同步互斥方法

線程Event

from threading import Event

e = Event()  創建線程event對象

e.wait([timeout])  阻塞等待e被set

e.set()  設置e,使wait結束阻塞(沒有這句話e.wait()一直堵塞)

e.clear() 使e回到未被設置狀態

e.is_set()  查看當前e是否被設置

重點代碼:

from threading import Thread, Event

s = None  # 全局變量用於通信
e = Event()

def yang():
    print("楊子榮前來拜山頭")
    global s
    s = "天王蓋地虎"
    e.set()  # 共享資源操作完畢

t = Thread(target=yang)
t.start()
print("說對口令就是自己人")

e.wait()  # 堵塞等待
if s == "天王蓋地虎":
    print("寶塔鎮河妖")
    print("確認過眼神,你是對的人")
else:
    print("打死他。。。")

t.join()

線程鎖 Lock

from  threading import Lock

lock = Lock()  創建鎖對象
lock.acquire() 上鎖  如果lock已經上鎖再調用會阻塞
lock.release() 解鎖

with  lock:  上鎖
...
...
	 with代碼塊結束自動解鎖

重點代碼:

from threading import Thread, Lock

a = b = 0
lock = Lock()

def value():
    while True:
        lock.acquire()  # 上鎖
        if a != b:
            print("a=%d,b=%d" % (a, b))
        lock.release()  # 解鎖

t = Thread(target=value)
t.start()
while True:
    with lock:
        a += 1
        b += 1
t.join()

死鎖及其處理

  1. 定義

死鎖是指兩個或兩個以上的線程在執行過程中,由於競爭資源或者由於彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖。
在這裏插入圖片描述
2. 死鎖產生條件

死鎖發生的必要條件:

  • 互斥條件:指線程對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個進程佔用。如果此時還有其它進程請求資源,則請求者只能等待,直至佔有資源的進程用畢釋放。
  • 請求和保持條件:指線程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程佔有,此時請求線程阻塞,但又對自己已獲得的其它資源保持不放。
  • 不剝奪條件:指線程已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放,通常CPU內存資源是可以被系統強行調配剝奪的。
  • 環路等待條件:指在發生死鎖時,必然存在一個線程——資源的環形鏈,即進程集合{T0,T1,T2,···,Tn}中的T0正在等待一個T1佔用的資源;T1正在等待T2佔用的資源,……,Tn正在等待已被T0佔用的資源。
  1. 死鎖的產生原因:

簡單來說造成死鎖的原因可以概括成三句話:

  • 當前線程擁有其他線程需要的資源
  • 當前線程等待其他線程已擁有的資源
  • 都不放棄自己擁有的資源
  1. 如何避免死鎖

死鎖是我們非常不願意看到的一種現象,我們要儘可能避免死鎖的情況發生。通過設置某些限制條件,去破壞產生死鎖的四個必要條件中的一個或者幾個,來預防發生死鎖。預防死鎖是一種較易實現的方法。但是由於所施加的限制條件往往太嚴格,可能會導致系統資源利用率。

  • 使用定時鎖
  • 使用重載鎖RLock(),用法同Lock。RLock內部維護着一個Lock和一個counter變量,counter記錄了acquire的次數,從而使得資源可以被多次acquire。直到一個線程所有的acquire都被release,其他的線程才能獲得資源。

使用定時鎖:

import time
import threading

# 交易類
class Account:
    def __init__(self, _id, balance, lock):
        self.id = _id  # 用戶
        self.balance = balance  # 存款
        self.lock = lock  # 鎖

    # 取錢
    def withdraw(self, amount):
        self.balance -= amount

    # 存錢
    def deposit(self, amount):
        self.balance += amount

    # 查看餘額
    def get_balance(self):
        return self.balance

# 創建賬戶
Tom = Account('Tom', 5000, threading.Lock())
Alex = Account('Alex', 8000, threading.Lock())


# 轉賬函數
def transfer(from_, to, amount):
    if from_.lock.acquire():  # 鎖自己賬戶
        from_.withdraw(amount)  # 自己賬戶減少
        time.sleep(0.5)  # 產生死鎖(因爲from和to同時上鎖)
        if to.lock.acquire():  # 對方賬戶上鎖
            to.deposit(amount)
            to.lock.release()  # 對方賬戶解鎖
        from_.lock.release()  # 自己賬戶解鎖
    print("%s 給 %s轉賬完成" % (from_.id, to.id))

t1 = threading.Thread(target=transfer,
                      args=(Tom, Alex, 2000))
t2 = threading.Thread(target=transfer,
                      args=(Alex, Tom, 2000))
t1.start()
time.sleep(2)  # t2等待一段時間再上鎖,確保t1先執行完
t2.start()
t1.join()
t2.join()

print("Tom:", Tom.get_balance())
print("Alex:", Alex.get_balance())

使用重載鎖RLock():

from threading import Thread, RLock
import time

num = 0  # 共享資源
lock = RLock()  # 在一個線程內可以對鎖進行重複加鎖

class MyThread(Thread):
    def fun1(self):
        global num
        with lock:
            num -= 1

    def fun2(self):
        global num
        if lock.acquire():
            num += 1
            if num > 5:
                self.fun1()
            print("Num=", num)
            lock.release()

    def run(self):
        while True:
            time.sleep(2)
            self.fun2()

t = MyThread()
t.start()
t.join()

Python線程GIL

簡單來說,就是利用python實現線程的效率低

  1. python線程的GIL問題(全局解釋器鎖)

什麼是GIL :由於python解釋器設計中加入瞭解釋器鎖,導致python解釋器同一時刻只能解釋執行一個線程,大大降低了線程的執行效率。
導致後果: 因爲遇到阻塞時線程會主動讓出解釋器,去解釋其他線程。所以python多線程在執行多阻塞高延遲IO時可以提升程序效率,其他情況並不能對效率有所提升。

GIL問題建議:

  • 儘量使用進程完成無阻塞的併發行爲
  • 不使用c作爲解釋器 (Java C#)
  1. 結論 : 在無阻塞狀態下,多線程程序和單線程程序執行效率幾乎差不多,甚至還不如單線程效率。但是多進程運行相同內容卻可以有明顯的效率提升。

作業:編寫程序完成效率測試

  • 使用單線程執行計算密集函數十次記錄時間,執行IO密集函數十次記錄時間
  • 使用10個線程分別執行計算密集函數1次記錄時間,執行IO密集函數1次記錄時間
  • 使用10個進程分別執行計算密集函數1次記錄時間,執行IO密集函數1次記錄時間

計算密集型函數:

def count(x,y):
	c = 0
	while c < 7000000:
		c += 1
		x += 1
		y += 1

IO密集型函數:

def io():
	write()
	read()

def write():
	f = open('test','w')
	for i in range(1500000):
		f.write("hello world\n")
	f.close()

def read():
	f = open('test')
	lines = f.readlines()
	f.close()	

  • 【單線程】計算密集函數十次:8.780869245529175;IO密集型函數十次:4.685685396194458
  • 【多線程】計算密集函數十次:14.451507091522217;IO密集型函數十次:6.815863847732544
  • 【多進程】計算密集函數十次:4.909349203109741;IO密集型函數十次:2.378950357437134

測試代碼

進程線程的區別聯繫

區別聯繫

  1. 兩者都是多任務編程方式,都能使用計算機多核資源
  2. 進程的創建刪除消耗的計算機資源比線程多
  3. 進程空間獨立,數據互不干擾,有專門通信方法;線程使用全局變量通信
  4. 一個進程可以有多個分支線程,兩者有包含關係
  5. 多個線程共享進程資源,在共享資源操作時往往需要同步互斥處理
  6. 進程線程在系統中都有自己的特有屬性標誌,如ID,代碼段,命令集等。

使用場景

  1. 任務場景:如果是相對獨立的任務模塊,可能使用多進程,如果是多個分支共同形成一個整體任務可能用多線程。
  2. 項目結構:多種編程語言實現不同任務模塊,可能是多進程,或者前後端分離應該各自爲一個進程。
  3. 難易程度:通信難度,數據處理的複雜度來判斷用進程間通信還是同步互斥方法。

要求

  1. 對進程線程怎麼理解/說說進程線程的差異
  2. 進程間通信知道哪些,有什麼特點
  3. 什麼是同步互斥,你什麼情況下使用,怎麼用
  4. 給一個情形,說說用進程還是線程,爲什麼
  5. 問一些概念,殭屍進程的處理,GIL問題,進程狀態
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章