python多線程詳解


前言

①多線程類似於同時執行多個不同程序,多線程運行有如下優點:

  • 使用線程可以把占據長時間的程序中的任務放到后台去處理。
  • 用戶界面可以更加吸引人,比如用戶點擊了一個按鈕去觸發某些事件的處理,可以彈出一個進度條來顯示處理的進度。
  • 程序的運行速度可能加快。
  • 在一些等待的任務實現上如用戶輸入、文件讀寫和網絡收發數據等,線程就比較有用了。在這種情況下我們可以釋放一些珍貴的資源如內存占用等等。(在用戶等待程序響應結束的同時,后台同時在執行釋放內存占用的任務)

②每個獨立的線程有一個程序運行的入口、順序執行序列和程序的出口。但是線程不能夠獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。

③每個線程都有他自己的一組CPU寄存器,稱為線程的上下文,該上下文反映了線程上次運行該線程的CPU寄存器的狀態。

④指令指針和堆棧指針寄存器是線程上下文中兩個最重要的寄存器,線程總是在進程得到上下文中運行的,這些地址都用於標志擁有線程的進程地址空間中的內存。

  • 線程可以被搶占(中斷)。
  • 在其他線程正在運行時,線程可以暫時擱置(也稱為睡眠) -- 這就是線程的退讓。

⑤線程可以分為:

  • 內核線程:由操作系統內核創建和撤銷。
  • 用戶線程:不需要內核支持而在用戶程序中實現的線程。

⑥python3 線程中常用的兩個模塊為:

  •  _thread 
  •  threading (推薦使用)

【注意】

 thread 模塊已被廢棄。用戶可以使用 threading 模塊代替。所以,在 Python3 中不能再使用 thread 模塊。為了兼容性,Python3 將 thread 重命名為  _thread 

線程和進程

進程和線程簡單舉例:

①進程:對於操作系統來說,一個任務就是一個進程(Process),比如打開一個瀏覽器就是啟動一個瀏覽器進程.

②線程:有些進程還不止同時干一件事,比如Word,它可以同時進行打字、拼寫檢查、打印等事情。在一個進程內部,要同時干多件事,就需要同時運行多個《子任務》,我們把進程內的這些《子任務》稱為線程(Thread)。

線程與進程的區別: 

①簡而言之,一個程序至少有一個進程一個進程至少有一個線程。

​②進程就是一個應用程序在處理機上的一次執行過程,它是一個動態的概念,而線程是進程中的一部分,進程包含多個線程在運行

​③多線程可以共享全局變量,多進程不能。多線程中,所有子線程的進程號相同;多進程中,不同的子進程進程號不同。

怎樣理解線程:

線程可以是一個python程序,也可以是python文件里的一個函數。如果python程序或者函數開始執行了,你也可以說線程開始執行了。

並行和並發:

並行處理:是計算機系統中能同時執行兩個或更多個處理的一種計算方法。並行處理可同時工作於同一程序的不同方面。並行處理的主要目的是節省大型和復雜問題的解決時間。

【①同個CPU,同一時間,執行多個線程;②多個CPU,同一時間,執行多個進程(進程與CPU一一對應)】

並發處理:一個時間段內有幾個程序都處於已啟動運行到運行完畢之間,且這幾個程序都是在同一個處理機(CPU)上運行,但是任意一個時刻點上只有一個程序在處理機(CPU)上運行。

【同個cpu,同一時間間隔(時間窗),執行多個線程】

總結:

python的多線程原理是並發

python多線程:在python中,同個CPU里同一時間點只能運行一個線程;為了數據安全,引入全局解釋鎖(GIL),相當於一張通行證;沒有GIL的線程不進入CPU運行。

python多線程原理:一個程序運行,其他的程序不運行;當運行的線程需要等待的時候(如網絡,IO等),該線程被掛起【通行證(GIL)被拿走】等待,其他線程競爭GIL;先拿到的GIL的線程先運行。

【掛起、競爭GIL的操作是由系統調度,不需要我們管;當線程掛起、競爭、另一個線程開始執行,這個過程會消耗一點時間,稱為切換時間】

【若切換時間<等待時間,則多線程提高了效率;若切換時間與等待時間差不多,就沒有什么必要使用python多線程了】

同步與異步

同步:指一個進程在執行某個請求的時候,若該請求需要一段時間才能返回信息,那么這個進程將會一直等待下去,直到收到返回信息才繼續執行下去。

異步:指進程不需要一直等待下去,而是繼續執行下面的操作,不管其他進程的狀態,當有消息返回時系統會通知進程進行處理,這樣可以提高執行效率。

主進程與子進程:

 Threading.Thread封裝的函數 == 子進程 ; 其他部分 == 主進程 

子線程其實就是並發(同一時間段內執行多個操作)的任務

這些任務一般都會寫進一個函數func或者類里面,然后兩步走,如下代碼示例:

thread  =  Threading.Thread(target=函數func, args=(參數1,參數2))    #用Thread類包(封裝)起來
thread.start()  #start之后就開始跑了

 setDaemon(Ture) :設置子進程為守護進程 ==  主進程關閉,子進程隨即關閉【當你覺得一些線程不重要的時候,可以設置守護線程。】

 Join() :設置阻塞 == 該子進程執行完才能執行主線程【當一些任務要先於另一些任務完成的時候,可以用】

對於非守護線程,就算主線程執行完了,也要等非守護進程完成才能退出

單線程:

在好些年前的MS-DOS時代,操作系統處理問題都是單任務的。比如我想做聽音樂和看電影兩件事兒,那么一定要先排一下順序。

from time import ctime,sleep

def music():
    for i in range(2):
        print "I was listening to music. %s" %ctime()
        sleep(1)

def movie():
    for i in range(2):
        print "I was at the movies! %s" %ctime()
        sleep(5)

if __name__ == '__main__':
    music()
    movie()
    print "all over %s" %ctime()

我們先聽了一首音樂,通過for循環來控制音樂的播放了兩次,每首音樂播放需要1秒鍾,sleep()來控制音樂播放的時長。接着我們又看了一場電影。

每一場電影需要5秒鍾,因為太好看了,所以我也通過for循環看兩遍。在整個休閑娛樂活動結束后,我通過 print "all over %s" %ctime() 看了一下當前時間,差不多該睡覺了。

運行結果:

>>=========================== RESTART ================================
>>> 
I was listening to music. Thu Apr 17 10:47:08 2014
I was listening to music. Thu Apr 17 10:47:09 2014
I was at the movies! Thu Apr 17 10:47:10 2014
I was at the movies! Thu Apr 17 10:47:15 2014
all over Thu Apr 17 10:47:20 2014

其實,music()函數和move()函數更應該被看作是音樂和視頻播放器,至於要播放什么歌曲和視頻應該由我們使用時決定。

所以對上述代碼進行適當改造:

#coding=utf-8
import threading
from time import ctime,sleep

def music(func):
    for i in range(2):
        print "I was listening to %s. %s" %(func,ctime())
        sleep(1)

def move(func):
    for i in range(2):
        print "I was at the %s! %s" %(func,ctime())
        sleep(5)



if __name__ == '__main__':
    music(u'愛情買賣')
    move(u'阿凡達')

    print "all over %s" %ctime()

運行結果:

>>> ======================== RESTART ================================
>>> 
I was listening to 愛情買賣. Thu Apr 17 11:48:59 2014
I was listening to 愛情買賣. Thu Apr 17 11:49:00 2014
I was at the 阿凡達! Thu Apr 17 11:49:01 2014
I was at the 阿凡達! Thu Apr 17 11:49:06 2014
all over Thu Apr 17 11:49:11 2014

多線程1:

Python中使用線程有兩種方式:函數或者用包裝線程對象

1、函數式:調用 _thread  模塊中的 start_new_thread() 函數來產生新線程。語法如下:

_thread.start_new_thread (function, args[, kwargs])

參數說明:

  • function - 線程函數。
  • args - 傳遞給線程函數的參數,必須是個tuple類型。
  • kwargs - 可選參數。

實例:

# -*- encoding:utf-8 -*-
import _thread
import time


# 為線程定義一個函數
def print_time(threadName, delay):
    count = 0
    while count < 5:
        time.sleep(delay)
        count += 1
        print("%s: %s" % (threadName, time.ctime(time.time())))


# 創建兩個線程
try:
    _thread.start_new_thread(print_time, ("Thread-1", 2,))
    _thread.start_new_thread(print_time, ("Thread-2", 4,))
except:
    print("Error: 無法啟動線程")

while 1:
    pass

運行結果:執行以上程后可以按下 ctrl-c 退出。

threading 線程模塊

①Python3 通過兩個標准庫  _thread  和  threading  提供對線程的支持。

_thread  提供了低級別的、原始的線程以及一個簡單的鎖,它相比於  threading 模塊的功能還是比較有限的。

③ threading 模塊除了包含  _thread  模塊中的所有方法外,還提供的其他方法:

  •  threading.currentThread() :返回當前的線程變量
  •  threading.enumerate() : 返回一個包含正在運行的線程的列表list。正在運行指線程啟動后、結束前,不包括啟動前和終止后的線程。
  •  threading.activeCount() :返回正在運行的線程數量,與 len(threading.enumerate()) 有相同的結果。

除了使用方法外,線程模塊同樣提供了Thread類來處理線程,Thread類提供了以下方法:

  •  run() :用以表示線程活動的方法。
  •  start() :啟動線程活動。
  •  join([time]) :等待至線程中止。這阻塞調用線程直至線程的join() 方法被調用中止-正常退出或者拋出未處理的異常-或者是可選的超時發生。
  •  isAlive() :返回線程是否活動的。
  •  getName() :返回線程名。
  •  setName() :設置線程名。

使用 threading 線程模塊創建線程

我們可以通過直接從 threading.Thread  繼承創建一個新的子類,並實例化后調用  start() 方法啟動新線程;即它調用了threading線程模塊中的Thread類中的 start() 方法:

import threading
import time

exitFlag = 0


class myThread(threading.Thread):
    def __init__(self, threadID, name, counter):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.counter = counter

    def run(self):
        print("開始線程:" + self.name)
        print_time(self.name, self.counter, 5)
        print("退出線程:" + self.name)


def print_time(threadName, delay, counter):
    while counter:
        if exitFlag:
            threadName.exit()
        time.sleep(delay)
        print("%s: %s" % (threadName, time.ctime(time.time())))
        counter -= 1


# 創建新線程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)

# 開啟新線程
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print("退出主線程")

運行結果:

線程同步

①如果多個線程共同對某個數據修改,則可能出現不可預料的結果,為了保證數據的正確性,需要對多個線程進行同步。

②使用 Thread 對象的  Lock 和  Rlock 可以實現簡單的線程同步,這兩個對象都有  acquire 方法和  release 方法,對於那些需要每次只允許一個線程操作的數據,可以將其操作放到  acquire 和  release 方法之間。如下:

③多線程的優勢在於可以同時運行多個任務(至少感覺起來是這樣)。但是當線程需要共享數據時,可能存在數據不同步的問題。

考慮這樣一種情況:一個列表里所有元素都是0,線程"set"從后向前把所有元素改成1,而線程"print"負責從前往后讀取列表並打印。

那么,可能線程"set"開始改的時候,線程"print"便來打印列表了,輸出就成了一半0一半1,這就是數據的不同步。為了避免這種情況,引入了鎖的概念。

④鎖有兩種狀態——鎖定和未鎖定。每當一個線程比如"set"要訪問共享數據時,必須先獲得鎖定;如果已經有別的線程比如"print"獲得鎖定了,那么就讓線程"set"暫停,也就是同步阻塞;等到線程"print"訪問完畢,釋放鎖以后,再讓線程"set"繼續。

經過這樣的處理,打印列表時要么全部輸出0,要么全部輸出1,不會再出現一半0一半1的尷尬場面。

實例:

import threading
import time

class myThread (threading.Thread):
    def __init__(self, threadID, name, counter):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.counter = counter
    def run(self):
        print ("開啟線程: " + self.name)
        # 獲取鎖,用於線程同步
        threadLock.acquire()
        print_time(self.name, self.counter, 3)
        # 釋放鎖,開啟下一個線程
        threadLock.release()

def print_time(threadName, delay, counter):
    while counter:
        time.sleep(delay)
        print ("%s: %s" % (threadName, time.ctime(time.time())))
        counter -= 1

threadLock = threading.Lock()
threads = []

# 創建新線程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)

# 開啟新線程
thread1.start()
thread2.start()

# 添加線程到線程列表
threads.append(thread1)
threads.append(thread2)

# 等待所有線程完成
for t in threads:
    t.join()
print ("退出主線程")

運行結果:

線程優先級隊列( Queue)

Python 的  Queue 模塊中提供了同步的、線程安全的隊列類,包括FIFO(先入先出)隊列Queue,LIFO(后入先出)隊列LifoQueue,和優先級隊列 PriorityQueue。

這些隊列都實現了鎖原語,能夠在多線程中直接使用,可以使用隊列來實現線程間的同步。

Queue 模塊中的常用方法:

  • Queue.qsize():返回隊列的大小
  • Queue.empty():如果隊列為空,返回True,反之False
  • Queue.full():如果隊列滿了,返回True,反之False
  • Queue.full 與 maxsize 大小對應
  • Queue.get([block[, timeout]]):獲取隊列,timeout等待時間
  • Queue.get_nowait():相當Queue.get(False)
  • Queue.put(item):寫入隊列,timeout等待時間
  • Queue.put_nowait(item):相當Queue.put(item, False)
  • Queue.task_done():在完成一項工作之后,Queue.task_done()函數向任務已經完成的隊列發送一個信號
  • Queue.join():實際上意味着等到隊列為空,再執行別的操作

實例:

import queue
import threading
import time

exitFlag = 0

class myThread (threading.Thread):
    def __init__(self, threadID, name, q):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.q = q
    def run(self):
        print ("開啟線程:" + self.name)
        process_data(self.name, self.q)
        print ("退出線程:" + self.name)

def process_data(threadName, q):
    while not exitFlag:
        queueLock.acquire()
        if not workQueue.empty():
            data = q.get()
            queueLock.release()
            print ("%s processing %s" % (threadName, data))
        else:
            queueLock.release()
        time.sleep(1)

threadList = ["Thread-1", "Thread-2", "Thread-3"]
nameList = ["One", "Two", "Three", "Four", "Five"]
queueLock = threading.Lock()
workQueue = queue.Queue(10)
threads = []
threadID = 1

# 創建新線程
for tName in threadList:
    thread = myThread(threadID, tName, workQueue)
    thread.start()
    threads.append(thread)
    threadID += 1

# 填充隊列
queueLock.acquire()
for word in nameList:
    workQueue.put(word)
queueLock.release()

# 等待隊列清空
while not workQueue.empty():
    pass

# 通知線程是時候退出
exitFlag = 1

# 等待所有線程完成
for t in threads:
    t.join()
print ("退出主線程")

運行結果:

多線程2

科技在發展,時代在進步,我們的CPU也越來越快,CPU抱怨,P大點事兒占了我一定的時間,其實我同時干多個活都沒問題的;於是,操作系統就進入了多任務時代。我們聽着音樂吃着火鍋的不在是夢想。

繼續對上面的例子進行改造,引入threadring來同時播放音樂和視頻:

#coding=utf-8
import threading
from time import ctime,sleep


def music(func):
    for i in range(2):
        print "I was listening to %s. %s" %(func,ctime())
        sleep(1)

def move(func):
    for i in range(2):
        print "I was at the %s! %s" %(func,ctime())
        sleep(5)

threads = []
t1 = threading.Thread(target=music,args=(u'愛情買賣',))
threads.append(t1)
t2 = threading.Thread(target=move,args=(u'阿凡達',))
threads.append(t2)

if __name__ == '__main__':
    for t in threads:
        t.setDaemon(True)
        t.start()

    print "all over %s" %ctime()

運行結果:

>>> ========================= RESTART ================================
>>> 
I was listening to 愛情買賣. Thu Apr 17 12:51:45 2014 
I was at the 阿凡達! Thu Apr 17 12:51:45 2014
all over Thu Apr 17 12:51:45 2014

從執行結果來看,子線程(muisc() 、movie() )和主線程(print "all over %s" %ctime())都是同一時間啟動,但由於主線程執行完結束,所以導致子線程也終止。 

繼續調整程序:

...
if __name__ == '__main__':
    for t in threads:
       t.setDaemon(True)
       t.start()
    
    for t in threads:
       t.join() 

    print "all over %s" %ctime()

我們只對上面的程序加了個join()方法,用於等待線程終止。

join()的作用是,在子線程完成運行之前,這個子線程的父線程將一直被阻塞。

注意:  join()方法的位置是在for循環外的,也就是說必須等待for循環里的兩個進程都結束后,才去執行主進程。

運行結果:

>>> ========================= RESTART ================================
>>> 
I was listening to 愛情買賣. Thu Apr 17 13:04:11 2014  I was at the 阿凡達! Thu Apr 17 13:04:11 2014
I was listening to 愛情買賣. Thu Apr 17 13:04:12 2014
I was at the 阿凡達! Thu Apr 17 13:04:16 2014
all over Thu Apr 17 13:04:21 2014

從執行結果可看到,music() 和movie() 是同時啟動的。

開始時間4分11秒,直到調用主進程為4分22秒,總耗時為10秒。從單線程時減少了2秒,我們可以把music的sleep()的時間調整為4秒。

...
def music(func):
    for i in range(2):
        print "I was listening to %s. %s" %(func,ctime())
        sleep(4)
...

執行結果:

>>> ====================== RESTART ================================
>>> 
I was listening to 愛情買賣. Thu Apr 17 13:11:27 2014I was at the 阿凡達! Thu Apr 17 13:11:27 2014

I was listening to 愛情買賣. Thu Apr 17 13:11:31 2014
I was at the 阿凡達! Thu Apr 17 13:11:32 2014
all over Thu Apr 17 13:11:37 2014

子線程啟動11分27秒,主線程運行11分37秒。

雖然music每首歌曲從1秒延長到了4 ,但通多程線的方式運行腳本,總的時間沒變化。

詳細解釋:

import threading   #首先導入threading 模塊,這是使用多線程的前提。

threads = []

t1 = threading.Thread(target=music,args=(u'愛情買賣',))

#創建了threads數組,創建線程t1,使用threading.Thread()方法,在這個方法中調用music方法target=music,args方法對music進行傳參。 把創建好的線程t1裝到threads數組中。
threads.append(t1)

#接着以同樣的方式創建線程t2,並把t2也裝到threads數組。  
t2 = threading.Thread(target=move,args=(u'阿凡達',))
threads.append(t2)

#最后通過for循環遍歷數組。(數組被裝載了t1和t2兩個線程)
for t in threads:

  t.setDaemon(True)

  t.start()

#setDaemon(True)將線程聲明為守護線程,必須在start() 方法調用之前設置,如果不設置為守護線程程序會被無限掛起。子線程啟動后,父線程也繼續執行下去,當父線程執行完最后一條語句print "all over %s" %ctime()后,沒有等待子線程,直接就退出了,同時子線程也一同結束。
setDaemon()

  
#開始線程活動。
start()


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM