python多線程學習筆記

線程 常用方法介紹
爲啥 要使用多線程
使用多線程應該注意的問題.

thread中join 的用法
線程安全
線程間如何通信 , 鎖機制比較複雜的內容 .
Queue,線程 同步問題 Event, Condition 等…

死鎖問題
線程池的使用, 爲啥要有線程池呢?
結合實戰,看看項目中如何使用多線程?

1 思考

python 多線程編程 爲什麼會有多線程呢?

多線程的優勢是什麼呢?

首先舉個例子,如果 你有兩件事情要做,這兩件事情相對相關性不高. 如果我選擇 先做事情A, 在做事情B, 假設 A 需要5s , B 需要2s . 那麼 如果要把兩件事情做完,是不是要需要7s 時間呢?

能不能這樣呢? 我 A 和B 同時做,這樣是不是我可以用 5s的時間把事情做完呢.

1-1 入門示例
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Time    : 2019/2/28 18:46
@File    : preface.py
@Author  : [email protected]
"""

import time
import threading


def job1():
    time.sleep(5)
    print('job1 finished')


def job2():
    time.sleep(2)
    print('job2 finished')


def multithread():
    start = time.time()

    # 創建 線程
    job1_thread = threading.Thread(target=job1, name='JOB1')

    # 創建線程
    job2_thread = threading.Thread(target=job2, name='JOB2')

    # 啓動線程
    job1_thread.start()
    job2_thread.start()

    job2_thread.join()
    job1_thread.join()

    end = time.time()
    print(f"all time: {end-start}")


def singlethread():
    start = time.time()

    job1()
    job2()

    end = time.time()

    print(f"all time: {end-start}")

    pass



if __name__ == '__main__':
    pass
    print("main thread begin")
    singlethread()
    multithread()
    print("main  thread  done")


結果如下:
multithread result:

job2 finished
job1 finished
all time: 5.00341010093689

singlethread result :

job1 finished
job2 finished
all time: 7.008417129516602

從結果看出, 顯然是多線程情況下, 即’同時’ 幹兩件事,速度快點. 如果 你有很多事情 是不是也可以 一起工作提高 效率呢? 所以 這就是多線程存在的意義.

在這裏插入圖片描述

image

什麼是多線程編程
線程創建

如何創建一個線程呢? 有兩種方法.

  • 方法1
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Time    : 2019/2/28 18:16
@File    : testthread1.py
@Author  : [email protected]


# 線程的創建
"""

import threading

import time
import random


def job(num):
    time.sleep(2)

    ret = num + 1

    print(f"ret:{ret}")

    return num + 1


if __name__ == '__main__':
    print("main thread begin")

    # 創建一個線程, target 就是對應 要創建 的函數, args 對應函數 的參數
    thread = threading.Thread(target=job, args=(10,))

    # 啓動線程
    thread.start()

    print("main  thread  done")
  • 方法2
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Time    : 2019/2/28 18:16
@File    : testthread1.py
@Author  : [email protected]


# 線程的創建2
"""

import threading
import time
import random


class MyJob(threading.Thread):

    def __init__(self, name='myjob', num=0):
        super().__init__(name=name)

        self.num = num

    def run(self) -> None:
        time.sleep(2)

        ret = self.num + 1
        print(f"ret:{ret}")


if __name__ == '__main__':
    print("main  thread begin")

    myjob = MyJob(num=10)
    myjob.start()
    print("main  thread  done")

    pass

線程中常用方法

thread.is_alive() # 判斷 線程是否存活

thread.ident # 這個是屬性, 拿到活動線程的唯一標識, 是一個非零整數.如果線程沒有啓動則是None.

thread.daemon # 屬性, 設置該線程是否 爲 daemon,之後我會給出 什麼叫daemon 線程.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Time    : 2019/2/28 19:42
@File    : thread_method.py
@Author  : [email protected]
"""

import time
import threading


def job1():
    time.sleep(5)
    print('job1 finished')


if __name__ == '__main__':
    job1_thread = threading.Thread(target=job1, name='JOB1')

    print(f"isalive: {job1_thread.is_alive()}")

    print(f"thread name: {job1_thread.getName()}")

    """
     線程 屬性 , 這個 可以用來唯一標識 一個線程,但是 如果線程退出後,可能 這個 值 會被重複使用!
     如果線程沒有啓動則返回None 
     返回唯一標識當前線程的非零整數 .
     同時存在的其他線程, 這可用於識別每線程資源。
     儘管在某些平臺上線程標識可能看起來像 分配從1開始的連續數字,這種行爲不應該可以依賴,這個數字應該被視爲一個神奇的餅乾。
     線程的標識可以在退出後重新用於另一個線程。
    """

    print(f"thread ident:{job1_thread.ident}")

    # 開啓線程
    job1_thread.start()

    print(f"isalive: {job1_thread.is_alive()}")

    print(f"thread.ident:{job1_thread.ident}")

result 如下:

isalive: False
thread name: JOB1
thread ident:None
isalive: True
thread.ident:123145335967744
job1 finished
官方文檔    https://docs.python.org/3/library/threading.html
線程的daemon 屬性
  • 還有daemon 屬性
一個布爾值,指示此線程是否爲守護程序線程(True)或不是(False). 必須在調用start() 之前設置它,否則引發RuntimeError. 它的初始值繼承自創建線程; 主線程不是守護程序線程,因此在主線程中創建的所有線程都默認爲daemon = False


這個屬性 就是默認值 是False

官方解釋: 
The entire Python program exits when no alive non-daemon threads are left.
當沒有剩下活着的非守護程序線程時,整個Python程序退出.

看一個簡單的例子理解一下:

import time
import threading


def job1():
    for _ in range(5):
        print('job1 sleep 1 second.')
        time.sleep(1)
    print('job1 finished')


if __name__ == '__main__':
    print("====main thread begin====")

    print('Thread name: {}'.format(threading.current_thread()))

    # 創建一個線程 ,默認情況
    job1_thread = threading.Thread(target=job1, name='JOB1',daemon=False)
    # job1_thread.daemon = True

    # 啓動線程
    job1_thread.start()

    print('Main Thread :  Hello World')
    print("====main  thread  done====")

看下這個圖形
在這裏插入圖片描述
image

結果如下:

====main thread begin====
Thread name: <_MainThread(MainThread, started 140735991698304)>
job1 sleep 1 second.
Main Thread :  Hello World
====main  thread  done====
job1 sleep 1 second.
job1 sleep 1 second.
job1 sleep 1 second.
job1 sleep 1 second.
job1 finishe

結果分析:

可以看出 主線程結束之後, job1 線程是非daemon 線程. 所以當主線程退出的時候, job1線程 還是要 執行完成 纔會退出. 

如果把 daemon 這個屬性 設置成 True, 則結果可能不是我想要的, 
主線程 退出的時候, job1 也被迫退出了. 

來看下另一個示例:

def job1():
    for _ in range(5):
        print('job1 sleep 1 second.')
        time.sleep(1)
    print('job1 finished')


if __name__ == '__main__':
    print("====main thread begin====")

    print('Thread name: {}'.format(threading.current_thread()))

    # 創建一個線程 設置daemon=True
    job1_thread = threading.Thread(target=job1, name='JOB1',daemon=True)
    # job1_thread.daemon = True

    # 啓動線程
    job1_thread.start()

    print('Main Thread :  Hello World')
    print("====main  thread  done====")


結果如下:

====main thread begin====
Thread name: <_MainThread(MainThread, started 140735991698304)>
job1 sleep 1 second.
Main Thread :  Hello World
====main  thread  done====

可以 清楚的看到主線程退出了, job1 其實還沒有完成任務,被迫退出了任務. 這個可能不是我們想要的結果.

看下運行時的圖形:
在這裏插入圖片描述
image

設置 daemon 線程 有兩種方式.可以在 線程創建 的時候, 也可以創建完成後 直接設置.

    job1_thread = threading.Thread(target=job1, name='JOB1',daemon=True)
    job1_thread = threading.Thread(target=job1, name='JOB1')
    job1_thread.daemon = True
簡單總結一下:  daemon 線程設置

如果設置成daemon 爲True ,這 主線程完成的時候, 子線程會跟着退出的. 
daemon 設置成  False(默認值) , 主線程 退出的時候, 子線程必須完成任務後纔會退出. 

thread中join 的用法

python3對多線程join的理解


線程安全

什麼是線程安全?

線程安全百科

線程安全
線程安全是多線程編程時的計算機程序代碼中的一個概念。在擁有共享數據的多條線程並行執行的程序中,線程安全的代碼會通過同步機制保證各個線程都可以正常且正確的執行,不會出現數據污染等意外情況。

線程不安全
線程不安全就是不提供數據訪問保護,有可能出現多個線程先後更改數據造成所得到的數據是髒數據.

來看下這個代碼

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Time    : 2019/3/9 16:07
@File    : python_gil.py
@Author  : [email protected]
python  中的


CPython 解釋器的實現
GIL  使得 同一時刻 只有一個線程 在一個cpu 上執行字節碼,  無法將多個線程  映射到 多個CPU 上 .

gil  會根據 字節碼行數, 以及 時間片 , 釋放 gil

gil  遇到IO 操作 的時候 ,這個時候  鎖會被釋放掉. 讓給其他線程.

# def add(a):
#     a = a + 1
#
#     return a
#
# print(dis.dis(add))

"""

import threading
import dis

total = 0


def add():
    # 把total 減100w 次 
    global total

    for i in range(100_0000):
        total += 1


def desc():
    global total
    # 把total加100w 次 
    for i in range(100_0000):
        total -= 1


thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=desc)

thread1.start()
thread2.start()

thread1.join()
thread2.join()

print(f"total:{total}")

結果 是什麼呢? 爲什麼會出現這樣的結果?

TODO 結果分析:

主要是因爲 +1 ,-1 並不是線程安全的操作. 如果 說 在 +1 或者-1 的過程中 另外的線程 拿到了CPU , 那麼就會出現 數據計算不正確的情況.


CPython 解釋器的實現
GIL  使得 同一時刻 只有一個線程 在一個cpu 上執行字節碼,  無法將多個線程  映射到 多個CPU 上 .

GIL  會根據 字節碼行數, 以及 時間片 , 釋放 gil

GIL  遇到IO 操作 的時候 ,這個時候鎖會被釋放掉.讓給其他線程.
如何取thread 運算結果,thread 間 如何通信?

有的時候 可能開啓一個線程任務,希望可以拿到 線程的結果這個時候 應該怎麼做呢 ?

我的建議可以用 python中 queue.Queue 這個對象.因爲這個首先是一個阻塞隊列, 並且是線程安全的.即使在多線程環境下,也可以保證線程的安全.

比如一個線程 需要把 list 中每一個數據 都要進行 翻倍的操作.這個時候就需要使用一個數據結構,把結果存放起來,可以用queue 存放結果.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Time    : 2019/2/28 20:42
@File    : thread communicate.py
@Author  : [email protected]
"""

import time
import threading

from queue import Queue


def job1(datas: list, q: Queue):
    """
    需要返回結果, 怎麼處理呢?
    可以把 處理結果存入到 Queue 中 .

    對datas 中的 數據 簡單 的*2 操作.
    :param datas: list
    :param q: Queue 對象
    :return:
    """

    time.sleep(2)
    for data in datas:
        tmp = data * 2
        # 把結果 存放到q中
        q.put(tmp)

if __name__ == '__main__':

    q = Queue()
    datas = list(range(10))
    # 創建一個線程
    job1_thread = threading.Thread(target=job1, args=(datas, q), name='JOB1')

    # 啓動線程
    job1_thread.start()

    # 等待線程結束
    job1_thread.join()

    while not q.empty():
        #  取出結果
        print(q.get())

結果如下:

0
2
4
6
8
10
12
14
16
18

lock 的使用

爲什麼要使用鎖呢?
其實就是保證線程安全操作, 因爲線程間變量 是共享的, 爲了 是每個線程,操作一個變量的時候,不受到影響. 需要 同一時間 只能有一個 線程 執行相應的代碼塊.

import  threading  
help(type(threading.Lock()))

舉個例子 有兩個線程, 他們 工作 的任務就是數數, job1 從 0 …9 , job2 從 100,…109
但是 這兩個線程 都不想被打擾, 就是我一旦開始工作, 不希望有人 打擾我數數, 怎麼辦呢?

import time
import threading
import random


def job1():
    """
    數數 的 任務 , 從  1,2...9
    :return:
    """

    for i in range(0, 10):
        time.sleep(random.randint(1, 10) * 0.1)
        print(f'job1 :{i}')


def job2():
    """
     數數 的 任務 , 從  100,101,...  109
    :return:
    """

    for i in range(100, 110):
        time.sleep(random.randint(1, 10) * 0.1)
        print(f'job2 :{i}')


if __name__ == '__main__':
    print("====main thread begin====")

    print('Thread name: {}'.format(threading.current_thread()))

    job1_thread = threading.Thread(target=job1, name='JOB1')
    job2_thread = threading.Thread(target=job2, name='JOB2')

    job1_thread.start()
    job2_thread.start()

    job1_thread.join()
    job2_thread.join()

    print('Main Thread :  Hello World')
    print("====main  thread  done====")

結果如下:

====main thread begin====
Thread name: <_MainThread(MainThread, started 140735991698304)>
job2 :100
job2 :101
job1 :0
job2 :102
job1 :1
job1 :2
job1 :3
job1 :4
job2 :103
job1 :5
job2 :104
job1 :6
job2 :105
job1 :7
job2 :106
job2 :107
job1 :8
job2 :108
job1 :9
job2 :109
Main Thread :  Hello World
====main  thread  done====

從 執行結果看, job2 先開始工作, 之後 job1 插入了進來, 然後job2 又插入了進來. 這樣如此反覆, 導致這兩個job 都不能好好的工作. 不能安心的把事情幹完. 就被迫停下來,繼續 等其他工作線程.

有一個辦法: 可以在 工作線程中加鎖,來解決這個問題.

看下 加鎖的代碼:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Time    : 2019/2/28 19:42
@File    : thread_method.py
@Author  : [email protected]


在多線程程序中安全使用可變對象,你需要使用 threading 庫中的 Lock 對象


refer:

https://python3-cookbook.readthedocs.io/zh_CN/latest/c12/p04_locking_critical_sections.html

https://github.com/MorvanZhou/tutorials/blob/master/threadingTUT/thread6_lock.py

https://juejin.im/post/5b17f4305188257d6b5cff6f


"""

import time
import threading
import random


def job1():
    """
    數數 的 任務 , 從  1,2...9
    :return:
    """
    global lock
    # 加鎖
    with lock:
        for i in range(0, 10):
            time.sleep(random.randint(1, 10) * 0.1)
            print(f'job1 :{i}')


def job2():
    """
     數數 的 任務 , 從  100,101,...  109
    :return:
    """
    global lock
    # 加鎖
    with lock:
        for i in range(100, 110):
            time.sleep(random.randint(1, 10) * 0.1)
            print(f'job2 :{i}')


if __name__ == '__main__':
    print("====main thread begin====")
    print('Thread name: {}'.format(threading.current_thread()))

    # 定義一個鎖
    lock = threading.Lock()
    job1_thread = threading.Thread(target=job1, name='JOB1')
    job2_thread = threading.Thread(target=job2, name='JOB2')

    job1_thread.start()
    job2_thread.start()

    job1_thread.join()
    job2_thread.join()

    print('Main Thread :  Hello World')
    print("====main  thread  done====")

結果如下:

====main thread begin====
Thread name: <_MainThread(MainThread, started 140735991698304)>
job1 :0
job1 :1
job1 :2
job1 :3
job1 :4
job1 :5
job1 :6
job1 :7
job1 :8
job1 :9
job2 :100
job2 :101
job2 :102
job2 :103
job2 :104
job2 :105
job2 :106
job2 :107
job2 :108
job2 :109
Main Thread :  Hello World
====main  thread  done====

這樣可以看到, job1, job2 之間 就能安全工作了. 每個線程都不會打擾另外的線程,都安心的開始數數了.

在舉一個例子 :

有一個線程函數 功能是:接收一個num, 把這個num, 加入到 一個list 裏面.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Time    : 2019/2/28 19:42
@File    : thread_method.py
@Author  : [email protected]


在多線程程序中安全使用可變對象,你需要使用 threading 庫中的 Lock 對象


refer:

https://python3-cookbook.readthedocs.io/zh_CN/latest/c12/p04_locking_critical_sections.html

https://github.com/MorvanZhou/tutorials/blob/master/threadingTUT/thread6_lock.py

https://juejin.im/post/5b17f4305188257d6b5cff6f


Python threading中lock的使用   https://blog.csdn.net/u012067766/article/details/79733801

"""

import time
import threading
import random


def job11(num=None):
    global lock
    with lock:
        # 模擬一些費時間的操作
        time.sleep(random.randint(1, 10) * 0.1)
        # 關鍵代碼 加鎖
        datas.append(num)
        print(datas)


def job(num=None):
    # 模擬一些費時間的操作
    time.sleep(random.randint(1, 10) * 0.1)
    # 關鍵代碼 
    datas.append(num)
    print(datas)


if __name__ == '__main__':
    print("====main thread begin====")
    print('Thread name: {}'.format(threading.current_thread()))

    # 定義一個鎖
    lock = threading.Lock()

    # 數據
    datas = []

    for i in range(10):
        t = threading.Thread(target=job, args=(i,))
        t.start()

    print("====main  thread  done====")

結果如下:

====main thread begin====
Thread name: <_MainThread(MainThread, started 140735991698304)>
====main  thread  done====
[9]
[9, 4]
[9, 4, 8]
[9, 4, 8, 7]
[9, 4, 8, 7, 3]
[9, 4, 8, 7, 3, 1]
[9, 4, 8, 7, 3, 1, 2]
[9, 4, 8, 7, 3, 1, 2, 5]
[9, 4, 8, 7, 3, 1, 2, 5, 0]
[9, 4, 8, 7, 3, 1, 2, 5, 0, 6]

可以看出 由於每個線程 拿到list 的時機不一樣, 所以 list 打印的結果也不一樣.

# 換成 加鎖的代碼
 t = threading.Thread(target=job11, args=(i,))

把上面的 target=job11 ,job11 定義了一個lock , 這樣就可以鎖住關鍵的代碼, 同一時刻,只會有一個線程進入到代碼段.

====main thread begin====
Thread name: <_MainThread(MainThread, started 140735991698304)>
====main  thread  done====
[0]
[0, 1]
[0, 1, 2]
[0, 1, 2, 3]
[0, 1, 2, 3, 4]
[0, 1, 2, 3, 4, 5]
[0, 1, 2, 3, 4, 5, 6]
[0, 1, 2, 3, 4, 5, 6, 7]
[0, 1, 2, 3, 4, 5, 6, 7, 8]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

加了一個鎖 ,之後 發現代碼 就能按照預想的方式 打印了.

如何 解決死鎖問題??

12.5 防止死鎖的加鎖機制

在多線程程序中,死鎖問題很大一部分是由於線程同時獲取多個鎖造成的。舉個例子:一個線程獲取了第一個鎖,然後在獲取第二個鎖的 時候發生阻塞,那麼這個線程就可能阻塞其他線程的執行,從而導致整個程序假死。 解決死鎖問題的一種方案是爲程序中的每一個鎖分配一個唯一的id,然後只允許按照升序規則來使用多個鎖,這個規則使用上下文管理器 是非常容易實現的

threading 中 event的使用

https://docs.python.org/3/library/threading.html

Event(事件):事件處理的機制:全局定義了一個內置標誌Flag,
如果Flag值爲 False,那麼當程序執行 event.wait()方法時就會阻塞,
如果Flag值爲True,那麼event.wait() 方法時便不再阻塞。

demon1 中 event 的基本使用
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Time    : 2019/3/1 15:04
@File    : thread_event.py
@Author  : [email protected]

refer :
python筆記12-python多線程之事件(Event)  https://www.cnblogs.com/yoyoketang/p/8341972.html

    event.is_set()

    event.wait()

    event.set()

    event.clear()

Python提供了Event對象用於線程間通信,它是由線程設置的信號標誌,如果信號標誌位真,則其他線程等待直到信號接觸。
Event對象實現了簡單的線程通信機制,它提供了設置信號,清除信號,等待等用於實現線程間的通信。

event = threading.Event() 創建一個event

1 設置信號
event.set()

使用Event的set()方法可以設置Event對象內部的信號標誌爲真。Event對象提供了isSet()方法來判斷其內部信號標誌的狀態。
當使用event對象的set()方法後,isSet()方法返回真


# isSet(): 獲取內置標誌狀態,返回True或False


2 清除信號
event.clear()

使用Event對象的clear()方法可以清除Event對象內部的信號標誌,即將其設爲假,當使用Event的clear方法後,isSet()方法返回假

3 等待
event.wait()

Event對象wait的方法只有在內部信號爲真的時候纔會很快的執行並完成返回。當Event對象的內部信號標誌位假時,
則wait方法一直等待到其爲真時才返回。也就是說必須set新號標誌位真



Event(事件):事件處理的機制:全局定義了一個內置標誌Flag,

如果Flag值爲 False,那麼當程序執行 event.wait()方法時就會阻塞,
如果Flag值爲True,那麼event.wait() 方法時便不再阻塞。

"""

import threading
import time


def eat_hotpot(name):
    """
    吃火鍋的函數
    :param name:
    :return:
    """
    # 等待事件,進入等待阻塞狀態
    print('%s 已經啓動' % threading.currentThread().getName())
    print('小夥伴 %s 已經進入就餐狀態!' % name)
    time.sleep(2)
    event.wait()
    # 收到事件後進入運行狀態
    print('%s 收到通知了.' % threading.currentThread().getName())
    print('小夥伴 %s 開始吃咯!' % name)


def test1():

    # 設置線程組
    threads = []

    # 創建新線程
    thread1 = threading.Thread(target=eat_hotpot, name='frank-thread', args=("Frank",))
    thread2 = threading.Thread(target=eat_hotpot, name='shawn-thread', args=("Shawn",))
    thread3 = threading.Thread(target=eat_hotpot, name='laoda-thread', args=("Laoda",))

    # 添加到線程組
    threads.append(thread1)
    threads.append(thread2)
    threads.append(thread3)

    # 開啓線程
    for thread in threads:
        thread.start()

    time.sleep(0.1)
    # 發送事件通知
    print('主線程通知小夥伴開吃咯!')
    # 把 flag 設置成 True
    event.set()
    # print(f" event.is_set():{event.is_set()}")


if __name__ == '__main__':

    # 創建一個事件
    event = threading.Event()
    test1()

結果如下:

frank-thread 已經啓動
小夥伴 Frank 已經進入就餐狀態!
shawn-thread 已經啓動
小夥伴 Shawn 已經進入就餐狀態!
laoda-thread 已經啓動
小夥伴 Laoda 已經進入就餐狀態!
主線程通知小夥伴開吃咯!
frank-thread 收到通知了.
小夥伴 Frank 開始吃咯!
shawn-thread 收到通知了.
小夥伴 Shawn 開始吃咯!
laoda-thread 收到通知了.
小夥伴 Laoda 開始吃咯!

demo2

假設 有兩個線程 ,有一個 打印 1 3 5 , 有一個線程 打印 2 4 6
他們如何協調工作 完成 按數數的順序打印 每一個數字呢?

如果同時啓動 兩個線程,會導致 每個線程 執行的機會 可能 完全不一樣, 如何控制呢?

import threading
from threading import Event
import  time,random

def print_odd():
    for item in [1, 3, 5,7,9]:
        time.sleep(random.randint(1,10)*0.1)
        print(item)


def print_even():
    for item in [2, 4, 6,8,10]:
        time.sleep(random.randint(1,10)*0.1)

        print(item)


if __name__ == '__main__':
    t1 = threading.Thread(target=print_odd)
    t2 = threading.Thread(target=print_even)
    t1.start()
    t2.start()

來看下 如何實現 這個功能呢?
用event 來實現 線程之間的同步

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Time    : 2019/3/5 13:54
@File    : thread_event2.py
@Author  : [email protected]


python多線程(6)---事件 Event  http://www.zhangdongshengtech.com/article-detials/185

"""

import threading
from threading import Event


def print_odd(e1, e2):
    """
    打印 奇數
    """
    for item in [1, 3, 5]:
        e1.wait()

        print(item)
        e1.clear()
        e2.set()


def print_even(e1, e2):
    """
    打印 偶數
    """
    for item in [2, 4, 6]:
        e1.wait()

        print(item)
        e1.clear()
        e2.set()


if __name__ == '__main__':
    e1, e2 = Event(), Event()
    t1 = threading.Thread(target=print_odd, args=(e1, e2))
    t2 = threading.Thread(target=print_even, args=(e2, e1))
    t1.start()
    t2.start()
    e1.set()
打印結果爲:  1 2 3  4 5 6  

通過 event.wait() , event.set(),event.clear() 來控制 線程之間協調工作.


theading中 Condition 的使用

線程之間的同步,condition介紹. condition 可以認爲是更加高級的鎖.

https://my.oschina.net/lionets/blog/194577

https://docs.python.org/3/library/threading.html

condition內部是含有鎖的邏輯,不然也沒法保證線程之間的同步。


condition  介紹: 

常用方法: 
acquire([timeout])/release(): 調用關聯的鎖的相應方法。
 with cond:
    pass


condition 實現了上下文協議, 可以使用 with 語句 

wait([timeout]): 調用這個方法將使線程進入Condition的等待池等待通知,並釋放鎖。使用前線程必須已獲得鎖定,否則將拋出異常。
    Wait until notified or until a timeout occurs.

notify(): 調用這個方法將從等待池挑選一個線程並通知,收到通知的線程將自動調用acquire()嘗試獲得鎖定(進入鎖定池);
        其他線程仍然在等待池中。調用這個方法不會釋放鎖定。使用前線程必須已獲得鎖定,否則將拋出異常。
notifyAll(): 調用這個方法將通知等待池中所有的線程,這些線程都將進入鎖定池嘗試獲得鎖定。調用這個方法不會釋放鎖定。
            使用前線程必須已獲得鎖定,否則將拋出異常。



Notice:

condition 原理 介紹:
condition 實際上有兩層鎖, 一把鎖 底層鎖,會在 線程調用 wait 方法的時候釋放.
上面的鎖(第二把鎖) 會在在每次調用wait 方法的時候 ,分配一把鎖,並且放入到cond 的等待隊列中, 等其他線程 notify喚醒該線程.

注意 :
 調用 with cond 之後, 才能調用  wait  ,notify 方法  ,這兩個方法必須拿到鎖之後 才能使用.

舉個吃火鍋的例子,來說明吃火鍋的如何使用線程完成同步的.

假設有一個需求:
Eat hotpot 開始吃火鍋, 的時候一次 放入5盤 牛肉 ,等 10min, 食物才能 煮熟, 才能開始吃.

吃完後,通知 繼續加食物 ,5盤 羊肉 , 等10min, 吃完就結束了. (大家 都吃飽了.)

這個用 生產者消費者來模擬
要保證每次開始吃的 時候, 食物必須要是熟的.
用condition 實現 線程同步.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Time    : 2019/3/5 13:54
@File    : thread_condition0.py
@Author  : [email protected]

refer:
https://www.cnblogs.com/yoyoketang/p/8337118.html


Condition():

acquire(): 線程鎖
release(): 釋放鎖
wait(timeout): 線程掛起,直到收到一個notify通知或者超時(可選的,浮點數,單位是秒s)纔會被喚醒繼續運行。
                wait()必須在已獲得Lock前提下才能調用,否則會觸發RuntimeError。
notify(n=1): 通知其他線程,那些掛起的線程接到這個通知之後會開始運行,默認是通知一個正等待該condition的線程,最多則喚醒n個等待的線程。
                notify()必須在已獲得Lock前提下才能調用,否則會觸發RuntimeError。notify()不會主動釋放Lock。
notifyAll(): 如果wait狀態線程比較多,notifyAll的作用就是通知所有線程



"""

import threading
import time


class Producer(threading.Thread):

    def __init__(self, cond):
        super().__init__()
        self.cond = cond

    def run(self):
        global num

        # 鎖定線程
        self.cond.acquire()

        print("開始添加食物: 牛肉")
        for i in range(5):
            num += 1

        # 來模擬10min
        print('\n食物 正在 煮熟中.... 請稍等.')
        time.sleep(2)
        print(f"\n現在 火鍋裏面牛肉個數:{num}")
        self.cond.notify()
        self.cond.wait()

        # 開始添加 羊肉
        print("添加食物:羊肉")
        for i in range(5):
            num += 1
        print('\n食物 正在 煮熟中.... 請稍等...')

        time.sleep(2)

        print(f"\n現在  火鍋裏面羊肉個數:{num}")

        self.cond.notify()
        self.cond.wait()

        # 釋放鎖
        self.cond.release()


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

    def run(self):
        self.cond.acquire()
        global num

        self.cond.wait()
        print("------食物已經熟了,開始吃啦------")

        for _ in range(5):
            num -= 1
            print("火鍋裏面剩餘食物數量:%s" % str(num))
            time.sleep(1)

        print("\n鍋底沒食物了,趕緊加食物吧!")
        self.cond.notify()  # 喚醒其它線程,wait的線程

        self.cond.wait()
        print("------食物已經熟了,開始吃啦------")
        for _ in range(5):
            num -= 1
            print("火鍋裏面剩餘食物數量:%s" % str(num))
            time.sleep(1)

        print('\n吃飽了,今天火鍋真好吃!')
        self.cond.notify()

        self.cond.release()


if __name__ == '__main__':
    condition = threading.Condition()

    # 每次放入鍋的數量
    num = 0
    p = Producer(condition)
    c = Consumers(condition)

    c.start()
    p.start()


結果如下:

開始添加食物: 牛肉

食物 正在 煮熟中.... 請稍等.

現在 火鍋裏面牛肉個數:5
------食物已經熟了,開始吃啦------
火鍋裏面剩餘食物數量:4
火鍋裏面剩餘食物數量:3
火鍋裏面剩餘食物數量:2
火鍋裏面剩餘食物數量:1
火鍋裏面剩餘食物數量:0

鍋底沒食物了,趕緊加食物吧!
添加食物:羊肉

食物 正在 煮熟中.... 請稍等...

現在  火鍋裏面羊肉個數:5
------食物已經熟了,開始吃啦------
火鍋裏面剩餘食物數量:4
火鍋裏面剩餘食物數量:3
火鍋裏面剩餘食物數量:2
火鍋裏面剩餘食物數量:1
火鍋裏面剩餘食物數量:0

吃飽了,今天火鍋真好吃!
demo1

通過條件, 實現兩個線程的同步.
例子 是兩個機器人的對話. 兩個一起數數. 要保證
小愛說一句, 天貓說一句. 來看下例子.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Time    : 2019/3/10 09:52
@File    : test_condition.py
@Author  : [email protected]


測試 condition

acquire([timeout])/release(): 調用關聯的鎖的相應方法。
 with cond:
    pass


wait([timeout]): 調用這個方法將使線程進入Condition的等待池等待通知,並釋放鎖。使用前線程必須已獲得鎖定,否則將拋出異常。

notify(): 調用這個方法將從等待池挑選一個線程並通知,收到通知的線程將自動調用acquire()嘗試獲得鎖定(進入鎖定池);
        其他線程仍然在等待池中。調用這個方法不會釋放鎖定。使用前線程必須已獲得鎖定,否則將拋出異常。
notifyAll(): 調用這個方法將通知等待池中所有的線程,這些線程都將進入鎖定池嘗試獲得鎖定。調用這個方法不會釋放鎖定。
            使用前線程必須已獲得鎖定,否則將拋出異常。


"""

import threading
from threading import Condition


class XiaoAi(threading.Thread):

    def __init__(self, cond):
        super().__init__(name='小愛')
        self.cond = cond

    def run(self) -> None:
        with self.cond:
            self.cond.wait()
            print(f"{self.name}: 在呢")
            self.cond.notify()

            self.cond.wait()
            print(f"{self.name}: 好啊.")
            self.cond.notify()

            for num in range(2, 11, 2):
                self.cond.wait()
                print(f'{self.name}: {num} ')
                self.cond.notify()


class TianMao(threading.Thread):

    def __init__(self, cond):
        super().__init__(name='天貓精靈')
        self.cond = cond

    def run(self) -> None:
        with self.cond:
            print(f'{self.name}: 小愛同學')
            self.cond.notify()
            self.cond.wait()

            print(f'{self.name}: 我們來數數吧')
            self.cond.notify()
            self.cond.wait()

            for num in range(1, 10, 2):
                print(f'{self.name}: {num}')
                self.cond.notify()
                self.cond.wait()

            # self.cond.notify_all()


if __name__ == '__main__':
    cond = Condition()

    xiaoai = XiaoAi(cond)
    tianmao = TianMao(cond)

    # 啓動 順序 很重要
    # 調用 with cond 之後, 才能調用  wait  ,notify 方法  ,這兩個方法 必須拿到鎖之後 才能使用.
    #  condition  有兩層鎖,一把底層鎖會在線程調用了 wait 方法的時候釋放, 上面的鎖會在每次調用wait方法時候,分配一把,並方式 到cond 隊列,等待 notify 喚醒.
    xiaoai.start()
    tianmao.start()


threading中 Semaphore,BoundedSemaphore 信號量
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Time    : 2019/3/10 11:17
@File    : test_Semaphore.py
@Author  : [email protected]

Semaphore   信號量


實現 是通過 threading.Condition() 來實現的, 來控制啓動線程的數量.可以看看源碼. 

acquire()
release()

每當調用acquire()時,內置計數器-1
每當調用release()時,內置計數器+1


1 控制啓動線程數量的類


"""
import threading
import time

from threading import Semaphore, BoundedSemaphore


class HtmlParser(threading.Thread):

    def __init__(self, url, sem, name='html_parser'):
        super().__init__(name=name)
        self.url = url
        self.sem = sem

    def run(self):
        time.sleep(2)
        print('got html success')
        
        # 注意在這裏釋放信號量,完成任務後釋放.
        self.sem.release()


class UrlProducer(threading.Thread):

    def __init__(self, sem, name='url_produce'):
        super().__init__(name=name)
        self.sem = sem

    def run(self):
        for i in range(20):
            self.sem.acquire()
            html_thread = HtmlParser(url=f"http://www.baidu.com/item={i}",
                                     sem=self.sem
                                     )

            html_thread.start()


if __name__ == '__main__':
    sem = Semaphore(5)  # 最多有5個線程.
    url_producer = UrlProducer(sem)
    url_producer.start()

結果如下:
每次輸出 5個 返回結果

got html success
got html success
got html success
got html success
got html success
....
....
....


python threading 線程隔離

參考文檔

12.6 保存線程的狀態信息

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Time    : 2019/2/27 10:43
@File    : test_local.py
@Author  : [email protected]
"""
from threading import local, Thread, currentThread

# 定義一個local實例
local_data = local()

# 在主線中,存入name這個變量
local_data.name = 'local_data'


class MyThread(Thread):

    def __init__(self, name) -> None:
        super().__init__(name=name)

    def run(self) -> None:
        print(f"before:  {currentThread()} ,  {local_data.__dict__}")

        local_data.name = self.getName()

        print(f"after:  {currentThread()} ,  {local_data.__dict__}")


if __name__ == '__main__':
    print("開始前-主線程:", local_data.__dict__)

    t1 = MyThread(name='T1')
    t1.start()

    t2 = MyThread(name='T2')
    t2.start()

    t1.join()
    t2.join()
    print("結束後-主線程:", local_data.__dict__)

    pass

結果如下:

開始前-主線程: {'name': 'local_data'}
before:  <MyThread(T1, started 123145487306752)> ,  {}
after:  <MyThread(T1, started 123145487306752)> ,  {'name': 'T1'}
before:  <MyThread(T2, started 123145487306752)> ,  {}
after:  <MyThread(T2, started 123145487306752)> ,  {'name': 'T2'}
結束後-主線程: {'name': 'local_data'}

線程池的使用,爲啥要有線程池呢?
初步常用函數
task.done()   # 判斷是否 完成 
task.result()
task.cancel()

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Time    : 2019/3/10 13:53
@File    : test_futures.py
@Author  : [email protected]

線程池  是什麼?     

爲什麼 需要線程池?

1  控制線程的數量
2  獲取某一個線程的狀態 以及返回值 
3  當一個線程 完成的時候,我們主線程能夠立即知道
4  concurrent.futures 這個包 可以讓 多線程,多進程編程 變得如此簡單. 多線程 與多進程接口幾乎一致. 


"""

from concurrent.futures import ThreadPoolExecutor
import time


def get_html(seconds):
    time.sleep(seconds)

    print(f"get_html {seconds} success.")

    return 'success'


executor = ThreadPoolExecutor(max_workers=2)

task1 = executor.submit(get_html, 2)
task2 = executor.submit(get_html, 4)
task3 = executor.submit(get_html, 6)


# 取消task3 , 只要task3 沒有開始運行,是可以直接取消的.
task3.cancel()


print(f"task2.result(): {task2.result()}")


# print(f"task1.done():{task1.done()}")
# time.sleep(3)
#
# print(f"task1.done():{task1.done()}")
#
# print(f"task2.result(): {task2.result()}")  # 阻塞的方法


如何使用線程池?

ThreadPoolExecutor 源碼分析 https://www.jianshu.com/p/b9b3d66aa0be

ThreadPoolExecutor
class ThreadPoolExecutor(_base.Executor):

    # Used to assign unique thread names when thread_name_prefix is not supplied.
    _counter = itertools.count().__next__

    def __init__(self, max_workers=None, thread_name_prefix=''):
        """Initializes a new ThreadPoolExecutor instance.

        Args:
            max_workers: The maximum number of threads that can be used to
                execute the given calls.
            thread_name_prefix: An optional name prefix to give our threads.
        """
        if max_workers is None:
            # Use this number because ThreadPoolExecutor is often
            # used to overlap I/O instead of CPU work.
            max_workers = (os.cpu_count() or 1) * 5
        if max_workers <= 0:
            raise ValueError("max_workers must be greater than 0")

        self._max_workers = max_workers
        self._work_queue = queue.Queue()
        self._threads = set()
        self._shutdown = False
        self._shutdown_lock = threading.Lock()
        self._thread_name_prefix = (thread_name_prefix or
                                    ("ThreadPoolExecutor-%d" % self._counter()))

有幾點說明 :

max_workers  最多的線程數 ,這個線程池裏面,最多同時又多少線程在啓動.
self._work_queue  這個任務隊列,提交的任務都會在這裏.
self._threads  啓動線程的集合 
self._shutdown  是否關閉 線程池的標誌位 
self._shutdown_lock  一把鎖 
self._thread_name_prefix  線程前綴名稱 

demo1

executor.submit 這種方式,返回的結果不一定是順序的.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Time    : 2019/3/10 15:27
@File    : test_futures_demo1.py
@Author  : [email protected]
"""
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
import random


def sleep(s):
    time.sleep(s * 0.1)
    return s


if __name__ == '__main__':

    wait_for = []
    executor = ThreadPoolExecutor(4)
    l = list(range(10))
    random.shuffle(l)
    for seconds in l:
        future = executor.submit(sleep, seconds)
        wait_for.append(future)
        print('Scheduled for {}:{}'.format(seconds, future))

    results = []

    for f in as_completed(wait_for):
        res = f.result()
        msg = '{} result:{!r}'
        print(msg.format(f, res))
        results.append(res)

    print(l)
    print(results)

結果如下

Scheduled for 5:<Future at 0x109d76160 state=running>
Scheduled for 3:<Future at 0x109d84e80 state=running>
Scheduled for 0:<Future at 0x109d9a748 state=finished returned int>
Scheduled for 9:<Future at 0x109d9a7f0 state=running>
Scheduled for 1:<Future at 0x109e55780 state=pending>
Scheduled for 4:<Future at 0x109e55e48 state=pending>
Scheduled for 2:<Future at 0x109e5c908 state=pending>
Scheduled for 6:<Future at 0x109e5c898 state=pending>
Scheduled for 8:<Future at 0x109e5ca90 state=pending>
Scheduled for 7:<Future at 0x109e5cfd0 state=pending>
<Future at 0x109d9a748 state=finished returned int> result:0
<Future at 0x109e55780 state=finished returned int> result:1
<Future at 0x109d84e80 state=finished returned int> result:3
<Future at 0x109d76160 state=finished returned int> result:5
<Future at 0x109e5c908 state=finished returned int> result:2
<Future at 0x109e55e48 state=finished returned int> result:4
<Future at 0x109d9a7f0 state=finished returned int> result:9
<Future at 0x109e5c898 state=finished returned int> result:6
<Future at 0x109e5cfd0 state=finished returned int> result:7
<Future at 0x109e5ca90 state=finished returned int> result:8
[5, 3, 0, 9, 1, 4, 2, 6, 8, 7]
[0, 1, 3, 5, 2, 4, 9, 6, 7, 8]

看結果返回 是無序的,和任務提交的不一樣的順序的.

demo2

用map 我發現是有序的.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Time    : 2019/3/10 15:27
@File    : test_futures_demo2.py
@Author  : [email protected]
"""
from concurrent.futures import ThreadPoolExecutor
import time
import random


def sleep(s):
    time.sleep(s * 0.1)
    return s


if __name__ == '__main__':

    e = ThreadPoolExecutor(4)
    s = list(range(10))

    # 打亂順序
    random.shuffle(s)
    print(s)
    start = time.time()
    for i in e.map(sleep, s):
        print(i, end='  ')
    print('\nelapsed:{} s'.format(time.time() - start))
    
    

"""
看結果返回是 有序的, s 列表的順序就是結果返回的順序. 
"""
   
[2, 5, 6, 7, 1, 3, 0, 9, 4, 8]
2  5  6  7  1  3  0  9  4  8  
elapsed:1.4122741222381592 s

map 返回的結果一定是有序的, 再看一個例子

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Time    : 2019/3/10 15:27
@File    : test_futures_demo3.py
@Author  : [email protected]
"""
from concurrent.futures import ThreadPoolExecutor
import time

all_list = range(100)


def fun(num):
    print(f'fun({num}) begin sleep 4s ')
    time.sleep(4)
    return num + 1


with ThreadPoolExecutor() as executor:
    for result in executor.map(fun, all_list):                                               
        print(f"result:{result}")

這兩者的區別是什麼呢?

首先 executor.map 這中提交方式 非常像 python 中的 map 函數, 這樣 就是 把一個函數 映射到一個序列中 ,

def map(self, fn, *iterables, timeout=None, chunksize=1) :pass 


好處:  1 返回 值,和任務提交順序一致
       2 map 函數的返回值 ,是這個線程的運行的結果. 

缺點:  1 提交任務的函數,是同一個函數,並且函數的參數 只能有一個.

executor.submit 這種方式的提交, 相對比較靈活, 
看下 submit 定義
第一個參數 是函數名稱, 第二個是 位置參數,第三個是關鍵字參數.
def submit(self, fn, *args, **kwargs):pass


這種好處是:  1 每次提交的函數 可以是不同的, 參數 可以根據實際需要進行傳遞
             2  submit 函數 會返回一個 future 對象. 
             3 可以通過 as_complete 這個方法 ,來取到任務完成的情況,也是比較方便.  需要調用f.result() 取值
             
缺點: 1  任務完成的返回,是無法確定順序的. 只是根據情況, 只要完成就返回.
總結

多線程編程,或者說併發編程是一個比較複雜的話題, 建議還是使用一些已經使用的工具包,這樣可以避免出現莫名奇怪的問題, 線程間數據交換可以考慮使用 Queue 這個隊列 是線程安全的. 方便我們操作數據的安全性.
說到python 多線程編程 肯定要說下 GIL ,其實 GIL 也沒有那麼糟糕,如何 你的應用是 I/O多一些, socket 編程, 網絡請求, 其實多線程 是完全沒有任何問題的.

參考文檔

Python的多線程編程模塊 threading 參考 https://my.oschina.net/lionets/blog/194577
python多線程、鎖、event事件機制的簡單使用 https://segmentfault.com/a/1190000014619654

Python併發編程之線程消息通信機制任務協調(四)https://juejin.im/post/5b1a9d7d518825137661ae82

官方文檔 https://docs.python.org/3/library/threading.html

分享快樂,留住感動. 2019-03-15 21:47:35 --frank
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章