【2020Python修煉記】python併發編程(五)多線程-應用部分

来源:https://www.cnblogs.com/bigorangecc/archive/2020/04/25/12762957.html
-Advertisement-
Play Games

【目錄】 一、 threading模塊介紹 二 、開啟線程的兩種方式 三 、在一個進程下開啟多個線程與在一個進程下開啟多個子進程的區別 四、 線程相關的其他方法 五、守護線程 六、Python GIL(Global Interpreter Lock) 八、同步鎖 九、死鎖現象與遞歸鎖 十、線程que ...


【目錄】

一、 threading模塊介紹

二 、開啟線程的兩種方式

三 、在一個進程下開啟多個線程與在一個進程下開啟多個子進程的區別

四、 線程相關的其他方法

五、守護線程 

六、Python GIL(Global Interpreter Lock)

八、同步鎖

九、死鎖現象與遞歸鎖

十、線程queue

 

一、 threading模塊介紹

進程的multiprocess模塊,完全模仿了threading模塊的介面,二者在使用層面,有很大的相似性,此處省略好多字。。

multiprocess模塊用法回顧:https://www.cnblogs.com/bigorangecc/p/12759151.html

官網鏈接:https://docs.python.org/3/library/threading.html?highlight=threading#

二 、開啟線程的兩種方式

1、實例化類創建對象

#方式一
from threading import Thread
import time
def sayhi(name):
    time.sleep(2)
    print('%s say hello' %name)

if __name__ == '__main__':
    t=Thread(target=sayhi,args=('egon',))
    t.start()
    print('主線程')

 

2、類的繼承

#方式二
from threading import Thread
import time
class Sayhi(Thread):
    def __init__(self,name):
        super().__init__()
        self.name=name
    def run(self):
        time.sleep(2)
        print('%s say hello' % self.name)


if __name__ == '__main__':
    t = Sayhi('egon')
    t.start()
    print('主線程')

 

三 、在一個進程下開啟多個線程與在一個進程下開啟多個子進程的區別

1、比較開啟速度

from threading import Thread
from multiprocessing import Process
import os

def work():
    print('hello')

if __name__ == '__main__':
    #在主進程下開啟線程
    t=Thread(target=work)
    t.start()
    print('主線程/主進程')
    '''
    列印結果:
    hello
    主線程/主進程
    '''

    #在主進程下開啟子進程
    t=Process(target=work)
    t.start()
    print('主線程/主進程')
    '''
    列印結果:
    主線程/主進程
    hello
    '''
誰的開啟速度快

2、pid

from threading import Thread
from multiprocessing import Process
import os

def work():
    print('hello',os.getpid())

if __name__ == '__main__':
    #part1:在主進程下開啟多個線程,每個線程都跟主進程的pid一樣
    t1=Thread(target=work)
    t2=Thread(target=work)
    t1.start()
    t2.start()
    print('主線程/主進程pid',os.getpid())

    #part2:開多個進程,每個進程都有不同的pid
    p1=Process(target=work)
    p2=Process(target=work)
    p1.start()
    p2.start()
    print('主線程/主進程pid',os.getpid())
瞅一瞅pid

3、同一進程內的線程之間共用進程內的數據

from  threading import Thread
from multiprocessing import Process
import os
def work():
    global n
    n=0

if __name__ == '__main__':
    # n=100
    # p=Process(target=work)
    # p.start()
    # p.join()
    # print('主',n) #毫無疑問子進程p已經將自己的全局的n改成了0,但改的僅僅是它自己的,查看父進程的n仍然為100


    n=1
    t=Thread(target=work)
    t.start()
    t.join()
    print('',n) #查看結果為0,因為同一進程內的線程之間共用進程內的數據
驗證

 

四、 線程相關的其他方法

1、thread實例對象的方法:

1、isAlive(): 返回線程是否活動的,存活的。
2、getName(): 返回線程名。
3、setName(): 設置線程名。

2、threading模塊提供的一些方法:

1、threading.currentThread()  

返回當前的線程變數。

2、threading.enumerate() 

返回一個包含正在運行的線程的list。正在運行指線程啟動後、結束前,不包括啟動前和終止後的線程。

3、threading.activeCount()

返回正在運行的線程數量,與 len(threading.enumerate()) 有相同的結果。

from threading import Thread
import threading
from multiprocessing import Process
import os

def work():
    import time
    time.sleep(3)
    print(threading.current_thread().getName())


if __name__ == '__main__':
    #在主進程下開啟線程
    t=Thread(target=work)
    t.start()

    print(threading.current_thread().getName())
    print(threading.current_thread()) #主線程
    print(threading.enumerate()) #連同主線程在內有兩個運行的線程
    print(threading.active_count())
    print('主線程/主進程')

    '''
    列印結果:
    MainThread
    <_MainThread(MainThread, started 140735268892672)>
    [<_MainThread(MainThread, started 140735268892672)>, <Thread(Thread-1, started 123145307557888)>]
    主線程/主進程
    Thread-1
    '''
應用舉例

3、join()的使用——主線程等待子線程結束

from threading import Thread
import time
def sayhi(name):
    time.sleep(2)
    print('%s say hello' %name)

if __name__ == '__main__':
    t=Thread(target=sayhi,args=('egon',))
    t.start()
    t.join()
    print('主線程')
    print(t.is_alive())
    '''
    egon say hello
    主線程
    False
    '''
慄子

 

五、守護線程 

1、強調

無論是進程還是線程,都遵循:守護xxx會等待主xxx運行完畢後被銷毀

需要強調的是:運行完畢(真正的任務已經做完,但還沒有下班) 並非 終止運行(任務已經完成,收拾好東西下班了)

#1.對主進程來說,運行完畢指的是主進程代碼運行完畢

#2.對主線程來說,運行完畢指的是主線程所在的進程內所有非守護線程統統運行完畢,主線程才算運行完畢

詳細解釋:

#1 主進程在其代碼結束後就已經算運行完畢了(守護進程在此時就被回收),

     然後主進程會一直等非守護的子進程都運行完畢後回收子進程的資源(否則會產生僵屍進程),才會結束運行。

#2 主線程在其他非守護線程運行完畢後才算運行完畢(守護線程在此時就被回收)。

    因為主線程的結束意味著進程的結束,進程整體的資源都將被回收,而進程必須保證非守護線程都運行完畢後才能結束。

2、兩顆慄子

from threading import Thread
import time
def sayhi(name):
    time.sleep(2)
    print('%s say hello' %name)

if __name__ == '__main__':
    t=Thread(target=sayhi,args=('egon',))
    t.setDaemon(True) #必須在t.start()之前設置
    t.start()

    print('主線程')
    print(t.is_alive())
    '''
    主線程
    True
    '''
慄子1
from threading import Thread
import time
def foo():
    print(123)
    time.sleep(1)
    print("end123")

def bar():
    print(456)
    time.sleep(3)
    print("end456")


t1=Thread(target=foo)
t2=Thread(target=bar)

t1.daemon=True
t1.start()
t2.start()
print("main-------")
慄子2——迷人版

 

六、Python GIL (Global Interpreter Lock)

1、強調

# 在Cpython解釋器中,同一個進程下開啟的多線程,同一時刻只能有一個線程執行,無法利用多核優勢.

# GIL 並不是python的特性,它是在實現python解釋器(cpython)時所引入的一個概念,

    即 GIL 是 python解釋器(cpython)的特性。

2、GIL

GIL 本質就是一把互斥鎖,既然是互斥鎖,所有互斥鎖的本質都一樣,都是將併發運行變成串列,以此來控制同一時間內共用數據只能被一個任務所修改,進而保證數據安全。有了GIL的存在,同一時刻同一進程中只有一個線程被執行

可以肯定的一點是:保護不同的數據的安全,就應該加不同的鎖。

參考閱讀:

https://www.cnblogs.com/linhaifeng/articles/7449853.html

 

八、同步鎖

1、三個需要註意的點:

#1.GIL & Lock

線程搶的是GIL鎖,GIL鎖相當於執行許可權,拿到執行許可權後才能拿到互斥鎖Lock

其他線程也可以搶到GIL,但如果發現Lock仍然沒有被釋放則阻塞,即便是拿到執行許可權GIL也要立刻交出來。

(GIL鎖 和 互斥鎖Lock 都搶到 ,才是王道)

#2. join() & Lock 

join是等待所有,即整體串列,而只是鎖住修改共用數據的部分,即部分串列

要想保證數據安全的根本原理在於讓併發變成串列,join與互斥鎖都可以實現,毫無疑問,互斥鎖的部分串列效率要更高

#3. GIL與互斥鎖的經典分析,見本小節末---必看

 

2、Python已經有一個GIL來保證同一時間只能有一個線程來執行了,為什麼這裡還需要lock? 

一個共識 —— 的目的是為了保護共用的數據,同一時間只能有一個線程來修改共用的數據

一個結論 —— 保護不同的數據就應該加不同的鎖

明朗答案 —— GIL 與Lock是兩把鎖,保護的數據不一樣,

      前者 GIL 是解釋器級別的(當然保護的就是解釋器級別的數據,比如垃圾回收的數據),

      後者Lock 保護用戶自己開發的應用程式的數據,很明顯GIL不負責這件事,只能用戶自定義加鎖處理,即Lock

 

 3、GIL鎖  VS  互斥鎖Lock ——代碼慄子

鎖通常被用來實現對共用資源的同步訪問。

為每一個共用資源創建一個Lock對象,當你需要訪問該資源時,調用acquire方法來獲取鎖對象

(如果其它線程已經獲得了該鎖,則當前線程需等待其被釋放),待資源訪問完後,再調用release方法釋放鎖:

import threading

R=threading.Lock()

R.acquire()
'''
對公共數據的操作
'''
R.release()
from threading import Thread
import os,time
def work():
    global n
    temp=n
    time.sleep(0.1)
    n=temp-1
if __name__ == '__main__':
    n=100
    l=[]
    for i in range(100):
        p=Thread(target=work)
        l.append(p)
        p.start()
    for p in l:
        p.join()

    print(n) #結果可能為99
join/未加鎖
from threading import Thread,Lock
import os,time
def work():
    global n
    lock.acquire()
    temp=n
    time.sleep(0.1)
    n=temp-1
    lock.release()
if __name__ == '__main__':
    lock=Lock()
    n=100
    l=[]
    for i in range(100):
        p=Thread(target=work)
        l.append(p)
        p.start()
    for p in l:
        p.join()

    print(n) #結果肯定為0,由原來的併發執行變成串列,犧牲了執行效率保證了數據安全
加鎖後

GIL鎖與互斥鎖綜合分析:
#1.100個線程去搶GIL鎖,即搶執行許可權

#2. 肯定有一個線程先搶到GIL(暫且稱為線程1),然後開始執行,一旦執行就會拿到lock.acquire()

#3. 極有可能線程1還未運行完畢,就有另外一個線程2搶到GIL,然後開始運行,但線程2發現互斥鎖lock還未被線程1釋放,於是阻塞,被迫交出執行許可權,即釋放GIL

#4.直到線程1重新搶到GIL,開始從上次暫停的位置繼續執行,直到正常釋放互斥鎖lock,然後其他的線程再重覆2 3 4的過程

4、讓併發變成串列——互斥鎖 和 join()的區別

join是等待所有,即整體串列,而鎖只是鎖住修改共用數據的部分,即部分串列,

要想保證數據安全的根本原理在於讓併發變成串列,join與互斥鎖都可以實現,毫無疑問,互斥鎖的部分串列效率要更高

#不加鎖:併發執行,速度快,數據不安全
from threading import current_thread,Thread,Lock
import os,time
def task():
    global n
    print('%s is running' %current_thread().getName())
    temp=n
    time.sleep(0.5)
    n=temp-1


if __name__ == '__main__':
    n=100
    lock=Lock()
    threads=[]
    start_time=time.time()
    for i in range(100):
        t=Thread(target=task)
        threads.append(t)
        t.start()
    for t in threads:
        t.join()

    stop_time=time.time()
    print('主:%s n:%s' %(stop_time-start_time,n))

'''
Thread-1 is running
Thread-2 is running
......
Thread-100 is running
主:0.5216062068939209 n:99
'''


#不加鎖:未加鎖部分併發執行,加鎖部分串列執行,速度慢,數據安全
from threading import current_thread,Thread,Lock
import os,time
def task():
    #未加鎖的代碼併發運行
    time.sleep(3)
    print('%s start to run' %current_thread().getName())
    global n
    #加鎖的代碼串列運行
    lock.acquire()
    temp=n
    time.sleep(0.5)
    n=temp-1
    lock.release()

if __name__ == '__main__':
    n=100
    lock=Lock()
    threads=[]
    start_time=time.time()
    for i in range(100):
        t=Thread(target=task)
        threads.append(t)
        t.start()
    for t in threads:
        t.join()
    stop_time=time.time()
    print('主:%s n:%s' %(stop_time-start_time,n))

'''
Thread-1 is running
Thread-2 is running
......
Thread-100 is running
主:53.294203758239746 n:0
'''

#有的同學可能有疑問:既然加鎖會讓運行變成串列,那麼我在start之後立即使用join,就不用加鎖了啊,也是串列的效果啊
#沒錯:在start之後立刻使用jion,肯定會將100個任務的執行變成串列,毫無疑問,最終n的結果也肯定是0,是安全的,但問題是
#start後立即join:任務內的所有代碼都是串列執行的,而加鎖,只是加鎖的部分即修改共用數據的部分是串列的
#單從保證數據安全方面,二者都可以實現,但很明顯是加鎖的效率更高.
from threading import current_thread,Thread,Lock
import os,time
def task():
    time.sleep(3)
    print('%s start to run' %current_thread().getName())
    global n
    temp=n
    time.sleep(0.5)
    n=temp-1


if __name__ == '__main__':
    n=100
    lock=Lock()
    start_time=time.time()
    for i in range(100):
        t=Thread(target=task)
        t.start()
        t.join()
    stop_time=time.time()
    print('主:%s n:%s' %(stop_time-start_time,n))

'''
Thread-1 start to run
Thread-2 start to run
......
Thread-100 start to run
主:350.6937336921692 n:0 #耗時是多麼的恐怖
'''
互斥鎖與join的區別

 

九、死鎖現象與遞歸鎖——進程和線程都有死鎖現象與遞歸鎖

所謂死鎖:

是指兩個或兩個以上的進程或線程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱為死鎖進程,如下就是死鎖:

from threading import Thread,Lock
import time
mutexA=Lock()
mutexB=Lock()

class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()
    def func1(self):
        mutexA.acquire()
        print('\033[41m%s 拿到A鎖\033[0m' %self.name)

        mutexB.acquire()
        print('\033[42m%s 拿到B鎖\033[0m' %self.name)
        mutexB.release()

        mutexA.release()

    def func2(self):
        mutexB.acquire()
        print('\033[43m%s 拿到B鎖\033[0m' %self.name)
        time.sleep(2)

        mutexA.acquire()
        print('\033[44m%s 拿到A鎖\033[0m' %self.name)
        mutexA.release()

        mutexB.release()

if __name__ == '__main__':
    for i in range(10):
        t=MyThread()
        t.start()

'''
Thread-1 拿到A鎖
Thread-1 拿到B鎖
Thread-1 拿到B鎖
Thread-2 拿到A鎖
然後就卡住,死鎖了
'''
死鎖現象

解決方法——遞歸鎖

在Python中為了支持在同一線程中多次請求同一資源,python提供了可重入鎖RLock。

這個RLock內部維護著一個Lock和一個counter變數,counter記錄了acquire的次數,從而使得資源可以被多次require。直到一個線程所有的acquire都被release,其他的線程才能獲得資源。上面的例子如果使用RLock代替Lock,則不會發生死鎖:

mutexA=mutexB=threading.RLock() 

#一個線程拿到鎖,counter加1,該線程內又碰到加鎖的情況,則counter繼續加1,這期間所有其他線程都只能等待,等待該線程釋放所有鎖,即counter遞減到0為止

 

十、線程 的類 queue

1、線程queue

queue隊列 :使用 import queue,用法與進程Queue一樣

     (註意區分——進程的Queue,進程的隊列首字母為大寫Q,線程的為小寫q

                   進程中的導入方法:from multiprocessing import Process,Queue)

2、主要用法

class queue.Queue(maxsize=0  #先進先出

import queue

q=queue.Queue()
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())
'''
結果(先進先出):
first
second
third
'''
View Code

 

class queue.LifoQueue(maxsize=0   #last in fisrt out 

import queue

q=queue.LifoQueue()
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())
'''
結果(後進先出):
third
second
first
'''
View Code

 

class queue.PriorityQueue(maxsize=0   #存儲數據時可設置優先順序的隊列

import queue

q=queue.PriorityQueue()
#put進入一個元組,元組的第一個元素是優先順序(通常是數字,也可以是非數字之間的比較),數字越小優先順序越高
q.put((20,'a'))
q.put((10,'b'))
q.put((30,'c'))

print(q.get())
print(q.get())
print(q.get())
'''
結果(數字越小優先順序越高,優先順序高的優先出隊):
(10, 'b')
(20, 'a')
(30, 'c')
'''
View Code

 

 

參考資料:

https://www.cnblogs.com/linhaifeng/articles/7428877.html

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1、如果之前有全局安裝過vue-2,先卸載掉 cnpm uni -g vue-cli 2、安裝vue-cli3 cnpm install -g @vue/cli 3、可以使用vue ui 進入圖形界面創建項目 4、也可以使用命令行進行創建 ...
  • 寫在前面 上一講「Vuex 旗下的 State 和 Getter」,告訴了我們怎麼去使用倉庫 store 中的狀態數據。當然,光會用肯定還不夠,大部分的應用場景還得對這些狀態進行操控,那麼具體如何操控呢,這就是這一講要說的重點。 只有 mutation 能動 State 更改 Vuex 的 stor ...
  • 頁面應用需要Vuex管理全局/模塊的狀態,大型單頁面組件如果靠事件(events)/屬性(props)通訊傳值會把各個組件耦合在一起。因此需要Vuex管理屬性之間都狀態 ...
  • echarts實現堆疊圖,可自定義MakeLine文字。 顯示效果: 1 var seriesName = ['直接訪問', '郵件營銷', '聯盟廣告', '視頻廣告', '搜索引擎']; 2 var labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周 ...
  • 一、將漢字轉換為Unicode 1 /* 2 *將漢字轉換為Unicode 3 *charCodeAt返回字元串指定位置的字元的Unicode編碼(十進位形式),在0-65535之間。 4 * 5 *toString(16) 將一個數字轉成十六進位。 6 */ 7 function toUnicod ...
  • class bldy(): def one (self): a = 5 return a # return 返回到self def two(self): b = 10 return b def sum(self, a, b): # 你給我兩個參數,我就執行下麵的方法 c = a + b return ...
  • 公司有一項儲值卡充值業務:客戶在微信公眾號開通儲值卡服務,通過微信支付往卡裡面充值,充值成功後客戶可收到消息通知,併進行消費。 看起來是一項很簡單的業務,最初我們儲值卡團隊的實現也確實很簡單。我們看看最初的實現: 相信聰明的你一眼就能看出問題: 1. 壓根沒有考慮分散式事務一致性,比如第 12 步根 ...
  • JavaScript 入門 1 JavaScript 概述 JavaScript 是一門世界上最流行的腳本語言。 一個合格的 後端 程式員,必須要精通 JavaScript 。 2 ECMAScript ECMAScript 是 JavaScript 的一個標準。 目前廣泛使用的版本為 ES6。 3 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...