【Python】【并发编程】基础基础基础基础知识

部署运行你感兴趣的模型镜像

一、串行、并行、并发的区别

  1. 串行(serial):一个CPU上,按顺序完成多个任务
  2. 并行(parallelism):指的是任务数小于等于cpu核数,即任务真的是一起执行的
  3. 并发(concurrency):一个CPU采用时间片管理方式,交替的处理多个任务。一般是是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已

二、线程、进程、协程

       串行、并行、并发是描述任务执行方式的概念,而进程、线程、协程是实现这些执行方式的具体技术手段(执行单元)。它们的关系可以概括为:进程、线程、协程是承载任务的 "容器",而串行、并行、并发是这些容器运行任务时的不同模式

用生产线车间来打比方

  • 生产线(进程):每条生产线有独立的场地、工具、原料(对应进程的独立内存、资源),多条生产线可以同时运转(对应多进程并行)。

  • 工人(线程):同一生产线上的工人共享这条线的资源(工具、原料),协作完成生产(对应线程共享进程资源);工人之间可以分工(比如一个组装、一个质检),但同一时间一个工人只能做一件事(对应线程的串行执行特性)。

  • 工人的空闲安排(协程):当工人等待零件(类似 IO 操作等待)时,不闲着而是临时去做其他轻便工作(比如整理工具、记录数据),之后再回来继续原来的任务 —— 这完美对应了协程 “在等待时主动让出执行权,实现单线程内高效切换” 的特点。

三、线程

1.线程的特点

  1. 线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
  2. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
  3. 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
  4. 拥有自己独立的栈和共享的堆,共享堆,不共享栈,标准线程由操作系统调度;
  5. 调度和切换:线程上下文切换比进程上下文切换要快得多。

2.方法包装创建线程

① 使用threading这个模块

② 线程的执行统一调用start()方法

from threading import Thread
from time import sleep   #让线程暂停,实现并发效果


#【方法包装创建线程】
def fact(name):
    for i in range(2):
        print(f"线程{name},启动{i}")
        sleep(1)

if __name__ == '__main__':
    print("主线程开始了")
    #创建线程
    t1=Thread(target=fact,args=("t1",))
    #用方法创建线程,有两个参数,target是线程要调用的方法,args是要调用的方法的参数,并将其放在一个元组中
    t2=Thread(target=fact,args=("t2",))
    #启动线程
    t1.start() #启动线程的方法是start()
    t2.start()
    print("主线程结束")
#输出结果如下:
# 主线程开始了 --》先启动主线程
# 线程t1,启动0  --》启动t1
# 线程t2,启动0 --》t1进入后,要睡眠一秒,此时并发模式,t2在t1睡眠时进行
# 主线程结束  --》t1和t2都在睡眠中,主线陈继续进行,不会等待子线程
# 线程t2,启动1  --》t2先睡眠结束,就先进行
# 线程t1,启动1  --》然后t1进行
#sleep模仿的是真实线程进行过程中有些快有些慢的问题

3.类包装创建线程

from threading import Thread
from time import sleep

class MyThread(Thread):  #继承Thread类
    def __init__(self,name):
        Thread.__init__(self) #调用父类Thread的初始化方法
        self.name = name
    def run(self):  #相当于是重写run方法,启动线程会自动调用run
        for i in range(2):
            print(f"线程{self.name}启动第{i}次")
            sleep(1)

if __name__=="__main__":
    print("主线程启动")
    #创建线程
    t1=MyThread("t1")
    t2=MyThread("t2")
    #启动线程
    t1.start()
    t2.start()
    print("主线程结束了")

3.join()

之前的代码会出现以下情况,是因为主线程不会等待子线程结束,如果需要等待子线程结束后,再结束主线程,可使用join()方法

#之前的代码主线程不会等待子线程结束
#加上jion(),主线程会等子线程结束后再结束

from threading import Thread
from time import sleep

def fact(name):
    for i in range(2):
        print(f"子线程{name}执行第{1}次")
        sleep(1)
if __name__=='__main__':
    print("主线程开始了...")
    #创建线程
    t1=Thread(target=fact,args=("t1",))
    t2=Thread(target=fact,args=("t2",))
    #启动线程
    t1.start()
    t2.start()
    #主线程等待t1,t2结束后再往下执行
    t1.join()
    t2.join()
    print("主线程已结束...")

4.守护线程

        在行为上还有一种叫守护线程,主要的特征是它的生命周期。主线程死亡,它也就随之死亡。在python中,线程通过setDaemon(True/False)来设置是否为守护线程

#守护线程随着主线程的死亡而死亡
#程序真正‘守护’的是所有非守护线程的完整执行,当所有非守护线程结束,无论守护线程是否完成都会被终止
#守护线程可以理解为随从
from threading import Thread
from time import sleep

class fact(Thread):
    def __init__(self,name):
        Thread.__init__(self)
        self.name=name

    def run(self):
        for i in range(1,3):
            print(f"线程{self.name}正在执行第{i}次")
            sleep(1)
if __name__=='__main__':
    print("主线程开始了...")
    #创建线程
    t1=fact("t1")
    #守护线程
    t1.daemon=True
    #启动线程
    t1.start()
    print("主线程结束...")

# 主线程开始了...
# 线程t1正在执行第1次
# 主线程结束...   -->主线程结束了,守护线程不管进行到哪里都会停止

5.全局解释器锁GIL问题

GIL 就是 Python 解释器加的一把 "锁":

  1. 核心特点:同一时间,一个进程里只能有一个线程真正干活(执行 Python 代码)

  2. 影响

    • 多线程跑计算密集型任务(比如大量数学运算)时,其实是轮流干活,没法利用多核同时算,效率可能还不如单线程
    • 但如果是 IO 密集型任务(比如下载、读写文件),一个线程等的时候,另一个能接着干,所以还是有用的
  3. 解决办法

    • 计算多就用多进程(每个进程有自己的 GIL)
    • IO 多就用多线程或协程

简单说就是:GIL 让 Python 多线程在 "算东西" 时没法并行,但 "等东西" 时还有用。

6.线程同步和互斥锁

① 线程同步的概念

处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。 线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。

② 多线程操作同一个对象的例子(未使用线程同步)

#未使用同步线程

from threading import Thread
from time import sleep

class Account:
    def __init__(self,name,money):
        self.name=name
        self.money=money

class drawing(Thread):
    def __init__(self,drawing_money,account):
        Thread.__init__(self)
        self.drawing_money=drawing_money
        self.account=account
        self.Total=0
    def run(self):
        if self.account.money<self.drawing_money:
            print("账户余额不足")
            return
        sleep(1)
        self.account.money-=self.drawing_money
        self.Total+=self.drawing_money
        print(f"账户{self.account.name}剩余{self.account.money}元")
        print(f"账户{self.account.name}总共取了{self.Total}元")

if __name__=='__main__':
    a1=Account("陈陈",100)
    draw1=drawing(80,a1)
    draw2=drawing(80,a1)
    draw1.start()
    draw2.start()

# 账户陈陈剩余20元
# 账户陈陈总共取了80元
# 账户陈陈剩余-60元
# 账户陈陈总共取了80元
#这是因为draw1判断完睡觉去了,并没有取钱,draw2也判断,所以发生异常,两个都取钱了

③ 我们可以通过锁机制解决以上问题

锁机制有以下几个要点

  1. 必须使用同一个锁对象
  2. 互斥锁的作用就是保证同一时刻只能有一个线程去操作共享数据,保证共享数据不会出现错误问题
  3. 使用互斥锁的好处确保某段关键代码只能由一个线程从头到尾完整地去执行
  4. 使用互斥锁会影响代码的执行效率
  5. 同时持有多把锁,容易出现死锁的情况

互斥锁: 对共享数据进行锁定,保证同一时刻只能有一个线程去操作。

注意: 互斥锁是多个线程一起去抢,抢到锁的线程先执行,没有抢到锁的线程需要等待,等互斥锁使用完释放后,其它等待的线程再去抢这个锁。

threading模块中定义了Lock变量,这个变量本质上是一个函数,通过调用这个函数可以获取一把互斥锁

以上代码就可以修改为:

#增加互斥锁,使用同步线程

from threading import Thread,Lock
from time import sleep

class Account:
    def __init__(self,name,money):
        self.name=name
        self.money=money

class drawing(Thread):
    def __init__(self,drawing_money,account):
        Thread.__init__(self)
        self.drawing_money=drawing_money
        self.account=account
        self.Total=0
    def run(self):
        #加上锁了
        lock1.acquire()
        if self.account.money<self.drawing_money:
            print("账户余额不足")
            return
        sleep(1)
        self.account.money-=self.drawing_money
        self.Total+=self.drawing_money
        lock1.release()
        print(f"账户{self.account.name}剩余{self.account.money}元")
        print(f"账户{self.account.name}总共取了{self.Total}元")

if __name__=='__main__':
    a1=Account("陈陈",100)
    #增加互斥锁
    lock1=Lock()
    draw1=drawing(80,a1)
    draw2=drawing(80,a1)
    draw1.start()
    draw2.start()

7.信号量

如果某个资源,我们同时想让N个(指定数值)线程访问的时候,可以使用信号量。

信号量控制同时访问资源的数量。信号量和锁相似,锁同一时间只允许一个对象(进程)通过,信号量同一时间允许多个对象(进程)通过。

'''
应用场景
1.在读写文件的时候,一般只能有一个线程再写,而读可以有多个线程同时进行,
如果需要限制同时读文件的线程个数,就可以用到信号量了
(如果用互斥锁,就是限制同一时刻只能有一个线程读取文件)
2.在做爬虫抓取数据时
'''

from threading import Thread,Semaphore  #注意导入
from time import sleep


'''一个房间一次只允许两个人通过'''
def home(name):
    se.acquire()  #和互斥锁的用法一样
    print(f"{name}进入了房间")
    sleep(1)
    print(f"-----{name}退出了房间")
    se.release()

if __name__ == '__main__':
    se=Semaphore(2)  #创建信号量的对象,一次有两把锁
    for i in range(1,7):
        p=Thread(target=home,args=(f"tom{i}",))
        p.start()

# tom1进入了房间
# tom2进入了房间
# -----tom1退出了房间
# tom3进入了房间
# -----tom2退出了房间
# tom4进入了房间
# -----tom3退出了房间
# tom5进入了房间
# -----tom4退出了房间
# tom6进入了房间
# -----tom6退出了房间
# -----tom5退出了房间

8.事件Event对象

        Event 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在初始情况下,event 对象中的信号标志被设置假。如果有线程等待一个 event 对象,而这个 event 对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个 event 对象的信号标志设置为真,它将唤醒所有等待个 event 对象的线程。如果一个线程等待一个已经被设置为真的 event 对象,那么它将忽略这个事件,继续执行

'''Event()可以创建一个事件管理标志,该标志默认为false,默认是阻塞状态,有以下四种方法可以调用
1.event.wait(timeout=None) -- 调用该方法的线程会被阻塞,如果设置了timeout参数,超时后,线程会停止阻塞继续进行
2.event.set() -- 将event的标志设置为True,调用wait方法的所有线程将被唤醒
3.event.clear() -- 将event的标志设置为False,调用wait方法的所有线程将被阻塞
4.event.is_set() -- 判断event的标志是否为True
'''

#小伙伴们,围着吃火锅,当菜上齐了,请客的主人说:开吃!
#于是小伙伴一起动筷子,这种场景如何实现
import threading
import time

def chihuoguo(name):
    print(f"{name}吃火锅线程已启动")
    print(f"{name}已经进入就餐状态,等待通知中!")
    time.sleep(2)
    event.wait()
    #收到事件后开始运行
    print(f"{name}收到通知了!")
    print(f"{name}开始吃咯!!")

if __name__ == '__main__':
    event=threading.Event()  #需要先创建一个 Event 对象才能使用
    #创建线程
    p1=threading.Thread(target=chihuoguo,args=("陈1",))
    p2=threading.Thread(target=chihuoguo,args=("陈2",))
    #开启线程
    p1.start()
    p2.start()

    time.sleep(10)
    #发送事件通知
    print("------>主线程通知大家可以开始吃咯")
    event.set()

9.生产者消费者模式

什么是生产者?

生产者指的是负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。

什么是消费者?

消费者指的是负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)

什么是缓冲区?

        消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。缓冲区是实现并发的核心!!

        从一个线程向另一个线程发送数据最安全的方式可能就是使用 queue 库中的队列了。创建一个被多个线程共享的 Queue 对象,这些线程通过使用 put() 和 get()操作来向队列中添加或者删除元素。Queue 对象已经包含了必要的锁,所以你可以通过它在多个线程间多安全地共享数据。

生产者消费者模式代码如下:

'''
缓冲区和queue对象
从一个线程向另一个线程发送数据最安全的方式可能就是使用 queue 库中的队列了。
创建一个被多个线程共享的 Queue 对象,这些线程通过使用 put() 和 get()操作来向队列中添加或者删除元素。
Queue对象已经包含了必要的锁,所以你可以通过它在多个线程间多安全地共享数据。
'''

from queue import Queue
from threading import Thread
from time import sleep

def producer():
    num=1
    while True:
        if queue.qsize()<=6:
            print(f"生产{num}号馒头")
            queue.put(f"{num}号大馒头")
            num+=1
        else:
            print("框子装不下了,快来买啊!!")
        sleep(1)

def consumer():
    while True:
        print(f"获取:{queue.get()}")
        sleep(3)

if __name__ == '__main__':
    queue=Queue()
    a=Thread(target=producer, args=())
    b=Thread(target=consumer, args=())
    a.start()
    b.start()

四、进程

1.进程的特点

进程(Process):拥有自己独立的堆和栈,既不共享堆,也不共享栈,进程由操作系统调度;进程切换需要的资源很最大,效率低。

对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。

进程优缺点:

  1. 可以使用计算机多核,进行任务的并行执行,提高执行效率
  2. 运行不受其他进程影响,创建方便
  3. 空间独立,数据安全
  4. 缺点:进程的创建和删除消耗的系统资源较多

2.方法模式创建进程

① 使用multiprocessing模块

② 进程创建后,使用start创建进程

'''
线程用信号量的时候,是并发,实际上同一块资源还是只有一个线程在访问;
进程能利用多核特性,是并行
'''

from multiprocessing import Process
import os #进程可以利用计算的的多核特性,一般会用到os模块
from time import sleep

def fact(name):
    print("当前进程:",os.getpid())
    print("当前父进程:",os.getppid())
    print(f"{name}start")
    sleep(3)
    print(f"{name}end")

if __name__ == '__main__':
    print("--当前进程ID:",os.getpid())
    #创建进程
    p1=Process(target=fact, args=("p1",))
    p2=Process(target=fact, args=("p2",))
    #启动进程
    p1.start()
    p2.start()
'''
main进程是父进程,创建了两个子进程p1和p2
每个进程都有一个唯一的标识--进程ID(PID,由os.getpid()获取,操作系统通过PID区分不同的进程)
'''

3.类模式创建进程

整体语法类似于创建线程

from multiprocessing import Process
from time import sleep

class MyProcess(Process):
    def __init__(self,name):
        Process.__init__(self)
        self.name=name

    def run(self):
        print(f"{self.name}进程启动了")
        sleep(3)
        print(f"{self.name}进程结束了")

if __name__ == '__main__':
    #创建进程
    p1=MyProcess("陈瑜如")
    p2=MyProcess("陈育斌")
    #启动进程
    p1.start()
    p2.start()

4.Queue实现进程通信

使用 Queue 模块中的 Queue 类实现线程间通信,但要实现进程间通信,需要使用 multiprocessing 模块中的 Queue 类。

简单的理解 Queue 实现进程间通信的方式,就是使用了操作系统给开辟的一个队列空间,各个进程可以把数据放到该队列中,当然也可以从队列中把自己需要的信息取走

'''
使用【Queue】模块中的【Queue】类实现线程间通信
使用【mulitprocessing】模块中的【Queue】类,给操作系统开辟了一个队列空间,实现进程之间的通信
'''

from multiprocessing import Queue,Process

class MyProcess(Process):
    def __init__(self,name,mq):
        Process.__init__(self)
        self.name=name
        self.mq = mq

    def run(self):
        print(f"{self.name}start...")
        print("--------","self.mq.get()","---------")
        self.mq.put(self.name)
        print(f"{self.name}end...")

if __name__ == '__main__':
    #创建进程列表
    t_list=[]
    mq=Queue()
    mq.put("1")
    mq.put("2")
    mq.put("3")
    for i in range(3):
        p=MyProcess(f"p{i}",mq)
        p.start()
        t_list.append(p)

    #等待进程结束
    for t in t_list:
        t.join()

    print("列表的最终内容为:")
    while not mq.empty():
        print(mq.get())

5.Pipe实现进程间通信

Pipe方法返回(conn1, conn2)代表一个管道的两个端。

Pipe方法有duplex参数,如果duplex参数为True(默认值),那么这个参数是全双工模式,也就是说conn1和conn2均可收发。若duplex为False,conn1只负责接收消息,conn2只负责发送消息。send和recv方法分别是发送和接受消息的方法。例如,在全双工模式下,可以调用conn1.send发送消息,conn1.recv接收消息。如果没有消息可接收,recv方法会一直阻塞。如果管道已经被关闭,那么recv方法会抛出EOFError。

'''
1.Pipe管道实现进程间通信,返回(conn1,conn2),代表一个管道的两个端
2.Pipe方法有duplex参数,如果duplex为True,则参数是全双工模式,也就是说conn1和conn2均可收发
如果duplex为False,则conn2发送,conn1接收
3.全双工模式下,可以用conn1.send()发送消息;用conn1.recv()接收消息
'''

import multiprocessing
from time import sleep

def f1(conn1):
    sub_info="hello!"
    print(f"进程1--{multiprocessing.current_process().pid}发送了数据:{sub_info}")
    sleep(1)
    conn1.send(sub_info)
    print(f"来自进程2",conn1.recv())
    sleep(1)
def f2(conn2):
    sub_info="泥嚎!"
    print(f"进程2--{multiprocessing.current_process().pid}发送了数据:{sub_info}")
    sleep(1)
    conn2.send(sub_info)
    print(f"来自进程1:",conn2.recv())
    sleep(1)

if __name__ == '__main__':
    #创建管道
    conn1,conn2=multiprocessing.Pipe()
    #创建子进程
    p1=multiprocessing.Process(target=f1,args=(conn1,))
    p2=multiprocessing.Process(target=f2,args=(conn2,))
    #启动子进程
    p1.start()
    p2.start()

6.Manager管理器实现进程通信

'''
1.Manager用于跨进程共享数据,可以创建多种类型的共享数据(列表、字典等),让不同进程都能够安全地访问和修改
2.支持的共享对象类型:列表、字典、信号量、锁...
'''

from multiprocessing import Manager,Process

def fact(name,m_list,m_dict):
    m_dict['name']='西安邮电大学'
    m_list.append('hello')

if __name__ == '__main__':
    with Manager() as mgr:
        m_list=mgr.list()
        m_dict=mgr.dict()
        m_list.append("泥嚎")
        ##两个进程不能直接互相使用对象,需要互相传递
        p1=Process(target=fact,args=('p1',m_list,m_dict))
        p1.start()
        p1.join()
        print(m_list)
        print(m_dict)

7.Pool进程池

Python提供了更好的管理多个进程的方式,就是使用进程池。

进程池可以提供指定数量的进程给用户使用,即当有新的请求提交到进程池中时,如果池未满,则会创建一个新的进程用来执行该请求;反之,如果池中的进程数已经达到规定最大值,那么该请求就会等待,只要池中有进程空闲下来,该请求就能得到执行。

使用进程池的优点:

  1. 提高效率,节省开辟进程和开辟内存空间的时间及销毁进程的时间
  2. 节省内存空间
'''
1.进程池可以提供指定数量的进程给用户使用,
即当有新的请求提交到进程池中时,如果池未满,则会创建一个新的进程用来执行该请求;
反之,如果池中的进程数已经达到规定最大值,那么该请求就会等待,只要池中有进程空闲下来,该请求就能得到执行。
2.我的理解:进程池里放的是固定数量的进程 “位子”(或称为 “槽位”),而不是固定的进程本身
'''

'''进程池使用案例'''
from multiprocessing import Pool
import os
from time import sleep

def f1(name):
    print(f"当前{name}进程的ID:{os.getpid()}")
    sleep(2)
    return name

def f2(args):
    print(args)

if __name__ == '__main__':
    pool=Pool(5)

    pool.apply_async(func=f1,args=("stx1",),callback=f2)
    #这个进程调用f1并返回给f2之后,就让出了在进程池中占的位子了
    pool.apply_async(func=f1, args=('sxt2',), callback=f2)
    pool.apply_async(func=f1, args=('sxt3',), callback=f2)
    pool.apply_async(func=f1, args=('sxt4',))
    pool.apply_async(func=f1, args=('sxt5',))
    pool.apply_async(func=f1, args=('sxt6',))
    pool.apply_async(func=f1, args=('sxt7',))
    pool.apply_async(func=f1, args=('sxt8',))

    pool.close()
    pool.join()

'''输出为:'''
# 当前stx1进程的ID:21896
# 当前sxt2进程的ID:20776
# 当前sxt3进程的ID:9072
# 当前sxt4进程的ID:18540
# 当前sxt5进程的ID:13596
# stx1当前sxt6进程的ID:21896
#
# 当前sxt7进程的ID:9072sxt3
#
# 当前sxt8进程的ID:18540
# sxt2

五、协程

1.协程的核心

  1. 每个协程有自己的执行栈,可以保存自己的执行现场
  2. 可以由用户程序按需创建协程(比如:遇到io操作)
  3. 协程“主动让出(yield)”执行权时候,会保存执行现场(保存中断时的寄存器上下文和栈),然后切换到其他协程
  4. 协程恢复执行(resume)时,根据之前保存的执行现场恢复到中断前的状态,继续执行,这样就通过协程实现了轻量的由用户态调度的多任务模型

2.优缺点

协程的优点

  1. 由于自身带有上下文和栈,无需线程上下文切换的开销,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级;
  2. 无需原子操作的锁定及同步的开销;
  3. 方便切换控制流,简化编程模型
  4. 单线程内就可以实现并发的效果,最大限度地利用cpu,且可扩展性高,成本低(注:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理)
  5. asyncio协程是写爬虫比较好的方式。比多线程和多进程都好. 开辟新的线程和进程是非常耗时的

协程的缺点

  1. 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上。
  2. 当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。

3.anyncio异步IO是实现协程

  1. 正常的函数执行时是不会中断的,所以你要写一个能够中断的函数,就需要加async
  2. async 用来声明一个函数为异步函数,异步函数的特点是能在函数执行过程中挂起,去执行其他异步函数,等到挂起条件(假设挂起条件是sleep(5))消失后,也就是5秒到了再回来执行
  3. await 用来用来声明程序挂起,比如异步程序执行到某一步时需要等待的时间很长,就将此挂起,去执行其他的异步程序。
  4. asyncio是python3.5之后的协程模块,是python实现并发重要的包,这个包使用事件循环驱动实现并发。

代码实例:

import asyncio
import time


async def f1():
    for i in range(3):
        print(f"北京,第{i}次被打印")
        await asyncio.sleep(1)
    return "f1执行完毕"

async def f2():
    for i in range(3):
        print(f"上海,第{i}次被打印")
        await asyncio.sleep(1)
    return "f2执行完毕"

async def main():
    res=await asyncio.gather(f1(),f2())
    #await异步执行func1方法
    #返回值为函数的返回值列表
    print(res)

if __name__ == '__main__':
    start_time=time.time()
    asyncio.run(main())
    end_time=time.time()
    print(f"耗时{end_time-start_time}")

代码解释:

① 为什么用 asyncio.sleep(1) 而不是 time.sleep(1)

  • time.sleep(1) 是阻塞的:它会让整个线程暂停 1 秒,期间线程什么都做不了,其他任务也无法执行。如果在异步函数中用它,会导致整个事件循环被卡住,失去异步的意义。

  • asyncio.sleep(1) 是异步的:它不会阻塞(阻塞)线程,而是会主动让出执行权。当执行到 await asyncio.sleep(1) 时,当前协程会暂停,允许事件循环切换到其他就绪的协程(比如 f1 暂停时,f2 可以开始执行)。1 秒后,当前协程会重新进入就绪状态,等待事件循环调度继续执行。

② res = await asyncio.gather(f1(), f2()) 这行代码是在干什么?

这句话的作用是并发运行 f1() 和 f2() 两个协程,并收集它们的返回值

  • asyncio.gather(...):接收多个协程对象(这里是 f1() 和 f2()),并把它们交给事件循环并发执行(不是串行)。

  • await:等待所有传入的协程都执行完毕后,再继续往下走。此时主线程不会被阻塞,事件循环会在 f1 和 f2 之间自动切换(比如 f1 执行到 await sleep 时,就切换到 f2 执行)。

  • 返回值 res:是一个列表,按传入协程的顺序保存它们的返回值。比如 f1 返回 "f1执行完毕"f2 返回 "f2执行完毕",那么 res 就是 ["f1执行完毕", "f2执行完毕"]

您可能感兴趣的与本文相关的镜像

Python3.9

Python3.9

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值