线程、进程、协程
进程是资源分配单位,系统会分配内存,屏幕,窗口。
线程是进程中真正执行的东西。
python中的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便的被使用。
1、多线程执行
#coding=utf-8
import threading
import time
def saySorry():
print("亲爱的,我错了,我能吃饭了吗?")
time.sleep(1)
if __name__ == "__main__":
for i in range(5):
t = threading.Thread(target=saySorry)
t.start() #启动线程,即让线程开始执行
2、自定义线程类
#coding=utf-8
import threading
import time
class MyThread(threading.Thread):
def __init__(self, name1, age):
super(MyThread, self).__init__()
self.name1 = name1
self.age = age
def run(self):
for i in range(3):
time.sleep(1)
msg = "I'm "+self.name+' @ '+str(i) #name属性中保存的是当前线程的名字
print(msg)
if __name__ == '__main__':
t = MyThread()
t.start()
python的threading.Thread类有一个run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。而创建自己的线程实例后,通过Thread类的start方法,可以启动该线程,交给python虚拟机进行调度,当该线程获得执行的机会时,就会调用run方法执行线程。
多线程之间共享全局变量,优点是方便在多个线程之间共享数据,缺点是线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全)。
如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确:
import threading
import time
g_num = 0
def work1(num):
global g_num
for i in range(num):
g_num += 1
print("----in work1, g_num is %d---"%g_num)
def work2(num):
global g_num
for i in range(num):
g_num += 1
print("----in work2, g_num is %d---"%g_num)
print("---线程创建之前g_num is %d---"%g_num)
t1 = threading.Thread(target=work1, args=(1000000,))
t1.start()
t2 = threading.Thread(target=work2, args=(1000000,))
t2.start()
while len(threading.enumerate()) != 1:
time.sleep(1)
print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)
测试结果:
---线程创建之前g_num is 0---
----in work1, g_num is 1088005---
----in work2, g_num is 1286202---
2个线程对同一个全局变量操作之后的最终结果是:1286202
同步:
同步就是协同步调,按预定的先后次序进行运行
互斥锁:
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
写法:
# 创建锁
mutex = threading.Lock()
# 锁定
mutex.acquire()
# 释放
mutex.release()
锁的好处:
确保了某段关键代码只能由一个线程从头到尾完整地执行
锁的坏处:
阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
死锁解决办法(
看门狗思想:过一一段时间就执行一次特殊的某行代码,如果长时间不执行,系统就自动重启
2.银行家算法
互斥锁:线程能够同步保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。某个线程要更改共享数据时,先将其锁定,此时资源的状态为锁定状态,其他线程不能更改,直到该线程释放资源。
当创建一个线程之后,函数里面所有的内存空间是这个线程独有的,在创建一个时,会重新创建一个内存空间。各人是各人的。函数里面的代码各人是各人的,不会共享。
非全局变量不需要加锁。
死锁:在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源时,就会造成死锁。
同步:同步就是协调步调,按照预定的先后次序进行运行 三把锁一环扣一环
异步:不同步
生产者与消费者模式:
1。队列 :进程中的队列和线程中的队列不是一个概念。队列就是用来给生产者和消费者解耦的。
2。栈
fifo: frist in frist out =>Queue
filo: frist in last out =>
一个函数想得到另一个函数的值,要么return 返回值,要么通过全局变量。
1、使用全局字典的方法
2、ThreadLocal:不用传参数,用一个全局变量,能过完成线程里边的所有的数据的传递,不会因为多个线程对参数的修改对程序产生影响。
孤儿进程:父进程先结束,子进程还没结束
僵尸进程:如果一个子进程死了,父进程没有收尸,在收尸前的整个期间,子进程就称为僵尸进程。
线程之间共享全局变量。
原子操作(原子性):要么不做,要做就做完。
线程安全问题:可能在一句代码还没执行完,操作系统就停止了代码的运行。
轮询:是一种CPU决策如何提供周边设备服务的方式,又称程控输出入
多任务UDP聊天器:
import socket
import threading
# 定义发送数据的函数
def send_data(udp_socket):
# 定义要发送的内容
send_content = input("请输入要发送的内容:")
# 请输入IP地址
ipddr = input("请输入IP地址,格式为:xxx.xxx.xxx.xxx :")
# 请输入端口号
port = int(input("请输入端口号:"))
# 把要发送的数据转换为 二进制
send_data = send_content.encode("utf-8")
# 发送数据
udp_socket.sendto(send_data, (ipddr, port))
# 测试
# 定义接收数据的函数
def recvData(udp_socket):
while True:
# 接收数据
recv_data = udp_socket.recvfrom(1024)
# 如果数据存在,则解析数据
if recv_data:
# 拆包,得到内容
msg, list_port = recv_data
# 拆包,得到msg
msg = msg.decode("gbk")
# 打印内容
print(msg, list_port)
else:
# 数据不存在停止循环
break
# 定义主入口函数
def main():
# 定义套接字
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定服务器客户端端口
# udp_socket.bind(("", 7878))
# 定义子线程并且开启
t1 = threading.Thread(target=recvData, args=(udp_socket, ))
# 设置守护线程
t1.setDaemon(True)
t1.start()
while True:
print("-------------------")
print("-- 1、发送数据 --")
print("-- 2、退出系统 --")
print("-------------------")
num = int(input("请选择功能【1/2/3】:"))
if num < 1 or num > 3:
print("输入不合法!,请重新输入")
else:
if num == 1:
# 发送数据
send_data(udp_socket)
if num == 2:
print("正在退出系统...")
print("系统已退出")
break
if __name__ == '__main__':
main()
===========================================================================
进程:操作系统中的算法包括:时间片轮转、优先级调度、
并发:看上去一起执行。当前的任务数量大于核数。
并行:真正的一起执行。当前任务数小于核数。
调度算法:什么样的情况下按照什么样的规则让谁去执行。
编写完毕的代码,在没有运行的时候称之为程序,在运行的时候称之为进程。
1、fork()创造子线程
import os
fork():可以在python程序中创建子进程。
ret = os.fork()
在fork()中,主进程想要结束,不会因为子进程没有结束而等待。只要子进程产生,子进程的执行顺序和执行过程和主进程一样,就是众所周知的代码执行的过程。
2.pid值:
getpid():获取当前进程的pid值。
pid值:在操作系统当中,当进程运行起来时,操作系统都会给这个进程分配一个独一无二的值,即pid值。processID
父进程中fork的返回值,就是刚刚创建出来的子进程的id。
getppid():获取父进程的pid值。
pid值小于等于65535
3.Process()创造子线程:
import multiprocessing
import time
# 定义函数
def work1():
for i in range(10):
print("work1----", i)
time.sleep(0.5)
if __name__ == '__main__':
# 创建进程
# 1. 导入 multiprocessing 模块
# 2. multiprocessing.Process() 创建子进程
# 3. start() 方法启动进程
p1 = multiprocessing.Process(group=None, target=work1)
p1.start()
for i in range(10):
print("这是主进程", i)
time.sleep(0.5)
p.join()#加了join之后,主进程会等子进程执行完代码之后,再开始执行join下面的代码
join([timeout])#堵塞:主进程等待子进程结束之后才结束。timeout表示操作时间。
terminate():不管任务是否完成,立即终止。
由于process的跨平台更好,以后不用fork,而是用process
Process语法结构如下:
Process([group [, target [, name [, args [, kwargs]]]]])
target:如果传递了函数的引用,可以任务这个子进程就执行这里的代码
args:给target指定的函数传递的参数,以元组的方式传递
kwargs:给target指定的函数传递命名参数
name:给进程设定一个名字,可以不设定
group:指定进程组,大多数情况下用不到
Process创建的实例对象的常用方法:
start():启动子进程实例(创建子进程)
is_alive():判断进程子进程是否还在活着
join([timeout]):是否等待子进程执行结束,或等待多少秒
terminate():不管任务是否完成,立即终止子进程
Process创建的实例对象的常用属性:
name:当前进程的别名,默认为Process-N,N为从1开始递增的整数
pid:当前进程的pid(进程号)
进程间不同享全局变量
进程线程对比:
进程,能够完成多任务,比如 在一台电脑上能够同时运行多个QQ
线程,能够完成多任务,比如 一个QQ中的多个聊天窗口
进程是系统进行资源分配和调度的一个独立单位.
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
区别:
一个程序至少有一个进程,一个进程至少有一个线程.
线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。
进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
线线程不能够独立执行,必须依存在进程中
可以将进程理解为工厂中的一条流水线,而其中的线程就是这个流水线上的工人
优缺点:
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。
4.进程池Pool创建子线程
import multiprocessing
import time
def copy_work():
print("拷贝中....",multiprocessing.current_process().pid)
time.sleep(0.3)
if __name__ == '__main__':
# 创建进程池
# Pool(3) 表示创建容量为3个进程的进程池
pool = multiprocessing.Pool(3)
for i in range(10):
# 利用进程池同步拷贝文件,进程池中的进程会必须等上一个进程退出才能执行下一个进程
# pool.apply(copy_work)
pool.apply_async(copy_work)
pool.close()
# 注意:如果使用异步方式执行copy_work任务,主线程不再等待子线程执行完毕再退出!
pool.join()
进程池Pool:主进程一般用来等待,真正的任务都在子进程中执行。
multiprocessing.Pool常用函数解析:
apply_async(func[, args[, kwds]]) :使用非阻塞方式调用func(并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),args为传递给func的参数列表,kwds为传递给func的关键字参数列表;
close():关闭Pool,使其不再接受新的任务;
terminate():不管任务是否完成,立即终止;
join():主进程阻塞,等待子进程的退出, 必须在close或terminate之后使用;
进程池中的Queue
如果要使用Pool创建进程,就需要使用multiprocessing.Manager()中的Queue(),而不是multiprocessing.Queue(),否则会得到一条如下的错误信息:
RuntimeError: Queue objects should only be shared between processes through inheritance.
下面的实例演示了进程池中的进程如何通信:
import multiprocessing
import time
def write_queue(queue):
# 循环写入数据
for i in range(10):
if queue.full():
print("队列已满!")
break
# 向队列中放入消息
queue.put(i)
time.sleep(0.5)
def read_queue(queue):
# 循环读取队列消息
while True:
# 队列为空,停止读取
if queue.empty():
print("---队列已空---")
break
# 读取消息并输出
result = queue.get()
print(result)
if __name__ == '__main__':
# 创建消息队列
queue = multiprocessing.Queue(3)
# 创建子进程
p1 = multiprocessing.Process(target=write_queue, args=(queue,))
p1.start()
# 等待p1写数据进程执行结束后,再往下执行
p1.join()
p1 = multiprocessing.Process(target=read_queue, args=(queue,))
p1.start()
q.put():存数据 q.get():取数据 q.full():判断数据是否是满的 q.empty()判断数据是否为空 q.get_nowait():立即存数据不等待 q.put_nowait():立即取数据不等待
进程间通信的方式:命名管道 无名管道 共享内存 队列 网络功能
fork ()是最底层的方法。
pool = Pool(3)
pool.apply_async(xx)
pool 中,主进程一般不干活,主要是创建的子进程干活,join()方法用来等待。
apply()=>堵塞式
进程共享数据,写实拷贝。
主进程的pid 是运行程序的那个软件的pid值
案例:文件夹copy器
import multiprocessing
import os
# file_name 文件名
# source_dir 源文件目录
# dest_dir 目标文件目录
def copy_work(file_name, source_dir, dest_dir):
# 拼接路径
source_path = source_dir+"/"+file_name
dest_path = dest_dir+"/"+file_name
print(source_path, "----->", dest_path)
# 打开源文件、创建目标文件
with open(source_path,"rb") as source_file:
with open(dest_path,"wb") as dest_file:
while True:
# 循环读取数据
file_data = source_file.read(1024)
if file_data:
# 循环写入到目标文件
dest_file.write(file_data)
else:
break
if __name__ == '__main__':
# 1、定义源文件目录和目标文件夹的目录
source_dir = "test"
dest_dir = "/home/teahcer/桌面/test"
try:
# 2、创建目标文件夹目录
os.mkdir(dest_dir)
except:
print("目标文件夹已经存在,未创建~")
# 3、列表得到所有的源文件中的文件
file_list = os.listdir(source_dir)
print(file_list)
# 4、创建进程池
pool = multiprocessing.Pool(3)
# 5、for 循环,依次拷贝每个文件
for file_name in file_list:
# copy_work(file_name, source_dir, dest_dir)
pool.apply_async(copy_work, args=(file_name, source_dir, dest_dir))
# 6、关闭进程池
pool.close()
# 7、设置主进程等待子进程执行结束再退出
pool.join()
============================================================================
协程:
迭代器(iterator):
迭代是访问集合元素的一种方式。
迭代器是一个可以记住遍历的位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。
迭代器只能往前不会后退。
可以对list、tuple、str等类型的数据使用for...in...的循环语法从其中依次拿到数据进行使用,这样的过程称为遍历,也叫迭代。
可迭代对象(Iterable):可以通过for...in...这类语句迭代读取一条数据供我们使用的对象。列表元组字典都是 可迭代对象。
可迭代对象通过__iter__
方法向我们提供一个迭代器,我们在迭代一个可迭代对象的时候,实际上就是先获取该对象提供的一个迭代器,然后通过这个迭代器来依次获取对象中的每一个数据.
一个具备了__iter__
方法的对象,就是一个可迭代对象。
一个实现了__iter__
方法和__next__
方法的对象,就是迭代器。
iter()函数与next()函数
list、tuple等都是可迭代对象,我们可以通过iter()函数获取这些可迭代对象的迭代器。然后我们可以对获取到的迭代器不断使用next()函数来获取下一条数据。iter()函数实际上就是调用了可迭代对象的__iter__方法。
-
-
-
-
11
-
-
22
-
-
33
-
-
44
-
-
55
-
-
Traceback (most recent call last):
-
File "<stdin>", line 1, in <module>
-
StopIteration
-
>>>
可以使用 isinstance() 判断一个对象是否是 Iterator 对象.
一个实现了__iter__
方法和__next__
方法的对象,就是迭代器。
自定义迭代器
from collections import Iterable
from collections import Iterator
class StudentList(object):
def __init__(self):
# 创建列表
self.items = list()
def addItem(self,item):
# 追加元素到列表中
self.items.append(item)
def __iter__(self):
# 创建迭代器对象
studentIterator = StudentIterator(self.items)
# 返回迭代器对象
return studentIterator
# 定义迭代器
class StudentIterator(object):
# 定义构造方法
# 1)完成 索引下标定义和初始化
# 2)接收要遍历的列表值
def __init__(self, items):
self.items = items
self.current_index = 0
def __iter__(self):
return self
def __next__(self):
# 判断位置是否合法
if self.current_index < len(self.items):
# 根据current_index 返回列表值
item = self.items[self.current_index]
# 让 下标+1
self.current_index += 1
# 返回元素内容
return item
else:
# 停止迭代
# 主动抛出异常,迭代器没有更多的值(到了迭代器末尾)
raise StopIteration
# 实例化对象
stulist = StudentList()
stulist.addItem("张三")
stulist.addItem("李四")
stulist.addItem("C罗")
# 检查是否是可迭代对象
result = isinstance(stulist, Iterable)
print(result)
for value in stulist:
print(value)
for...in...循环的本质
for item in Iterable 循环的本质就是先通过iter()函数获取可迭代对象Iterable的迭代器,然后对获取到的迭代器不断调用next()方法来获取下一个值并将其赋值给item,当遇到StopIteration的异常后循环结束。
8. 迭代器的应用场景
我们发现迭代器最核心的功能就是可以通过next()函数的调用来返回下一个数据值。如果每次返回的数据值不是在一个已有的数据集合中读取的,而是通过程序按照一定的规律计算生成的,那么也就意味着可以不用再依赖一个已有的数据集合,也就是说不用再将所有要迭代的数据都一次性缓存下来供后续依次读取,这样可以节省大量的存储(内存)空间。
斐波那契数:
class Fibonacci():
def __init__(self, num):
# 通过构造方法,保存num到类的成员属性中
self.num = num
# 定义变量保存斐波那契数列前两个值
self.a = 0
self.b = 1
# 记录当前的变量值
self.current_index = 0
def __iter__(self):
# 返回迭代器,因自身就是迭代器,故可以返回自己
return self
def __next__(self):
# 判断是否生成完毕
if self.current_index < self.num:
# 返回
result = self.a
# 交换两个变量值
self.a, self.b = self.b, self.a+self.b
self.current_index += 1
return result
else:
# 停止迭代
raise StopIteration
if __name__ == '__main__':
# 创建迭代器
fib_iterator = Fibonacci(5)
# 使用迭代器,输出斐波那契数列值
for value in fib_iterator:
print(value, end=" ")
并不是只有for循环能接收可迭代对象,除了for循环能接收可迭代对象,list、tuple等也能接收。
-
li = list(FibIterator(15))
-
print(li)
-
tp = tuple(FibIterator(6))
-
print(tp)
生成器:
利用迭代器,我们可以在每次迭代获取数据(通过next()方法)时按照特定的规律进行生成。但是我们在实现一个迭代器时,关于当前迭代到的状态需要我们自己记录,进而才能根据当前状态生成下一个数据。为了达到记录当前状态,并配合next()函数进行迭代使用,我们可以采用更简便的语法,即生成器(generator)。生成器是一类特殊的迭代器。
创建生成器方法1
要创建一个生成器,有很多种方法。第一种方法很简单,只要把一个列表生成式的 [ ] 改成 ( )。
In [15]: L = [ x*2 for x in range(5)]
In [16]: L
Out[16]: [0, 2, 4, 6, 8]
In [17]: G = ( x*2 for x in range(5))
In [18]: G
Out[18]: <generator object <genexpr> at 0x7f626c132db0>
创建生成器方法2
generator非常强大。如果推算的算法比较复杂,用类似列表生成式的 for 循环无法实现的时候,还可以用函数来实现。
使用了yield关键字的函数不再是函数,而是生成器。(使用了yield的函数就是生成器)
yield关键字有两点作用:
保存当前运行状态(断点),然后暂停执行,即将生成器(函数)挂起
将yield关键字后面表达式的值作为返回值返回,此时可以理解为起到了return的作用
可以使用next()函数让生成器从断点处继续执行,即唤醒生成器(函数)
Python3中的生成器可以使用return返回最终运行的返回值,而Python2中的生成器不允许使用return返回一个返回值(即可以使用return从生成器中退出,但return后不能有任何表达式)。
使用send唤醒
我们除了可以使用next()函数来唤醒生成器继续执行外,还可以使用send()函数来唤醒执行。使用send()函数的一个好处是可以在唤醒的同时向断点处传入一个附加数据。
例子:执行到yield时,gen函数作用暂时保存,返回i的值; temp接收下次c.send("python"),send发送过来的值,c.next()等价c.send(None)
协程:
又称微线程。通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定
协程和线程差异:
在实现多任务时, 线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。 操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。 所以线程的切换非常耗性能。但是协程的切换只是单纯的操作CPU的上下文(可以理解为方法之间的切换),所以一秒钟切换个上百万次系统都抗的住。简单说,就是比线程占用资源更少。
用yield()和next()实现简单协程:
import time
def work1():
while True:
print("----work1---")
yield
time.sleep(0.5)
def work2():
while True:
print("----work2---")
yield
time.sleep(0.5)
def main():
w1 = work1()
w2 = work2()
while True:
next(w1)
next(w2)
if __name__ == "__main__":
main()
用greenlet实现协程:
greenlet 相当于集成了yield()和next(),使用的时候自动调用这两个方法。
先装包:sudo pip3 install preenlet
#coding=utf-8
from greenlet import greenlet
import time
def test1():
while True:
print "---A--"
gr2.switch()
time.sleep(0.5)
def test2():
while True:
print "---B--"
gr1.switch()
time.sleep(0.5)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
#切换到gr1中运行
gr1.switch()
用gevent实现协程:
gevent比greenlet更厉害,直不用调用switch方法,可以自动切换任务。
gevent的原理:当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
安装:pip install gevent
import time
import gevent
def work1():
for i in range(5):
print("work1 -----1")
time.sleep(0.5)
def work2():
for i in range(5):
print("work2 -----2")
time.sleep(0.5)
# 创建携程并指派任务
g1 = gevent.spawn(work1)
g2 = gevent.spawn(work2)
# 等待协程执行完成再关闭主线程
g1.join()
g2.join()
注意:上面代码中的time方法用的是gevent包中的。
用gevent时,只要加入堵塞的方法都需要gevent里面的,所有的延时堵塞方法都要用gevent里面的。
这样使用时就不是很方便,可能会不知道哪些方法需要用gevent里面的 。因此需要导入monkey。并在代码的最前面写一句:
monkey .patch_all()#相当于monkey把代码中的所有延时操作都改成用gevent里面的。
协程并发下载器:
# 导入urllib模块
import urllib.request
import gevent
def download_img(img_url, filename):
try:
# 打开url
response = urllib.request.urlopen(img_url)
# 创建文件
with open(filename, "wb") as img_file:
# 通过循环不断读取数据
while True:
# 将读取到的数据保存到变量中
img_data = response.read(1024)
# 如果读取成功,则写数据到文件中
if img_data:
# 写数据
img_file.write(img_data)
else:
break
except Exception as e:
print("下载图片出现错误~!",e)
else:
print("图片 %s 下载完成!" % filename)
def main():
# 定义变量保存要下载的图片地址
img_url1 = "http://img.mp.itc.cn/upload/20170716/8e1b835f198242caa85034f6391bc27f.jpg"
img_url2 = "http://pic1.wed114.cn/allimg/180227/1023303521-1.gif"
img_url3 = "http://image.uczzd.cn/11867042470350090334.gif?id=0&from=export"
# 开启协程 调用下载方法
gevent.joinall([
gevent.spawn(download_img, img_url1, "1.gif"),
gevent.spawn(download_img, img_url2, "2.gif"),
gevent.spawn(download_img, img_url3, "3.gif")
])
# 主入口
if __name__ == '__main__':
main()
进程线程协程之间的区别:
进程是资源分配的单位
线程是操作系统调度的单位
进程切换需要的资源很最大,效率很低
线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)
协程切换任务资源很小,效率高
多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中 所以是并发