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

篇幅较长!

导入

看一下下面的程序:

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的前提下,优先考虑线程,再考虑协程,再考虑进程。
  • 协程利用进程在等待的时间去做别的事情,协程切换资源消耗小,效率高。
  • 进程最稳定。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259