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

从结果看出, 显然是多线程情况下, 即’同时’ 干两件事,速度快点. 如果 你有很多事情 是不是也可以 一起工作提高 效率呢? 所以 这就是多线程存在的意义.

python多线程学习笔记_第1张图片

python多线程学习笔记_第2张图片

什么是多线程编程
线程创建

如何创建一个线程呢? 有两种方法.

  • 方法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====")

看下这个图形
python多线程学习笔记_第3张图片
python多线程学习笔记_第4张图片

结果如下:

====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 其实还没有完成任务,被迫退出了任务. 这个可能不是我们想要的结果.

看下运行时的图形:
python多线程学习笔记_第5张图片
python多线程学习笔记_第6张图片

设置 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:   ,  {}
after:   ,  {'name': 'T1'}
before:   ,  {}
after:   ,  {'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:
Scheduled for 3:
Scheduled for 0:
Scheduled for 9:
Scheduled for 1:
Scheduled for 4:
Scheduled for 2:
Scheduled for 6:
Scheduled for 8:
Scheduled for 7:
 result:0
 result:1
 result:3
 result:5
 result:2
 result:4
 result:9
 result:6
 result:7
 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

你可能感兴趣的:(python基础&进阶)