文章目录
- 一、Python 线程
- 二、threading 模块
- 三、例程
- 3.1 基本用法
- 3.2 同步
- 3.21 Lock(锁)
- 3.22 RLock(递归锁)
- 3.23 Condition(条件变量)
- 3.24 Semaphore(信号量)
- 四、GIL
- 4.1 简述
- 4.2 详细
- 4.3 有GIL多线程仍要加锁
- 五、协程 asyncio
一、Python 线程
线程是一种轻量级的并行执行方式,它可以让我们在一个程序中同时执行多个任务。线程编程是多任务处理的一种特殊形式,它允许我们创建多个线程来执行不同的任务。
多线程通常是为了利用计算机的多核CPU(并行处理),来提升程序的运行速度。然而在多核CPU出现的几十年之前就出现多线程的概念了,其目的是不显式的切换任务的情况下,让CPU可以并发的处理若干个任务,提高资源利用率。
因为GIL的存在,有人说“python的多线程没有意义”。的确,由于GIL的存在,python的多线程无法像其它语言一样通过利用多核CPU提高程序运行速度,但其在处理IO瓶颈任务和处理需要低延迟的小任务时,仍然具有优势。同时,多线程也可解决协程调度的问题。
我第一次使用python线程是GUI程序,当主窗口打开一个子窗口的时候,子窗口如果有正在处理的任务,主窗口就会卡住,所以使用了线程。
二、threading 模块
Python的threading模块提供了多个类来支持多线程编程:
- Thread类:这是threading模块中最核心的类。它代表了一个线程,可以独立执行任务。创建Thread对象时,可以传递一个callable对象作为目标函数。Thread类还有一个重要的方法
start()
用于启动线程,以及join()
方法用于等待线程完成。- Lock类:用于实现线程间的互斥锁机制,确保同一时间只有一个线程能够访问特定的资源或代码段。它有两个主要的方法:
acquire()
和release()
,分别用于获取和释放锁。- RLock类:与Lock类似,但RLock允许同一个线程多次获得锁,这在递归调用时非常有用。
- Semaphore类:是一个更高级的锁机制,允许一定数量的线程同时访问资源。Semaphore维护了一个计数器,通过
acquire()
和release()
方法来增加或减少计数值。- Event类:用于实现线程间的同步,一个线程可以通过Event对象向另一个线程发送信号。Event对象有一个内部标志,可以通过
set()
和clear()
方法来设置和清除这个标志。- Condition类:类似于Event,但提供了更复杂的同步机制。它允许线程等待某个条件成立,然后才继续执行。通常与Lock或RLock一起使用。
- Barrier类:用于实现线程间的同步屏障,当所有线程都达到某个点时,它们才会被允许继续执行。
- Timer类:用于在指定的延迟后调用一个函数。它不是直接用于线程同步,但可以用于安排未来的任务。
- ThreadLocal类:提供了线程局部数据的功能,允许每个线程拥有自己独立的对象实例。
部分方法:
类 | 方法 | 描述 |
---|---|---|
Thread | start() | 开始线程执行。 |
run() | 线程要执行的方法。默认情况下是调用target 参数指定的函数,可以被子类重写。 | |
join(timeout=None) | 等待线程终止。如果设置了timeout ,则最多等待timeout 秒。 | |
is_alive() | 判断线程是否在运行。 | |
getName() | 获取线程名称。 | |
setName(name) | 设置线程名称。 | |
Lock | acquire(blocking=True, timeout=-1) | 获取锁。blocking=True 时,阻塞直到获得锁或超时;blocking=False 时,非阻塞获取锁。timeout 指定非阻塞等待时间。 |
release() | 释放锁。 | |
locked() | 检查锁的状态。 | |
RLock | acquire(blocking=True, timeout=-1) | 获取重入锁。行为类似于Lock ,但支持同一线程多次获取锁。 |
release() | 释放重入锁。 | |
locked() | 检查锁的状态。 | |
Condition | acquire(blocking=True, timeout=-1) | 获取锁,用于线程间同步。行为类似于Lock ,但用于线程间协调。 |
release() | 释放锁,用于线程间同步。 | |
wait(timeout=None) | 等待直到被通知或超时。必须在已获得锁的情况下调用。 | |
notify(n=1) | 通知等待的线程,至少通知n 个线程。 | |
notify_all() | 通知所有等待的线程。 | |
Semaphore | acquire(blocking=True, timeout=None) | 获取信号量。类似于Lock ,但允许多个线程同时访问临界区,但有一定限制。 |
release() | 释放信号量。 | |
Event | set() | 设置事件标志为True,通知等待该事件的所有线程。 |
clear() | 设置事件标志为False。 | |
is_set() | 检查事件标志是否为True。 | |
Timer | start() | 开始计时器线程。在指定时间后调用指定函数。 |
cancel() | 取消计时器。如果计时器仍在等待运行,则取消。 |
三、例程
3.1 基本用法
import threading
def my_function():
print("Thread {} is running...".format(threading.Thread.getName(my_thread)))
# 创建线程
my_thread = threading.Thread(target=my_function, name="myThread")
# 启动线程
my_thread.start()
# 等待线程执行完成
my_thread.join()
print("Main thread ends.")
3.2 同步
Python中线程同步的方法有以下几种:
- 锁(Lock):这是最基本的同步机制,用于确保同一时间只有一个线程能够访问特定的资源或代码段。通过
acquire()
方法加锁和release()
方法解锁来实现线程间的互斥。 - 递归锁(RLock):与普通锁类似,但允许同一个线程多次获得锁,适用于递归调用的情况。
- 信号量(Semaphore):用于控制同时访问特定资源的线程数量。当一个线程完成对资源的访问后,会释放信号量,允许其他线程进入。
- 事件(Event):用于通知所有等待的线程某个事件已经发生。线程可以通过
wait()
方法等待事件发生,通过set()
方法来通知事件已发生。 - 条件变量(Condition):允许线程等待某些条件成立,然后才继续执行。通常与锁一起使用,以防止多个线程同时改变条件。
- 队列(Queue):提供了一种适合多线程编程的数据结构,可以在不同线程之间安全地传递消息。
- 全局解释器锁(GIL):虽然不是直接由程序员控制的同步机制,但它是Python中的一个内置机制,用于确保在任何给定时刻,只有一个线程能够执行Python字节码。
它们的特点和适用场景:
工具 | 特点 | 适用场景 |
---|---|---|
Lock | 最基本的互斥锁,一次只允许一个线程访问共享资源 不可重入,即同一线程再次获取会导致死锁 | 简单的线程同步需求 需要确保一段代码同一时间只能被一个线程执行 |
RLock | 可重入锁,同一线程可以多次获取锁并释放 允许同一线程多次调用 acquire() | 复杂的递归线程同步需求 某些情况下需要允许同一线程多次获取和释放锁 |
Semaphore | 允许一定数量的线程同时访问共享资源 控制并发数量 | 有限资源的并发控制 控制同时运行的线程数量,比如限流 |
Event | 可以通过 set() 和 clear() 设置和清除事件状态 线程可以等待事件的发生 | 线程间通信和同步 一个线程等待某个事件的发生,另一个线程触发事件 |
Condition | 提供了更高级的线程同步机制,结合了锁和事件 | 复杂的线程协调和通信需求 - 允许线程等待某个条件,其他线程在满足条件时通知等待的线程继续执行 |
3.21 Lock(锁)
threading.Lock
类提供了最基本的线程同步机制,它可以确保一次只有一个线程可以访问共享资源。acquire()
方法用于获取锁,release()
方法用于释放锁。- 示例:
import threading
import time
# 创建一个锁对象
lock = threading.Lock()
def worker():
# 获取锁
lock.acquire()
try:
# 执行需要同步的操作
print("Thread {} is working...".format(threading.Thread.getName(t)))
# 模拟耗时操作
time.sleep(1)
finally:
# 释放锁
lock.release()
# 创建多个线程并启动它们
threads = []
for i in range(5):
t = threading.Thread(target=worker)
threads.append(t)
t.start()
# 等待所有线程完成
for t in threads:
t.join()
print("All threads finished.")
3.22 RLock(递归锁)
threading.RLock
是一个可重入锁,允许同一线程多次获得锁。acquire()
和release()
方法的使用方式与Lock
类似,但允许同一线程多次调用acquire()
。- 示例:
import threading
import time
class Counter:
def __init__(self):
self.value = 0
self.lock = threading.RLock()
def increment(self):
with self.lock:
self.value += 1
print(f"Incremented to {self.value} by {threading.currentThread().getName()}")
time.sleep(0.1) # 模拟一些计算或I/O操作
def decrement(self):
with self.lock:
self.value -= 1
print(f"Decremented to {self.value} by {threading.currentThread().getName()}")
time.sleep(0.1) # 模拟一些计算或I/O操作
def worker(counter):
for _ in range(3):
counter.increment()
counter.decrement()
# 创建 Counter 实例
counter = Counter()
# 创建多个线程
threads = []
for i in range(3):
thread = threading.Thread(target=worker, args=(counter,))
threads.append(thread)
thread.start()
# 等待线程执行完成
for thread in threads:
thread.join()
print("Final counter value:", counter.value)
3.23 Condition(条件变量)
threading.Condition
是一个高级的线程同步工具,同时提供了锁和条件等待/通知机制。acquire()
和release()
方法用于加锁和解锁,wait()
方法用于等待条件的通知,notify()
和notify_all()
方法用于发送通知。- 示例:
import threading
shared_resource = []
condition = threading.Condition()
def consumer():
with condition:
print("Consumer waiting...")
condition.wait()
print("Consumer consumed the resource:", shared_resource.pop(0))
def producer():
with condition:
print("Producer producing resource...")
shared_resource.append("New Resource")
condition.notify()
print("Producer notified the consumer.")
# 创建线程
consumer_thread = threading.Thread(target=consumer)
producer_thread = threading.Thread(target=producer)
# 启动线程
consumer_thread.start()
producer_thread.start()
# 等待线程执行完成
consumer_thread.join()
producer_thread.join()
print("Main thread ends.")
3.24 Semaphore(信号量)
threading.Semaphore
是一种控制并发访问的计数器,它允许多个线程同时访问共享资源,但可以限制同时访问的线程数量。acquire()
和release()
方法用于获取和释放信号量。- 示例:
import threading
semaphore = threading.Semaphore(value=2) # 允许同时两个线程访问
def access_resource():
with semaphore:
print(threading.currentThread().getName(), "is accessing the resource.")
# 假设这里是对共享资源的访问
# 创建多个线程
threads = []
for i in range(5):
thread = threading.Thread(target=access_resource)
threads.append(thread)
thread.start()
# 等待线程执行完成
for thread in threads:
thread.join()
print("Main thread ends.")
这些是 Python 中常用的线程同步方法。选择合适的方法取决于你的应用场景,例如是否需要多次获取锁、是否需要等待条件、是否需要限制并发数量等。这些同步工具能够有效地管理多线程程序中的竞态条件,确保线程安全地访问共享资源。
四、GIL
4.1 简述
Python的全局解释器锁(Global Interpreter Lock
,简称GIL)是CPython解释器中的一种线程同步机制。具体如下:
- 原理与作用:GIL是一种互斥锁,它确保在任何时刻只有一个线程执行Python字节码。这意味着即使在多核CPU上,使用多线程的Python程序也无法实现真正的并行执行。
- 优缺点:GIL的存在简化了内存管理和解释器的实现,因为不需要担心多个线程同时修改内存中的数据结构。然而,这也限制了多线程在计算密集型任务中的应用,因为GIL会阻止多个线程同时利用多核处理器的优势。
- 性能瓶颈:对于I/O密集型任务,GIL的影响相对较小,因为线程大部分时间都在等待I/O操作,而不是执行计算。但是,对于计算密集型任务,GIL可能导致性能瓶颈,因为它限制了多线程的并行能力。
- 解决方案:为了克服GIL的限制,可以使用多进程代替多线程(多进程未必使程序更快),因为每个进程都有自己的Python解释器和GIL,从而可以在多核CPU上并行运行。此外,还可以使用Jython或IronPython这样的替代Python解释器,它们没有GIL的限制。
- 未来展望:Python社区正在努力解决GIL的问题。例如,Python 3.12引入了GIL可选项,允许在编译时关闭GIL,以提高CPU密集型场景的性能。
在cpython
的PR中可以看到前几天的一条PR,即添加GIL的开关。
当然不是release版,能用到可能还需要很久。
有时候,限制程序性能的可能不是GIL,而是程序的生产者。🤣
4.2 详细
看的高天视频。
在讲解GIL之前,首先需要澄清一个概念:什么是线程?
- 线程是操作系统进行计算和调度的最小单位。我们可以简单地理解为,程序运行在线程中。每个线程有自己的上下文。
- 而进程是比线程更大的单位,每个进程有自己的内存等。一个进程可以包含多个线程,这些线程共享进程的内存,也就是说这些线程可以读写相同的变量。
当一个进程有不止一个线程时,就会出现一种情况,称为"racing
"或"竞争冒险"。因为一个进程中的多个线程,既可能同时运行,也可能交替运行。无论是同时运行还是交替运行,你都无法控制它们之间的相对顺序。
举例:
a = 1
if a > 0:
a -= 1
- 假设两个线程都在运行这个函数。
- 我们有"线程一"和"线程二"。假设它们两个都成功地将 A 初始化为 1。注意,他们两个共享变量 A。
- 假设 “多线程一” 先来判断 if A 大于零,他发现是 true,然后进入了 if 语句。
- 这时候 “多线程二” 开始判断,同时也发现 A 大于零,然后也进入了 if 语句。
- 由于他们两个线程都进了 if 语句,所以 A 被减了两次。然而,左边这个程序的目的显然是将 A 减到零。
- 在更多情况下,可能是线程一运行,然后 if A 大于零,A 减一,然后 A 变成零了。这时候线程二再来判断 if A 大于零,他就发现 A 不大于零了,然后他就跳过了 if 语句。
- 这种情况,由于线程之间的相对运行顺序不同,导致了结果不同,我们称之为"竞争冒险"。
如果你学过 C 和 C++ 的话,你会知道在这些语言里,你需要显式地分配(如malloc)和释放(free)内存。如果你只分配不释放,随着程序的运行,你占用的内存会越来越多,最终导致内存泄漏。但是在 Python 中,你不需要显式地去分配和释放内存。所有的 Python 对象,包括列表和字典等,你拿来就可以直接使用,不用担心这些繁琐的事情,因为 Python 的解释器会帮你管理内存。
那么 Python 是如何实现自动分配和释放内存的呢?内存分配相对容易,我需要内存时我就拿就行了,关键是什么时候可以释放它。Python 使用的机制叫做"引用计数"。引用计数的原理并不复杂,每一个 Python 对象都数着有多少个地方在使用它。当没有新的地方在使用它时,对象的引用计数加一;当这个对象不再被使用时,引用计数减一。这样一来,只要你数数是对的,Python 就可以知道什么时候这个对象的引用计数变为零,这时候就没有人需要它了,于是自动帮你释放掉这块内存。
这个过程本身并不难理解,就是数数嘛。
然而,结合刚才我们提到的"竞争冒险",我们可以想象,如果一个进程中有多个线程在运行的话,这里就会存在一个"竞争冒险"的问题。因为这个减少引用计数的操作并不是"原子性(atomic)"的。"原子性"的意思是在运行的时候不会被其他线程打断。这个减少引用计数虽然在 C 语言中看起来像一个操作符,但它实际上也要先读取这个引用计数的信息,然后减一再存回去。在这三个步骤中间,就有可能有其他的线程过来,在你存回去之前也进行这个操作。这种情况的发生就可能导致你数数数错了,多数了一个,或者少数了一个。一旦你数数数不清楚了,你就无法保证每一个 Python 对象都能被正确释放,那就会出现严重的内存泄漏问题。
在多线程中一般来说,我们会使用"加锁"来解决这个问题。加锁的意思是,我要保证这一段程序只有一个线程在运行,其他线程不能进入这段程序。
伪代码:
# 在这个if之前,先锁住
lock.acquire()
if a > 0:
a -= 1
# 在这个if之后,释放锁
lock.release()
通过这种方式,在运行 if a > 0: 语句块时,其他线程无法进入这段代码。他们需要等待当前线程释放锁之后,才能再次运行这段程序。
回到 Python。你可能会认为在这个例子中,只需在 if 外面加一个锁就可以解决问题。但是在 Python 中,并不仅仅是引用计数存在这个问题,所有与 Python 对象相关的代码都有可能存在竞争冒险的问题,都有可能有多个线程同时尝试读取或写入 Python 对象的数据。因此,当 Python 设计者决定给 Python 设计一个全局锁,也就是我们所说的 GIL ,这是一个比较简单的解决方案。
GIL,全局解释器锁,位于我们之前提到的 CPython
的主循环中。它的作用是确保在它运行完之前,当前线程持有 GIL 锁。通过这种机制,Python 可以确保每一个字节码在运行时都拿到线程锁。换言之,没有线程能够在运行期间打断执行任何字节码。因此,在每一个字节码中运行的 C 程序都是线程安全的。你可以在里面放心地增加引用计数、减少引用计数,而不必担心锁的问题,因为你知道锁已经被拿住了。
全局锁带来的好处是非常多的:
- 首先,这是一个非常简单的设计。在编写较大项目时,你会发现简单真的很重要。程序越简单,你维护所需要的努力就越少。相比于为每个对象实现自己的锁,全局锁要简单得多。
- 其次,由于只有一个线程锁,它避免了死锁问题。死锁是指一个线程拥有两个以上的锁时可能发生的情况。
- 第三,对于单线程程序或者不能并行的多线程程序来说,全局锁的性能是非常优秀的。因为全局锁保证了每次运行一个字节码时最多只需要一次锁。但是如果每个对象都有自己的锁,你可能需要多次锁。
- 最后,它让编写 C 扩展变得更容易。因为你可以确定每个字节码运行时都没有竞争冒险的问题。这样在你的 C 代码中修改 Python 对象时,你就不必担心锁的问题了,这让第三方开发者编写扩展变得更容易。
正因为这些优点,GIL 至今仍然存在于 Python 中。当然,也有人尝试过从 Python 中移除 GIL,但没有一次尝试能够保证 Python 在单线程下的运行速度不受影响。另外,还有一个非常严重的问题,即所谓的向后兼容性。也就是说,之前写的 C 扩展都默认现在有线程锁。如果你现在把这个东西拿掉了,那之前写的扩展很可能就无法使用了。
上面谈到了 GIL 的一些优势,但是它也受到很多人的批评,因为它限制了 Python 在多核 CPU 下的表现。然而,在 Python 中,有其他的方法来避免这个问题。
- 最简单也是最 Python 的方法就是**使用多进程。**虽然一个进程不能利用多个 CPU 核心,但我可以有很多个进程,通过多进程可以避开 GIL 的问题,并利用多核 CPU 来加速程序。
- 第二种方法是编写 C 扩展,然后在 C 中实现多线程,让多线程运行的是 C 代码而不是 Python 代码。当然,这样一来,你需要自己解决竞争冒险的问题。
- 最后,你还可以尝试使用一些没有 GIL 的 Python 解释器,像 Jython 和 IronPython。不过,它们也有自己的问题。
4.3 有GIL多线程仍要加锁
尽管Python的全局解释器锁(GIL)确保了同一时刻只有一个线程执行Python字节码,这防止了多个线程同时修改Python对象,从而避免了一些竞态条件问题。然而,这并不意味着你可以完全不需要锁。
简单例程:
import threading
counter = 0
def f():
global counter
for _ in range(1000000):
counter += 1
t1 = threading.Thread(target=f)
t2 = threading.Thread(target=f)
t1.start()
t2.start()
t1.join()
t2.join()
print(counter)
分别用 python3.12 和 python3.8 来运行:
结果是不一样的。Why ???
python虚拟机的核心在_PyEval_EvalFrameDefault
函数里面,先看一下3.8版本的,主要是main loop:
,它负责一个一个运行字节码,循环开始时,对eval_breaker
的值进行了检查,它保存的信息是是否需要交出GIL。
每一次循环,即一个字节码的运行时,都会检查是否需要交出GIL(给其他线程)。但这样开销较大。可以看到不同字节码可能对应 FAST_DISPATCH
和 DISPATCH
,前者不会触发GIL检查,后者会。python3.8 里面使用
DISPATCH的占大多数。即大部分字节码运行后有可能交出GIL,给其它线程使用。
查看前面程序的字节码:python -m dis .\gil.py
9 12 LOAD_GLOBAL 1 (counter)
14 LOAD_CONST 2 (1)
16 INPLACE_ADD
而INPLACE_ADD
对应的是DISPATCH
,即第一个线程运行+1后还没将结果保存到counter就可能会交出GIL,然后线程2在那里自加,之后又将运行权交给线程1,线程1将很久之前的值拿来加,导致结果错误。
在python3.8中
#define FAST_DISPATCH() goto fast_next_opcode
#define DISPATCH() continue
而3.10中:
#define DISPATCH() goto predispatch;
#endif
#define CHECK_EVAL_BREAKER() \
if (_Py_atomic_load_relaxed(eval_breaker)) { \
continue; \
}
FAST_DISPATCH
消失,新加CHECK_EVAL_BREAKER
(检查是否释放GIL),DISPATCH宏变成了goto predispatch
。
从3.10开始只有少数字节码会触发GIL转移,即函数调用、jump。
验证:之前在3.12中运行不加锁会出现正确结果,但现在加一个中间变量,线程的racing又出现了:
因为这里的jump(JUMP_BACKWARD)操作可能会触发GIL的转移。
11 46 LOAD_GLOBAL 0 (counter)
56 LOAD_CONST 2 (1)
58 BINARY_OP 0 (+)
62 STORE_FAST 0 (temp)
64 JUMP_BACKWARD 15 (to 36)
尽管前面3.12能运行出正确的结果,但这只是一种behavior,而不是feature。
五、协程 asyncio
协程(Coroutine
)是一种运行在单线程中的并发操作结构,它允许程序在特定的位置挂起(暂停)和恢复执行。与线程相比,协程更加轻量级,因为它们不需要像线程那样维护操作系统的上下文切换开销。协程的实现通常由程序员自己控制,可以更加灵活地控制执行流程,有助于编写高效、简洁的异步代码。
在 Python 中,协程通常使用 asyncio
模块来实现。Python 3.5 引入了 async 和 await 关键字,使得定义和使用协程变得更加方便。
协程的特点包括:
- 轻量级: 协程是由程序员自己控制的,不需要操作系统的上下文切换开销,因此更加轻量级。
- 非阻塞式的异步编程: 协程允许在等待 I/O 操作(如网络请求、文件读写等)时让出执行权,让其他协程继续执行,从而实现非阻塞式的异步编程。这使得 Python 应用程序可以高效地处理大量并发的 I/O 操作。
- 易于理解和编写: 使用 async 和 await 关键字,协程的编写更加直观和易于理解。相比于回调函数或者使用生成器实现的协程,async/await 的语法更加清晰。
- 可以并发执行: 在一个单线程中,可以同时运行多个协程,由事件循环来调度它们的执行。
await
关键字用于在异步函数中等待一个协程的执行结果。它只能在async def
定义的异步函数中使用。当程序执行到await语句时,它会暂停当前协程的执行,将控制权交还给事件循环,直到等待的协程完成执行并返回结果。然后,程序会继续执行后续代码。
asyncio
方法:
- 创建和管理任务(Tasks):
asyncio.create_task(coroutine)
:创建一个Task
对象来运行指定的协程。asyncio.gather(*coroutines, return_exceptions=False)
:并发运行多个协程,并等待它们全部完成。如果return_exceptions
设置为True
,则不会在协程抛出异常时立即取消其他协程,而是等待所有协程完成,异常信息会放在结果列表中。
- 事件循环(Event Loop):
asyncio.get_event_loop()
:获取当前事件循环。asyncio.set_event_loop(loop)
:设置当前事件循环。loop.run_until_complete(future)
:运行直到future
完成。future
可以是一个任务(Task)或协程对象。loop.run_forever()
:运行事件循环,直到调用loop.stop()
。loop.stop()
:停止事件循环。
- 等待和挂起:
await asyncio.sleep(delay, result=None)
:挂起当前协程,等待指定的时间(秒),然后继续执行。await asyncio.wait(tasks, timeout=None, return_when=ALL_COMPLETED)
:等待指定的任务(Tasks),直到满足指定的返回条件。asyncio.wait_for(future, timeout)
:等待future
完成或者超时。asyncio.shield(aw)
:保护一个协程,使其不受取消的影响。
- 事件和回调:
loop.call_soon(callback, *args)
:调度一个回调函数让事件循环尽快执行。loop.call_later(delay, callback, *args)
:调度一个回调函数让事件循环在指定延迟后执行。loop.call_at(when, callback, *args)
:在指定的时间调度一个回调函数。loop.time()
:返回事件循环的时间。
- 信号和事件:
asyncio.Event()
:创建一个事件对象,用于协程间的通信。asyncio.Queue(maxsize=0)
:创建一个队列,用于协程间的数据交换。loop.add_signal_handler(signal, callback, *args)
:为信号添加一个回调函数。
从 Python 3.7 开始,Python 提供了 asyncio.run()
函数来简化运行 async 函数的过程。这个函数会自动创建一个新的事件循环,并运行给定的 async 函数,然后在完成后关闭事件循环。
import asyncio
async def foo():
print('Start foo')
await asyncio.sleep(1)
print('End foo')
async def main():
print('Start main')
await foo()
print('End main')
asyncio.run(main())