篇幅较长!
导入
看一下下面的程序:
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的方法。
所以创建线程有两种方式:
t1 = threading.Thread(target=函数名)
- 函数里面的代码是什么,线程就去执行什么
- 用类
定义一个类,这个类必须继承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的前提下,优先考虑线程,再考虑协程,再考虑进程。
- 协程利用进程在等待的时间去做别的事情,协程切换资源消耗小,效率高。
- 进程最稳定。