一. 概念
进程
计算机程序是磁盘中可执行的二进制(或其他类型)的数据,只有在被读取到内存中,被操作系统调用的时候才开始其生命周期。进程是程序的一次执行。每个进程都有自己的地址空间,内存,数据及其它记录其运轨迹的辅助数据。操作系统管理再起上运行的所有进程,并为这些进程公平分配时间,进程也可以通过fork和spwan操作来完成其他的任务。不过各个进程有自己的内存空间,数据栈等,所以只能使用进程间通信(IPC),不可以直接共享信息。
线程
线程与进程有些相似,不同的是,所有的线程运行在同一个进程中,共享相同的运行环境。线程一般是并发执行的,由于这种并行和数据共享机制使得多个任务的合作变为可能。
在单cpu中,真正的并发是不可能的,每个进程会被安排成每次运行一小会儿,然后让出cpu,让其他线程运行。
进程与线程的关系
进程和线程的关系:
- 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
- 资源分配给进程,同一进程的所有线程共享该进程的所有资源。
- CPU分给线程,即真正在CPU上运行的是线程。
并行与并发
并行处理(Parallel Processing)是计算机系统中能同时执行两个或更多个处理的一种计算方法。并行处理可同时工作于同一程序的不同方面。并行处理的主要目的是节省大型和复杂问题的解决时间。并发处理(concurrency Processing):指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机(CPU)上运行,但任一个时刻点上只有一个程序在处理机(CPU)上运行
并发的关键是有处理多个任务的能力,不一定要同时。
并行的关键是有同时处理多个任务的能力。
同步与异步
在计算机领域,同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去;
异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。
二. Python全局解释器锁(GIL)
对Python虚拟机的访问有GIL来控制,这个锁保证同一时刻只有一个线程在运行。在多线程环境中,Python虚拟机按以下方式执行:
- 设置GIL
- 切换到一个线程中运行
- 运行
- 指定数量的字节码指令,或者
- 线程主动让出控制
- 把线程设置为睡眠状态
- 解锁GIL
- 再次重复以上所有步骤
无论启多少个线程,有多少个cpu, Python在执行一个进程的时候在同一时刻只允许一个线程运行。
所以,python是无法利用多核CPU实现多线程的。
这样,python对于计算密集型的任务开多线程的效率甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。
三. threading模块
threading类直接创建
import threading
import time
def countNum(n): # 定义某个线程要运行的函数
print("running on number:%s" %n)
time.sleep(3)
if __name__ == '__main__':
t1 = threading.Thread(target=countNum,args=(23,)) #生成一个线程实例
t2 = threading.Thread(target=countNum,args=(34,))
t1.start() #启动线程
t2.start()
print("ending!")
thread类继承方式创建
#继承Thread式创建
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:%s" %self.num)
time.sleep(3)
t1=MyThread(56)
t2=MyThread(78)
t1.start()
t2.start()
print("ending")
join 和 setDaemon
# join():在子线程完成运行之前,这个子线程的父线程将一直被阻塞。
# setDaemon(True):
'''
将线程声明为守护线程,必须在start() 方法调用之前设置,如果不设置为守护线程程序会被无限挂起。
当我们在程序运行中,执行一个主线程,如果主线程又创建一个子线程,主线程和子线程 就分兵两路,分别运行,那么当主线程完成
想退出时,会检验子线程是否完成。如果子线程未完成,则主线程会等待子线程完成后再退出。但是有时候我们需要的是只要主线程
完成了,不管子线程是否完成,都要和主线程一起退出,这时就可以 用setDaemon方法啦'''
import threading
from time import ctime,sleep
import time
def Music(name):
print ("Begin listening to {name}. {time}".format(name=name,time=ctime()))
sleep(3)
print("end listening {time}".format(time=ctime()))
def Blog(title):
print ("Begin recording the {title}. {time}".format(title=title,time=ctime()))
sleep(5)
print('end recording {time}'.format(time=ctime()))
threads = []
t1 = threading.Thread(target=Music,args=('FILL ME',))
t2 = threading.Thread(target=Blog,args=('',))
threads.append(t1)
threads.append(t2)
if __name__ == '__main__':
#t2.setDaemon(True)
for t in threads:
#t.setDaemon(True) #注意:一定在start之前设置
t.start()
#t.join()
#t1.join()
#t2.join() # 考虑这三种join位置下的结果?
print ("all over %s" %ctime())
四. 锁
锁通常被用来实现对共享资源的同步访问。为每一个共享资源创建一个Lock对象,当你需要访问该资源时,调用acquire方法来获取锁对象(如果其它线程已经获得了该锁,则当前线程需等待其被释放),待资源访问完后,再调用release方法释放锁:
import time
import threading
def addNum():
global num #在每个线程中都获取这个全局变量
#num-=1
temp=num
time.sleep(0.1)
num =temp-1 # 对此公共变量进行-1操作
num = 100 #设定一个共享变量
thread_list = []
for i in range(100):
t = threading.Thread(target=addNum)
t.start()
thread_list.append(t)
for t in thread_list: #等待所有线程执行完毕
t.join()
print('Result: ', num)
上述的程序每次运行都会产生不同的结果:
- 注释掉
time.sleep(1)
结果唯一,为0 time.sleep(1)
结果唯一,为99time.sleep(0.1)
结果不确定
第一种情况,sleep语句上下两句的间隔时间为0,小于进程间的切换时间,线程切换后能够拿到上一个线程计算后的结果,即每次都可以完成减一操作
第二种情况,sleep语句上下两句的间隔时间为1,大于100个线程的切换时间,在切换的过程中每个线程在执行sleep函数,那么每一个线程拿到的num都是100,每个线程得到的结果都是99,最后得到的num就是99
sleep语句上下两句的间隔时间大于0但不足以完成100个线程的切换,开始执行的线程完成了减一操作,剩下的线程拿到的num就不是100了,最后得到的结果也就不确定了。
如何保证计算结果的正确性
使用本节开始提到的锁,可以保证在计算减一操作的过程中单线程执行,不执行其他线程,在解锁之后在进行线程调度。加锁方式如下所示
# -*- coding: utf-8 -*-
import time
import threading
lock = threading.Lock()
def subNum():
global num
lock.acquire()
temp = num
time.sleep(0.01)
num = temp - 1
lock.release()
num = 100
thread_list = []
for i in range(100):
t = threading.Thread(target=subNum)
t.start()
thread_list.append(t)
for t in thread_list:
t.join()
print('Result: ', num)
五. 递归锁
import threading
import time
A = threading.Lock()
B = threading.Lock()
class myThread(threading.Thread):
def actionA(self):
A.acquire()
print(self.name, 'got A from actionA', time.time())
time.sleep(2)
B.acquire()
print(self.name, 'got B from actionA', time.time())
time.sleep(1)
B.release()
A.release()
def actionB(self):
B.acquire()
print(self.name, 'got B from actionB', time.time())
time.sleep(2)
A.acquire()
print(self.name, 'got A from actionB', time.time())
time.sleep(1)
A.release()
B.release()
def run(self):
self.actionA()
self.actionB()
if __name__ == '__main__':
L = []
for i in range(5):
t = myThread()
t.start()
L.append(t)
for i in L:
i.join()
print('end...')
"""
5个线程有一个线程T-1先拿到A这把锁,在有一个线程先拿到A这把锁后,其他四个线程只能等着,一直等到A这把锁释放掉。
T-1继续执行拿到B锁,然后T-1释放B锁,再释放A锁
第二个线程T-2获得actionA中的A锁,T-1向下执行获得actionB的B锁,
继续向下执行
T-2已经获得actionA中的A锁,T-2试图获得actionA中的B锁,
T-1已经获得actionB中的B锁,T-1试图获得actionB中的A锁,
T-2不释放A锁,T-1不释放B锁,造成死锁
"""
使用递归锁可以解决死锁的问题,将上述代码中的锁换成递归锁
import threading
import time
rLock = threading.RLock()
class myThread(threading.Thread):
def actionA(self):
rLock.acquire()
print(self.name, 'got A from actionA', time.time())
time.sleep(2)
rLock.acquire()
print(self.name, 'got B from actionA', time.time())
time.sleep(1)
rLock.release()
rLock.release()
def actionB(self):
rLock.acquire()
print(self.name, 'got B from actionB', time.time())
time.sleep(2)
rLock.acquire()
print(self.name, 'got A from actionB', time.time())
time.sleep(1)
rLock.release()
rLock.release()
def run(self):
self.actionA()
self.actionB()
if __name__ == '__main__':
L = []
for i in range(5):
t = myThread()
t.start()
L.append(t)
for i in L:
i.join()
print('end...')
六. 同步对象
线程的一个关键特性是每个线程都是独立运行且状态不可预测。
如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就 会变得非常棘手。
为了解决这些问题,我们需要使用threading库中的Event对象。
- 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。
- 在初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。
- 一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。
- 如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行
event类中的方法
函数名 | 使用方法 |
---|---|
event.isSet() | :返回event的状态值 |
event.wait() | 如果 event.isSet()==False将阻塞线程 |
event.set() | 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度 |
event.clear() | 恢复event的状态值为False |
# -*- coding: utf-8 -*-
# python version:
# author:
# date:
# description:
import threading
import time
event = threading.Event()
class Boss(threading.Thread):
def run(self):
print('Boss: 今晚大家都要加班到22:00')
print(event.is_set())
event.set()
time.sleep(5)
print('Boss: <22:00>可以下班了')
print(event.isSet())
event.set()
class Worker(threading.Thread):
def run(self):
event.wait() # 等待event被设定,一旦event被设定,等同于pass
print('Worker: holy shit')
time.sleep(1)
event.clear() # 清除被设定的event
event.wait() # 等待下一次event被设定
print('worker: nice')
if __name__ == '__main__':
threads = []
for i in range(5):
threads.append(Worker())
threads.append(Boss())
for t in threads:
t.start()
for t in threads:
t.join()
>>Boss: 今晚大家都要加班到22:00
False
Worker: holy shit
Worker: holy shit
Worker: holy shit
Worker: holy shit
Worker: holy shit
Boss: <22:00>可以下班了
False
worker: nice
worker: nice
worker: nice
worker: nice
worker: nice
七. 信号量
信号量是用来控制线程并发数的,BoundedSemaphore 或 Semaphore 管理一个内置的计数器,
每当调用acquire()时内置计数器-1;
调用release() 时内置计数器+1;
计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
BoundedSemaphore 与 Semaphore 的唯一区别在于前者在调用release()时检查计数器的值是否超过了计数器的初始值,如果超过了则抛出一个异常
实例:(同时只有5个线程可以获得semaphore,即可以限制最大连接数为5):
import threading
import time
semaphore = threading.Semaphore(5)
class myThread(threading.Thread):
def run(self):
if semaphore.acquire():
print(self.name)
time.sleep(1)
semaphore.release()
if __name__ == '__main__':
threads = []
for i in range(100):
threads.append(myThread())
for t in threads:
t.start()
可以看出五个一组打印结果
八. 线程队列
创建一个“队列”对象
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模块有三种队列及构造函数:
类型 | 用法 |
---|---|
class queue.Queue(maxsize) | FIFO,先进先出 |
class queue.LifoQueue(maxsize) | LIFO,即先进后出。 |
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() | 等到队列为空,再执行别的操作 |
九. 生产者消费者模型
为什么要使用生产者和消费者模式
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决生产者和消费者强耦合造成的时间浪费的问题,引入了生产者和消费者模式。
什么是生产者消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力
生产者消费者实例
import time
import random
import queue
import threading
q = queue.Queue()
def producer(name):
count = 1
while count < 10:
print('cooking...')
time.sleep(random.randrange(3))
q.put(count)
print('producer %s has cooked %s hamburgers' % (name, count))
count += 1
print('ok...')
def consumer(name):
count = 0
while count < 10:
time.sleep(random.randrange(4))
if not q.empty():
data = q.get()
print('\033[32;1mConsumer %s has eaten %s hamburger...\033[0m' % (name, data))
else:
print("-----no hamburger anymore----")
count += 1
p1 = threading.Thread(target=producer, args='A')
c1 = threading.Thread(target=consumer, args='B')
c2 = threading.Thread(target=consumer, args='C')
c3 = threading.Thread(target=consumer, args='D')
p1.start()
c1.start()
c2.start()
c3.start()
使用队列后,消费者与管理者解耦,消费者不管生产者是否生产出产品都可以去队列中取,生产者不管消费者是否取走商品都可以生产产品,两者互不干扰
但还有一种场景需要生产者生产完产品后通知消费者,消费者得到通知再做自己的事,这一需求可以通过task_done 和 join配合完成,task_done用来向队列发送消息通知队列自己的动作以完成,接收方通过join方法得到消息,继续执行下面的操作。没有task_done那么jion一直阻塞,通过下面的例子进说明。
task_done & join
import time
import random
import queue
import threading
q = queue.Queue()
def producer(name):
count = 1
while count < 30:
print('cooking...')
time.sleep(1)
q.put(count)
print('\033[31;1mProducer %s has cooked %s hamburgers\031[0m' % (name, count))
count += 1
q.task_done() # 通知队列消息发送完毕
print('ok...')
def consumer(name):
count = 1
while count < 10:
q.join()
data = q.get()
print('\033[32;1mConsumer %s has eaten %s hamburger...\033[0m' % (name, data))
count += 1
p1 = threading.Thread(target=producer, args='A')
c1 = threading.Thread(target=consumer, args='B')
c2 = threading.Thread(target=consumer, args='C')
c3 = threading.Thread(target=consumer, args='D')
p1.start()
c1.start()
c2.start()
c3.start()
从结果可以看出,生产者没生产一个货物,消费者消耗一个货物,生产者在生产完成消费者所需货物后把计划生产的货品生产完毕,结束整个程序
十. 多进程调用
由于GIL的存在,python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程。
multiprocessing包是Python中的多进程管理包。与threading.Thread类似,它可以利用multiprocessing.Process对象来创建一个进程。该进程可以运行在Python程序内部编写的函数。该Process对象与Thread对象的用法相同,也有start(), run(), join()的方法。此外multiprocessing包中也有Lock/Event/Semaphore/Condition类 (这些对象可以像多线程那样,通过参数传递给各个进程),用以同步进程,其用法与threading包中的同名类一致。所以,multiprocessing的很大一部份与threading使用同一套API,只不过换到了多进程的情境。
下面通过例子说明一下multiprocessing模块的使用方法
from multiprocessing import Process
import time
def f(name):
time.sleep(1)
print('hello', name, time.time())
def direct_call_process():
p_list = []
for i in range(3):
p = Process(target=f, args=('process:%s' % i,))
p_list.append(p)
p.start()
for i in p_list:
p.join()
print('end')
#--------------------------------------------
class myProcess(Process):
def __init__(self):
super(myProcess, self).__init__()
def run(self):
print('hello', self.name, time.time())
time.sleep(1)
def inherit_call_process():
p_list = []
for i in range(3):
p = myProcess()
p.start()
p_list.append(p)
for p in p_list:
p.join()
print('end')
if __name__ == '__main__':
direct_call_process()
print('------------------------------------')
inherit_call_process()
上下两种使用方式,打印的时间是几乎一样的,理论上是一样的,但存在打印时间差和cpu本身的误差,会存在一点误差。
结果说明开辟了多个进程。
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:进程号。
from multiprocessing import Process
import os
import time
def info(name):
print('name:', name)
print('parent process:', os.getppid())
print('process id', os.getpid())
print('-----------------------')
time.sleep(1)
def foo(name):
info(name)
if __name__ == '__main__':
info('main process line')
p1 = Process(target=info, args=('p1', ))
p2 = Process(target=foo, args=('p2', ))
p1.start()
p2.start()
p1.join()
p2.join()
print('ending')
十一. 进程间通信
进程间通信使用三种方法:进程队列Queue,管道,manager
进程队列Queue
from multiprocessing import Process, Queue # 导入进程间队列
def f(q, n):
q.put(n + 1)
print('sub process: ', id(q))
def conn_queue():
q = Queue()
print('main process: ', id(q))
for i in range(3):
p = Process(target=f, args=(q, i))
p.start()
print(q.get())
print(q.get())
print(q.get())
if __name__ == '__main__':
conn_queue()
数据进入不同进程是经过拷贝的
管道
Pipe()返回的两个连接对象代表管道的两端。 每个连接对象都有send()和recv()方法。 请注意,如果两个进程(或线程)尝试同时读取或写入管道的同一端,管道中的数据可能会损坏。
from multiprocessing import Process, Pipe
def f(conn):
conn.send('hello')
response = conn.recv()
print('process-1 ', response)
conn.close()
if __name__ == '__main__':
parent_conn, sub_conn = Pipe()
p = Process(target=f, args=(sub_conn,))
p.start()
print('process-2 ', parent_conn.recv())
parent_conn.send("world")
p.join()
>>process-2 hello
>>process-1 world
通过管将两个进程连接起来,通过p.start()
启动进程1,进程1发送消息hello
,然后等下接受消息,进程2在接收消息后发送消息world
,进程1收到消息后关闭进程。
manager
Queue和pipe只是实现了数据交互,并没实现数据共享。实现数据共享,即实现一个进程修改数据,另一个进程的数据也随之改变。
使用manager可以实现数据共享,manager支持共享的数据类型包括:list,dict,Namespace,Lock,RLock,Semaphore,BoundedSemaphore,Condition,Event,Barrier,Queue,Value ,Array
使用方法如下所示
from multiprocessing import Process, Manager
def f(d, l, n):
d[n] = n
l.append(n)
if __name__ == '__main__':
with Manager() as manager:
d = manager.dict()
l = manager.list()
p_list = []
for i in range(10):
p = Process(target=f, args=(d, l, i))
p.start()
p_list.append(p)
for res in p_list:
res.join()
print(d)
print(l)
>>{1: 1, 0: 0, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9}
>>[1, 0, 2, 3, 4, 5, 6, 7, 8, 9]
可以看出主进程声明了一个空列表和一个空字典,在子进程中对字典和列表进行了修改,主进程中的字典和列表也随着修改。实现了数据共享。
十二. 进程池
进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进程,那么程序就会等待,直到进程池中有可用进程为止。
进程池内部维护一个进程序列,当使用时,去进程池中获取一个进程,如果进程池序列中没有可供使用的进程,那么程序就会等待,直到进程池中有可用进程为止。
进程池中有以下几个主要方法:
- apply:从进程池里取一个进程并执行
- apply_async:apply的异步版本
- terminate:立刻关闭线程池
- join:主进程等待所有子进程执行完毕,必须在close或terminate之后
- close:等待所有进程结束后,才关闭线程池
from multiprocessing import Pool
import time
import os
def foo(args):
time.sleep(1)
print(args)
print('foo: ', os.getpid())
def Bar(arg): # 回调函数在主进程中调用,arg是foo函数的返回值
print('Bar: ', os.getpid())
if __name__ == '__main__':
print('main:', os.getpid())
p = Pool(5)
for i in range(30):
p.apply_async(func=foo, args=(i,), callback=Bar)
p.close() # 等子进程执行完毕后关闭线程池
p.join()
# 结果5个5个打印
# 必须先close再join
十三. 协程(coroutine)
协程的优点
- 协程的执行效率非常高。因为子程序切换不是线程切换,而是由程序自身控制。因此,没有线程切换的开销,和多线程相比,线程数量越多,相同数量的协程体现出的优势越明显
- 不需要多线程的锁机制。由于只有一个线程,也不存在同时写变量的冲突,在协程中控制共享资源不需要加锁,只需要判断数据的状态,所以执行效率远高于线程
对于多核cpu,可以使用多进程+协程来尽可能高效率地利用cpu
yield实现生产者消费者模型
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。
如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率比之传统高非常多。
import time
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] <--- Consuming %s...' % n)
time.sleep(1)
r = 'ok'
def producer(c):
next(c)
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] ---> Producing %s...' % n)
c_ret = c.send(n) # 和next有同样的同能,可以触发函数到下一个yield,却别于next的是可以像yield的左边的变量传值,例如上面yield