python 34 进程、线程、协程

目录

一、进程和线程

    1、进程

    2、线程

    3、进程和线程的关系

    4、python的GIL

二、线程和threading模块

    1、线程的两种调用方式

        直接调用

        继承调用

    2、threading.thread的实例方法

        join() & setDaemon() 方法

         其他方法

     3、同步锁(Lock)

    4、线程死锁和递归锁

    5、同步条件(Event)

    6、信号量(Semaphore)

    7、Condition 类

    8、多线程利器:队列(queue)

        线程队列:queue

        生产者消费者模型

  三、多进程和multiprocessing

    1、进程的调用

        直接调用

        继承调用

        进程id 和 父进程id

    2、Process类

        构造方法

        实例方法

        属性

    3、进程间通讯

        进程队列Queue

        管道 Pipe

        Managers

    4、进程同步

    5、进程池

五、协程

    1、yield的简单实现

    2、greenlet 

greenlet 初体验 

greenlet module 与 class

Switch not call

Greenlet 生命周期

Greentlet Traceing

greenlet 使用建议

总结:

    3、gevent

参考:


 

一、进程和线程

    1、进程

假如有两个程序A和B,程序A在执行到一半的过程中,需要读取大量的数据输入(I/O操作),
    而此时CPU只能静静地等待任务A读取完数据才能继续执行,这样就白白浪费了CPU资源。
    是不是在程序A读取数据的过程中,让程序B去执行,当程序A读取完数据之后,让
    程序B暂停,然后让程序A继续执行?
    当然没问题,但这里有一个关键词:切换
    既然是切换,那么这就涉及到了状态的保存,状态的恢复,加上程序A与程序B所需要的系统资
    源(内存,硬盘,键盘等等)是不一样的。自然而然的就需要有一个东西去记录程序A和程序B
    分别需要什么资源,怎样去识别程序A和程序B等等,所以就有了一个叫进程的抽象

进程定义:

进程就是一个程序在一个数据集上的一次动态执行过程。
进程一般由程序、数据集、进程控制块三部分组成。
我们编写的程序用来描述进程要完成哪些功能以及如何完成;
数据集则是程序在执行过程中所需要使用的资源;
进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系
统感知进程存在的唯一标志。

举一例说明进程

    想象一位有一手好厨艺的计算机科学家正在为他的女儿烘制生日蛋糕。他有做生日蛋糕的食谱,厨房里有所需
    的原料:面粉、鸡蛋、糖、香草汁等。在这个比喻中,做蛋糕的食谱就是程序(即用适当形式描述的算法)计算机科学家就是处理器(cpu),
    而做蛋糕的各种原料就是输入数据。进程就是厨师阅读食谱、取来各种原料以及烘制蛋糕等一系列动作的总和。
    现在假设计算机科学家的儿子哭着跑了进来,说他的头被一只蜜蜂蛰了。计算机科学家就记录下他
    照着食谱做到哪儿了(保存进程的当前状态),然后拿出一本急救手册,按照其中的指示处理蛰伤。这
    里,我们看到处理机从一个进程(做蛋糕)切换到另一个高优先级的进程(实施医疗救治),每个进程
    拥有各自的程序(食谱和急救手册)。当蜜蜂蛰伤处理完之后,这位计算机科学家又回来做蛋糕,从他
    离开时的那一步继续做下去。

    2、线程

线程的出现是为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程只能干一样事的缺陷,
使到进程内并发成为可能。

假设,一个文本程序,需要接受键盘输入,将内容显示在屏幕上,还需要保存信息到硬盘中。若只有
一个进程,势必造成同一时间只能干一样事的尴尬(当保存时,就不能通过键盘输入内容)。若有多
个进程,每个进程负责一个任务,进程A负责接收键盘输入的任务,进程B负责将内容显示在屏幕上的
任务,进程C负责保存内容到硬盘中的任务。这里进程A,B,C间的协作涉及到了进程通信问题,而且
有共同都需要拥有的东西-------文本内容,不停的切换造成性能上的损失。若有一种机制,可以使
任务A,B,C共享资源,这样上下文切换所需要保存和恢复的内容就少了,同时又可以减少通信所带
来的性能损耗,那就好了。是的,这种机制就是线程。

线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序
计数器、寄存器集合和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发
性能。线程没有自己的系统资源。

    3、进程和线程的关系

1 一个程序至少有一个进程,一个进程至少有一个线程.(进程可以理解成线程的容器)

2 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

3 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和
  程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。 

4 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调
  度的一个独立单位. 
  线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程
  自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈)但是
  它可与同属一个进程的其他的线程共享进程所拥有的全部资源. 
  一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.

    4、python的GIL

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

上面的核心意思就是,无论你启多少个线程,你有多少个cpu, Python在执行的时候会淡定的在同一时刻只允许一个线程运行

(GIL内容建议看完进程和线程内容再理解)

并发 和 并行
并发:是指系统具有处理多个任务(动作)的能力
并行:是指系统具有 同时 处理多个任务(动作)的能力
并行是并发的一个子集

下面的代码,通过线程并发和单线程串行执行3次的时间如下
通过线程并发执行(单位:s)  16.29,   14.86,   15.38
通过主线程串行执行(单位:s)  14.37, 15.05, 14.88


问题: 多核没有利用上!
GIL 全局解释锁
    因为有GIL, 所以一个进程内,同一时刻,只有一个线程被CPU执行


    
任务:IO密集型、 计算密集型
对于io密集型的任务:python多线程有利于提高效率,可以采用 多进程+协程
对于计算密集型任务:python多线程就不推荐了, python就不适用了

代码:

import threading
import time

def add():
    sum1 = 0
    for i in  range(50000000):
        sum1 += i
    print('sum1 = {}'.format(sum1))

def mul():
    sum2 = 1
    for i in range(1, 100000):
        sum2 *= i
    print('sum2 = {}'.format(sum2))

t1 = threading.Thread(target=add)
t2 = threading.Thread(target=mul)
l = [t1, t2]

if __name__ == '__main__':
    print("---->start-----")
    start_time = time.time()

    # 通过线程并发执行(单位:s)  16.29,   14.86,   15.38
    # for t in l:
    #     t.start()
    # for t in l:
    #     t.join()

    # 通过主线程串行执行(单位:s)  14.37, 15.05, 14.88
    add()
    mul()

    print("-----> 执行时间: {}".format(time.time() - start_time))

 

二、线程和threading模块

    1、线程的两种调用方式

threading 模块建立在thread 模块之上。thread模块以低级、原始的方式来处理和控制线程,而threading 模块通过对thread进行二次封装,提供了更方便的api来处理线程。

        直接调用

import threading
import time

def sayHi(name):  # 定义需要在线程中运行的函数
    print("[{}], hello {}".format(time.ctime(), name))
    time.sleep(3)

if __name__ == '__main__':
    t1 = threading.Thread(target=sayHi, args=("x1",))  # 生成线程实例
    t2 = threading.Thread(target=sayHi, args=("x2",))
    t1.start()  # 启动线程
    t2.start()
    # time.sleep(0.01)
    print("main ending ...")

"""
结果(3秒后程序关闭):
[Fri May 21 19:38:50 2021], hello x1
[Fri May 21 19:38:50 2021], hello x2
main ending ...

"""

        继承调用

import threading
import time

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

    def run(self):  # 定义线程需要执行的代码
        print("running on number: {}".format(self.num))
        time.sleep(3)

if __name__ == '__main__':
    t1 = MyThread("n1")
    t2 = MyThread("n2")
    t1.start()
    t2.start()
    print("main ending ...")

"""
结果(3秒后程序关闭):
    running on number: n1
    running on number: n2
    main ending ...
"""

    2、threading.thread的实例方法

        join() & setDaemon() 方法

join()
    在子线程完成运行之前,这个子线程的父线程将一直被阻塞

    如果设置 timeout 属性,表明在调用 join() 方法的地方(当前)等待,超过等待时间,调用 join() 处继续执行之后代码
    
setDeamon():

    将线程声明为守护线程,必须在start() 方法调用之前设置, 如果不设置为守护线程程序会被无限挂起。

    当我们在程序运行中,执行一个主线程,如果主线程又创建一个子线程,主线程和子线程 就分兵两路,分别运行,
    那么当主线程完成想退出时,会检验子线程是否完成。如果子线程未完成,则主线程会等待子线程完成后再退出。

    但是有时候我们需要的是只要主线程完成了,不管子线程是否完成,都要和主线程一起退出,这时就可以 用setDaemon方法

import threading
import time

def music():
    print("begin to listen     {}".format(time.ctime()))
    time.sleep(3)
    print("stop to listen      {}".format(time.ctime()))

def game():
    print("begin to play game  {}".format(time.ctime()))
    time.sleep(5)
    print("stop to play game   {}".format(time.ctime()))

t1 = threading.Thread(target=music)
t2 = threading.Thread(target=game)

thread_list = [t1, t2]

if __name__ == '__main__':
    for t in thread_list:
    # -------------不同设置----------------
    #     t.setDaemon(True)
        if t is t2: t.setDaemon(True)
        t.start()
        # t.join()
    # t.join()  # 对列表最后一个执行 join() 方法
    # t1.join()
    # -------------------------------------
    time.sleep(0.01)  # 休眠0.01 避免输出到控制台打印到同一行
    print("main ending ...     {}".format(time.ctime()))

join 测试结果

"""
# -------------不同设置----------------
    t.start()
# -------------------------------------
结果:
    begin to listen     Fri May 21 20:42:40 2021
    begin to play game  Fri May 21 20:42:40 2021
    main ending ...     Fri May 21 20:42:40 2021
    stop to listen      Fri May 21 20:42:43 2021
    stop to play game   Fri May 21 20:42:45 2021
    
    
# -------------不同设置----------------
    t.start()
    t.join()
# -------------------------------------
结果:启动一个线程,阻塞直到这个线程执行完,接着继续执行同样的操作
    begin to listen     Fri May 21 20:44:34 2021
    stop to listen      Fri May 21 20:44:37 2021
    begin to play game  Fri May 21 20:44:37 2021
    stop to play game   Fri May 21 20:44:42 2021
    main ending ...     Fri May 21 20:44:42 2021
    
    
# -------------不同设置----------------
    t.start()
t1.join()
# -------------------------------------
结果:for循环结束,所有线程启动,t1阻塞主线程,等到t1结束,才打印[main ending ...     Fri May 21 20:47:30 2021]
    begin to listen     Fri May 21 20:47:27 2021
    begin to play game  Fri May 21 20:47:27 2021
    stop to listen      Fri May 21 20:47:30 2021
    main ending ...     Fri May 21 20:47:30 2021
    stop to play game   Fri May 21 20:47:32 2021
"""

setDaemin 测试结果

"""

# -------------不同设置----------------
    t.setDaemon(True)
    t.start()
# -------------------------------------
结果:t1,t2 都设置为守护线程,主线程结束,两个守护线程也跟着结束
    begin to listen     Fri May 21 21:22:29 2021
    begin to play game  Fri May 21 21:22:29 2021
    main ending ...     Fri May 21 21:22:29 2021
    
    
# -------------不同设置----------------
    if t is t1: t.setDaemon(True)
    t.start()
# -------------------------------------
结果:设置t1为守护线程,但是由于t2执行时间长于t1,所以t1能正常执行完毕
    begin to listen     Fri May 21 21:24:55 2021
    begin to play game  Fri May 21 21:24:55 2021
    main ending ...     Fri May 21 21:24:55 2021
    stop to listen      Fri May 21 21:24:58 2021
    stop to play game   Fri May 21 21:25:00 2021
    
    
# -------------不同设置----------------
    if t is t2: t.setDaemon(True)
    t.start()
# -------------------------------------
结果:设置t2为守护线程,t1不设置,因为t1的执行时间比t2短,执行完t1,主线程执行结束,t2也跟着结束。
    begin to listen     Fri May 21 21:26:34 2021
    begin to play game  Fri May 21 21:26:34 2021
    main ending ...     Fri May 21 21:26:34 2021
    stop to listen      Fri May 21 21:26:37 2021
"""

         其他方法

# run():  线程被cpu调度后自动执行线程对象的run方法
# start():启动线程活动。
# isAlive(): 返回线程是否活动的。
# getName(): 返回线程名。
# setName(): 设置线程名。

threading模块提供的一些方法:
# threading.currentThread(): 返回当前的线程变量。
# threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
# threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

import threading
import time

def music():
    print("begin to listen     {}".format(time.ctime()))
    time.sleep(3)
    print("stop to listen      {}".format(time.ctime()))

def game():
    print("begin to play game  {}".format(time.ctime()))
    time.sleep(5)
    print("stop to play game   {}".format(time.ctime()))

if __name__ == '__main__':
    t1 = threading.Thread(target=music)
    t2 = threading.Thread(target=game)
    t1.start()
    t2.start()

    time.sleep(4)
    print("t1.isAlive = {}, t2.isAlive = {}".format(t1.isAlive(), t2.isAlive()))
    # 结果: t1.isAlive = False, t2.isAlive = True

    print("线程 t1 设置之前名字: {}".format(t1.getName()))
    t1.setName("my-Thread-t1")
    print("线程 t1 设置之后名字: {}".format(t1.getName()))
    # 结果
    #   线程 t1 设置之前名字: Thread-1
    #   线程 t1 设置之后名字: my-Thread-t1

    print(threading.currentThread())
    # 结果: <_MainThread(MainThread, started 15076)>

    print(threading.enumerate())
    # 结果: [<_MainThread(MainThread, started 15776)>, <Thread(my-Thread-t1, started 6664)>, <Thread(Thread-2, started 13352)>]

    print(threading.activeCount())
    # 结果:3

     3、同步锁(Lock)

Lock 中文称为锁,是一种多线程同步的手段。

Lock 有 locked 和 unlocked 两种状态,而这两中状态之间是可以转换的.

    当 Lock 是 unlocked 状态时候,某个线程调用 acquire() 可以获取这个 Lock,并且将 Lock将状态转换成 locked 状态,并且线程不会阻塞。
    但当 Lock 是 locked 状态时,某个线程调用 acquire() 会阻塞自己,直到其他的线程将 Lock 的状态变成 unlocked。
    当 Lock 是 locked 状态时,调用 release() 方法,可以释放一个 Lock,这样其它线程就可以获取这个 Lock 了。
    但当 Lock 是 unlocked 状态时,某个线程调用 release(),程序会抛出 RuntimeError 异常。

所以,acquire() 和 release() 方法在单个线程当中都是成对使用的。

 在这里插入图片描述

有效利用 Lock 的状态转换机制,就可以避免多个线程同时修改同一份数据。

import threading
import time

num = 10  # 定义一个全局变量
l = []
lock = threading.Lock()  # 创建一个锁

def sub():
    global num  # 每个进程都获取这个全局变量
    # ----------》 执行结果都为 0
    # num -= 1

    # ----------》 执行结果: 9
    # tmp = num
    # time.sleep(0.001)
    # num = tmp -1

    # ----------》 执行结果: 0
    print(threading.currentThread().getName())
    lock.acquire()
    print("lock ---> {}".format(threading.currentThread().getName()))
    tmp = num
    time.sleep(0.001)
    num = tmp -1  # 对公共变量 -1
    lock.release()

for i in range(10):  # 创建100个线程执行 sub 方法
    t = threading.Thread(target=sub, name="thread-num-{}".format(i))
    t.start()
    l.append(t)

for t in l:
    t.join()  # 等待所有线程执行完毕

print("num = {}".format(num))


观察:time.sleep(0.1)  /0.001/0.0000001 结果分别是多少?
多个线程都在同时操作同一个共享资源,所以造成共享数据不同步,怎么办呢?(join会造成串行,失去所线程的意义),我们可以通过同步锁来解决这种问题

    lock.acquire()
    tmp = num
    time.sleep(0.001)
    num = tmp -1
    lock.release()

    4、线程死锁和递归锁

在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁,因为系统判断这部分资源都正在使用,所有这两个线程在无外力作用下将一直等待下去。

下面是一个死锁的例子:

import threading
import time


class MyThread(threading.Thread):

    def __init__(self, lockA, lockB):
        threading.Thread.__init__(self)
        self.lockA = lockA
        self.lockB = lockB


    def run(self):
        self.actionA()
        time.sleep(0.01)
        self.actionB()

    def actionA(self):
        self.lockA.acquire()  # 获取 A 锁
        print(self.name, "getLock:A", time.ctime())
        time.sleep(2)

        self.lockB.acquire()  # 获取 B 锁
        print(self.name, "getLock:B", time.ctime())
        time.sleep(1)

        self.lockB.release()  # 释放 A 锁
        self.lockA.release()  # 释放 B 锁

    def actionB(self):
        self.lockB.acquire()
        print(self.name, "getLock:B", time.ctime())
        time.sleep(2)

        self.lockA.acquire()
        print(self.name, "getLock:A", time.ctime())
        time.sleep(1)

        self.lockA.release()
        self.lockB.release()

if __name__ == '__main__':
    A = threading.Lock()
    B = threading.Lock()
    tl = []
    for i in range(3):
        t = MyThread(A, B)
        t.start()
        tl.append(t)
    for t in tl:
        t.join()
    print("main thread ending ...")

"""
结果:
    Thread-1 getLock:A Sat May 22 09:36:46 2021
    Thread-1 getLock:B Sat May 22 09:36:48 2021
    Thread-2 getLock:A Sat May 22 09:36:49 2021
    Thread-1 getLock:B Sat May 22 09:36:49 2021
"""

Thread-1 获取到 A锁, B锁,使用完都释放,这时候 Thread-2 获取到 A 锁,而 Thread-1 的 actionB 获取到 B锁, 这时候双方互相等待对方手里的锁,导致线程锁死,其他线程也无法获得 A 锁和 B 锁。

递归锁

解决办法:使用递归锁,将 

    A = threading.Lock()
    B = threading.Lock()

改用为 

    r_lcok=threading.RLock()

为了支持在同一线程中多次请求同一资源,python提供了“可重入锁”:threading.RLock。RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源。

import threading
import time


class MyThread(threading.Thread):

    def __init__(self, r_lock):
        threading.Thread.__init__(self)
        self.r_lock = r_lock

    def run(self):
        self.actionA()
        time.sleep(0.01)
        self.actionB()

    def actionA(self):
        self.r_lock.acquire()  # counter = 1
        print(self.name, "getLock:A", time.ctime())
        time.sleep(2)

        self.r_lock.acquire()  # counter = 2
        print(self.name, "getLock:B", time.ctime())
        time.sleep(1)

        self.r_lock.release()  # counter = 1
        self.r_lock.release()  # counter = 0

    def actionB(self):
        self.r_lock.acquire()
        print(self.name, "getLock:B", time.ctime())
        time.sleep(2)

        self.r_lock.acquire()
        print(self.name, "getLock:A", time.ctime())
        time.sleep(1)

        self.r_lock.release()
        self.r_lock.release()

if __name__ == '__main__':
    r_lock = threading.RLock()
    tl = []
    for i in range(3):
        t = MyThread(r_lock)
        t.start()
        tl.append(t)
    for t in tl:
        t.join()
    print("main thread ending ...")

"""
结果(顺序不一定一样):
    Thread-1 getLock:A Sat May 22 09:38:40 2021
    Thread-1 getLock:B Sat May 22 09:38:42 2021
    Thread-2 getLock:A Sat May 22 09:38:43 2021
    Thread-2 getLock:B Sat May 22 09:38:45 2021
    Thread-2 getLock:B Sat May 22 09:38:46 2021
    Thread-2 getLock:A Sat May 22 09:38:48 2021
    Thread-1 getLock:B Sat May 22 09:38:49 2021
    Thread-1 getLock:A Sat May 22 09:38:51 2021
    Thread-3 getLock:A Sat May 22 09:38:52 2021
    Thread-3 getLock:B Sat May 22 09:38:54 2021
    Thread-3 getLock:B Sat May 22 09:38:55 2021
    Thread-3 getLock:A Sat May 22 09:38:57 2021
    main thread ending ...
"""

    5、同步条件(Event)

Event 管理这一个标识,这个标识可以通过 set 设置为 true , 通过 clear 设置为 false , 当 标识为false 时,wait  会一直阻塞。

is_set():当 flag 为 true 时返回 True
set():设置 flag为 true 
clear():设置 flag为 false 
wait(self, timeout=None):阻塞直到 flag 为 true 
        timeout 调用 wait 方法的代码块的等待时间

import threading
import time

class Boss(threading.Thread):

    def __init__(self, event):
        threading.Thread.__init__(self)
        self.event = event

    def run(self):
        print("Boss: 今晚加班到 22:00")
        print("event flag = {}".format(self.event.is_set()))
        self.event.set()
        time.sleep(5)

        print("Boss: 22:00 到了,可以下班")
        print("event flag = {}".format(self.event.is_set()))
        self.event.set()

class Worker(threading.Thread):

    def __init__(self, event):
        threading.Thread.__init__(self)
        self.event = event

    def run(self):
        self.event.wait()  # flag 默认 False ,等待直到有人调用 set()
        print("{} Worker: 又加班".format(threading.currentThread().name))
        time.sleep(1)  # 休眠1s 再 clear 其他线程才有机会执行完 wait()
        self.event.clear()
        # self.event.wait(1)  # 只等待1s ,老板没发话,下班
        self.event.wait()
        print("{} Worker: 下班啦".format(threading.currentThread().name))

if __name__ == '__main__':
    tl = []
    event = threading.Event()

    for i in range(3):
        t = Worker(event)
        t.start()
        tl.append(t)

    boss = Boss(event)
    boss.start()
    tl.append(boss)

    for t in tl:
        t.join()
    print("执行完成")

"""
结果:
    Boss: 今晚加班到 22:00
    event flag = False
    Thread-1 Worker: 又加班
    Thread-2 Worker: 又加班
    Thread-3 Worker: 又加班
    Boss: 22:00 到了,可以下班
    event flag = False
    Thread-3 Worker: 下班啦
    Thread-2 Worker: 下班啦
    Thread-1 Worker: 下班啦
    执行完成
"""

    6、信号量(Semaphore)

Semaphore [ˈseməfɔː(r)]
    semaphore = threading.Semaphore() 默认的值为1
    acquire(blocking=True, timeout=None):请求一个信号量,内部计数器-1
            不设置参数: 如果计数器大于零,计数器-1并且立即返回;如果不为零,阻塞直到有其他线程调用 release
            blocking:  true 和没有设置参数一样
                        false 不阻塞线程,如果计数器0,返回 False
            timeout: 如果在设定超时 时间内没有请求成功,返回False   
    release(): 释放一个信号量,内部计数器+1
    
注意:调用 semaphore.release() 会给内部计数器+1,如果调用 release() 次数多于 acquire() 次数,
    那么内部计数器允许的最大值会大于设定的值
    
BoundedSemaphore(Semaphore)
    release(self):当 调用 release() 次数多于 acquire() 次数时,会抛出
                    ValueError: Semaphore released too many times

import threading
import time

class MyThread(threading.Thread):

    def __init__(self, semaphore):
        threading.Thread.__init__(self)
        self.semaphore = semaphore

    def run(self):
        acquire_res = self.semaphore.acquire()
        # acquire_res = self.semaphore.acquire(blocking=False)
        # acquire_res = self.semaphore.acquire(timeout=0.1)
        print("{} 执行: {}, acquire_res = {}".format(threading.currentThread().name,
                            time.ctime(), acquire_res))
        time.sleep(1)
        self.semaphore.release()

if __name__ == '__main__':
    semaphore = threading.Semaphore(2)

    # # 下面 没有调用 acquire() 之前调用 release() 抛出异常(否则导致计数器大于默认值)
    # semaphore = threading.BoundedSemaphore(2)
    # semaphore.release()

    for i in range(6):
        t = MyThread(semaphore)
        t.start()
    semaphore.release()
"""
acquire_res = self.semaphore.acquire()
结果:每间隔1秒,有两个线程获取到信号量,执行打印
    Thread-1 执行: Sat May 22 11:32:37 2021, acquire_res = True
    Thread-2 执行: Sat May 22 11:32:37 2021, acquire_res = True
    Thread-3 执行: Sat May 22 11:32:38 2021, acquire_res = True
    Thread-4 执行: Sat May 22 11:32:38 2021, acquire_res = True
    Thread-5 执行: Sat May 22 11:32:39 2021, acquire_res = True
    Thread-6 执行: Sat May 22 11:32:39 2021, acquire_res = True
    
acquire_res = self.semaphore.acquire(blocking=False)
结果:第1,2 个线程获取到信号量,其他没有获取到,不阻塞跳过, 返回 False
    Thread-1 执行: Sat May 22 11:33:12 2021, acquire_res = True
    Thread-2 执行: Sat May 22 11:33:12 2021, acquire_res = True
    Thread-3 执行: Sat May 22 11:33:12 2021, acquire_res = False
    Thread-4 执行: Sat May 22 11:33:12 2021, acquire_res = False
    Thread-5 执行: Sat May 22 11:33:12 2021, acquire_res = False
    Thread-6 执行: Sat May 22 11:33:12 2021, acquire_res = False
    
acquire_res = self.semaphore.acquire(timeout=0.1)
结果:第1,2 个线程获取到信号量,其他在设定等待时间内没有获取到, 返回 False
Thread-1 执行: Sat May 22 11:34:35 2021, acquire_res = True
Thread-2 执行: Sat May 22 11:34:35 2021, acquire_res = True
Thread-3 执行: Sat May 22 11:34:35 2021, acquire_res = False
Thread-4 执行: Sat May 22 11:34:35 2021, acquire_res = False
Thread-5 执行: Sat May 22 11:34:35 2021, acquire_res = False
Thread-6 执行: Sat May 22 11:34:35 2021, acquire_res = False
"""

    7、Condition 类

详见:第四点 Condition版的生产者与消费者模式

    8、多线程利器:队列(queue)

列表不是线程安全的数据结构

import time
import threading

def func_remove(l):
    while l:
        el = l[-1]
        print("el = ", el)
        time.sleep(1)
        l.remove(el)

l = [1, 2, 3, 4, 5]
t1 = threading.Thread(target= func_remove, args=(l,))
t2 = threading.Thread(target= func_remove, args=(l,))

if __name__ == '__main__':
    t1.start()
    t2.start()

"""
结果:其中一个线程在删除数据时,找不到 5 报错
    el =  5
    el =  5
    Exception in thread Thread-1:
    Traceback (most recent call last):
      File "E:\0000_Python_3.7.3\lib\threading.py", line 917, in _bootstrap_inner
        self.run()
      File "E:\0000_Python_3.7.3\lib\threading.py", line 865, in run
        self._target(*self._args, **self._kwargs)
      File "E:/0000_Python/repositories/python_base/python_base/03_python_base/day34/lesson3.py", line 10, in func_remove
        l.remove(el)
    ValueError: list.remove(x): x not in list
    
    el =  4
    el =  3
    el =  2
    el =  1
"""

        线程队列:queue

        queue队列类的方法

创建一个“队列”对象
import Queue
q = Queue.Queue(maxsize = 10)
Queue.Queue类即是一个队列的同步实现。队列长度可为无限或者有限。可通过Queue的构造函数的可选参数maxsize来设定队列长度。如果maxsize小于1就表示队列长度无限。

将一个值放入队列中
q.put(10)
调用队列对象的put()方法在队尾插入一个项目。put()有两个参数,第一个item为必需的,为插入项目的值;第二个block为可选参数,默认为
1。如果队列当前为空且block为1,put()方法就使调用线程暂停,直到空出一个数据单元。如果block为0,put方法将引发Full异常。

将一个值从队列中取出
q.get()
调用队列对象的get()方法从队头删除并返回一个项目。可选参数为block,默认为True。如果队列为空且block为True,get()就使调用线程暂停,直至有项目可用。如果队列为空且block为False,队列将引发Empty异常。

Python Queue模块有三种队列及构造函数:
1、Python Queue模块的FIFO队列先进先出。   class queue.Queue(maxsize)
2、LIFO类似于堆,即先进后出。               class queue.LifoQueue(maxsize)
3、还有一种是优先级队列级别越低越先出来。        class queue.PriorityQueue(maxsize)

此包中的常用方法(q = Queue.Queue()):
q.qsize() 返回队列的大小
q.empty() 如果队列为空,返回True,反之False
q.full() 如果队列满了,返回True,反之False, q.full() 与 maxsize 大小对应
q.get([block[, timeout]]) 获取队列,timeout等待时间
q.get_nowait() 相当q.get(False)
非阻塞 q.put(item) 写入队列,timeout等待时间
q.put_nowait(item) 相当q.put(item, False)
q.task_done() 在完成一项工作之后,q.task_done() 函数向任务已经完成的队列发送一个信号
q.join() 实际上意味着等到队列为空,再执行别的操作

        三种队列: queue、LifoQueue、PriorityQueue

import queue  # 线程队列
q = queue.Queue()  # 默认 maxsize = 0 , f maxsize is <= 0, the queue size is infinite.
q.put(11)
q.put("12")
q.put({"a": "11"})
print("一般队列 queue:")
while not q.empty():
    data = q.get()
    print("data = {}, type is {}".format(data, type(data)))

# 其他队列
import queue  # 线程队列
q1 = queue.LifoQueue()  # 默认 maxsize = 0 , f maxsize is <= 0, the queue size is infinite.
q1.put(11)
q1.put("12")
q1.put({"a": "11"})
print("先进后出队列 LifoQueue:")
while not q1.empty():
    data = q1.get()
    print("data = {}, type is {}".format(data, type(data)))

import queue  # 线程队列
q2 = queue.PriorityQueue() # 设置的值越小,越优先
q2.put([2, 11])
q2.put([4, "12"])
q2.put([3, {"a": "11"}])
print("优先级队列 PriorityQueue:")
while not q2.empty():
    data = q2.get()
    print("data = {}, type is {}".format(data, type(data)))

"""
结果:
    一般队列 queue:
        data = 11, type is <class 'int'>
        data = 12, type is <class 'str'>
        data = {'a': '11'}, type is <class 'dict'>
    先进后出队列 LifoQueue:
        data = {'a': '11'}, type is <class 'dict'>
        data = 12, type is <class 'str'>
        data = 11, type is <class 'int'>
    优先级队列 PriorityQueue:
        data = [2, 11], type is <class 'list'>
        data = [3, {'a': '11'}], type is <class 'list'>
        data = [4, '12'], type is <class 'list'>
"""

        生产者消费者模型

为什么要使用生产者和消费者模式

在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。

什么是生产者消费者模式

生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

这就像,在餐厅,厨师做好菜,不需要直接和客户交流,而是交给前台,而客户去饭菜也不需要不找厨师,直接去前台领取即可,这也是一个结耦的过程。

import threading, time, queue, random

q = queue.Queue()

def Producer(name, q1):
    count = 0
    while count < 10:
        print("making ......")
        time.sleep(random.randrange(3))
        q1.put(count)
        print('Producer %s has produced %s baozi..' % (name, count))
        count += 1

def Consumer(name, q1):
    count = 0
    while count < 10:
        time.sleep(random.randrange(4))
        if not q1.empty():
            data = q1.get()
            print('\033[32;1mConsumer %s has eat %s baozi...\033[0m' % (name, data))
        else:
            print("------ 现在还没有包子 ------> {} 走了".format(name))
        count += 1

if __name__ == '__main__':
    p1 = threading.Thread(target=Producer, args=('A君', q,))
    c1 = threading.Thread(target=Consumer, args=('B君', q,))
    # c2 = threading.Thread(target=Consumer, args=('C君',))
    # c3 = threading.Thread(target=Consumer, args=('D君',))
    p1.start()
    c1.start()
    # c2.start()
    # c3.start()

 结合队列的 queue.task_done() 和 queue.jion():实现包子先预定后制作

import threading, time, queue, random

q = queue.Queue()

def Producer(name, q1):
    while 1:
        q1.join()
        data = q1.get()
        time.sleep(0.01)
        print("%s 开始制作 %s 个包子" % (name, data))
        time.sleep(1)

        q1.put(data)
        q1.task_done()
        print("%s 制作完 %s 个包子" % (name, data))

def Consumer(name, q1):
    count = 0
    while count < 3:
        time.sleep(random.randrange(4))
        q1.put(name + str(count))
        q1.task_done()
        print('\033[32;1m 消费者[ %s ]预定了 %s baozi...\033[0m' % (name, count))

        q1.join()
        date = q1.get()
        time.sleep(0.01)
        print('\033[32;1m 消费者[ %s ]拿到了 %s baozi...\033[0m' % (name, date))
        count += 1

if __name__ == '__main__':
    p1 = threading.Thread(target=Producer, args=('A君', q,))
    c1 = threading.Thread(target=Consumer, args=('B君', q,))
    c2 = threading.Thread(target=Consumer, args=('C君', q,))
    p1.start()
    c1.start()
    c2.start()

  
三、多进程和multiprocessing

    1、进程的调用

        直接调用

ime

def fun1(name):
    time.sleep(1)
    print("{} {}".format(time.ctime(), name))

if __name__ == '__main__':
    pl = []
    for i in range(3):
        process = multiprocessing.Process(target=fun1, args=("xu{}".format(i),))
        process.start()
        pl.append(process)
    for p in pl:
        process.join()
    print("--- ending ---")

"""
Sat May 22 16:37:15 2021 xu0
Sat May 22 16:37:15 2021 xu1
Sat May 22 16:37:15 2021 xu2
--- ending ---
"""

        继承调用

cessing, time

class MyProcess(multiprocessing.Process):
    def run(self):
        time.sleep(1)
        print("{} {}".format(time.ctime(), self.name))

if __name__ == '__main__':
    pl = []
    for i in range(3):
        process = MyProcess()
        process.start()
        pl.append(process)
    for p in pl:
        p.join()
    print("--- ending ---")

"""
Sat May 22 16:39:46 2021 MyProcess-1
Sat May 22 16:39:46 2021 MyProcess-2
Sat May 22 16:39:46 2021 MyProcess-3
--- ending ---
"""

        进程id 和 父进程id

import multiprocessing, os, time

def show_info(title):
    print("title:{}".format(title))
    print("parant process id: {}".format(os.getppid()))
    print("process id; {}".format(os.getpid()))

if __name__ == '__main__':
    show_info("-->main process")
    time.sleep(1)
    print("------------------------")
    p = multiprocessing.Process(target=show_info, args=("sub1", ))
    p.start()
    p.join()

"""
结果:当前在pycharm 里面执行,main进程的父进程是pycharm 执行的进程
    title:-->main process
    parant process id: 9276 
    process id; 3300
    ------------------------
    title:sub1
    parant process id: 3300
    process id; 11668
"""

    2、Process类

        构造方法

        Process([group [, target [, name [, args [, kwargs]]]]])
          group: 线程组,目前还没有实现,库引用中提示必须是None; 
          target: 要执行的方法; 
          name: 进程名; 
          args/kwargs: 要传入方法的参数。

        实例方法

  is_alive():返回进程是否在运行。
  join([timeout]):阻塞当前上下文环境的进程程,直到调用此方法的进程终止或到达指定的timeout(可选参数)。
  start():进程准备就绪,等待CPU调度
  run():strat()调用run方法,如果实例进程时未制定传入target,这star执行t默认run()方法。
  terminate():不管任务是否完成,立即停止工作进程

        属性

  daemon:和线程的setDeamon功能一样
  name:进程名字。
  pid:进程号。

    3、进程间通讯

        进程队列Queue

# 进程间通信:Queue
import multiprocessing, time, queue

def fun1(q):
    time.sleep(1)
    q.put("name:xu")
    q.put("pwd:123")

if __name__ == '__main__':
    # process_q = queue.Queue()  # 这个是线程队列
    process_q = multiprocessing.Queue()  # 进程队列
    sub_p = multiprocessing.Process(target=fun1, args=(process_q,))
    sub_p.start()
    print("main--->:{}".format(process_q.get()))
    print("main--->:{}".format(process_q.get()))
    
"""
结果:
    main--->:name:xu
    main--->:pwd:123
"""

        管道 Pipe

The Pipe() function returns a pair of connection objects connected by a pipe which by default is duplex (two-way). For example: 

# 进程间通信:Pipe
import multiprocessing, time

def fun2(conn):
    conn.send(["message from sub process"])
    response = conn.recv()
    print("sub--->{}".format(response))
    conn.close()

if __name__ == '__main__':
    parent_conn, child_conn = multiprocessing.Pipe()
    sub_p = multiprocessing.Process(target=fun2, args=(child_conn,))
    sub_p.start()

    data = parent_conn.recv()
    print("main--->{}, type = {}".format(data, type(data)))
    parent_conn.send("我的好儿子")

"""
main--->['message from sub process'], type = <class 'list'>
sub--->我的好儿子
"""

The two connection objects returned by Pipe() represent the two ends of the pipe. Each connection object has send() and recv() methods (among others). Note that data in a pipe may become corrupted if two processes (or threads) try to read from or write to the same end of the pipe at the same time. Of course there is no risk of corruption from processes using different ends of the pipe at the same time. 

        Managers

Queue和pipe只是实现了数据交互,并没实现数据共享,即一个进程去更改另一个进程的数据。

A manager object returned by Manager() controls a server process which holds Python objects and allows other processes to manipulate them using proxies.

A manager returned by Manager() will support types listdictNamespaceLockRLockSemaphoreBoundedSemaphoreConditionEventBarrierQueueValue and Array. For example:

# 进程间通信:Manager
import multiprocessing, time

def fun3(d, l, i):
    d[i] = str(i)
    l.append(i)

if __name__ == '__main__':
    p_list = []
    with multiprocessing.Manager() as manager:
        d = manager.dict()
        l = manager.list(range(3))
        l.append("\\")
        print(l)
        for i in range(5):
            p = multiprocessing.Process(target=fun3,args=(d, l, i))
            p.start()
            p_list.append(p)
        for p in p_list:
            p.join()
        print(d)
        print(l)
"""
结果:
    [0, 1, 2, '\\']
    {4: '4', 1: '1', 0: '0', 3: '3', 2: '2'}
    [0, 1, 2, '\\', 4, 1, 0, 3, 2]
"""

    4、进程同步

 如果不加锁,多个进程同时执行,在python2 下执行,有可能打印到控制台的内容会输出到同一行

import multiprocessing, time

def fun1(l, num):
    # time.sleep(1)
    l.acquire()  # 获取锁
    print("进程同步,执行第 {} 号进程".format(num))
    l.release()  # 释放锁

if __name__ == '__main__':
    lock = multiprocessing.Lock()

    for i in range(5):
        p = multiprocessing.Process(target=fun1, args=(lock, i,))
        p.start()

"""
进程同步,执行第 3 号进程
进程同步,执行第 1 号进程
进程同步,执行第 4 号进程
进程同步,执行第 2 号进程
进程同步,执行第 0 号进程
"""

    5、进程池

进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。
进程池中有两个方法:
    apply: 同步执行
    apply_async: 异步执行

import multiprocessing, time, os

def Foo(num):
    time.sleep(1)
    print("Foo ---> sub_{}, pid = {}".format(num, os.getpid()))
    return "sub_{}".format(num)  # 将进程执行结果返回给 回调函数

def Bar(arg):  # 进程回调函数: arg 接收进程执行方法返回的结果
    print("Bar +++> arg = {}, pid = {}".format(arg, os.getpid()))

if __name__ == '__main__':
    print("main ###> pid = {}".format(os.getpid()))
    pool = multiprocessing.Pool(3)  # 创建一个进程池,同时能并发执行 3 个进程

    for i in range(10):
        # pool.apply(func=Foo, args=(i, ))
        # pool.apply_async(func=Foo, args=(i,))
        # 回调函数:  就是某个动作或者函数执行成功后再去执行的函数
        pool.apply_async(func=Foo, args=(i,), callback=Bar)

    pool.close()
    pool.join()         # close() 与 join() 调用顺序是固定的

四、上下文管理器(contextlib模块)
    1、如何使用上下文管理器
    2、自定义上下文管理器
    3、contextlib模块
    4、contextlib.nested:减少嵌套
    5、contextlib.closing()
    

五、协程

协程,又称微线程,纤程。英文名Coroutine。
优点1: 协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
优点2: 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

    1、yield的简单实现

import time, queue

def consumer(name):
    print("---> 准备开始吃包子。。。")
    while True:
        new_baozi = yield
        print("{} 吃了 {} 个包子".format(name, new_baozi))
        time.sleep(1)

def producer(consumer1, consumer2):
    r = consumer1.__next__()  # 先执行一次 next 走到 yields 处等待
    r = consumer2.__next__()

    n = 0
    while n < 10:
        time.sleep(1)
        print("\033[32;1m[producer]\033[0m is making baozi {} and {} [time: {}]".format(n,
                                                                                        n +1, time.ctime()))
        consumer1.send(n)
        consumer2.send(n+1)
        n += 2

if __name__ == '__main__':
    c1 = consumer("xu1")
    c2 = consumer("xu2")
    p = producer(c1, c2)


    2、greenlet 

               本文参考自:greenlet 详解

上面python原生的generator是semicoroutine,而greenlet是 真 协程。
下面主要包括以下内容:什么是greenlet,greenlet的切换与函数调用的区别,greenlet的生命周期,以及使用greenlet的注意事项。

greenlet的安装很简单:pip install greenlet 即可,安装好了之后我们来看一个官方的例子

Greenlet是python的一个C扩展,来源于Stackless python,旨在提供可自行调度的‘微线程’, 即协程。generator实现的协程在yield value时只能将value返回给调用者(caller)。 而在greenlet中,target.switch(value)可以切换到指定的协程(target), 然后yield value。greenlet用switch来表示协程的切换,从一个协程切换到另一个协程需要显式指定。

greenlet 初体验 

from greenlet import greenlet
def fun1():
    print(12)
    gr2.switch()
    print(34)

def fun2():
    print(56)
    gr1.switch()
    print(78)

gr1 = greenlet(fun1)
gr2 = greenlet(fun2)
gr1.switch()

"""
结果:
    12
    56
    34
"""

 当创建一个greenlet时,首先初始化一个空的栈, switch到这个栈的时候,会运行在greenlet构造时传入的函数(首先在test1中打印 12), 如果在这个函数(test1)中switch到其他协程(到了test2 打印34),那么该协程会被挂起,等到切换回来(在test2中切换回来 打印34)。当这个协程对应函数执行完毕,那么这个协程就变成dead状态。

 注意 上面没有打印test2的最后一行输出 78,因为在test2中切换到gr1之后挂起,但是没有地方再切换回来。这个可能造成泄漏,后面细说

greenlet module 与 class

 其中,比较重要的是getcurrent(), 类greenlet、异常类GreenletExit。
  getcurrent() 返回当前的greenlet实例;
  GreenletExit:是一个特殊的异常,当触发了这个异常的时候,即使不处理,也不会抛到其parent(后面会提到协程中对返回值或者异常的处理)

>>> import greenlet
>>> dir(greenlet)

['GREENLET_USE_CONTEXT_VARS', 'GREENLET_USE_GC', 'GREENLET_USE_TRACING', 'GreenletExit', '_C_API', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__lo
ader__', '__name__', '__package__', '__path__', '__spec__', '__version__', '_greenlet', 'absolute_import', 'division', 'error', 'getcurrent', 'gettrace', 'greenlet', 'p
rint_function', 'settrace']

 再来看看greenlet.greenlet这个类:

>>> dir(greenlet.greenlet)
['GreenletExit', '__bool__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__
', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str_
_', '__subclasshook__', '_stack_saved', 'dead', 'error', 'getcurrent', 'gettrace', 'gr_context', 'gr_frame', 'parent', 'run', 'settrace', 'switch', 'throw']
>>>

比较重要的几个属性:
  run:当greenlet启动的时候会调用到这个callable,如果我们需要继承greenlet.greenlet时,需要重写该方法
  switch:前面已经介绍过了,在greenlet之间切换
  parent:可读写属性,后面介绍
  dead:如果greenlet执行结束,那么该属性为true
  throw:切换到指定greenlet后立即跑出异常 

Switch not call

 对于greenlet,最常用的写法是 x = gr.switch(y)。 这句话的意思是切换到gr,传入参数y。当从其他协程(不一定是这个gr)切换回来的时候,将值付给x。

from greenlet import greenlet
def fun1(x, y):
    z = gr2.switch(x + y)
    print("fun1: {}".format(z))

def fun2(u):
    print("fun2: {}".format(u))
    gr1.switch(10)

gr1 = greenlet(fun1)
gr2 = greenlet(fun2)
print(gr1.switch("hello", "  world"))
"""
结果:
    fun2: hello  world
    fun1: 10
    None
"""

 上面的例子,从main greenlet切换到了gr1,在fun1内切换到了gr2,然后gr1挂起,接着从gr2切回gr1时,将值(10)返回值给了 z。 然后返回 main greenlet。

 

每一个Greenlet都有一个parent,一个新的greenlet在哪里创生,当前环境的greenlet就是这个新greenlet的parent。所有的greenlet构成一棵树,其跟节点就是还没有手动创建greenlet时候的”main” greenlet(事实上,在首次import greenlet的时候实例化)。当一个协程 正常结束,执行流程回到其对应的parent;或者在一个协程中抛出未被捕获的异常,该异常也是传递到其parent。学习python的时候,有一句话会被无数次重复”everything is oblect”, 在学习greenlet的调用中,同样有一句话应该深刻理解, “switch not call”

import greenlet
def fun1(x, y):
    print("{}, {}".format(id(greenlet.getcurrent()),id(greenlet.getcurrent().parent)))
    z = gr2.switch(x + y)
    print("back z = {}".format(z))

def fun2(u):
    print("{}, {}".format(id(greenlet.getcurrent()), id(greenlet.getcurrent().parent)))
    return  "hehe"

if __name__ == '__main__':
    gr1 = greenlet.greenlet(fun1)
    gr2 = greenlet.greenlet(fun2)
    print("main_id = {}, gr1_id = {}, gr2_id = {}".format(id(greenlet.getcurrent()),
            id(gr1), id(gr2)))
    print(gr1.switch("hello", "world"), "back to main")
"""
结果:
    main_id = 3197756783256, gr1_id = 3197756783608, gr2_id = 3197762760776
    3197756783608, 3197756783256
    3197762760776, 3197756783256
    hehe back to main
"""

 上述例子可以看到,尽量是从fun1所在的协程gr1 切换到了gr2,但gr2的parent还是’main’ greenlet,因为默认的parent取决于greenlet的创生环境。另外 在fun2中return之后整个返回值返回到了其parent,而不是switch到该协程的地方(即不是fun1),这个跟我们平时的函数调用不一样,记住“switch not call”。对于异常 也是展开至parent。

import greenlet
def fun1(x, y):
    try:
        z = gr2.switch(x + y)
    except Exception:
        print("catch Exception in fun1")

def fun2(u):
    assert False

gr1 = greenlet.greenlet(fun1)
gr2 = greenlet.greenlet(fun2)
try:
    gr1.switch("hello", "world")
except:
    print("catch Exception in main")
"""
结果:
    catch Exception in main
"""

Greenlet 生命周期

 文章开始的地方提到第一个例子中的gr2其实并没有正常结束,我们可以借用greenlet.dead这个属性来查

from greenlet import greenlet
def fun1():
    gr2.switch(1)
    print("fun1 finished")

def fun2(x):
    print("fun2 first x = {}".format(x))
    z = gr1.switch()
    print("fun2 back z = ".format(z))

if __name__ == '__main__':
    gr1 = greenlet(fun1)
    gr2 = greenlet(fun2)
    gr1.switch()
    print("gr1 si dead?: {}, gr2 is dead?: {}".format(gr1.dead, gr2.dead))
    gr2.switch()
    print("gr1 si dead?: {}, gr2 is dead?: {}".format(gr1.dead, gr2.dead))
    print(gr2.switch(10))
"""
结果:
    fun2 first x = 1
    fun1 finished
    gr1 si dead?: True, gr2 is dead?: False
    fun2 back z = 
    gr1 si dead?: True, gr2 is dead?: True
    10
"""

从这个例子可以看出
只有当协程对应的函数执行完毕,协程才会die,所以第一次Check的时候gr2并没有die,因为gr2切换出去了就没切回来。在main中再switch到gr2的时候, 执行后面的逻辑,gr2 die
如果试图再次switch到一个已经是dead状态的greenlet会怎么样呢,事实上会切换到其parent greenlet。 

Greentlet Traceing

 Greenlet也提供了接口使得程序员可以监控greenlet的整个调度流程。主要是gettrace 和 settrace(callback)函数。下面看一个例子

import greenlet
def test_greenlet_tracing():
    def callback(event, args):
        print("event = {}, from {} to {}".format(event,id(args[0]), id(args[1])))

    def dummy():
        gr2.switch()

    def dummyException():
        raise Exception("excep in coroutine")

    main = greenlet.getcurrent()
    gr1 = greenlet.greenlet(dummy)
    gr2 = greenlet.greenlet(dummyException)
    print("main_id = {}, gr1_id = {}, gr2_id = {}".format(id(main),id(gr1), id(gr2)))
    oldtrace = greenlet.settrace(callback)
    try:
        gr1.switch()
    except:
        print("Exception")
    finally:
        greenlet.settrace(oldtrace)

test_greenlet_tracing()
"""
结果:
    main_id = 2149705792152, gr1_id = 2149705792504, gr2_id = 2149711704136
    event = switch, from 2149705792152 to 2149705792504
    event = switch, from 2149705792504 to 2149711704136
    event = throw, from 2149711704136 to 2149705792152
    Exception
"""

 其中callback函数event是switch或者throw之一,表明是正常调度还是异常跑出;args是二元组,表示是从协程args[0]切换到了协程args[1]。上面的输出展示了切换流程:从main到gr1,然后到gr2,最后回到main。

greenlet 使用建议

使用greenlet需要注意一下三点:
  第一:greenlet创生之后,一定要结束,不能switch出去就不回来了,否则容易造成内存泄露
  第二:python中每个线程都有自己的main greenlet及其对应的sub-greenlet ,不能线程之间的greenlet是不能相互切换的
  第三:不能存在循环引用,这个是官方文档明确说明
  ”Greenlets do not participate in garbage collection; cycles involving data that is present in a greenlet’s frames will not be detected. “

对于第一点,我们来看一个例子:

from greenlet import greenlet, GreenletExit
huge = []
def show_leak():
    def fun1():
        gr2.switch()

    def fun2():
        huge.extend([x for x in range(100)])
        gr1.switch()
        print("finish switch del huge")
        del huge[:]

    gr1 = greenlet(fun1)
    gr2 = greenlet(fun2)
    gr1.switch()
    gr1 = gr2 = None
    print("length of huge is {}".format(len(huge)))  # length of huge is 100

if __name__ == '__main__':
    show_leak()

在fun2函数中我们将huge清空,然后再通过 gr1 = gr2 = None 将gr1、gr2的引用计数降到了0。但运行结果告诉我们,第11行并没有执行,所以如果一个协程没有正常结束是很危险的,往往不符合程序员的预期。 

 greenlet提供了解决这个问题的办法,官网文档提到:如果一个greenlet实例的引用计数变成0,那么会在上次挂起的地方抛出GreenletExit异常,这就使得我们可以通过try ... finally 处理资源泄露的情况。如下面的代码:

from greenlet import greenlet, GreenletExit
huge = []
def show_leak():
    def fun1():
        gr2.switch()

    def fun2():
        huge.extend([x for x in range(100)])
        try:
            gr1.switch()
        finally:
            print("finish switch del huge")
            del huge[:]

    gr1 = greenlet(fun1)
    gr2 = greenlet(fun2)
    gr1.switch()
    gr1 = gr2 = None
    print("length of huge is {}".format(len(huge)))

if __name__ == '__main__':
    show_leak()
"""
结果:
    finish switch del huge
    length of huge is 0
"""

 上述代码的switch流程:main greenlet --> gr1 --> gr2 --> gr1 --> main greenlet, 很明显gr2没有正常结束(在第10行刮起了)。
执行[ gr1 = gr2 = None ]之后gr1,gr2的引用计数都变成0,那么会在 fun2 的 gr1.switch() 抛出GreenletExit异常,因此finally语句有机会执行。
同时,在文章开始介绍Greenlet module的时候也提到了,GreenletExit这个异常并不会抛出到parent,所以main greenlet也不会出异常。

  看上去貌似解决了问题,但这对程序员要求太高了,百密一疏。所以最好的办法还是保证协程的正常结束

总结:

之前的文章其实已经提到提到了coroutine协程的强大之处,对于异步非阻塞,而且还需要保留上下文的场景非常适用。greenlet跟强大,可以从一个协程切换到任意其他协程,这是generator做不到的,但这种能力其实也是双刃剑,前面的注意事项也提到了,必须保证greenlet的正常结束,在协程之间任意的切换很容易出问题。

    3、gevent

Python通过yield提供了对协程的基本支持,但是不完全。而第三方的gevent为Python提供了比较完善的协程支持。
gevent是第三方库,通过greenlet实现协程,其基本思想是:
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。

from gevent import monkey; monkey.patch_all() # 首行添加下面的语句即可

# 由于IO操作非常耗时,程序经常会处于等待状态
# 比如请求多个网页有时候需要等待,gevent可以自动切换协程
# 遇到阻塞自动切换协程,程序启动时执行monkey.patch_all()解决
# 首行添加下面的语句即可
from gevent import monkey; monkey.patch_all()
import gevent
import requests
import time

start = time.time()

def fun1(url):
    print("GET: {}, time = {}".format(url, time.ctime()))
    resp = requests.get(url)
    data = resp.text
    print("{} bytes received from {}".format(len(data), url))

# fun1('https://www.python.org/')
# # fun1('https://www.yahoo.com/')
# fun1('https://baidu.com/')
# fun1('https://www.sina.com.cn/')

gevent.joinall([
    gevent.spawn(fun1, "https://github.com/kubernete"),
    gevent.spawn(fun1, 'https://www.sina.com.cn/'),
    gevent.spawn(fun1, 'https://www.python.org/'),
    gevent.spawn(fun1, 'https://www.yahoo.com/'),
    gevent.spawn(fun1, 'https://www.baidu.com/')
])

if __name__ == '__main__':
    print("cost time = {}".format(time.time() - start))

1、在最开头的地方gevent.monkey.patch_all();把标准库中的thread/socket等给替换掉.这样我们在后面使用socket的时候能够跟寻常一样使用,

无需改动不论什么代码,可是它变成非堵塞的了.
2、就是把里面的函数转变成gevent协程里面的相同的函数,这样就可以说都变成导入的gevent的函数,

gevent是类似一个协程空间,当你用生成协程时,就会在gevent容器空间中生成一个标记好的协程,

当多个协程同时存储在容器空间时,gevent就会统一调配CPU的使用,当那个协程出现堵塞时,

gevent就会马上切换执行到下一个协程标记中去,实现一个非堵塞的运行gevent容器空间!

运行结果

GET: https://github.com/kubernete
GET: https://www.sina.com.cn/
GET: https://www.python.org/
GET: https://www.yahoo.com/
GET: https://www.baidu.com/
2443 bytes received from https://www.baidu.com/
163858 bytes received from https://github.com/kubernete
50810 bytes received from https://www.python.org/
529297 bytes received from https://www.sina.com.cn/
517452 bytes received from https://www.yahoo.com/
cost time = 2.3377764225006104

 

参考:

https://www.cnblogs.com/yuanchenqi/articles/6248025.html

threading 模块中 Lock 类的用法详解

greenlet 部分 参考自 xybaby (如果侵权,请联系删除)

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值