1. 程式人生 > >線程與全局解釋器鎖(GIL)

線程與全局解釋器鎖(GIL)

lob directly nds true 大寫 時間 pen 概論 har

一、線程概論

1、何為線程

每個進程有一個地址空間,而且默認就有一個控制線程。如果把一個進程比喻為一個車間的工作過程那麽線程就是車間裏的一個一個流水線。

進程只是用來把資源集中到一起(進程只是一個資源單位,或者說資源集合),而線程才是cpu上的執行單位。

多線程(即多個控制線程)的概念是,在一個進程中存在多個控制線程,多個控制線程共享該進程的地址空間(資源)

創建進程的開銷要遠大於線程開進程相當於建一個車間,而開線程相當於建一條流水線。

2、線程和進程的區別

技術分享
1.Threads share the address space of the process that created it; processes have their own address space.
2.Threads have direct access to the data segment of its process; processes have their own copy of the data segment of the parent process.
3.Threads can directly communicate with other threads of its process; processes must use interprocess communication to communicate with sibling processes.
4.New threads are easily created; new processes require duplication of the parent process.
5.Threads can exercise considerable control over threads of the same process; processes can only exercise control over child processes.
6.Changes to the main thread (cancellation, priority change, etc.) may affect the behavior of the other threads of the process; changes to the parent process does not affect child processes.
技術分享

中譯:

1、線程共享創建它的進程的地址空間;進程有自己的地址空間。
2、線程可以直接訪問其進程的數據段;進程有它們自己的父進程數據段的副本。
3、線程可以直接與進程的其他線程通信;進程必須使用進程間通信來與兄弟進程通信。
4、新線程很容易創建;新進程需要復制父進程。
5、線程可以對同一進程的線程進行相當大的控制;進程只能對子進程執行控制。
6、對主線程的更改(取消、優先級更改等)可能會影響該進程的其他線程的行為;對父進程的更改不會影響子進程。

3、多線程的優點

多線程和多進程相同指的是,在一個進程中開啟多個線程

1)多線程共享一個進程的地址空間(資源)

2) 線程比進程更輕量級,線程比進程更容易創建可撤銷,在許多操作系統中,創建一個線程比創建一個進程要快10-100倍,在有大量線程需要動態和快速修改時,這一特性很有用

3) 若多個線程都是cpu密集型的,那麽並不能獲得性能上的增強,但是如果存在大量的計算和大量的I/O處理,擁有多個線程允許這些活動彼此重疊運行,從而會加快程序執行的速度。

4) 在多cpu系統中,為了最大限度的利用多核,可以開啟多個線程,比開進程開銷要小的多。(這一條並不適用於python)


二、python的並發編程之多線程

1、threading模塊介紹

multiprocessing模塊的完全模仿了threading模塊的接口,二者在使用層面,有很大的相似性,因而不再詳細介紹

對multiprocessing模塊也不是很熟悉的朋友可以復習一下多線程時介紹的隨筆:

30、進程的基礎理論,並發(multiprocessing模塊):http://www.cnblogs.com/liluning/p/7419677.html

官方文檔:https://docs.python.org/3/library/threading.html?highlight=threading#(英語好的可以嘗試挑戰)

2、開啟線程的兩種方式(和進程一模一樣)

兩種方式裏我們都有開啟進程的方式可以簡單復習回顧

1)方式一:

技術分享
from threading import Thread
#from  multiprocessing  import  Process
import os
def talk():
    print(‘%s is running‘ %os.getpid())

if __name__ == ‘__main__‘:
    t=Thread(target=talk)
    # t=Process(target=talk)
    t.start()
    print(‘主‘,os.getpid())
技術分享

2)方式二:

技術分享
#開啟線程
from threading import Thread
import os
class MyThread(Thread):
    def __init__(self,name):
        super().__init__()
        self.name=name
    def run(self):
        print(‘pid:%s name:[%s]is running‘ %(os.getpid(),self.name))

if __name__ == ‘__main__‘:
    t=MyThread(‘lln‘)
    t.start()
    print(‘主T‘,os.getpid())

#開啟進程
from multiprocessing import Process
import os
class MyProcess(Process):
    def __init__(self,name):
        super().__init__()
        self.name=name
    def run(self):
        print(‘pid:%s name:[%s]is running‘ % (os.getpid(), self.name))
if __name__ == ‘__main__‘:
    t=MyProcess(‘lll‘)
    t.start()
    print(‘主P‘,os.getpid())
技術分享

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

1)比較速度:(看看hello和主線程/主進程的打印速度)

技術分享
from threading import Thread
from multiprocessing import Process
import os

def work():
    print(‘hello‘)

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

    #在主進程下開啟子進程
    t=Process(target=work)
    t.start()
    print(‘主線程/主進程‘)
技術分享

2)pid的區別:(線程和主進程相同,子進程和主進程不同)

技術分享
from threading import Thread
from multiprocessing import Process
import os

def work():
    print(‘我的pid:‘,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())
技術分享

3)數據是否共享(線程與主進程共享數據,子進程只是將主進程拷貝過去操作的並非同一份數據)

技術分享
from  threading import Thread
from multiprocessing import Process
def work():
    global n
    n -= 1
n = 100  #主進程數據
if __name__ == ‘__main__‘:
    # p=Process(target=work)
    # p.start()
    # p.join()
    # print(‘主‘,n) #毫無疑問子進程p已經將自己的全局的n改成了99,但改的僅僅是它自己的,查看父進程的n仍然為100

    t=Thread(target=work)
    t.start()
    t.join()
    print(‘主‘,n) #查看結果為99,因為同一進程內的線程之間共享進程內的數據
技術分享

4、練習

1)三個任務,一個接收用戶輸入,一個將用戶輸入的內容格式化成大寫,一個將格式化後的結果存入文件

技術分享
from threading import Thread
msg = []
msg_fort = []
def Inp():
    while True :
        msg_l = input(‘>>:‘)
        if not msg_l : continue
        msg.append(msg_l)
def Fort():
    while True :
        if msg :
            res = msg.pop()
            msg_fort.append(res.upper())
def Save():
    with open(‘db.txt‘,‘a‘) as f :
        while True :
            if msg_fort :
                f.write(‘%s\n‘ %msg_fort.pop())
                f.flush()  #強制將緩沖區中的數據發送出去,不必等到緩沖區滿
if __name__ == ‘__main__‘:
    p1 = Thread(target=Inp)
    p2 = Thread(target=Fort)
    p3 = Thread(target=Save)
    p1.start()
    p2.start()
    p3.start()
技術分享

2)將前面隨筆中的服務端客戶端例子用多線程實現(不了解的可以翻閱前幾篇隨筆)

技術分享 服務端 技術分享 客戶端

5、threading模塊其他方法

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

threading模塊提供的一些方法:
  # threading.currentThread(): 返回當前的線程變量。
  # threading.enumerate(): 返回一個包含正在運行的線程的list。正在運行指線程啟動後、結束前,不包括啟動前和終止後的線程。
  # threading.activeCount(): 返回正在運行的線程數量,與len(threading.enumerate())有相同的結果。
技術分享 技術分享 測試

主線程等其它線程

技術分享
from threading import Thread,currentThread,activeCount
import os,time,threading
def talk():
    time.sleep(2)
    print(‘%s is running‘ %currentThread().getName())

if __name__ == ‘__main__‘:
    t=Thread(target=talk)
    t.start()
    t.join()
    print(‘主‘)
技術分享

6、守護線程

1)守護線程和守護進程的區別

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

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

2)詳細說明

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

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

技術分享 守護線程 技術分享 迷惑人的例子

三、Python GIL(Global Interpreter Lock)

1、定義:

技術分享
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once.

在CPython中,全局解釋器鎖是一個互斥鎖,或GIL,它可以防止多個本地線程執行Python字節碼。

This lock is necessary mainly because CPython’s memory management is not thread-safe.

這個鎖是必需的,主要是因為CPython的內存管理不是線程安全的。

(However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

然而,由於GIL存在,其他的特性已經發展到依賴於它的保證。
技術分享

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

註意:

首先需要明確的一點是GIL並不是Python的特性,它是在實現Python解析器(CPython)時所引入的一個概念。就好比C++是一套語言(語法)標準,但是可以用不同的編譯器來編譯成可執行代碼。有名的編譯器例如GCC,INTEL C++,Visual C++等。Python也一樣,同樣一段代碼可以通過CPython,PyPy,Psyco等不同的Python執行環境來執行。像其中的JPython就沒有GIL。然而因為CPython是大部分環境下默認的Python執行環境。所以在很多人的概念裏CPython就是Python,也就想當然的把GIL歸結為Python語言的缺陷。所以這裏要先明確一點:GIL並不是Python的特性,Python完全可以不依賴於GIL

對自己英語水平有信心的可以看一下:http://www.dabeaz.com/python/UnderstandingGIL.pdf (這篇文章透徹的剖析了GIL對python多線程的影響)

2、GIL介紹

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

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

要想了解GIL,首先確定一點:每次執行python程序,都會產生一個獨立的進程。例如python test.py,python aaa.py,python bbb.py會產生3個不同的python進程

技術分享
‘‘‘
#驗證python test.py只會產生一個進程
#test.py內容
import os,time
print(os.getpid())
time.sleep(1000)
‘‘‘
python3 test.py 
#在windows下
tasklist |findstr python
#在linux下
ps aux |grep python

驗證python test.py只會產生一個進程
技術分享

在一個python的進程內,不僅有test.py的主線程或者由該主線程開啟的其他線程,還有解釋器開啟的垃圾回收等解釋器級別的線程,總之,所有線程都運行在這一個進程內,毫無疑問:

#1 所有數據都是共享的,這其中,代碼作為一種數據也是被所有線程共享的(test.py的所有代碼以及Cpython解釋器的所有代碼)
#2 所有線程的任務,都需要將任務的代碼當做參數傳給解釋器的代碼去執行,即所有的線程要想運行自己的任務,首先需要解決的是能夠訪問到解釋器的代碼。

綜上:

如果多個線程的target=work,那麽執行流程是多個線程先訪問到解釋器的代碼,即拿到執行權限,然後將target的代碼交給解釋器的代碼去執行

GIL保護的是解釋器級的數據,保護用戶自己的數據則需要自己加鎖處理

技術分享 保護自己的數據還是需要自己加鎖

3、GIL與多線程

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

聽到這裏,你是否會有疑問:進程可以利用多核,但是開銷大,而python的多線程開銷小,但卻無法利用多核優勢,也就是說python沒用了

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

#1. cpu到底是用來做計算的,還是用來做I/O的?
#2. 多cpu,意味著可以有多個核並行完成計算,所以多核提升的是計算性能
#3. 每個cpu一旦遇到I/O阻塞,仍然需要等待,所以多核對I/O操作沒什麽用處 

結論:

對計算來說,cpu越多越好,但是對於I/O來說,再多的cpu也沒用

當然對運行一個程序來說,隨著cpu的增多執行效率肯定會有所提高(不管提高幅度多大,總會有所提高),這是因為一個程序基本上不會是純計算或者純I/O,所以我們只能相對的去看一個程序到底是計算密集型還是I/O密集型,從而進一步分析python的多線程到底有無用武之地

技術分享
#分析:
我們有四個任務需要處理,處理方式肯定是要玩出並發的效果,解決方案可以是:
方案一:開啟四個進程
方案二:一個進程下,開啟四個線程
#單核情況下,分析結果: 
如果四個任務是計算密集型,沒有多核來並行計算,方案一徒增了創建進程的開銷,方案二勝
如果四個任務是I/O密集型,方案一創建進程的開銷大,且進程的切換速度遠不如線程,方案二勝
#多核情況下,分析結果:
如果四個任務是計算密集型,多核意味著並行計算,在python中一個進程中同一時刻只有一個線程執行用不上多核,方案一勝
如果四個任務是I/O密集型,再多的核也解決不了I/O問題,方案二勝
#結論:
現在的計算機基本上都是多核,python對於計算密集型的任務開多線程的效率並不能帶來多大性能上的提升,甚至不如串行(沒有大量切換),但是,對於IO密集型的任務效率還是有顯著提升的。
技術分享

4、性能測試

技術分享 計算密集型:多進程效率高 技術分享 I/O密集型:多線程效率高

總結:

多線程用於IO密集型,如socket,爬蟲,web

多進程用於計算密集型,如金融分析

線程與全局解釋器鎖(GIL)