亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

threading 模塊的 Thread 類的使用

1. 多線程的基本概念

程序要完成兩個任務:

  • 任務 1 進行一項復雜的計算,需要 1 秒才能完成。
  • 任務 2 讀取磁盤,需要 1 秒才能完成。

我們可以串行的執行這兩項任務,先執行任務 1,再執行任務 2,完成這兩項任務總共需要 2 秒,如下圖所示:

圖片描述

我們可以并行的執行這兩項任務,同時執行這兩項任務,完成這兩項任務只需要 1 秒,如下圖所示:

圖片描述

顯然,并行執行的時間小于串行執行的時間。很多場景下,我們希望程序能夠同時執行多個任務,操作系統提供了多線程的機制用于實現并行執行多個任務。在操作系統中,線程是一個可以獨立執行的任務。程序執行時至少包含一個線程,可以使用線程相關的 API 創建新的線程。

Python 的 threading 模塊提供了類 Thread,用戶通過新建一個類 Thread 創建新的線程,本文描述了類 Thread 的基本使用。

2. 多線程的基本使用

Python 的 threading 模塊中提供了類 Thread 用于實現多線程,用戶有兩種使用多線程的方式:

  • 在線程構造函數中指定線程的入口函數。
  • 自定義一個類,該類繼承類 Thread,在自定義的類中實現 run 方法。

2.1 線程的構造函數和重要的成員方法

本節介紹 Thread 相關的三個函數的功能:

  • 類 Thread 的構造函數
  • 類 Thread 的 start 方法
  • 類 Thread 的 join 方法

2.1.1 類Thread的構造函數

Thread(group = None, target = None, name = None, args = (), kwargs = {}) 

參數的含義如下:

  • group: 線程組,目前還沒有實現,在此處必須是 None。
  • target: 線程的入口函數,線程從該函數開始執行。
  • name: 線程名。
  • args: 線程的入口函數的參數,以元組的形式傳入。
  • kwargs: 線程的入口函數的參數,以字典的形式傳入。

使用 Thread 構造一個新線程時,必須指定 target 和 args 兩個參數,target 為線程的入口,args 為線程入口函數的參數。

2.1.2 類 Thread 的 start 方法

start()

在線程對象的構造函數中 target 指定了線程入口函數,args 指定了線程入口函數的參數。線程對象的 start 方法使新線程開始執行,執行函數 target(args)。

2.1.3 類 Thread 的 join 方法

join()

調用線程對象的 start 方法后,新線程開始執行函數 target(args)。調用線程對象的 join 方法,主線程阻塞,等待新線程執行完畢。

2.2 指定線程的入口函數

下面通過一個具體的例子,說明通過指定線程的入口函數的方式使用多線程。

import time
import threading

def thread_entry(begin, end):
    for i in range(begin, end):
        time.sleep(1)
        print(i)

t0 = threading.Thread(target = thread_entry, args = (1, 4))
t1 = threading.Thread(target = thread_entry, args = (101, 104))
t0.start()
t1.start()
t0.join()
t1.join()
  • 在第 9 行和第 10 行,通過調用 Thread 的構造函數創建了兩個線程。

  • 在第 9 行,設定線程的入口函數為 thread_entry,傳遞給入口函數兩個參數:1 和 4,新的線程將執行 thread_entry(1, 4),變量 t0 指向新創建的線程對象。

  • 在第 10 行,設定線程的入口函數為 thread_entry,傳遞給入口函數兩個參數:101 和 104,新的線程將執行 thread_entry(101, 104),變量 t1 指向新創建的線程對象。

  • 在第 4 行到第 7 行,定義了線程入口函數,該函數的功能是打印在 [begin, end) 區間的整數,每打印一個整數,調用 time.sleep(1) 睡眠 1 秒鐘。

  • 在第 11 行,調用 start 方法啟動線程 t0,t0 開始執行 thread_entry(1, 4)

  • 在第 12 行,調用 start 方法啟動線程 t1,t1 開始執行 thread_entry(101, 104)。

  • 在第 13 行和第 14 行,調用 join 方法,等待線程 t0 和 t1 執行完畢。

程序的運行結果如下:

1
101
2
102
3
103

線程 t0 的輸出結果為 1、2、3,線程 t1 的輸出結果為 101、102、103。由于兩者是并發執行的,所以結果交織在一起。

2.3 繼承 Thread

下面通過一個具體的例子,說明通過繼承 Thread 的方式使用多線程。

import time
import threading

class MyThread(threading.Thread):
    def __init__(self, begin, end):
        threading.Thread.__init__(self)
        self.begin = begin
        self.end = end

    def run(self):
        for i in range(self.begin, self.end):
            time.sleep(1)
            print(i)

t0 = MyThread(1, 4)
t1 = MyThread(101, 104)
t0.start()
t1.start()
t0.join()
t1.join()
  • 在第 4 行,定義類 MyThread,繼承 threading.Thread。

  • 在第 5 行,定義了構造函數 __init__,首先調用父類 thread.Thread.__init__ 初始化 Thread 對象,然后將參數 begin 和 end 保存在 MyThread 的成員變量中。

  • 在第 10 行,定義了方法 run,當線程開始運行時,run 方法會被調用。在 run 方法中,打印在 [begin, end) 區間的整數,每打印一個整數,調用 time.sleep(1) 睡眠 1 秒鐘。

  • 在第 15 行和第 16 行,通過調用 MyThread 的構造函數創建了兩個線程。

  • 在第 17 行,調用 start 方法啟動線程 t0,t0 開始執行 MyThread 的方法 run()。

  • 在第 18 行,調用 start 方法啟動線程 t1,t1 開始執行 MyThread 的方法 run()

  • 在第 19 行和第 20 行,調用 join 方法,等待線程 t0 和 t1 執行完畢。

程序的運行結果如下:

1
101
2
102
3
103

線程 t0 執行 thread_entry(1, 4),輸出結果為 1、2、3,線程 t1 執行 thread_entry(101, 104),輸出結果為 101、102、103。由于兩者是并發執行的,所以結果交織在一起。

2.4 常見的錯誤

2.4.1 自定義的類的 __init__ 方法忘記調用父類 Thread 的 __init__ 方法

通過自定義類繼承 Thread 的方式實現線程時,要求自定義的類的 __init__ 方法調用父類 Thread 的 __init__ 方法,如果忘記調用 Thread 的 __init__ 方法,則會報錯。編寫 forget_init.py,其內容如下:

import time
import threading;

class MyThread(threading.Thread):
    def __init__(self, id):
        # 在此處沒有調用父類 threading.Thread.__init__ 方法
        self.id = id

    def run(self):
        for i in range(3):
            print('This is thread %s' % self.id)
            time.sleep(3)

t1 = MyThread(0)
t1 = MyThread(1)
t0.start()
t1.start()
t0.join()
t1.join()

運行 forget_init.py,程序輸出如下:

Traceback (most recent call last):
  File "forget_init.py", line 14, in <module>
    t0 = MyThread(0)
  File "forget_init.py", line 7, in __init__
    self.id = id
  File "/usr/lib/python3.6/threading.py", line 1089, in name
    assert self._initialized, "Thread.__init__() not called"
AssertionError: Thread.__init__() not called

以上錯誤信息顯示,Thread.__init__ 沒有被調用。

2.4.2 只有一個線程參數時,使用 (arg) 表示線程參數

元組只包含一個元素時,必須加一個逗號,在下面的定義中,變量 tuple 表示的是一個元組,該元組包含了一個元素 123。

>>> tuple = (123,)
>>> tuple
(123,)

在下面的定義中,忘記加逗號,則變量 expression 表示的是一個整數類型的表達式,變量 expression 是一個整數 123,而不是元組 (123,)。

>>> expression = (123)
>>> expression
123

通過指定線程入口函數的方式實現線程時,使用元組傳遞線程參數,如果只有一個線程參數 arg,使用 (arg) 表示線程參數時,則會報錯。編寫程序 not_tuple.py,內容如下:

import time
import threading;

def run(id):
    for i in range(3):
        print('This is thread %d' % id)
        time.sleep(3)

t0 = threading.Thread(target = run, args = (0)) # 此處錯誤,應為(0,)
t1 = threading.Thread(target = run, args = (1)) # 此處錯誤,應為(1,)
t0.start()
t1.start()
t0.join()
t1.join()

運行 not_tuple.py,程序輸出如下:

Exception in thread Thread-1:
Traceback (most recent call last):
  File "/usr/lib/python3.6/threading.py", line 916, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.6/threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
TypeError: run() argument after * must be an iterable, not int

以上顯示錯誤信息 “TypeError: run() argument after * must be an iterable, not int”,初學者很難看明白這段錯誤信息,這段錯誤信息表示 run() 的 arguments 必須是可以遍歷的(iterable)。線程入口參數是一個元組,而參數 (0) 表示的是一個整數而不是元組 (0,)。

3. 使用多線程進行并行 IO 操作

本節通過實例說明 Python 多線程的使用場景?,F在需要編寫程序獲取 baidu.com、taobao.com、qq.com 首頁,程序包括 3 個任務:

本節需要使用到 python 的 requests 模塊,requests 模塊的用于 http 請求,requests 模塊提供了 get 方法用于獲取網頁。

在 3.1 小節演示串行執行這 3 個任務,并記錄串行完成 3 個任務總共所需要的時間;在 3.2 小節演示并行執行這 3 個任務,并記錄并行完成 3 個任務總共所需要的時間。

3.1 串行獲取 baidu.com、taobao.comqq.com 首頁

編寫程序 serial.py,該程序以串行的方式獲取 baidu、taobao、qq 的首頁,內容如下:

from datetime import datetime
import requests
import threading

def fetch(url):
    response = requests.get(url)
    print('Get %s: %s' % (url, response))

time0 = datetime.now()

fetch("https://www.baidu.com/")
fetch("https://www.taobao.com/")
fetch("https://www.qq.com/")

time1 = datetime.now()
time = time1 - time0
print(time.microseconds)
  • 在第 5 行,定義了函數 fetch,函數 fetch 獲取指定 url 的網頁。

  • 在第 6 行,調用 requests 模塊的 get 方法獲取獲取指定 url 的網頁。

  • 在第 9 行,記錄執行的開始時間。

  • 在第 11 行到第 13 行,串行執行獲取 baidu、taobao、qq 的首頁。

  • 在第 15 行到第 17 行,記錄執行的結束時間,并計算總共花費的時間,time.micoseconds 表示完成需要的時間(微秒)。

執行 serial.py,輸出如下:

Get https://www.baidu.com/: <Response [200]>
Get https://www.taobao.com/: <Response [200]>
Get https://www.qq.com/: <Response [200]>
683173

在輸出中,<Response [200]> 是服務器返回的狀態碼,表示獲取成功。成功獲取了 baidu、taobao、qq 的首頁,總共用時為 683173 微秒。

3.2 并行獲取 baidu.com、taobao.comqq.com 首頁

編寫程序 parallel.py,該程序以并行的方式獲取 baidu、taobao、qq 的首頁,內容如下:

from datetime import datetime
import requests
import threading

def fetch(url):
    response = requests.get(url)
    print('Get %s: %s' % (url, response))

time0 = datetime.now()

t0 = threading.Thread(target = fetch, args = ("https://www.baidu.com/",))
t1 = threading.Thread(target = fetch, args = ("https://www.taobao.com/",))
t2 = threading.Thread(target = fetch, args = ("https://www.qq.com/",))
t0.start()
t1.start()
t2.start()
t0.join()
t1.join()
t2.join()

time1 = datetime.now()
time = time1 - time0
print(time.microseconds)
  • 在第 5 行,定義了函數 fetch,函數 fetch 獲取指定 url 的網頁。

  • 在第 6 行,調用 requests 模塊的 get 方法獲取獲取指定 url 的網頁。

  • 在第 9 行,記錄執行的開始時間。

  • 在第 11 行到第 13 行,創建了 3 個線程,分別執行獲取 baidu、taobao、qq 的首頁。

  • 在第 14 行到第 16 行,啟動這 3 個線程,這 3 個線程并行執行。

  • 在第 17 行到第 19 行,等待這 3 個線程執行完畢。

  • 在第 21 行到第 23 行,記錄執行的結束時間,并計算總共花費的時間,time.micoseconds 表示完成需要的時間(微秒)。

執行 parallel.py,輸出如下:

Get https://www.baidu.com/: <Response [200]>
Get https://www.qq.com/: <Response [200]>
Get https://www.taobao.com/: <Response [200]>
383800

在輸出中,<Response [200]> 是服務器返回的狀態碼,表示獲取成功。成功獲取了 baidu、taobao、qq的首頁,總共用時為 383800 微秒。相比執行,串行執行總共用時為 683173 微秒,因此使用多線程加快了程序的執行速度。

4. 獲取線程的返回值

在繼承 Thread 實現多線程的方式中,將線程的返回值保存在線程對象中,使用一個成員變量保存線程的返回值。下面通過一個具體的例子,說明如何獲取線程的返回值。使用多線程技術計算 1+2+3 … + 100 的累加和,算法思路如下:

  • 主程序創建 2 個線程:

  • 線程 1,計算前 50 項的累加和,即 1+2+3 … + 50,保存計算結果。

  • 線程 2,計算后 50 項的累加和,即 51+52+53 … + 100,保存計算結果。

  • 主程序等待線程 1 和線程 2 執行完畢,獲取它們各自的計算結果,并相加得到最終的計算結果。

編寫程序 get_return_value.py,其內容如下:

import threading

class MyThread(threading.Thread):
    def __init__(self, begin, end):
        threading.Thread.__init__(self)
        self.begin = begin
        self.end = end

    def run(self):
        self.result = 0
        for i in range(self.begin, self.end):
            self.result += i

t0 = MyThread(1,51)
t1 = MyThread(51,101)
t0.start()
t1.start()
t0.join()
t1.join()
print(t0.result)
print(t1.result)
print(t0.result + t1.result)
  • 在第 14 行,創建第一個線程,計算區間 [1, 51) 內的累加和。

  • 在第 15 行,創建第二個線程,計算區間 [51, 101) 內的累加和。

  • 在第 4 行,函數 __init__ 將線程參數 begin 和 end 保存到線程對象中。

  • 在第 9 行,線程啟動后執行函數 run。

  • 在第 10 行到第 12 行,使用 self.result 保存線程的計算結果。

  • 在第 16 行到第 19 行,啟動線程進行計算,主程序等待子線程計算結束。

  • 在第 20 行到第 22 行,從 t0.result 中獲取線程 t0 的計算結果,從 t1.result 中獲取線程 t1 的計算結果,將兩者相加,打印最終的結果。

運行程序 get_return_value.py,輸出如下:

1275
3775
5050

線程 t0 計算前 50 項,計算結果為 1275;線程 t1 計算后 50 項,計算結果為 3775;主程序將兩者相加,得到最終的計算結果為 5050。