十五、Python 多任务-线程、进程和协程(迭代器、生成器)

1、线程

  并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多任务‘’一起“执行。(实际上总有一些任务不在执行,因为切换速度很快,看似一起执行)

  并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的。

  

    创建线程时,除了以下两种方法,还可以使用线程池的方法。

from multiprocessing.dummy import Pool
# 实现线程池需要借助于multiprocessing下的dummy模块


def proc_some(args):
    pass


if __name__=="__main__":
    pool = Pool(2)
    str_list = list()
    results = pool.map(proc_some,str_list)
    print(results)

  1.1 创建线程

  python通过threading模块实现线程控制

import time 
import threading 


def test1(def_name):
    for i in range(2):
        print('这是一个线程测试:%s,编号为 %d' % (def_name,i))
        time.sleep(1)

def test2(def_name):
    for i in range(2):
        print('这是一个线程测试:%s,编号为 %d' % (def_name,i))
        time.sleep(1)


def main():
    
    print('开始  {}'.format(time.ctime()))
    # 通过threading模块的Thread类创建一个线程对象
    # 这个对象接收两个参数,target=目标函数名,args为元组,包含传入函数的所有参数
    t1 = threading.Thread(target=test1,args=('test1',))
    t2 = threading.Thread(target=test2,args=('test2',))
    # 当调用start()时,才会真正的创建线程,并且开始执行
    t1.start()
    t2.start()

    # 查看线程数
    while True:
        length = len(threading.enumerate())
        # print('线程名:{}'.format(threading.enumerate()))
        print('当前运行的线程数为:{}'.format(length))
        if length<=1:
            break

    time.sleep(5)
    # 主线程会等待所有的子线程结束后才结束
    print('结束  {}'.format(time.ctime()))

if __name__=='__main__':
    main()

  1.2 线程执行代码的封装

    使用threading模块时,可以定义一个新的子类class,需要继承threading.Thread,并重写__inti__方法和run方法。

    python的threading.Thread类有一个run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。

    创建子类实例后,通过Thread类的start方法,可以启动该线程,交给虚拟机进行调度,当该线程获得执行的机会时,就会调用run方法执行线程。

    线程的执行顺序不能确定。

    当线程的run()方法结束时该线程完成。

    

import threading
import time

class MyThread(threading.Thread):
    def run(self):
        for i in range(3):
            time.sleep(1)
            # python会自动为线程制定一个名字,通过self.name调用
            msg = "I'm "+self.name+' @ '+str(i)
            print(msg)
def test():
    for i in range(5):
        t = MyThread()
        t.start()
if __name__ == '__main__':
    test()

  

  1.3 多线程共享全局变量

    在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据。

    缺点是:线程对全局变量随意改动可能造成多线程之间的全局变量混乱,此时线程不安全。

    如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确。

from threading import Thread
import time

def work1(nums):
    nums.append(44)
    print("----in work1---",nums)


def work2(nums):
    #延时一会,保证t1线程中的事情做完
    time.sleep(1)
    print("----in work2---",nums)

g_nums = [11,22,33]

t1 = Thread(target=work1, args=(g_nums,))
t1.start()

t2 = Thread(target=work2, args=(g_nums,))
t2.start()

    

 

 

   1.4 同步

    当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制

    线程同步能保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。

    互斥锁为资源引入一个状态:锁定/非锁定

    当某个线程要更改共享数据时,先将其锁上,此时资源的状态为’锁定‘,其他线程不能更改,直到该线程释放资源,将资源的状态变成’非锁定‘,其他的线程才能再次锁定该资源。

    互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

    对于Lock对象而言,如果一个线程连续两次进行acquire操作,那么由于第一次acquire之后没有release,第二次acquire将挂起线程,这会导致Lock对象永远不会release,使得线程死锁。RLock对象允许一个线程多次对其进程acquire操作,因为在其内部通过一个counter变量维护着线程acquire的次数,而且每一次的acquire操作必须有一个release操作与之对应,在所有的release操作完成之后,别的线程才能申请该RLock对象。

    threading模块中定义了Lock类,可以方便的处理锁定:

# 创建锁
mutex = threading.Lock()

# 锁定
mutex.acquire()

# 释放
mutex.release()

    

# 使用互斥锁完成2个线程对同一个全局变量各加100万次的操作
import threading
import time

g_num = 0

def test1(num):
    global g_num
    for i in range(num):
        mutex.acquire()  # 上锁
        g_num += 1
        mutex.release()  # 解锁

    print("---test1---g_num=%d"%g_num)

def test2(num):
    global g_num
    for i in range(num):
        mutex.acquire()  # 上锁
        g_num += 1
        mutex.release()  # 解锁

    print("---test2---g_num=%d"%g_num)

# 创建一个互斥锁
# 默认是未上锁的状态
mutex = threading.Lock()

# 创建2个线程,让他们各自对g_num加1000000次
p1 = threading.Thread(target=test1, args=(1000000,))
p1.start()

p2 = threading.Thread(target=test2, args=(1000000,))
p2.start()

# 等待计算完成
while len(threading.enumerate()) != 1:
    time.sleep(1)

print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)

 

 

   1.5 案例:多线程聊天器

  

import threading
import socket


def recv_message(udp_socket):
    while True:
        recv_data = udp_socket.recvfrom(1024)
        print('接收到的消息:{}'.format(recv_data[0].decode('utf-8')))

def send_message(udp_socket,dest_addr):
    while True:
        send_data = input('请输入要发送的消息:')
        udp_socket.sendto(send_data.encode('utf-8'),dest_addr)


def main():
    """完成udp聊天器的整体控制"""
    # 创建套接字
    udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    # 绑定本地地址
    localaddr = ('',8081)
    udp_socket.bind(localaddr)

    # 获取对方的地址
    dest_ip = input('请输入对方的ip:')
    dest_port = int(input('请输入对方的port:'))
    dest_addr = (dest_ip,dest_port)

    # 创建2个线程,去执行相应的功能
    t_recv = threading.Thread(target=recv_message,args=(udp_socket,))
    t_send = threading.Thread(target=send_message,args=(udp_socket,dest_addr))

    t_recv.start()
    t_send.start()


if __name__=='__main__':
    main()

 

2、进程

  2.1 概念

    进程:一个程序运行起来后,代码+用到的资源称为进程,它是操作系统分配资源的基本单位。

  2.2 进程的状态

    

 

 

    就绪态:运行的条件满足,正在等在cpu执行

    执行态:cpu正在执行其功能

    等待态:等待某些条件满足

  2.3 进程的创建-multiprocessing

    multiprocessing模块是多进程模块,提供了一个Process类来代表一个进程对象。

    创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方式启动。

    

from multiprocessing import Process
import time

def run_proc():
    """子进程要执行的代码"""
    while True:
        print("---2---")
        time.sleep(1)

if __name__=='__main__':
    p = Process(target=run_proc)
    p.start()

    while True:
        print('---1---')
        time.sleep(1)

  2.4 进程pid

    

from multiprocessing import Process
import os
import time

def run_proc():
    """子进程要执行的代码"""
    print('子进程运行中,pid=%d...' % os.getpid())  
    # os.getpid获取当前进程的进程号
    print('子进程将要结束...')

if __name__ == '__main__':
    print('父进程pid: %d' % os.getpid())  
    # os.getpid获取当前进程的进程号
    p = Process(target=run_proc)
    p.start()
    time.sleep(1)
    print('--父进程结束--')

  

 

 

  2.5 Process语法结构如下:

    Process(【group【,target【,name【,args【,kwargs】】】】】)

    (1)target:如果传递了函数的引用,这个子进程就执行函数里的代码

    (2)args:给target指定的函数传递的参数,以元组的方法传递

    (3)kwargs:给target指定的函数传递命名参数

    (4)name:给进程设定一个名字,可以不设定

    (5)group:指定进程组,大多数情况下用不到

    Process创建的实例对象的常用方法:

    (1)start():启动子进程实例(创建子进程)

    (2)is_alive():判断进程子进程是否还在活着

    (3)join([timeout]):是否等待子进程执行结束,或等待多少秒

    (4)terminate():不管任务是否完成,立即终止子进程

    Process创建的实例对象的常用属性:

    (1)name:当前进程的别名,默认为Process-N,N为从1开始递增的整数

    (2)pid:当前进程的pid(进程号)

    

from multiprocessing import Process
import os
from time import sleep


def run_proc(name, age, **kwargs):
    for i in range(10):
        print('子进程运行中,name= %s,age=%d ,pid=%d...' % (name, age, os.getpid()))
        print(kwargs)
        sleep(0.2)

if __name__=='__main__':
    p = Process(target=run_proc, args=('test',18), kwargs={"m":20})
    p.start()
    sleep(1)  # 1秒中之后,立即结束子进程
    p.terminate()
    p.join()

     

 

 

  2.6 进程间不共享全局变量

  2.7 进程间通信——Queue

    2.7.0 Pipe的使用

      Pipe常用来在两个进程间进行通信,两个进程分别位于管道的两端。

      Pipe方法:`pipe = multiprocessing.Pipe()`,返回(conn1,conn2)代表一个管道的两个端。

      Pipe方法有duplex参数,如果duplex参数为True(默认值),那么这个管道是全双工模式,也就是说conn1和conn2均可收发。当duplex为False,conn1只负责接收消息,conn2只负责发送消息。send和recv方法分别是发送和接收消息的方法。

      在全双工模式下,可以调用conn1.send发送消息,conn1.recv接收消息。如果没有消息可接收,recv方法会一直阻塞,如果管道已经被关闭,那么recv方法会抛出EOFError。

import multiprocessing
import random
import time,os


def proc_send(pipe,args):
    for arg_ in args:
        print("Process (%s) send: (%s)" % (os.getpid(),arg_))
        pipe.send(arg_)
        time.sleep(random.random())


def proc_recv(pipe):
    while True:
        print("Process (%s) recv: %s" % (os.getpid(), pipe.recv()))
        time.sleep(random.random())


if __name__=="__main__":
    pipe = multiprocessing.Pipe()
    p1 = multiprocessing.Process(target=proc_send, args=(pipe[0], ["da_da_da{}".format(str(i)) for i in range(10)]))
    p2 = multiprocessing.Process(target=proc_recv, args=(pipe[1],))
    p1.start()
    p2.start()
    p1.join()
    p2.join()

    2.7.1 Queue的使用

      可以使用multiprocessing模块的Queue实现多进程之间的数据传递,Queue本身是一个消息队列程序。

from multiprocessing import Queue
q=Queue(3) #初始化一个Queue对象,最多可接收三条put消息
q.put("消息1") 
q.put("消息2")
print("Queue队列是否已满:",q.full())  #False
q.put("消息3")
print("Queue队列是否已满:",q.full()) #True

# 因为消息列队已满下面的try都会抛出异常,第一个try会等待2秒后再抛出异常,
# 第二个Try会立刻抛出异常
try:
    q.put("消息4",True,2)
except:
    print("消息列队已满,现有消息数量:%s"%q.qsize())

try:
    q.put_nowait("消息4")
except:
    print("消息列队已满,现有消息数量:%s"%q.qsize())

#推荐的方式,先判断消息列队是否已满,再写入
if not q.full():
    q.put_nowait("消息4")

#读取消息时,先判断消息列队是否为空,再读取
if not q.empty():
    for i in range(q.qsize()):
        print(q.get_nowait())

      

 

 

       初始化Queue对象时(q=Queue()),若括号中没有指定最大可接收的消息数量,或数量为负值,那么就代表可接受的消息数量没有上限

      Queue.qsize():返回当前队列包含的消息数量

      Queue.empty():如果队列为空,返回True,反之False

      Queue.full():如果队列满了,返回True,反之False

      Queue.get([block[,timeout]]):获取队列中的一条消息,然后将其从队列中移除,block默认值为True

        1)如果block使用默认值,且没有设置timeout(单位秒),消息列队如果为空,此时程序将被阻塞(停在读取状态),直到从消息列队读到消息为止,如果设置了timeout,则会等待timeout秒,若还没读取到任何消息,则抛出"Queue.Empty"异常;

        2)如果block值为False,消息列队如果为空,则会立刻抛出"Queue.Empty"异常

      Queue.get_nowait():相当于Queue.get(False)

      Queue.put(item,[block[,timeout]]):将item消息写入队列,block默认为True

      1)如果block使用默认值,且没有设置timeout(单位秒),消息列队如果已经没有空间可写入,此时程序将被阻塞(停在写入状态),直到从消息列队腾出空间为止,如果设置了timeout,则会等待timeout秒,若还没空间,则抛出"Queue.Full"异常

      2)如果block值为False,消息列队如果没有空间可写入,则会立刻抛出"Queue.Full"异常

      Queue.put_nowait(item):相当于Queue.put(item,False)

# 父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读数据
from
multiprocessing import Process, Queue import os, time, random # 写数据进程执行的代码: def write(q): for value in ['A', 'B', 'C']: print('Put %s to queue...' % value) q.put(value) time.sleep(random.random()) # 读数据进程执行的代码: def read(q): while True: if not q.empty(): value = q.get(True) print('Get %s from queue.' % value) time.sleep(random.random()) else: break if __name__=='__main__': # 父进程创建Queue,并传给各个子进程: q = Queue() pw = Process(target=write, args=(q,)) pr = Process(target=read, args=(q,)) # 启动子进程pw,写入: pw.start() # 等待pw结束: pw.join() # 启动子进程pr,读取: pr.start() pr.join() # pr进程里是死循环,无法等待其结束,只能强行终止: print('') print('所有数据都写入并且读完')

      

 

 

   2.8 进程池Pool

    通过multiprocessing模块提供的Pool方法创建进程池

from multiprocessing import Pool
import os, time, random

def worker(msg):
    t_start = time.time()
    print("%s开始执行,进程号为%d" % (msg,os.getpid()))
    # random.random()随机生成0~1之间的浮点数
    time.sleep(random.random()*2) 
    t_stop = time.time()
    print(msg,"执行完毕,耗时%0.2f" % (t_stop-t_start))

print("----start----")

po = Pool(3)  # 定义一个进程池,最大进程数3
for i in range(0,10):
    # Pool().apply_async(要调用的目标,(传递给目标的参数元祖,))
    # 每次循环将会用空闲出来的子进程去调用目标
    po.apply_async(worker,(i,))


po.close()  # 关闭进程池,关闭后po不再接收新的请求
po.join()  # 等待po中所有子进程执行完成,必须放在close语句之后
print("-----end-----")

    

 

 

     multiporcessing.Pool常用函数解析:

    (1)apply_async(func[, args[, kwds]]) :使用非阻塞方式调用func(并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),args为传递给func的参数列表,kwds为传递给func的关键字参数列表

    (2)close():关闭Pool,使其不再接受新的任务

    (3)terminate():不管任务是否完成,立即终止

    (4)join():主进程阻塞,等待子进程的退出, 必须在close或terminate之后使用

    进程池中的Queue

      如果要使用Pool创建进程,就需要使用multiprocessing.Manager()中的Queue(),而不是multiprocessing.Queue(),否则会得到一条如下的错误信息:

      RuntimeError: Queue objects should only be shared between processes through inheritance

      

# 修改import中的Queue为Manager
from multiprocessing import Manager,Pool
import os,time,random

def reader(q):
    print("reader启动(%s),父进程为(%s)" % (os.getpid(), os.getppid()))
    for i in range(q.qsize()):
        print("reader从Queue获取到消息:%s" % q.get(True))

def writer(q):
    print("writer启动(%s),父进程为(%s)" % (os.getpid(), os.getppid()))
    for i in "itcast":
        q.put(i)

if __name__=="__main__":
    print("(%s) start" % os.getpid())
    q = Manager().Queue()  # 使用Manager中的Queue
    po = Pool()
    po.apply_async(writer, (q,))

    time.sleep(1)  # 先让上面的任务向Queue存入数据,然后再让下面的任务开始从中取数据

    po.apply_async(reader, (q,))
    po.close()
    po.join()
    print("(%s) End" % os.getpid())

      

 

 

  2.9 案例:多进程版本的文件夹复制器

    https://www.cnblogs.com/nuochengze/p/12639596.html

3、协程

  3.1 迭代器

    迭代是访问集合元素的一种方式,迭代器是一个可以记住遍历的位置的对象,迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束,迭代器只能往前不会后退

    可以通过for...in...这类语句迭代读取一条数据供我们使用的对象称之为可迭代对象(Iterable)。

    使用isinstance(item,Iterable)判断一个对象是否是Iterable对象。

    可迭代对象的本质:可迭代对象通过__iter__方法向我们提供一个迭代器,我们在迭代一个迭代对象的时候,实际上就是先获取该对象提供的一个迭代器,然后通过这个迭代器来依次获取对象中的每一个数据。

    一个具备了__iter__方法的对象,就是一个可迭代对象。

    通过iter()函数获取可迭代对象的迭代器,对获取到的迭代器不断使用next()函数来获取下一条数据。

    iter()函数实际上就是调用了可迭代对象的__iter__方法。

    迭代器用来帮助我们记录每次迭代访问到的位置,当我们对迭代器使用next()函数的时候,迭代器会向我们返回它所记录位置的下一个位置的数据。

    next()函数实际上就是调用了迭代器对象的__next__方法。

    python要求迭代器本身也是可迭代的,所以我们要为迭代器实现__iter__方法,而__iter__方法要返回一个迭代器,迭代器自身正是一个迭代器,所以迭代器的__iter__方法返回自身即可。

    一个实现了__iter__方法和__next__方法的对象,就是一个迭代器。

    

class MyList(object):
    """自定义的一个可迭代对象"""
    def __init__(self):
        self.items = []

    def add(self, val):
        self.items.append(val)

    def __iter__(self):
        myiterator = MyIterator(self)
        return myiterator


class MyIterator(object):
    """自定义的供上面可迭代对象使用的一个迭代器"""
    def __init__(self, mylist):
        self.mylist = mylist
        # current用来记录当前访问到的位置
        self.current = 0

    def __next__(self):
        if self.current < len(self.mylist.items):
            item = self.mylist.items[self.current]
            self.current += 1
            return item
        else:
            raise StopIteration

    def __iter__(self):
        return self


if __name__ == '__main__':
    mylist = MyList()
    mylist.add(1)
    mylist.add(2)
    mylist.add(3)
    mylist.add(4)
    mylist.add(5)
    for num in mylist:
        print(num)

    for...in...循环的本质:for item in Iterable 循环的本质就是先通过iter()函数获取可迭代对象Iterable的迭代器,然后对获取到的迭代器不断调用next()方法来获取下一个值并将其赋值给item,当遇到StopIteration的异常后循环结束

    除了for循环,list、tuple等也能接收可迭代对象:

    li = list(FibIterator(15))
    print(li)
    tp = tuple(FibIterator(6))
    print(tp)

  3.2 生成器

    生成器(generator)是一类特殊的迭代器。

    3.2.1 创建生成器的方法

      方法1:将列表推导的中括号[]换成圆括号()

      方法2:只要函数中有关键字yield就称为生成器。

def fib(n):
    current = 0
    num1,num2 = 0, 1
    while current<n:
        num = num1
        num1,num2 = num2,num1+num2
        current+=1
        yield num
    return 'done'
#在使用生成器实现的方式中,我们将原本在迭代器__next__方法中实现的基本逻辑放到一个#函数中来实现,但是将每次迭代返回数值的return换成了yield,此时新定义的函数便不再是#函数,而是一个生成器了

# 输出
 next(fib(5))

# 输出
for n in fib(5):
    print(n)

#但是用for循环调用generator时,发现拿不到generator的return语句的返回值。如果想#要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中
g = fib(5)
while True:
    try:
        x=next(g)
        print('value:{}'.format(x))
    except StopIteration as e:
        print('生成器返回值:{}'.format(e.value))
        break

# 总结
使用了yield关键字的函数不再是函数,而是生成器。(使用了yield的函数就是生成器)

yield关键字有两点作用:
(1)保存当前运行状态(断点),然后暂停执行,即将生成器(函数)挂起
(2)将yield关键字后面表达式的值作为返回值返回,此时可以理解为起到了return的作用

可以使用next()函数让生成器从断点处继续执行,即唤醒生成器(函数)

Python3中的生成器可以使用return返回最终运行的返回值,而Python2中的生成器不允许使用return返回一个返回值(即可以使用return从生成器中退出,但return后不能有任何表达式)

    3.2.2 使用send唤醒

      除了可以使用next()函数来唤醒生成器继续执行外,还可以使用send()函数来唤醒执行。使用send()函数的一个好处是可以在唤醒的同时向断点处传入一个附加数据。

      执行到yield时,gen函数作用暂时保存,返回i的值; temp接收下次c.send("python"),send发送过来的值,c.next()等价c.send(None)

      

  3.3 协程

    协程(Coroutine),又称微线程。

    协程是python中另外一种实现多任务的方式,它自带CPU上下文,比线程更小占用更小执行单元。

    协程和线程差异:在实现多任务时, 线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。 操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。 所以线程的切换非常耗性能。但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。

    协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,因此协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。

    并发编程中,协程和线程类似,每个协程表示一个执行单元,有自己的本地数据,与其他协程共享全局资源。

# 简单实现协程

import time

def work1():
    while True:
        print("----work1---")
        yield
        time.sleep(0.5)

def work2():
    while True:
        print("----work2---")
        yield
        time.sleep(0.5)

def main():
    w1 = work1()
    w2 = work2()
    while True:
        next(w1)
        next(w2)

if __name__ == "__main__":
    main()

# 运行结果
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
...省略...
 

  3.4 greenlet

    为了更好使用协程来完成多任务,python中的greenlet模块对其封装,从而使得切换任务变的更加简单

    

from greenlet import greenlet
import time

def test1():
    while True:
        print "---A--"
        gr2.switch()
        time.sleep(0.5)

def test2():
    while True:
        print "---B--"
        gr1.switch()
        time.sleep(0.5)

gr1 = greenlet(test1)
gr2 = greenlet(test2)

#切换到gr1中运行
gr1.switch()

# 运行结果
---A--
---B--
---A--
---B--
---A--
---B--
---A--
---B--
...省略...

  3.5 gevent

    python有一个比greenlet更强大的并且能够自动切换任务的模块gevent。

    gevent是一个基于协程的python网络函数库,使用greenlet在libev(libev是一个事件库)事件循环顶部提供了一个有高级别并发性的API。

    gevent的主要特性:(1)基于libev的快速事件循环,Linux上是epoll机制。

             (2)基于greenlet的轻量级执行单元

             (3)API复用了python标准库里的内容

             (4)支持SSL的协作式sockets

             (5)可通过线程池或c-ares实现DNS查询

             (6)通过monkey patching功能使得第三方模块变成协作式(from gevent import monkey; mokey.patch_all()----类似于补丁)

    其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。

    由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO,这就是协程一般比多线程效率高的原因。由于切换是在IO操作时自动完成的,所以gevent需要修改python自带的一些标准库,将一些常见的阻塞,如socket、select等地方实现协程跳转,这一过程在启动时通过monkey.patch_all()完成。

    spawn方法可以看成是用来形成协程,joinall方法就是添加这些协程任务并启动运行的。

# gevent同步执行
import gevent

def f(n):
    for i in range(n):
        print(gevent.getcurrent(), i)

g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()

# 结果
<Greenlet at 0x10e49f550: f(5)> 0
<Greenlet at 0x10e49f550: f(5)> 1
<Greenlet at 0x10e49f550: f(5)> 2
<Greenlet at 0x10e49f550: f(5)> 3
<Greenlet at 0x10e49f550: f(5)> 4
<Greenlet at 0x10e49f910: f(5)> 0
<Greenlet at 0x10e49f910: f(5)> 1
<Greenlet at 0x10e49f910: f(5)> 2
<Greenlet at 0x10e49f910: f(5)> 3
<Greenlet at 0x10e49f910: f(5)> 4
<Greenlet at 0x10e49f4b0: f(5)> 0
<Greenlet at 0x10e49f4b0: f(5)> 1
<Greenlet at 0x10e49f4b0: f(5)> 2
<Greenlet at 0x10e49f4b0: f(5)> 3
<Greenlet at 0x10e49f4b0: f(5)> 4
# gevent切换执行
import gevent

def f(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        #用来模拟一个耗时操作,注意不是time模块中的sleep
        gevent.sleep(1)

g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()

# 结果
<Greenlet at 0x7fa70ffa1c30: f(5)> 0
<Greenlet at 0x7fa70ffa1870: f(5)> 0
<Greenlet at 0x7fa70ffa1eb0: f(5)> 0
<Greenlet at 0x7fa70ffa1c30: f(5)> 1
<Greenlet at 0x7fa70ffa1870: f(5)> 1
<Greenlet at 0x7fa70ffa1eb0: f(5)> 1
<Greenlet at 0x7fa70ffa1c30: f(5)> 2
<Greenlet at 0x7fa70ffa1870: f(5)> 2
<Greenlet at 0x7fa70ffa1eb0: f(5)> 2
<Greenlet at 0x7fa70ffa1c30: f(5)> 3
<Greenlet at 0x7fa70ffa1870: f(5)> 3
<Greenlet at 0x7fa70ffa1eb0: f(5)> 3
<Greenlet at 0x7fa70ffa1c30: f(5)> 4
<Greenlet at 0x7fa70ffa1870: f(5)> 4
<Greenlet at 0x7fa70ffa1eb0: f(5)> 4
# 程序未打补丁
from gevent import monkey
import gevent
import random
import time

def coroutine_work(coroutine_name):
    for i in range(10):
        print(coroutine_name, i)
        time.sleep(random.random())

gevent.joinall([
        gevent.spawn(coroutine_work, "work1"),
        gevent.spawn(coroutine_work, "work2")
])

# 结果
work1 0
work1 1
work1 2
work1 3
work1 4
work1 5
work1 6
work1 7
work1 8
work1 9
work2 0
work2 1
work2 2
work2 3
work2 4
work2 5
work2 6
work2 7
work2 8
work2 9
# 程序打了补丁
from gevent import monkey
import gevent
import random
import time

# 有耗时操作时需要
monkey.patch_all()  # 将程序中用到的耗时操作的代码,换为gevent中自己实现的模块

def coroutine_work(coroutine_name):
    for i in range(10):
        print(coroutine_name, i)
        time.sleep(random.random())

gevent.joinall([
        gevent.spawn(coroutine_work, "work1"),
        gevent.spawn(coroutine_work, "work2")
])


# 结果 work1 0 work2 0 work1
1 work1 2 work1 3 work2 1 work1 4 work2 2 work1 5 work2 3 work1 6 work1 7 work1 8 work2 4 work2 5 work1 9 work2 6 work2 7 work2 8 work2 9

    gevent中还提供了对池的支持,当拥有动态数量的greenlet需要进行并发管理(限制并发数)时,就可以使用池,在处理大量的网络和IO操作时时非常需要。

from gevent.pool import Pool
from gevent import monkey
monkey.patch_all()


def proc_some(args):
    pass


if __name__=="__main__":
    pool = Pool(2)
    str_list = ["hahah", 'hahahaaaa']
    results = pool.map(proc_some,str_list)
    print(results)

4、总结

  1. 进程是资源分配的单位
  2. 线程是操作系统调度的单位
  3. 进程切换需要的资源很最大,效率很低
  4. 线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)
  5. 协程切换任务资源很小,效率高
  6. 多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中 所以是并发
posted @ 2020-04-06 00:28  Norni  阅读(958)  评论(0编辑  收藏  举报