• 十五、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核数不一样可能是并行的,但是协程是在一个线程中 所以是并发
  • 相关阅读:
    Java类加载机制
    DAY18
    DAY17
    DAY16
    DAY15
    DAY14
    DAY13
    DAY12
    DAY11
    DAY10
  • 原文地址:https://www.cnblogs.com/nuochengze/p/12640152.html
Copyright © 2020-2023  润新知