美文网首页
深入浅出Python多任务(线程,进程,协程)

深入浅出Python多任务(线程,进程,协程)

作者: 李白开水 | 来源:发表于2020-08-13 20:00 被阅读0次

    篇幅较长!

    导入

    看一下下面的程序:

    import time
    
    def sing():
        for i in range(5):
            print("sing....")
            time.sleep(1)
    
    
    def dance():
        for i in range(5):
            print("dancing...")
            time.sleep(1)
    
    
    def main():
        sing()
        dance()
    
    
    if __name__ == "__main__":
        main()
    

    执行这个程序,输出结果如下:

    $ python test01.py
    sing....
    sing....
    sing....
    sing....
    sing....
    dancing...
    dancing...
    dancing...
    dancing...
    dancing...
    

    一共花费了十秒。
    修改程序为多任务的程序:

    import time
    import threading
    
    def sing():
        for i in range(5):
            print("sing....")
            time.sleep(1)
    
    
    def dance():
        for i in range(5):
            print("dancing...")
            time.sleep(1)
    
    
    def main():
        t1 = threading.Thread(target=sing)
        t2 = threading.Thread(target=dance)
        t1.start()
        t2.start()
    
    
    if __name__ == "__main__":
        main()
    

    执行这个程序:

    $ python test02.py
    sing....
    dancing...
    dancing...
    sing....
    dancing...
    sing....
    dancing...
    sing....
    dancing...
    sing....
    

    所谓的多任务,就是同时可以做多件事情。
    一台计算机同时可以做多少件事情,是由CPU觉得的,如果CPU的双核的,说明同时可以做两件事情,如果是四核的,就可以同时做四件事情。

    如果计算机是单核的,怎么实现多任务?
    一个比较好理解的办法是,时间片轮转,比如当前有四个程序,CPU是一核的,那么就让第一个程序先执行0.00001s,然后让下一个程序再执行0.00001s,这样每个程序都执行了0.00001s之后,再来执行第一个程序。因为人是察觉不到0.0001s的切换的,所以在我们看来,程序就像在“一起”执行一样,这样的多任务,叫并发,并发是加到多任务。

    如果计算机是双核的,现在有两个程序,那么这两个程序可以同时执行,这样真的一起同时在执行叫做“并行”,并行是真的多任务。

    线程

    那么怎样才能让Python程序完成多任务呢。
    线程就是实现多任务的一种手段。

    1.threading模块

    在Python中有一个模块是threading,这个模块中有一个类是Thread,用法如下:

    def test():
        while True:
            print("123456")
    
    # target接收的是函数名,不是函数的调用
    t1 = threading.Thread(target = test)
    t1.start()
    

    类名+“()”就创建了一个对象,这个对象就是之后要启动的线程。
    当t1.start()的时候,这个线程就真正开始创建并被执行。
    还是这个程序:

    import time
    import threading
    
    def sing():
        for i in range(5):
            print("sing....")
            time.sleep(1)
    
    
    def dance():
        for i in range(5):
            print("dancing...")
            time.sleep(1)
    
    
    def main():
        t1 = threading.Thread(target=sing)
        t2 = threading.Thread(target=dance)
        t1.start()
        t2.start()
    
    
    if __name__ == "__main__":
        main()
    

    主线程从上往下执行,当走到def sing的时候,不进入函数执行,因为这是一个函数的定义,继续往下走,到dance的时候也不执行,走到main也不执行,遇到if __name__ == "__main__":执行调用里面的main(),然后进入main这个函数。
    t1 = threading.Thread(target=sing)的时候,创建了一个对象,把它赋给了t1,遇到t2 = threading.Thread(target=dance)的时候,创建了一个对象,把它赋给了t2。
    当主线程运行到t1.start()的时候,主线程创建了一个子线程,这个子线程去执行sing函数,主线程继续往下执行,当遇到t2.start()的时候,主线程创建另外一个子线程,这个子线程去执行dance函数。
    主线程往下就没有执行的代码了,这时候主线程会等待子线程执行结束,然后主线程再结束。

    2.查看线程的数量

    threading模块中有一个有一个方法是enumerate,只要调用threading.enumerate()他的返回值就是一个列表,这个列表中的元素就是主线程和子线程。
    修改程序如下:

    import threading
    
    def sing():
        for i in range(5):
            print("sing....")
    
    
    def dance():
        for i in range(5):
            print("dancing...")
    
    
    def main():
        t1 = threading.Thread(target=sing)
        t2 = threading.Thread(target=dance)
        t1.start()
        t2.start()
        print(threading.enumerate())
    
    
    if __name__ == "__main__":
        main()
    

    然后执行程序:

    $ python test01.py
    sing....
    sing....
    sing....
    sing....
    sing....
    dancing...
    dancing...
    dancing...
    dancing...
    dancing...
    [<_MainThread(MainThread, started 9080)>]
    

    发现打印出来的线程里面只有一个是主线程,没有其他两个子线程。
    原因是当主线程走到t1.start()的时候创建了一个子线程,在t2.start()的时候又创建了一个子线程,然后主线程继续往下走,走到print,这时候这个程序有三个线程,又因为这三个线程都是没有延迟的,所以先让哪个线程执行就取决于操作系统,操作系统在调度这三个线程的时候,让谁先执行是不确定的。所以线程的执行是没有先后顺序的。
    如果想让某个线程先执行可以采用一个方法就是让其他的线程延时。
    想要看到系统什么时刻有哪几个线程在运行,方法如下:
    修改代码如下:

    import threading
    import time
    
    def sing():
        for i in range(5):
            print("sing....第%d秒--" % i)
            time.sleep(1)
    
    
    def dance():
        for i in range(10):
            print("dancing...第%d秒--" % i)
            time.sleep(1)
    
    
    def main():
        t1 = threading.Thread(target=sing)
        t2 = threading.Thread(target=dance)
    
        t1.start()
        t2.start()
    
        while True:
            print(threading.enumerate())
            time.sleep(1)
            if len(threading.enumerate()) <= 1:
                break
    
    
    if __name__ == "__main__":
        main()
    

    循环打印当前系统中的线程,如果当前系统中只剩下一个线程,就是主线程,那么就退出循环。
    执行程序:

    $ python test01.py
    sing....▒▒0▒▒--
    dancing...▒▒0▒▒--
    [<_MainThread(MainThread, started 14184)>, <Thread(Thread-1, started 12124)>, <Thread(Thread-2, started 16304)>]
    dancing...▒▒1▒▒--
    [<_MainThread(MainThread, started 14184)>, <Thread(Thread-1, started 12124)>, <Thread(Thread-2, started 16304)>]
    sing....▒▒1▒▒--
    sing....▒▒2▒▒--
    dancing...▒▒2▒▒--
    [<_MainThread(MainThread, started 14184)>, <Thread(Thread-1, started 12124)>, <Thread(Thread-2, started 16304)>]
    dancing...▒▒3▒▒--
    sing....▒▒3▒▒--
    [<_MainThread(MainThread, started 14184)>, <Thread(Thread-1, started 12124)>, <Thread(Thread-2, started 16304)>]
    dancing...▒▒4▒▒--
    sing....▒▒4▒▒--
    [<_MainThread(MainThread, started 14184)>, <Thread(Thread-1, started 12124)>, <Thread(Thread-2, started 16304)>]
    dancing...▒▒5▒▒--
    [<_MainThread(MainThread, started 14184)>, <Thread(Thread-2, started 16304)>]
    [<_MainThread(MainThread, started 14184)>, <Thread(Thread-2, started 16304)>]
    dancing...▒▒6▒▒--
    dancing...▒▒7▒▒--
    [<_MainThread(MainThread, started 14184)>, <Thread(Thread-2, started 16304)>]
    dancing...▒▒8▒▒--
    [<_MainThread(MainThread, started 14184)>, <Thread(Thread-2, started 16304)>]
    dancing...▒▒9▒▒--
    [<_MainThread(MainThread, started 14184)>, <Thread(Thread-2, started 16304)>]
    

    可以看到每一秒钟程序的线程,前五秒是三个线程,到了第五秒,程序里面有两个线程,因为循环5次,每次休眠1秒的sing线程结束了,然后到了第十秒,第二个子线程也执行完了,程序中就只剩一个主线程,所以退出循环不打印了。这时候主线程也结束,程序结束。
    所以如果创建Thread来执行函数,当这个函数执行完,这个子线程也就结束了。
    主线程结束,程序就结束了。所以主线程会等待所有的子线程执行结束再结束。

    3.子线程是什么时候被创建的,什么时候被执行的

    In [1]: import threading
    
    In [2]: def test():
       ...:     print("------1--------")
       ...:
    
    In [3]: t1 = threading.Thread(target=test)
    
    In [4]: t1.start()
    ------1--------
    

    当t1被赋值的时候,test函数并没有被调用没有被执行,而是start的时候test函数才被执行,所以说明线程是在被调用的时候才执行。
    那线程是什么时候被创建的?
    t1 = threading.Thread(target=test)的时候被创建的,还是t1.start()的时候被创建的?

    import threading
    import time
    
    def sing():
        for i in range(5):
            print("sing....")
            time.sleep(1)
    
    def main():
        print(threading.enumerate())
        t1 = threading.Thread(target=sing)
        print(threading.enumerate())
        t1.start()
        print(threading.enumerate())
    
    if __name__ == "__main__":
        main()
    

    t1 = threading.Thread(target=sing)之前查看当前程序有多少线程,在t1 = threading.Thread(target=sing)之后查看当前程序有多少线程,在start之后再查看一次。
    运行程序:

    $ python test01.py
    [<_MainThread(MainThread, started 11772)>]
    [<_MainThread(MainThread, started 11772)>]
    sing....
    [<_MainThread(MainThread, started 11772)>, <Thread(Thread-1, started 11456)>]
    sing....
    sing....
    sing....
    sing....
    

    在start前打印的两次线程中,都没有线程Thread-1,说明当t1.start()的时候,线程才真正被创建和执行。
    所以当调用Thread的时候,不会创建线程,当调用Thread创建出来的实例对象的start方法的时候线程才会被创建,以及开始运行线程。

    总结:

    • 如果想完成多任务,就可以通过Thread创建一个对象,这个对象一调用start,子线程就会被创建和执行,这个对象执行什么就看传递的target是哪个函数名。当这个被执行的函数结束了,这个子线程就结束了。
    • 线程真正创建是start,真正结束是函数结束。
    • 多个线程被创建之后,执行顺序是不确定的,执行顺序取决于操作系统,如果想指定执行的先后顺序,可以通过延时来实现。
    • 主线程最后结束,因为主线程结束程序就结束了。
      创建Thread对象这个过程可以理解为线程的准备工作。

    4.target也可以是一个类

    当target是一个类的时候,写法就有变化了。
    例如:

    import threading
    import time
    
    class MyThread(threading.Thread):
        def run(self):
            for i in range(5):
                print(i)
                time.sleep(i)
    
    def main():
        t = MyThread()
        t.start()
        
    if __name__ == "__main__":
        main()
    

    用类来创建线程的时候,直接把类实例化一个对象传给t就可以,调用的时候还是t.start()就可以。
    这个类必须要继承threading.Thread,这个类里必须定义run方法,线程start之后,会自动调用run方法,执行run里面的代码。
    类中没有定义start方法,这个start方法是继承自Thread的方法。

    所以创建线程有两种方式:

    1. t1 = threading.Thread(target=函数名)
    • 函数里面的代码是什么,线程就去执行什么
    1. 用类
      定义一个类,这个类必须继承threading.Thread,而且这个类必须实现run方法,这个run方法中写了什么,线程就去执行什么。
      这种方式适合一个线程做的事情比较复杂,而且涉及多个函数,一般就把这些函数封装成一个类。
      相比较而言,函数的方式更加简单。

    如果类中还有其他函数想要在线程执行的适合执行,那么这个函数的调用可以写在run函数中。

    5.多个线程之间共享全局变量

    修改全局变量前提:

    在一个函数中,对全局变量进行修改的时候,到底是否需要使用global取决于是否对变量的指向进行了修改。

    • 如果进行了修改,即让全局变量指向了一个新的地方,那么必须使用global
    • 如果仅仅是修改了全局变量指向空间的数据,就不必须使用global
      能不能修改还看全局变量是否可变,数字、字符串、元组不可变

    例如,如果全局变量是数字:

    In [9]: num = 100
    
    In [10]: def test():
        ...:     global num
        ...:     num += 100
        ...:
    
    In [11]: print(num)
    100
    
    In [12]: test()
    
    In [13]: print(num)
    200
    

    此时修改全局变量就要加global。
    如果不加:

    In [16]: num = 100
    
    In [17]: def test():
        ...:     num += 100
        ...:
    
    In [18]: print(num)
    100
    
    In [19]: test()
    ---------------------------------------------------------------------------
    UnboundLocalError                         Traceback (most recent call last)
    <ipython-input-19-fbd55f77ab7c> in <module>
    ----> 1 test()
    
    <ipython-input-17-fabd8b626a8d> in test()
          1 def test():
    ----> 2     num += 100
          3
    
    UnboundLocalError: local variable 'num' referenced before assignment
    
    In [20]: print(num)
    100
    

    如果全局变量是可变对象,不修改它的指向:

    In [22]: nums = [11,22]
    
    In [23]: def test():
        ...:     nums.append(33)
        ...:
    
    In [24]: print(nums)
    [11, 22]
    
    In [25]: test()
    
    In [26]: print(nums)
    [11, 22, 33]
    

    如果全局变量是可变对象,修改它的指向:

    In [28]: nums = [11,22]
    
    In [29]: def test():
        ...:     nums += [33]
        ...:
    
    In [30]: print(nums)
    [11, 22]
    
    In [31]: test()
    ---------------------------------------------------------------------------
    UnboundLocalError                         Traceback (most recent call last)
    <ipython-input-31-fbd55f77ab7c> in <module>
    ----> 1 test()
    
    <ipython-input-29-8d9294d083c5> in test()
          1 def test():
    ----> 2     nums += [33]
          3
    
    UnboundLocalError: local variable 'nums' referenced before assignment
    
    In [32]: print(nums)
    [11, 22]
    

    验证线程之间是共享全局变量的方法:
    1.创建两个线程,一个用来修改全局变量,一个在修改完全局变量后打印这个全局变量。

    import threading
    import time
    
    # 全局变量
    num = 100
    
    def test01():
        global num
        num += 100
        print("test01:   num = %d" % num)
    
    
    def test02():
        print("test02:   num = %d" % num)
    
    def main():
        t1 = threading.Thread(target=test01)
        t2 = threading.Thread(target=test02)
    
        t1.start()
        time.sleep(1)
        t2.start()
        time.sleep(1)
    
        print("main thread:     num = %d" % num)
    
    if __name__ == "__main__":
        main()
    

    执行:

    $ python test01.py
    test01:   num = 200
    test02:   num = 200
    main thread:     num = 200
    

    说明线程之间是共享全局变量的。
    2.把全局变量当作参数传给函数
    修改程序如下:

    import threading
    import time
    
    def test01(tmp1):
        tmp1.append(33)
        print("test01:   tmp = %s" % str(tmp1))
    
    def test02(tmp2):
        print("test02:   tmp = %s" % str(tmp2))
    
    
    nums = [11,22]
    
    def main():
        t1 = threading.Thread(target=test01,args=(nums,))
        t2 = threading.Thread(target=test02,args=(nums,))
    
        t1.start()
        time.sleep(1)
        t2.start()
        time.sleep(1)
    
        print("main thread:     nums = %s" % str(nums))
    
    if __name__ == "__main__":
        main()
    

    args是要传递给函数的数据,这个数据必须是个元组,就是需要写括号,可以传递多个参数,参数最后多加一个逗号“,”,不然会报错

    运行这个程序:

    $ python test01.py
    test01:   tmp = [11, 22, 33]
    test02:   tmp = [11, 22, 33]
    main thread:     nums = [11, 22, 33]
    

    6.共享全局变量可能遇到的问题:资源竞争

    可能遇到资源竞争的问题。
    定义两个函数,他们都用来把全局变量加1,循环100次,这样如果这两个函数都执行完,如果全局变量一开始是0,那么执行完两个函数就应该是200。
    程序如下:

    import threading
    import time
    
    # 全局变量
    global_num = 0
    
    def test01(num):
        global global_num
        for i in range(num):
            global_num += 1
        print("test01:   num = %d" % global_num)
    
    
    def test02(num):
        global global_num
        for i in range(num):
            global_num += 1
        print("test02:   num = %d" % global_num)
    
    def main():
        t1 = threading.Thread(target=test01,args=(100,))
        t2 = threading.Thread(target=test02,args=(100,))
    
        t1.start()
        t2.start()
    
        time.sleep(5)
        print("main thread:     num = %d" % global_num)
    
    if __name__ == "__main__":
        main()
    

    执行程序:

    $ python test01.py
    test01:   num = 100
    test02:   num = 200
    main thread:     num = 200
    

    这时候执行结果是正确的。
    如果不是循环100次,而是循环1000000次,那么结果应该是2000000。
    修改程序:

    import threading
    import time
    
    # 全局变量
    global_num = 0
    
    def test01(num):
        global global_num
        for i in range(num):
            global_num += 1
        print("test01:   num = %d" % global_num)
    
    
    def test02(num):
        global global_num
        for i in range(num):
            global_num += 1
        print("test02:   num = %d" % global_num)
    
    def main():
        t1 = threading.Thread(target=test01,args=(1000000,))
        t2 = threading.Thread(target=test02,args=(1000000,))
    
        t1.start()
        t2.start()
    
        time.sleep(5)
        print("main thread:     num = %d" % global_num)
    
    if __name__ == "__main__":
        main()
    

    执行程序:

    $ python test01.py
    test01:   num = 1132201
    test02:   num = 1402099
    main thread:     num = 1402099
    

    这时候执行结果就出错了。
    因为global_num += 1这个语句在执行的时候,会分成几步,1.先取到global_num的值。2.把这个值加一。3.把这个值存起来。
    操作系统如果使用了时间片轮转法,在global_num等于0的时候,线程1取到了0这个值,然后把它加一,变成了1,这时候操作系统去执行线程2,取到的值还是0,把它加一,然后存储起来,这时候global的值为1,操作系统再去执行线程1,线程1把它计算的1保存,这时候global的值依旧为1,所以虽然global的值被计算了两次,但是实际得到的结果是错误的。

    所以如果多线程共享全局变量,而且同一时刻都在操作全局变量,就可能出现问题,这就是资源竞争。

    7.解决资源竞争问题

    可以通过线程同步来解决问题。

    同步就是协同步调,协同就是一个先执行,再执行另一个,互相配合着执行。

    同步可以用互斥锁来实现。

    互斥锁就是当线程1要修改全局变量之前,先把这个全局变量上锁,这样其他的线程就无法修改这个数据,当前线程1用完这个全局变量后,再把锁解开。

    互斥锁的创建:

    # 创建所
    mutex = threading.Lock()
    
    # 锁定
    mutex.acquire()
    
    # 释放
    mutex.release()
    

    如果一个数据是没有上锁的,那么acquire不会堵塞。
    如果这个数据已经被其他线程锁定了,那么此时再上锁(acquire),会堵塞,当其他线程解锁之后,当前的线程才能上锁。

    互斥锁解决资源竞争问题:
    创建一个全局变量是锁:

    import threading
    import time
    
    # 全局变量
    global_num = 0
    
    def test01(num):
        global global_num
        # 上锁
        mutex.acquire()
        for i in range(num):
            global_num += 1
        mutex.release()
        print("test01:   num = %d" % global_num)
    
    
    def test02(num):
        global global_num
        mutex.acquire()
        for i in range(num):
            global_num += 1
        mutex.release()
        print("test02:   num = %d" % global_num)
    
    
    # 创建一个互斥锁,默认没有上锁
    mutex = threading.Lock()
    
    def main():
        t1 = threading.Thread(target=test01,args=(1000000,))
        t2 = threading.Thread(target=test02,args=(1000000,))
    
        t1.start()
        t2.start()
    
        time.sleep(5)
        print("main thread:     num = %d" % global_num)
    
    if __name__ == "__main__":
        main()
    

    执行程序:

    $ python test01.py
    test01:   num = 1000000
    test02:   num = 2000000
    main thread:     num = 2000000
    

    这时候结果是正确的,线程1和线程2不一定哪个线程会先抢到这把锁,如果一个线程给全局变量上锁了,另一个线程只能等待这个线程解锁,才能上锁,对全局变量进行操作。

    上锁有一个原则是锁定的代码越少越好,所以修改程序如下:

    import threading
    import time
    
    # 全局变量
    global_num = 0
    
    def test01(num):
        global global_num
        for i in range(num):
            mutex.acquire()
            global_num += 1
            mutex.release()
        print("test01:   num = %d" % global_num)
    
    
    def test02(num):
        global global_num
        for i in range(num):
            mutex.acquire()
            global_num += 1
            mutex.release()
        print("test02:   num = %d" % global_num)
    
    
    # 创建一个互斥锁,默认没有上锁
    mutex = threading.Lock()
    
    def main():
        t1 = threading.Thread(target=test01,args=(1000000,))
        t2 = threading.Thread(target=test02,args=(1000000,))
    
        t1.start()
        t2.start()
    
        time.sleep(5)
        print("main thread:     num = %d" % global_num)
    
    if __name__ == "__main__":
        main()
    

    执行程序:
    执行第一遍:

    $ python test01.py
    test01:   num = 1921277
    test02:   num = 2000000
    main thread:     num = 2000000
    

    执行第二遍:

    $ python test01.py
    test01:   num = 1884133
    test02:   num = 2000000
    main thread:     num = 2000000
    

    这是因为,因为加锁只加了global_num += 1这一行代码,所以有可能出现在线程1执行完之前,线程1给global_num 加了一些值,线程2也给global加了一些值,所以到线程1执行完之前,global被加了超过1000000次,所以有了这样的结果。

    8.互斥锁带来的死锁问题

    如果有两个资源,资源A和资源B,有两个线程,线程1和线程2。
    线程1要先使用资源A,然后使用资源B,线程2要先使用资源B,再使用资源A。
    如果线程1先给A上了锁,然后使用了A,进行一些操作,与此同时,线程2给B上了锁,进行了一些操作。
    这时候线程1要使用资源B,发现资源B被上锁了,那线程1就等待线程2解锁。
    线程2要使用资源A,发现资源A被上锁了,那线程2就等待线程1解锁。
    两个线程就一直等待互相释放资源,这种现象就是死锁。

    如何解决死锁问题
    1.银行家算法:设计程序时尽量避免死锁
    2.添加超时时间

    银行家算法:
    如果一个银行家有10块钱,有三个客户要贷款,客户A要贷款9块钱,客户B要贷款3块钱,客户C要贷款8块钱。
    那么这个时候,银行家手里的钱不足以让三个客户都拿到贷款。
    那么这个时候,先借给客户A 2块钱,借给客户B 2块钱,借给客户C 4块钱,这时候银行家手里还有2块钱。

    银行家 客户A(9) 客户B(3) 客户C(8)
    10 0 0 0
    2 2 2 4

    这时候银行家手里的2块钱借给客户B 1块钱,告诉其他的客户剩下的前过几天再借给他。

    银行家 客户A(9) 客户B(3) 客户C(8)
    10 0 0 0
    2 2 2 4
    1 2 3(满足) 4

    和客户B约定好还钱的时间,当客户B归还了3块钱的时候,银行家手里就有了4块钱,把这4块钱再借给客户C。

    银行家 客户A(9) 客户B(3) 客户C(8)
    10 0 0 0
    2 2 2 4
    1 2 3(满足) 4
    4 2 (已归还) 4
    0 2 (已归还) 8

    这之后客户C用完了钱,归还后再借给A 7块钱。

    银行家 客户A(9) 客户B(3) 客户C(8)
    10 0 0 0
    2 2 2 4
    1 2 3(满足) 4
    4 2 (已归还) 4
    0 2 (已归还) 8
    8 2 (已归还) (已归还)
    1 9 (已归还) (已归还)

    最后客户A归还了钱,银行家收获了利息。

    所以,每个客户必须一开始就声明他们所要借款或贷款的总额,然后银行家根据资源的情况和客户的情况先算好什么时候借给谁,什么时候谁归还。
    这个想法应用到操作系统,操作系统就是这个银行家,它必须提前计算好每个线程何时上锁,何时解锁,这样就在程序执行之前避免了死锁。

    添加超时时间
    就是为死锁设定一个超时时间,如果两个线程产生了死锁,到达这个超时时间之后,采用kill线程的方式解开死锁。

    进程

    实现多任务的另一种方式。
    程序是静态的,是一个exe文件或者是其他的东西,运行起来就是进程,一个进程包含多个线程。
    一个程序一般来说可以开多个,比如QQ程序,打开之后就是多个QQ进程。
    进程是启动的程序,所以进程比程序多拥有了资源,比如QQ进程可以使用内存资源,可以通过网卡连接网络,以及鼠标键盘等资源。所以进程是一个资源分配的单位。

    1.使用进程实现多任务

    程序如下:

    import threading
    import time
    import multiprocessing
    
    def test1():
        while True:
            print("=======111111=======")
            time.sleep(1)
    
    def test2():
        while True:
            print("=======222222=======")
            time.sleep(1)
    
    def main():
        t1 = multiprocessing.Process(target=test1)
        t2 = multiprocessing.Process(target=test2)
    
        t1.start()
        t2.start()
    
    if __name__ == "__main__":
        main()
    

    运行程序:

    D:\>python test02.py
    =======111111=======
    =======222222=======
    =======222222=======
    =======111111=======
    =======222222=======
    =======111111=======
    =======111111=======
    =======222222=======
    =======222222=======
    =======111111=======
    =======111111=======
    =======222222=======
    =======222222=======
    =======111111=======
    =======222222=======
    =======111111=======
    =======222222=======
    

    此时如果是Windows系统可以在另外一个终端输入tasklist命令来查看当前系统运行的所有进程。
    一部分如下:

    映像名称                       PID 会话名              会话'#       内存使用
    ========================= ======== ================ =========== ============
    chrome.exe                   11504 Console                    1     51,440 K
    chrome.exe                   10496 Console                    1     21,892 K
    python.exe                    7680 Console                    1     11,908 K
    python.exe                    5356 Console                    1     12,032 K
    python.exe                    8984 Console                    1     12,036 K
    tasklist.exe                  6004 Console                    1      9,028 K
    

    当结束程序,再次查看:

    映像名称                       PID 会话名              会话'#       内存使用
    ========================= ======== ================ =========== ============
    conhost.exe                  11024 Console                    1     16,896 K
    chrome.exe                   11224 Console                    1     91,268 K
    chrome.exe                   12280 Console                    1     71,120 K
    chrome.exe                    1608 Console                    1     66,956 K
    chrome.exe                   11504 Console                    1     51,436 K
    chrome.exe                   10496 Console                    1     21,896 K
    tasklist.exe                  7740 Console                    1      9,016 K
    

    发现python的程序已经没有了。
    如果是linux系统可以用ps -aux来查看。
    Windows系统用taskkill /pid 端口 /F来停止进程:

    D:\>taskkill /pid 1996 /F
    成功: 已终止 PID 为 1996 的进程。
    

    linux系统可以用kill + 进程ID来停止进程。

    当主进程从上到下扫描代码,开始执行时,执行到t1.start()这一行,会创建一个新的子进程,这个新的子进程拥有另一份属于自己的资源,新的子进程被创建的时候,要修改的东西会拷贝一份自己的,不修改的就不拷贝了,共享代码(因为在运行过程中不会修改代码),也就是说能共享的就会共享,不能共享的就复制一份自己的(所以有一个概念是“写时拷贝”,即修改的时候拷贝)。所以多进程会占用较大的资源,所以进程数不是越多越好。

    所以在执行刚刚的程序时,可以看到三个Python进程,一个是主进程,另外两个是子进程。

    2.进程和线程的对比

    进程:是资源的总称,包括代码,包括内存等。
    线程:比较轻量级,线程之间资源共享。

    • 进程仅仅是一个资源分配的单位,是一个资源总和,而线程是操作系统调度的单位。
    • 多线程是在同一份资源的前提下执行代码,而多进程是多份资源,同一份代码或多份代码,各自使用各自的资源去执行。
    • 每一个进程都至少拥有一个线程(主线程),真正去执行的是线程。也就是说每创建一个进程,这个进程就会有一个主线程去使用资源执行代码。
    • 进程依赖与进程,比如一个网易云音乐运行之后是一个进程,这个进程可以开启多个线程,比如下载歌曲线程和播放歌曲线程,这两个线程之间共享资源,下载后的歌曲可以由播放线程来播放,但是一旦关闭网易云,就是关闭了进程,线程也就不存在了。
    • 进程之间是互相独立的,比如QQ音乐和网易云音乐之间是独立的。

    3.使用队列完成进程间通信

    如果想使用多进程来实现多任务,比如想用一个进程来下载音乐,一个进程来播放下载好的音乐,那进程之间就需要进行通信。
    进程间通信的一种方式是队列:Queue。

    Queue如何使用:

    # 使用Queue要导入这个模块
    import multiprocessing
    
    # 创建队列,括号中可以填一个数字,表示创建的队列中可以放多少元素,队列中的元素可以放不同数据类型的数据
    q = multiprocessing.Queue()
    q = multiprocessing.Queue(3)
    
    # 往队列中存放数据
    q.put()
    
    # 从队列中取数据,如果这时候队列为空,q.get()就会一直等待,等到队列中有数据了再取出来。
    q.get()
    
    # 如果不想等待可以使用get_nowait,但是如果这时候队列中没有数据,就会抛异常。
    q.get_nowait()
    
    # 判断队列是否为空,如果为空,返回True,否则返回False
    q.empty()
    
    # 判断队列是否已满,已满则返回True,否则返回False
    q.full()
    

    进程之间通过Queue通信的实现:
    一个进程用来下载数据,这个进程把下载好的数据存放在队列中,另一个进程从这个队列取数据来进行操作。
    代码:

    import multiprocessing
    
    def download(q):
        data = [11, 22, 33, 44]
        for tmp in data:
            q.put(tmp)
        print("download OK!")
    
    def use(q):
        res = []
        while not q.empty():
            res.append(q.get())
        print("res is:")
        print(res)
    
    
    def main():
        q = multiprocessing.Queue()
    
        p1 = multiprocessing.Process(target=download, args=(q,))
        p2 = multiprocessing.Process(target=use, args=(q,))
    
        p1.start()
        p2.start()
    
    if __name__ == "__main__":
        main()
    
    
    

    运行程序:

    $ python test02.py
    download OK!
    res is:
    [11, 22, 33, 44]
    

    4.进程池

    进程的创建和销毁需要消耗很多资源。所以为了减少创建和消耗进程的次数,使用进程池,先创建好固定数量的进程,如果需要执行程序,让这些进程去执行,当执行结束后,把这些进程再放入进程池,这样可以反复利用创建好的进程,不用反复的创建和销毁。

    进程池Pool的使用

    # Pool也是在multiprocessing这个包中
    from multiprocessing import Pool
    
    # 传递参数,进程池里放多少进程,也可以不传参数
    pool = Pool(3)
    
    # pool.apply_async(调用的目标,(传递的参数元组,)) 这里的逗号是必须的。
    # 用进程池里空闲的子进程去调用目标,如果当前没有空闲的子进程,任务也会被添加到进程池里,等待子进程空闲的时候去执行
    pool.apply_async(work,(i,))
    
    # 关闭进程池,不再接收新的请求
    pool.close()
    
    # 等待所有的子进程结束再结束程序,必须放在close语句之后,如果没有这句话,不能保证子进程都执行完毕。因为通过进程池创建的子进程,主进程不会自动等待他们执行完毕。
    pool.join()
    
    from multiprocessing import Pool
    import time
    
    def work(num):
        print("------start-------")
        time.sleep(1)
        print(num)
        print("------end---------")
    
    def main():
        pool = Pool(3)
        for i in range(10):
            pool.apply_async(work,(i,))
    
        print("------come to close---------")
        pool.close()
        pool.join()
        print("------come to an end--------")
    
    if __name__ == "__main__":
        main()
    

    运行程序:

    $ python test03.py
    ------start-------
    2
    ------end---------
    ------start-------
    5
    ------end---------
    ------start-------
    6
    ------end---------
    ------start-------
    0
    ------end---------
    ------start-------
    4
    ------end---------
    ------start-------
    7
    ------end---------
    ------start-------
    1
    ------end---------
    ------start-------
    3
    ------end---------
    ------start-------
    8
    ------end---------
    ------start-------
    9
    ------end---------
    ------come to close---------
    ------come to an end--------
    

    可以看到,进程的调度顺序也是不确定的。

    5.多进程实现复制文件夹下的多个文件

    1.获取要复制的文件夹的名字
    2.创建一个新的文件夹
    3.获取文件夹所有要复制的文件名字
    4.创建进程池,主进程往进程池里添加要复制的文件
    5.子进程把文件复制到新的文件夹中去

    当前有如下文件夹及文件:

    /d/test:
    $ ls
    1.py  2.py  3.py  4.py  5.py  6.py  7.py  8.py  9.py
    

    代码:

    import os
    import multiprocessing
    
    def copy(file, old_folder, new_folder):
        old_f =  open(old_folder + "/" + file, "rb")
        content = old_f.read()
        old_f.close()
    
        new_f = open(new_folder + "/" + file, "wb")
        new_f.write(content)
        new_folder.close()
    
    
    def main():
        source_folder = 'test'
    
        try:
            new_folder = source_folder + "_copy"
            os.mkdir(new_folder)
        except:
            pass
    
        files = os.listdir(source_folder)
    
        pool = multiprocessing.Pool(5)
    
        for f in files:
            pool.apply_async(copy, (f, source_folder, new_folder))
    
        pool.close()
        pool.join()
    
    if __name__ == '__main__':
        main()
    

    运行程序:

    $ python test04.py
    

    查看当前目录:

     test.py      
     test_copy/ 
    

    已经成功复制了test,进入test_copy目录下,看看文件是否复制成功:

    $ ls test_copy/
    1.py  2.py  3.py  4.py  5.py  6.py  7.py  8.py  9.py
    

    已经成功复制了。
    改进:

    import os
    import multiprocessing
    
    def copy(queue, file, old_folder, new_folder):
        old_f =  open(old_folder + "/" + file, "rb")
        content = old_f.read()
        old_f.close()
    
        new_f = open(new_folder + "/" + file, "wb")
        new_f.write(content)
        new_folder.close()
        queue.put(file)
    
    
    def main():
        source_folder = 'test'
    
        try:
            new_folder = source_folder + "_copy"
            os.mkdir(new_folder)
        except:
            pass
    
        files = os.listdir(source_folder)
    
        pool = multiprocessing.Pool(5)
        queue = multiprocessing.Manager().Queue()
    
        for f in files:
            pool.apply_async(copy, (queue, f, source_folder, new_folder))
    
        pool.close()
        # pool.join()
        lenth = len(files)
        num = 0
        while True:
            try:
                name = queue.get_nowait()
            except:
                pass
            num += 1
            print("\r拷贝进度为 %.2f %%" % (num*100/lenth), end="")
            if num >= lenth:
                break
    
    if __name__ == '__main__':
        main()
    
    • 这里使用的不是multiprocessing下的Queue,而是multiprocessing下的manager下的Queue。
    • 结合了进程之间使用Queue进行通信,实现多进程复制文件。
    • 用一个简单的方式打印了拷贝进度。

    运行:

    D:\>python test04.py
    拷贝进度为 100.00 %
    

    协程

    1.迭代器

    for tmp in a:
       print(tmp)
    

    如果a是可以在上面的代码里使用的数据类型,a就是可迭代对象。
    元组,列表,字典,集合,字符串都是可迭代的对象。

    如何判断一个对象是否是可迭代的?

    from collections import Iterable
    isinstance(要判断的对象,Iterable)
    

    如果是可迭代的对象会返回一个True,否则返回False。
    例如:

    In [5]: from collections import Iterable
    
    In [6]: a = [11,22,33]
    
    In [7]: isinstance(a, Iterable)
    Out[7]: True
    

    如果想要一个类也变成一个可迭代对象,那么可以在类中添加一个__iter__方法。
    例如:

    from collections import Iterable
    
    class Classmate(object):
        """docstring for ClassName"""
        def __init__(self):
            self.name = list()
    
        def add(self, name):
            self.name.append(name)
    
    classmate = Classmate()
    
    classmate.add("同学一")
    classmate.add("同学二")
    classmate.add("同学三")
    
    print("判断classmate是否是可迭代对象:", isinstance(classmate,Iterable))
    

    运行这个程序:

    D:\>python test05.py
    test05.py:1: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3,and in 3.9 it will stop working
      from collections import Iterable
    判断classmate是否是可迭代对象: False
    

    提示了collections的用法变了,所以修改一下程序:
    from collections.abc import Iterable
    再次执行:

    D:\>python test05.py
    判断classmate是否是可迭代对象: False
    

    这时候给类添加一个__iter__方法:

    from collections.abc import Iterable
    
    class Classmate(object):
        """docstring for ClassName"""
        def __init__(self):
            self.name = list()
    
        def add(self, name):
            self.name.append(name)
    
        def __iter__(self):
            pass
    
    classmate = Classmate()
    
    classmate.add("同学一")
    classmate.add("同学二")
    classmate.add("同学三")
    
    print("判断classmate是否是可迭代对象:", isinstance(classmate,Iterable))
    

    运行一下:

    D:\>python test05.py
    判断classmate是否是可迭代对象: True
    

    即使是没有具体实现,只要有这个魔法函数,这个类的实例对象也是可迭代对象。
    这时候就满足了最基本的for循环条件,但是这时候还用不了。这是因为for循环的时候,每次要取一个值,还需要一个东西来记录取到了哪。
    所以__iter__方法必须返回一个对象的引用,这个对象必须要有__iter____next__方法,这个对象就是一个迭代器。

    判断一个对象是否是迭代器:

    • 这时候导入的是collections下的Iterator
    • 使用iter(对象)方法可以自动调用对象的__iter__方法
    from collections.abc import Iterable
    from collections.abc import Iterator
    
    class Classmate(object):
        """docstring for Classmate"""
        def __init__(self):
            self.name = list()
    
        def add(self, name):
            self.name.append(name)
    
        def __iter__(self):
            return ClassIterator()
    
    class ClassIterator(object):
        """docstring for ClassIterator"""
        def __iter__(self):
            pass
    
        def __next__(self):
            pass
    
    classmate = Classmate()
    
    classmate.add("同学一")
    classmate.add("同学二")
    classmate.add("同学三")
    
    print("判断classmate是否是可迭代对象:", isinstance(classmate,Iterable))
    # 因为`__iter__`方法返回的是ClassIterator()的引用,所以`iter()`方法得到的也是ClassIterator()的引用。
    classmate_iterator = iter(classmate)
    print("判断classmate是否是迭代器:",isinstance(classmate_iterator,Iterator))
    

    运行程序:

    D:\>python test05.py
    判断classmate是否是可迭代对象: True
    判断classmate是否是迭代器: True
    

    for循环每次调用的是这个对象的__iter__方法返回的另一个对象的引用的__next__方法。
    所以如果使用for循环这个Classmate对象,每次取到的是Classmate对象的__iter__函数返回的ClassIterator对象的引用的__next__方法返回的值。

    也就是如果把__next__pass改为return 11的话,for循环每次返回的都应该是11。
    修改代码如下:

    from collections.abc import Iterable
    from collections.abc import Iterator
    
    class Classmate(object):
        """docstring for Classmate"""
        def __init__(self):
            self.name = list()
    
        def add(self, name):
            self.name.append(name)
    
        def __iter__(self):
            return ClassIterator()
    
    class ClassIterator(object):
        """docstring for ClassIterator"""
        def __iter__(self):
            pass
    
        def __next__(self):
            return 11
    
    classmate = Classmate()
    
    classmate.add("同学一")
    classmate.add("同学二")
    classmate.add("同学三")
    
    # print("判断classmate是否是可迭代对象:", isinstance(classmate,Iterable))
    # classmate_iterator = iter(classmate)
    # print("判断classmate是否是迭代器:",isinstance(classmate_iterator,Iterator))
    
    for name in classmate:
        print(name)
    

    运行程序:

    D:\>python test05.py
    11
    11
    11
    11
    11
    

    打印11无限循环。

    但是实际上,希望ClassIterator实现的功能是把Classmate的name列表逐个取到,那么就可以在Classmate的__iter__函数返回ClassIterator引用的时候,把Classmate传过去。这样ClassIterator就可以取到name这一个列表。

    修改代码如下:

    import time
    from collections.abc import Iterable
    from collections.abc import Iterator
    
    
    class Classmate(object):
        """docstring for Classmate"""
        def __init__(self):
            self.name = list()
    
        def add(self, name):
            self.name.append(name)
    
        def __iter__(self):
            return ClassIterator(self)
    
    class ClassIterator(object):
        """docstring for ClassIterator"""
        def __init__(self, obj):
            self.obj = obj
    
        def __iter__(self):
            pass
    
        def __next__(self):
            return self.obj.name[0]
    
    classmate = Classmate()
    
    classmate.add("同学一")
    classmate.add("同学二")
    classmate.add("同学三")
    
    # print("判断classmate是否是可迭代对象:", isinstance(classmate,Iterable))
    # classmate_iterator = iter(classmate)
    # print("判断classmate是否是迭代器:",isinstance(classmate_iterator,Iterator))
    
    for name in classmate:
        print(name)
        time.sleep(1)
    

    这时候ClassIterator类可以取到Classmate的name列表。但是每次取到的都是name[0],想要继续往下取,就需要一个下标,每次取完一次,下标就加一。
    修改代码如下:

    import time
    from collections.abc import Iterable
    from collections.abc import Iterator
    
    
    class Classmate(object):
        """docstring for Classmate"""
        def __init__(self):
            self.name = list()
    
        def add(self, name):
            self.name.append(name)
    
        def __iter__(self):
            return ClassIterator(self)
    
    class ClassIterator(object):
        """docstring for ClassIterator"""
        def __init__(self, obj):
            self.obj = obj
            self.cur_index = 0
    
        def __iter__(self):
            pass
    
        def __next__(self):
                    # 防止下标越界
            if self.cur_index < len(self.obj.name):
                res = self.obj.name[self.cur_index]
                self.cur_index += 1
                return res
    
    
    classmate = Classmate()
    
    classmate.add("同学一")
    classmate.add("同学二")
    classmate.add("同学三")
    
    # print("判断classmate是否是可迭代对象:", isinstance(classmate,Iterable))
    # classmate_iterator = iter(classmate)
    # print("判断classmate是否是迭代器:",isinstance(classmate_iterator,Iterator))
    
    for name in classmate:
        print(name)
        time.sleep(1)
    

    运行代码:

    D:\>python test05.py
    同学一
    同学二
    同学三
    None
    None
    None
    None
    None
    

    这时候会发生一种情况就是,当for已经取完name列表中的所有元素之后,并不会停止,会继续取name的值,但是因为当前的下标已经等于或超过name列表的长度,所以__next__方法没有返回任何值,所以for循环继续,但是每次取到的值都是None。
    所以如果想让for循环结束,需要抛出一个StopIteration异常,for就会结束循环。

    import time
    from collections.abc import Iterable
    from collections.abc import Iterator
    
    
    class Classmate(object):
        """docstring for Classmate"""
        def __init__(self):
            self.name = list()
    
        def add(self, name):
            self.name.append(name)
    
        def __iter__(self):
            return ClassIterator(self)
    
    class ClassIterator(object):
        """docstring for ClassIterator"""
        def __init__(self, obj):
            self.obj = obj
            self.cur_index = 0
    
        def __iter__(self):
            pass
    
        def __next__(self):
            if self.cur_index < len(self.obj.name):
                res = self.obj.name[self.cur_index]
                self.cur_index += 1
                return res
            else:
                raise StopIteration
    
    
    classmate = Classmate()
    
    classmate.add("同学一")
    classmate.add("同学二")
    classmate.add("同学三")
    
    for name in classmate:
        print(name)
        time.sleep(1)
    

    运行代码:

    D:\>python test05.py
    同学一
    同学二
    同学三
    

    那么Classmate的__iter__可不可以返回自身,这样就不用返回别的类的引用。
    修改代码如下:

    import time
    from collections.abc import Iterable
    from collections.abc import Iterator
    
    
    class Classmate(object):
        """docstring for Classmate"""
        def __init__(self):
            self.name = list()
            self.cur_index = 0
    
        def add(self, name):
            self.name.append(name)
    
        def __iter__(self):
            return self
    
        def __next__(self):
            if self.cur_index < len(self.name):
                res = self.name[self.cur_index]
                self.cur_index += 1
                return res
            else:
                raise StopIteration
    
    
    classmate = Classmate()
    
    classmate.add("同学一")
    classmate.add("同学二")
    classmate.add("同学三")
    
    
    for name in classmate:
        print(name)
        time.sleep(1)
    

    执行代码:

    D:\>python test05.py
    同学一
    同学二
    同学三
    

    总结:

    • 如果一个对象是迭代器,那么它一定可以迭代。因为它一定包含__iter____next__
    • 一个对象可迭代,它不一定是迭代器。

    2.生成器

    生成器是一种特殊的迭代器。

    (1)创建生成器的方式

    方法1:

    nums = [x*2 for i in range(10)]
    

    nums会得到一个列表,如果把中括号变为小括号,nums得到的就是一个生成器。

    In [11]: nums = [ x*2 for x in range(10)]
    
    In [12]: nums
    Out[12]: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
    
    In [13]: nums2 = (x*2 for x in range(10))
    
    In [14]: nums2
    Out[14]: <generator object <genexpr> at 0x0000015761EE6DC8>
    
    In [15]: for i in nums:
        ...:     print(i)
        ...:
    0
    2
    4
    6
    8
    10
    12
    14
    16
    18
    
    In [16]: for i in nums2:
        ...:     print(i)
        ...:
    0
    2
    4
    6
    8
    10
    12
    14
    16
    18
    

    生成器和列表都可以用for遍历。
    方法2:
    把函数变为生成器。
    只要函数里有yield,那么这个函数就会变成生成器。

    如果想得到斐波那契数列的前多少位,可以这样来实现:

    def worker(tmp):
        a, b = 0, 1
        cur = 0
        while cur < tmp:
            print(a)
            a , b = b, a+b
            cur += 1
    
    worker(10)
    

    运行程序:

    D:\>python test06.py
    0
    1
    1
    2
    3
    5
    8
    13
    21
    34
    

    如果把print改为yield,那么这个函数就变成了一个生成器。调用这个生成器的方式和原来调用函数的方式不同。如果这时候还使用worker(10)的方式,不是在调用函数,而是在创建一个生成器对象。在使用for循环遍历这个生成器对象,就可以每次取到一个值。

    def worker(tmp):
        a, b = 0, 1
        cur = 0
        while cur < tmp:
            # print(a)
            yield a
            a , b = b, a+b
            cur += 1
    
    obj = worker(10)
    
    for i in obj:
        print(i)
    

    执行代码:

    D:\>python test06.py
    0
    1
    1
    2
    3
    5
    8
    13
    21
    34
    

    for从obj里逐个取值的时候,第一次从worker的开始执行,执行到yield把a的值返回,然后第二次for再取值的时候,不是从worker的头开始执行,而是从上一次yield停止的位置继续往下执行。

    如果每次只想取一个值打印出来,可以使用next:

    def worker(tmp):
        a, b = 0, 1
        cur = 0
        while cur < tmp:
            # print(a)
            yield a
            a , b = b, a+b
            cur += 1
    
    obj = worker(10)
    
    res = next(obj)
    print(res)
    
    res = next(obj)
    print(res)
    
    res = next(obj)
    print(res)
    
    res = next(obj)
    print(res)
    

    next会取到当前yield后面的值,然后下次调用再取下一个值。

    D:\>python test06.py
    0
    1
    1
    2
    

    也就是想让生成器执行,要使用next让他执行,而不是调用生成器。

    如果创建的是多个生成器:

    def worker(tmp):
        a, b = 0, 1
        cur = 0
        while cur < tmp:
            # print(a)
            yield a
            a , b = b, a+b
            cur += 1
    
    obj = worker(10)
    
    res = next(obj)
    print(res)
    
    res = next(obj)
    print(res)
    
    obj2 = worker(10)
    
    res = next(obj2)
    print(res)
    
    res = next(obj2)
    print(res)
    

    运行一下:

    D:\>python test06.py
    0
    1
    0
    1
    

    两个生成器对象之间互相没有影响。

    生成器的结束
    还是使用异常让生成器结束遍历。

    def worker(tmp):
        a, b = 0, 1
        cur = 0
        while cur < tmp:
            # print(a)
            yield a
            a , b = b, a+b
            cur += 1
    
    obj = worker(2)
    
    while True:
        try:
            print(next(obj))
        except Exception as res:
            break
    

    运行程序:

    D:\>python test06.py
    0
    1
    

    如果这个生成器有返回值,可以用下面的方式得到它的返回值:

    def worker(tmp):
        a, b = 0, 1
        cur = 0
        while cur < tmp:
            # print(a)
            yield a
            a , b = b, a+b
            cur += 1
        return "OK!"
    
    obj = worker(10)
    
    while True:
        try:
            print(next(obj))
        except Exception as res:
            print(res.value)
            break
    

    运行程序:

    D:\>python test06.py
    0
    1
    1
    2
    3
    5
    8
    13
    21
    34
    OK!
    

    生成器的启动还可以用send
    用法:生成器对象.sen(参数)
    程序:

    def worker(tmp):
        a, b = 0, 1
        cur = 0
        while cur < tmp:
            res = yield a
            print(res)
            a , b = b, a+b
            cur += 1
    
    obj = worker(10)
    
    print(next(obj))
    print(obj.send("haha"))
    

    运行结果:

    D:\>python test06.py
    0
    haha
    1
    

    执行过程:
    当用next启动生成器的时候,从worker的头开始执行,执行到yield a把a的值返回,然后打印出来。当send启动生成器的时候,系统从上一次yield的位置接着往下执行,执行到的第一条语句是把yield a 的值赋给res,yield a的值就是send中传递的参数,这时候res = “haha”,然后程序继续往下执行,打印出这个res。

    send与next相比,优点就是可以传递参数。

    send如果传递了参数不能一开始就调用,调用,会出错:

    def worker(tmp):
        a, b = 0, 1
        cur = 0
        while cur < tmp:
            res = yield a
            print(res)
            a , b = b, a+b
            cur += 1
    
    obj = worker(10)
    
    print(obj.send("haha"))
    

    结果:

    D:\>python test06.py
    Traceback (most recent call last):
      File "test06.py", line 12, in <module>
        print(obj.send("haha"))
    TypeError: can't send non-None value to a just-started generator
    

    如果没有参数:

    def worker(tmp):
        a, b = 0, 1
        cur = 0
        while cur < tmp:
            res = yield a
            print(res)
            a , b = b, a+b
            cur += 1
    
    obj = worker(10)
    
    print(obj.send(None))
    

    运行:

    D:\>python test06.py
    0
    
    (2)使用yield实现多任务(协程实现多任务)

    只要在函数里面写上yield,函数就变成了一个生成器,再创建生成器对象,调用next即可。
    代码:

    import time
    
    def task01():
        while True:
            print("task01")
            time.sleep(1)
            yield
    
    def task02():
        while True:
            print("task02")
            time.sleep(1)
            yield
    
    def main():
        t1 = task01()
        t2 = task02()
        while True:
            next(t1)
            next(t2)
    
    if __name__ == '__main__':
        main()
    

    结果:

    D:\>python test07.py
    task01
    task02
    task01
    task02
    task01
    task02
    task01
    task02
    task01
    task02
    task01
    task02
    

    这是一个协程的并发(假的“一起”执行)。

    (3)使用greenlet、gevent实现多任务(协程实现多任务)

    使用greenlet可以替换yield。
    要使用greenlet需要导入:
    from greenlet import greenlet
    例如:

    from greenlet import greenlet
    import time
    
    def test1():
        while True:
            print("test1")
            # 切换到gr2去执行
            gr2.switch()
            time.sleep(0.5)
    
    def test2():
        while True:
            print("test2")
            # 切换到gr1去执行
            gr1.switch()
            time.sleep(0.5)
    
    # 返回值是一个greenlet对象
    gr1 = greenlet(test1)
    gr2 = greenlet(test2)
    
    gr1.switch()
    

    运行结果:

    D:\>python test07.py
    test1
    test2
    test1
    test2
    test1
    test2
    

    greenlet的切换是在单线程内切换,而如果想要切换到其他的协程,真正实现多任务,就需要用到gevent。

    gevent也需要导入:
    import gevent
    例如:

    import gevent
    import time
    
    def test(n):
        for i in range(n):
            print(gevent.getcurrent(), i)
    
    # 指定去哪执行
    g1 = gevent.spawn(test, 5)
    g2 = gevent.spawn(test, 5)
    g3 = gevent.spawn(test, 5)
    
    g1.join()
    g2.join()
    g3.join()
    

    运行:

    D:\>python test07.py
    <Greenlet at 0x158e64a3e18: test(5)> 0
    <Greenlet at 0x158e64a3e18: test(5)> 1
    <Greenlet at 0x158e64a3e18: test(5)> 2
    <Greenlet at 0x158e64a3e18: test(5)> 3
    <Greenlet at 0x158e64a3e18: test(5)> 4
    <Greenlet at 0x158e6514048: test(5)> 0
    <Greenlet at 0x158e6514048: test(5)> 1
    <Greenlet at 0x158e6514048: test(5)> 2
    <Greenlet at 0x158e6514048: test(5)> 3
    <Greenlet at 0x158e6514048: test(5)> 4
    <Greenlet at 0x158e6514378: test(5)> 0
    <Greenlet at 0x158e6514378: test(5)> 1
    <Greenlet at 0x158e6514378: test(5)> 2
    <Greenlet at 0x158e6514378: test(5)> 3
    <Greenlet at 0x158e6514378: test(5)> 4
    

    在greenlet中,遇到延时,程序会等待这个延时结束,再去切换另一个任务,而gevent遇到延时就会自动切换。
    例如:

    import gevent
    
    def test01(n):
        for i in range(n):
            print(gevent.getcurrent(), i)
            gevent.sleep(0.5)
    
    def test02(n):
        for i in range(n):
            print(gevent.getcurrent(), i)
            gevent.sleep(0.5)
    
    def test03(n):
        for i in range(n):
            print(gevent.getcurrent(), i)
            gevent.sleep(0.5)
    
    g1 = gevent.spawn(test01, 5)
    g2 = gevent.spawn(test02, 5)
    g3 = gevent.spawn(test03, 5)
    
    g1.join()
    g2.join()
    g3.join()
    

    运行:

    D:\>python test07.py
    <Greenlet at 0x255d7d63e18: test01(5)> 0
    <Greenlet at 0x255d7dd4048: test02(5)> 0
    <Greenlet at 0x255d7dd4378: test03(5)> 0
    <Greenlet at 0x255d7d63e18: test01(5)> 1
    <Greenlet at 0x255d7dd4048: test02(5)> 1
    <Greenlet at 0x255d7dd4378: test03(5)> 1
    <Greenlet at 0x255d7d63e18: test01(5)> 2
    <Greenlet at 0x255d7dd4048: test02(5)> 2
    <Greenlet at 0x255d7dd4378: test03(5)> 2
    <Greenlet at 0x255d7d63e18: test01(5)> 3
    <Greenlet at 0x255d7dd4048: test02(5)> 3
    <Greenlet at 0x255d7dd4378: test03(5)> 3
    <Greenlet at 0x255d7d63e18: test01(5)> 4
    <Greenlet at 0x255d7dd4048: test02(5)> 4
    <Greenlet at 0x255d7dd4378: test03(5)> 4
    

    如果是三个协程执行的是同一份代码:

    import gevent
    
    def test01(n):
        for i in range(n):
            print(gevent.getcurrent(), i)
            gevent.sleep(0.5)
    
    g1 = gevent.spawn(test01, 5)
    g2 = gevent.spawn(test01, 5)
    g3 = gevent.spawn(test01, 5)
    
    g1.join()
    g2.join()
    g3.join()
    

    运行:

    D:\>python test07.py
    <Greenlet at 0x19a8ef33e18: test01(5)> 0
    <Greenlet at 0x19a8efa4048: test01(5)> 0
    <Greenlet at 0x19a8efa4378: test01(5)> 0
    <Greenlet at 0x19a8ef33e18: test01(5)> 1
    <Greenlet at 0x19a8efa4048: test01(5)> 1
    <Greenlet at 0x19a8efa4378: test01(5)> 1
    <Greenlet at 0x19a8ef33e18: test01(5)> 2
    <Greenlet at 0x19a8efa4048: test01(5)> 2
    <Greenlet at 0x19a8efa4378: test01(5)> 2
    <Greenlet at 0x19a8ef33e18: test01(5)> 3
    <Greenlet at 0x19a8efa4048: test01(5)> 3
    <Greenlet at 0x19a8efa4378: test01(5)> 3
    <Greenlet at 0x19a8ef33e18: test01(5)> 4
    <Greenlet at 0x19a8efa4048: test01(5)> 4
    <Greenlet at 0x19a8efa4378: test01(5)> 4
    

    先打印了每个协程的0,然后打印了1、2、3、4。
    所以gevent在有延迟的时候,自动切换了。
    协程依赖与线程,线程依赖于线程。

    进程、线程、协程对比

    • 进程是资源分配的基本单位,多进程耗费资源最多。
    • 多线程的程序,同一时间只有一个线程在运行。
    • 在不考虑GIL的前提下,优先考虑线程,再考虑协程,再考虑进程。
    • 协程利用进程在等待的时间去做别的事情,协程切换资源消耗小,效率高。
    • 进程最稳定。

    相关文章

      网友评论

          本文标题:深入浅出Python多任务(线程,进程,协程)

          本文链接:https://www.haomeiwen.com/subject/bsdydktx.html