Python併發編程(五):多線程(實戰基礎篇)


先說下爲了方便大家學習,模塊的開發者專門將多線程和多進程的使用方式弄的幾乎完全一樣。
注意:在編寫和審查多線程、多進程代碼的不同時,因該時刻注意線程和進程的最重要的一個區別:線程創建極快,瞬間完成的事情,進程創建很慢

一、threading模塊介紹

multiprocess模塊的完全模仿了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('主線程')

三、多線程併發的socket服務端

# 服務端
import multiprocessing
import threading

import socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(('127.0.0.1',8080))
s.listen(5)

def action(conn):
    while True:
        data=conn.recv(1024)
        print(data)
        conn.send(data.upper())

if __name__ == '__main__':

    while True:
        conn,addr=s.accept()


        p=threading.Thread(target=action,args=(conn,))
        p.start()
# 客戶端
import socket

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('127.0.0.1',8080))

while True:
    msg=input('>>: ').strip()
    if not msg:continue

    s.send(msg.encode('utf-8'))
    data=s.recv(1024)
    print(data)

四、線程相關的其他方法

Thread實例對象的方法
  # isAlive(): 返回線程是否活動的。
  # getName(): 返回線程名。
  # setName(): 設置線程名。

threading模塊提供的一些方法:
  # threading.currentThread(): 返回當前的線程變量。
  # threading.enumerate(): 返回一個包含正在運行的線程的list。正在運行指線程啓動後、結束前,不包括啓動前和終止後的線程。
  # 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
    '''

主線程等待子線程結束:

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
    '''

五、守護線程

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

需要強調的是:運行完畢並非終止運行

詳細解釋:

#1 主進程在其代碼結束後就已經算運行完畢了(守護進程在此時就被回收),然後主進程會一直等非守護的子進程都運行完畢後回收子進程的資源(否則會產生殭屍進程),纔會結束,

#2 主線程在其他非守護線程運行完畢後纔算運行完畢(守護線程在此時就被回收)。因爲主線程的結束意味着進程的結束,進程整體的資源都將被回收,而進程必須保證非守護線程都運行完畢後才能結束。

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-------")

# 執行結果:
123
456
main-------
end123
end456

六、GIL全局解釋器鎖(瞭解)

如果多個線程的target=work,那麼執行流程是

多個線程先訪問到解釋器的代碼,即拿到執行權限,然後將target的代碼交給解釋器的代碼去執行

解釋器的代碼是所有線程共享的,所以垃圾回收線程也可能訪問到解釋器的代碼而去執行,這就導致了一個問題:對於同一個數據100,可能線程1執行x=100的同時,而垃圾回收執行的是回收100的操作,解決這種問題沒有什麼高明的方法,就是加鎖處理,如下圖的GIL,保證python解釋器同一時間只能執行一個任務的代碼
在這裏插入圖片描述因此總結:GIL保護的是解釋器級的數據,保護用戶自己的數據則需要自己加鎖處理
GIL是Cpython的特點,不是python的特點。但是現在用的python絕大部分是Cpython。

七、GIL與多線程(重點)

有了GIL的存在,同一時刻同一進程中只有一個線程被執行

聽到這裏,有的同學立馬質問:進程可以利用多核,但是開銷大,而python的多線程開銷小,但卻無法利用多核優勢,也就是說python沒用了,php纔是最牛逼的語言?

要解決這個問題,我們需要在以下三點上達成一致:

1. cpu到底是用來做計算的,還是用來做I/O的?

2. 多cpu,意味着可以有多個核並行完成計算,所以多核提升的是計算性能

3. 每個cpu一旦遇到I/O阻塞,仍然需要等待,所以多核對I/O操作沒什麼用處 

結論:
  對計算密集型程序來說,cpu越多越好,使用多進程比較好,但是對於IO密集型程序來說,再多的cpu也沒用,因此選用多線程比較好。

#分析:
我們有四個任務需要處理,處理方式肯定是要玩出併發的效果,解決方案可以是:
方案一:開啓四個進程
方案二:一個進程下,開啓四個線程

#單核情況下,分析結果: 
  如果四個任務是計算密集型,沒有多核來並行計算,方案一徒增了創建進程的開銷,方案二勝
  如果四個任務是I/O密集型,方案一創建進程的開銷大,且進程的切換速度遠不如線程,方案二勝

#多核情況下,分析結果:
  如果四個任務是計算密集型,多核意味着並行計算,在python中一個進程中同一時刻只有一個線程執行用不上多核,方案一勝
  如果四個任務是I/O密集型,再多的核也解決不了I/O問題,方案二勝

 
#結論:現在的計算機基本上都是多核,python對於計算密集型的任務開多線程的效率並不能帶來多大性能上的提升,甚至不如串行(沒有大量切換),但是,對於IO密集型的任務效率還是有顯著提升的。

八、計算密集型(多進程與多線程性能對比)

from multiprocessing import Process
from threading import Thread
import os,time
def work():
    res=0
    for i in range(100000000):
        res*=i


if __name__ == '__main__':
    l=[]
    print(os.cpu_count()) #本機爲4核
    start=time.time()
    for i in range(4):
        # p=Process(target=work) #耗時5s多
        p=Thread(target=work) #耗時18s多
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop=time.time()
    print('run time is %s' %(stop-start))

九、IO密集型(多進程與多線程性能對比)

from multiprocessing import Process
from threading import Thread
import threading
import os,time
def work():
    time.sleep(2)
    print('===>')

if __name__ == '__main__':
    l=[]
    print(os.cpu_count()) #本機爲4核
    start=time.time()
    for i in range(400):
        # p=Process(target=work) #耗時12s多,大部分時間耗費在創建進程上
        p=Thread(target=work) #耗時2s多
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop=time.time()
    print('run time is %s' %(stop-start))

實際應用:

多線程用於IO密集型,如socket,爬蟲,web
多進程用於計算密集型,如金融分析

十、互斥鎖和GIL區別

互斥鎖詳見:https://blog.csdn.net/weixin_44571270/article/details/106584108

GIL主要用於解釋器級別的數據安全。且只會在多線程情況下,Cpython纔會有這個特點。所有線程搶GIL鎖,搶的是執行權限

互斥鎖是保護程序猿自己開發的應用程序的數據,很明顯GIL不負責這件事,只能用戶自定義加鎖處理,即Lock。所有線程搶Lock鎖,一般搶的是數據的修改權限

十一、死鎖現象與遞歸鎖(瞭解)

進程也有死鎖與遞歸鎖,在進程那裏忘記說了,放到這裏來說了

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

解決方法,遞歸鎖,在Python中爲了支持在同一線程中多次請求同一資源,python提供了可重入鎖RLock。

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

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