鍍金池/ 教程/ Python/ 多線程編程
二、Enum 的源碼
前言
一、Python 模塊簡介
一、List(列表)
五、匿名函數(shù)
三、什么是元類
二、循環(huán)語句
二、模塊的使用
三、第一個 Python 程序
線程與進(jìn)程
Python
三、條件語句和循環(huán)語句綜合實(shí)例
四、對象的描述器
三、類的屬性
一、迭代
五、迭代器和生成器綜合例子
六、運(yùn)算符相關(guān)的魔術(shù)方法
一、枚舉類的使用
前言
一、簡明概述
二、Python 的基本數(shù)據(jù)類型
多線程編程
五、作用域
四、包
四、枚舉的比較
四、Python 中的變量
六、類的多態(tài)
一、Python 中類也是對象
一、Python 的 Magic Method
前言
四、生成器
一、面向?qū)ο蟮母拍?/span>
五、類的繼承
二、類
二、使用 <code>type()</code> 動態(tài)創(chuàng)建類
進(jìn)程
二、set
三、主模塊和非主模塊
一、字典(Dictionary)
前言
前言
前言
前言
四、集成開發(fā)環(huán)境(IDE): PyCharm
前言
四、函數(shù)的參數(shù)
三、lsit 生成式(列表生成式)
四、自定義元類
四、類的方法
二、函數(shù)傳值問題
二、注釋
一、條件語句
一、Python 語法的簡要說明
三、函數(shù)返回值
三、基本數(shù)據(jù)類型轉(zhuǎn)換
三、屬性的訪問控制
二、Python 的安裝
前言
三、命名規(guī)范
一、Python 自定義函數(shù)的基本步驟
三、自定義類型的枚舉
五、自定義容器(Container)
二、Python 迭代器
前言
二、tuple(元組)
一、Python 簡介
前言
前言
前言
二、構(gòu)造(<code>__new__</code>)和初始化(<code>__init__</code>)
前言

多線程編程

其實(shí)創(chuàng)建線程之后,線程并不是始終保持一個狀態(tài)的,其狀態(tài)大概如下:

  • New 創(chuàng)建
  • Runnable 就緒。等待調(diào)度
  • Running 運(yùn)行
  • Blocked 阻塞。阻塞可能在 Wait Locked Sleeping
  • Dead 消亡

線程有著不同的狀態(tài),也有不同的類型。大致可分為:

  • 主線程
  • 子線程
  • 守護(hù)線程(后臺線程)
  • 前臺線程

簡單了解完這些之后,我們開始看看具體的代碼使用了。

1、線程的創(chuàng)建

Python 提供兩個模塊進(jìn)行多線程的操作,分別是 threadthreading

前者是比較低級的模塊,用于更底層的操作,一般應(yīng)用級別的開發(fā)不常用。

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

import time
import threading

class MyThread(threading.Thread):
    def run(self):
        for i in range(5):
            print('thread {}, @number: {}'.format(self.name, i))
            time.sleep(1)

def main():
    print("Start main threading")

    # 創(chuàng)建三個線程
    threads = [MyThread() for i in range(3)]
    # 啟動三個線程
    for t in threads:
        t.start()

    print("End Main threading")

if __name__ == '__main__':
    main()

運(yùn)行結(jié)果:

Start main threading
thread Thread-1, @number: 0
thread Thread-2, @number: 0
thread Thread-3, @number: 0
End Main threading
thread Thread-2, @number: 1
thread Thread-1, @number: 1
thread Thread-3, @number: 1
thread Thread-1, @number: 2
thread Thread-3, @number: 2
thread Thread-2, @number: 2
thread Thread-2, @number: 3
thread Thread-3, @number: 3
thread Thread-1, @number: 3
thread Thread-3, @number: 4
thread Thread-2, @number: 4
thread Thread-1, @number: 4

注意喔,這里不同的環(huán)境輸出的結(jié)果肯定是不一樣的。

2、線程合并(join方法)

上面的示例打印出來的結(jié)果來看,主線程結(jié)束后,子線程還在運(yùn)行。那么我們需要主線程要等待子線程運(yùn)行完后,再退出,要怎么辦呢?

這時候,就需要用到 join 方法了。

在上面的例子,新增一段代碼,具體如下:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

import time
import threading

class MyThread(threading.Thread):
    def run(self):
        for i in range(5):
            print('thread {}, @number: {}'.format(self.name, i))
            time.sleep(1)

def main():
    print("Start main threading")

    # 創(chuàng)建三個線程
    threads = [MyThread() for i in range(3)]
    # 啟動三個線程
    for t in threads:
        t.start()

    # 一次讓新創(chuàng)建的線程執(zhí)行 join
    for t in threads:
        t.join()

    print("End Main threading")

if __name__ == '__main__':
    main()

從打印的結(jié)果,可以清楚看到,相比上面示例打印出來的結(jié)果,主線程是在等待子線程運(yùn)行結(jié)束后才結(jié)束的。

Start main threading
thread Thread-1, @number: 0
thread Thread-2, @number: 0
thread Thread-3, @number: 0
thread Thread-1, @number: 1
thread Thread-3, @number: 1
thread Thread-2, @number: 1
thread Thread-2, @number: 2
thread Thread-1, @number: 2
thread Thread-3, @number: 2
thread Thread-2, @number: 3
thread Thread-1, @number: 3
thread Thread-3, @number: 3
thread Thread-3, @number: 4
thread Thread-2, @number: 4
thread Thread-1, @number: 4
End Main threading

3、線程同步與互斥鎖

使用線程加載獲取數(shù)據(jù),通常都會造成數(shù)據(jù)不同步的情況。當(dāng)然,這時候我們可以給資源進(jìn)行加鎖,也就是訪問資源的線程需要獲得鎖才能訪問。

其中 threading 模塊給我們提供了一個 Lock 功能。

lock = threading.Lock()

在線程中獲取鎖

lock.acquire()

使用完成后,我們肯定需要釋放鎖

lock.release()

當(dāng)然為了支持在同一線程中多次請求同一資源,Python 提供了可重入鎖(RLock)。RLock 內(nèi)部維護(hù)著一個 Lock 和一個 counter 變量,counter 記錄了 acquire 的次數(shù),從而使得資源可以被多次 require。直到一個線程所有的 acquire 都被 release,其他的線程才能獲得資源。

那么怎么創(chuàng)建重入鎖呢?也是一句代碼的事情:

r_lock = threading.RLock()

4、Condition 條件變量

實(shí)用鎖可以達(dá)到線程同步,但是在更復(fù)雜的環(huán)境,需要針對鎖進(jìn)行一些條件判斷。Python 提供了 Condition 對象。使用 Condition 對象可以在某些事件觸發(fā)或者達(dá)到特定的條件后才處理數(shù)據(jù),Condition 除了具有 Lock 對象的 acquire 方法和 release 方法外,還提供了 wait 和 notify 方法。線程首先 acquire 一個條件變量鎖。如果條件不足,則該線程 wait,如果滿足就執(zhí)行線程,甚至可以 notify 其他線程。其他處于 wait 狀態(tài)的線程接到通知后會重新判斷條件。

其中條件變量可以看成不同的線程先后 acquire 獲得鎖,如果不滿足條件,可以理解為被扔到一個( Lock 或 RLock )的 waiting 池。直達(dá)其他線程 notify 之后再重新判斷條件。不斷的重復(fù)這一過程,從而解決復(fù)雜的同步問題。

Condition

該模式常用于生產(chǎn)者消費(fèi)者模式,具體看看下面在線購物買家和賣家的示例:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

import threading, time

class Consumer(threading.Thread):
    def __init__(self, cond, name):
        # 初始化
        super(Consumer, self).__init__()
        self.cond = cond
        self.name = name

    def run(self):
        # 確保先運(yùn)行Seeker中的方法
        time.sleep(1)
        self.cond.acquire()
        print(self.name + ': 我這兩件商品一起買,可以便宜點(diǎn)嗎')
        self.cond.notify()
        self.cond.wait()
        print(self.name + ': 我已經(jīng)提交訂單了,你修改下價格')
        self.cond.notify()
        self.cond.wait()
        print(self.name + ': 收到,我支付成功了')
        self.cond.notify()
        self.cond.release()
        print(self.name + ': 等待收貨')

class Producer(threading.Thread):
    def __init__(self, cond, name):
        super(Producer, self).__init__()
        self.cond = cond
        self.name = name

    def run(self):
        self.cond.acquire()
        # 釋放對瑣的占用,同時線程掛起在這里,直到被 notify 并重新占有瑣。
        self.cond.wait()
        print(self.name + ': 可以的,你提交訂單吧')
        self.cond.notify()
        self.cond.wait()
        print(self.name + ': 好了,已經(jīng)修改了')
        self.cond.notify()
        self.cond.wait()
        print(self.name + ': 嗯,收款成功,馬上給你發(fā)貨')
        self.cond.release()
        print(self.name + ': 發(fā)貨商品')

cond = threading.Condition()
consumer = Consumer(cond, '買家(兩點(diǎn)水)')
producer = Producer(cond, '賣家(三點(diǎn)水)')
consumer.start()
producer.start()

輸出的結(jié)果如下:

買家(兩點(diǎn)水): 我這兩件商品一起買,可以便宜點(diǎn)嗎
賣家(三點(diǎn)水): 可以的,你提交訂單吧
買家(兩點(diǎn)水): 我已經(jīng)提交訂單了,你修改下價格
賣家(三點(diǎn)水): 好了,已經(jīng)修改了
買家(兩點(diǎn)水): 收到,我支付成功了
買家(兩點(diǎn)水): 等待收貨
賣家(三點(diǎn)水): 嗯,收款成功,馬上給你發(fā)貨
賣家(三點(diǎn)水): 發(fā)貨商品

5、線程間通信

如果程序中有多個線程,這些線程避免不了需要相互通信的。那么我們怎樣在這些線程之間安全地交換信息或數(shù)據(jù)呢?

從一個線程向另一個線程發(fā)送數(shù)據(jù)最安全的方式可能就是使用 queue 庫中的隊(duì)列了。創(chuàng)建一個被多個線程共享的 Queue 對象,這些線程通過使用 put()get() 操作來向隊(duì)列中添加或者刪除元素。

# -*- coding: UTF-8 -*-
from queue import Queue
from threading import Thread

isRead = True

def write(q):
    # 寫數(shù)據(jù)進(jìn)程
    for value in ['兩點(diǎn)水', '三點(diǎn)水', '四點(diǎn)水']:
        print('寫進(jìn) Queue 的值為:{0}'.format(value))
        q.put(value)

def read(q):
    # 讀取數(shù)據(jù)進(jìn)程
    while isRead:
        value = q.get(True)
        print('從 Queue 讀取的值為:{0}'.format(value))

if __name__ == '__main__':
    q = Queue()
    t1 = Thread(target=write, args=(q,))
    t2 = Thread(target=read, args=(q,))
    t1.start()
    t2.start()

輸出的結(jié)果如下:

寫進(jìn) Queue 的值為:兩點(diǎn)水
寫進(jìn) Queue 的值為:三點(diǎn)水
從 Queue 讀取的值為:兩點(diǎn)水
寫進(jìn) Queue 的值為:四點(diǎn)水
從 Queue 讀取的值為:三點(diǎn)水
從 Queue 讀取的值為:四點(diǎn)水

Python 還提供了 Event 對象用于線程間通信,它是由線程設(shè)置的信號標(biāo)志,如果信號標(biāo)志位真,則其他線程等待直到信號接觸。

Event 對象實(shí)現(xiàn)了簡單的線程通信機(jī)制,它提供了設(shè)置信號,清楚信號,等待等用于實(shí)現(xiàn)線程間的通信。

  • 設(shè)置信號

使用 Event 的 set() 方法可以設(shè)置 Event 對象內(nèi)部的信號標(biāo)志為真。Event 對象提供了 isSe() 方法來判斷其內(nèi)部信號標(biāo)志的狀態(tài)。當(dāng)使用 event 對象的 set() 方法后,isSet() 方法返回真

  • 清除信號

使用 Event 對象的 clear() 方法可以清除 Event 對象內(nèi)部的信號標(biāo)志,即將其設(shè)為假,當(dāng)使用 Event 的 clear 方法后,isSet() 方法返回假

  • 等待

Event 對象 wait 的方法只有在內(nèi)部信號為真的時候才會很快的執(zhí)行并完成返回。當(dāng) Event 對象的內(nèi)部信號標(biāo)志位假時,則 wait 方法一直等待到其為真時才返回。

示例:

# -*- coding: UTF-8 -*-

import threading

class mThread(threading.Thread):
    def __init__(self, threadname):
        threading.Thread.__init__(self, name=threadname)

    def run(self):
        # 使用全局Event對象
        global event
        # 判斷Event對象內(nèi)部信號標(biāo)志
        if event.isSet():
            event.clear()
            event.wait()
            print(self.getName())
        else:
            print(self.getName())
            # 設(shè)置Event對象內(nèi)部信號標(biāo)志
            event.set()

# 生成Event對象
event = threading.Event()
# 設(shè)置Event對象內(nèi)部信號標(biāo)志
event.set()
t1 = []
for i in range(10):
    t = mThread(str(i))
    # 生成線程列表
    t1.append(t)

for i in t1:
    # 運(yùn)行線程
    i.start()

輸出的結(jié)果如下:

1
0
3
2
5
4
7
6
9
8

6、后臺線程

默認(rèn)情況下,主線程退出之后,即使子線程沒有 join。那么主線程結(jié)束后,子線程也依然會繼續(xù)執(zhí)行。如果希望主線程退出后,其子線程也退出而不再執(zhí)行,則需要設(shè)置子線程為后臺線程。Python 提供了 setDeamon 方法。