Python: 一篇文章帶你全面解析不一樣的線程

前言

在將今天的知識點之前,大家是否瞭解線程,進程和協程了,那我們先來初步瞭解下吧。

線程

中央處理器的調度單元,簡單點說就是程序中的末端執行者,相當於小弟的位置。

有人說 python 中的線程是個雞肋,這是因爲有了 GIL,但是又不是一味的雞肋,畢竟在執行 io 操作時還是挺管用的,只是在執行計算時就顯得不盡人意。下面我們來看下線程的具體使用方法:

1. 導入線程模塊:

import threading as t

2. 線程的用法

tt=t.Thread(group=None,target=None,name=None,args=(),kwargs={},name='',daemon=None)
group:線程組,必須是None
target:運行的函數
args:傳入函數的參數元組
kwargs:傳入函數的參數字典
name:線程名
daemon:線程是否隨主線程退出而退出(守護線程)
Thread方法的返回值還有以下方法:
tt.start() : 激活線程,
tt.getName() : 獲取線程的名稱
tt.setName() :設置線程的名稱 
tt.name : 獲取或設置線程的名稱
tt.is_alive() :判斷線程是否爲激活狀態
tt.isAlive() :判斷線程是否爲激活狀態
tt.setDaemon() 設置爲守護線程(默認:False)
tt.isDaemon() :判斷是否爲守護線程
tt.ident :獲取線程的標識符。只有在調用了start()方法之後該屬性纔有效
tt.join() :逐個執行每個線程,執行完畢後繼續往下執行
tt.run() :自動執行線程對象
t的方法也有:
t.active_count(): 返回正在運行線程的數量
t.enumerate(): 返回正在運行線程的列表
t.current_thread().getName() 獲取當前線程的名字
t.TIMEOUT_MAX 設置t的全局超時時間

下面我們來看下吧:

3. 創建線程

線程可以使用 Thread 方法創建,也可以重寫線程類的 run 方法實現,線程可分爲單線程和多線程。

一、使用 Thread 方法來創建:

1. 單線程
def xc():
    for y in range(100):
        print('運行中'+str(y))
tt=t.Thread(target=xc,args=()) #方法加入到線程
tt.start()  #開始線程
tt.join() #等待子線程結束
2. 多線程
def xc(num):
    print('運行:'+str(num))
c=[]
for y in range(100):
    tt=t.Thread(target=xc,args=(y,))
    tt.start() #開始線程
    c.append(tt) #創建列表並添加線程
for x in c:
    x.join()  #等待子線程結束

二、重寫線程的類方法

1. 單線程
class Xc(t.Thread): #繼承Thread類
    def __init__(self):
        super(Xc, self).__init__() 
    def run(self):  #重寫run方法
        for y in range(100):
            print('運行中'+str(y))
x=Xc() 
x.start() #開始線程
x.join()  #等待子線程結束
也可以這麼寫:
Xc().run() 和上面的效果是一樣的
2. 多線程
class Xc(t.Thread): #繼承Thread類
    def __init__(self):
        super(Xc, self).__init__() 
    def run(self,num):  #重寫run方法
        print('運行:'+str(num))
x=Xc()
for y in range(10):
    x.run(y) #運行

4. 線程鎖

爲什麼要加鎖,看了這個你就知道了:

多線程在運行時同時訪問一個對象會產生搶佔資源的情況,所以我們必須得束縛它,所以就要給他加一把鎖把他鎖住,這就是同步鎖。要了解鎖,我們得先創建鎖,線程中有兩種鎖:Lock 和 RLock。

一、Lock

使用方法:

# 獲取鎖
當獲取不到鎖時,默認進入阻塞狀態,設置超時時間,直到獲取到鎖,後才繼續。非阻塞時,timeout禁止設置。如果超時依舊未獲取到鎖,返回False。
Lock.acquire(blocking=True,timeout=1)   
#釋放鎖,已上鎖的鎖,會被設置爲unlocked。如果未上鎖調用,會拋出RuntimeError異常。
Lock.release()

互斥鎖,同步數據,解決多線程的安全問題:

n=10
lock=t.Lock()
def xc(num):
    lock.acquire()
    print('運行+:'+str(num+n))
    print('運行-:'+str(num-n))
    lock.release()
c=[]
for y in range(10):
    tt=t.Thread(target=xc,args=(y,))
    tt.start()
    c.append(tt)
for x in c:
    x.join()

這樣就顯得有條理了,而且輸出也是先 + 後 -。Lock 在一個線程中多次使用同一資源會造成死鎖。

死鎖問題:

n=10
lock1=t.Lock()
lock2=t.Lock()
def xc(num):
  lock1.acquire()
  print('運行+:'+str(num+n))
  lock2.acquire()
  print('運行-:'+str(num-n))
  lock2.release()
  lock1.release()
c=[]
for y in range(10):
  tt=t.Thread(target=xc,args=(y,))
  tt.start()
  c.append(tt)
for x in c:
  x.join()

二、RLock

相比 Lock 它可以遞歸,支持在同一線程中多次請求同一資源,並允許在同一線程中被多次鎖定,但是 acquire 和 release 必須成對出現。

使用遞歸鎖來解決死鎖:

n=10
lock1=t.RLock()
lock2=t.RLock()
def xc(num):
  lock1.acquire()
  print('運行+:'+str(num+n))
  lock2.acquire()
  print('運行-:'+str(num-n))
  lock2.release()
  lock1.release()
c=[]
for y in range(10):
  tt=t.Thread(target=xc,args=(y,))
  tt.start()
  c.append(tt)
for x in c:
  x.join()

這時候,輸出變量就變得僅僅有條了,不在隨意搶佔資源。關於線程鎖,還可以使用 with 更加方便:

#with上下文管理,鎖對象支持上下文管理
with lock:   #with表示自動打開自動釋放鎖
  for i in range(10): #鎖定期間,其他人不可以幹活
    print(i)
  #上面的和下面的是等價的
if lock.acquire(1):#鎖住成功繼續幹活,沒有鎖住成功就一直等待,1代表獨佔
  for i in range(10): #鎖定期間,其他線程不可以幹活
    print(i)
  lock.release() #釋放鎖

三、條件鎖

等待通過,Condition(lock=None), 可以傳入 lock 或者 Rlock,默認 Rlock,使用方法:

Condition.acquire(*args)      獲取鎖
Condition.wait(timeout=None)  等待通知,timeout設置超時時間
Condition.notify(num)喚醒至多指定數目個數的等待的線程,沒有等待的線程就沒有任何操作
Condition.notify_all()  喚醒所有等待的線程 或者notifyAll()
def ww(c):
  with c:
    print('init')
    c.wait(timeout=5) #設置等待超時時間5
    print('end')
def xx(c):
  with c:
    print('nono')
    c.notifyAll() #喚醒所有線程
    print('start')
    c.notify(1) #喚醒一個線程
    print('21')
c=t.Condition() #創建條件
t.Thread(target=ww,args=(c,)).start()
t.Thread(target=xx,args=(c,)).start()

這樣就可以在等待的時候喚醒函數里喚醒其他函數里所存在的其他線程了。

5. 信號量

信號量可以分爲有界信號量和無解信號量,下面我們來具體看看他們的用法:

一、有界信號量

它不允許使用 release 超出初始值的範圍,否則,拋出 ValueError 異常。

#構造方法。value爲初始信號量。value小於0,拋出ValueError異常
b=t.BoundedSemaphore(value=1)  
#獲取信號量時,計數器減1,即_value的值減少1。如果_value的值爲0會變成阻塞狀態。獲取成功返回True
BoundedSemaphore.acquire(blocking=True,timeout=None)  
#釋放信號量,計數器加1。即_value的值加1,超過初始化值會拋出異常ValueError。
BoundedSemaphore.release()  
#信號量,當前信號量
BoundedSemaphore._value

可以看到了多了個 release 後報錯了。

二、無界信號量

它不檢查 release 的上限情況,只是單純的加減計數器。

可以看到雖然多了個 release,但是沒有問題,而且信號量的數量不受限制。

6.Event

線程間通信,通過線程設置的信號標誌 (flag) 的 False 還是 True 來進行操作,常見方法有:

event.set()      flag設置爲True
event.clear()  flag設置爲False
event.is_set()  flag是否爲True,如果 event.isSet()==False將阻塞線程;
設置等待flag爲True的時長,None爲無限等待。等到返回True,未等到超時則返回False
event.wait(timeout=None)

下面通過一個例子具體講述:

import time
e=t.Event()
def ff(num):
  while True:
    if num<5:
      e.clear()   #清空信號標誌
      print('清空')
    if num>=5:
      e.wait(timeout=1) #等待信號標誌爲真
      e.set()
      print('啓動')
      if e.isSet(): #如果信號標誌爲真則清除標誌
        e.clear()
        print('停止')
    if num==10:
      e.wait(timeout=3)
      e.clear()
      print('退出')
      break
    num+=1
    time.sleep(2)
ff(1)

設置延遲後可以看到效果相當明顯,我們讓他幹什麼事他就幹什麼事。

7.local

可以爲各個線程創建完全屬於它們自己的變量 (線程局部變量),而且它們的值都在當前調用它的線程當中,以字典的形式存在。下面我們來看下:

l=t.local()  #創建一個線程局部變量
def ff(num):
  l.x=100  #設置l變量的x方法的值爲100
  for y in range(num):
    l.x+=3 #改變值
  print(str(l.x))
for y in range(10):
  t.Thread(target=ff,args=(y,)).start() #開始執行線程

那麼,可以將變量的 x 方法設爲全局變量嗎?我們來看下:

可以看出他報錯了,產生錯誤的原因是因爲這個類中沒有屬性 x, 我們可以簡單的理解爲局部變量就只接受局部。

8.Timer

設置定時計劃,可以在規定的時間內反覆執行某個方法。他的使用方法是:

t.Timer(num,func,*args,**kwargs) #在指定時間內再次重啓程序

下面我們來看下:

def f():
  print('start')
  global t #防止造成線程堆積導致最終程序退出
  tt= t.Timer(3, f)
  tt.start()
f()

這樣就達到了每三秒執行一次 f 函數的效果。

總結

通過對線程的全面解析我們瞭解到了線程的重要性,它可以將我們複雜的問題變得簡單化,對於喜歡玩爬蟲的小夥伴們可以說是相當有用了,本文基本覆蓋了線程的所有概念,希望能幫到大家。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/kFAr9fVYxt0UjYAKnv5OZg