最近会开始继续 Python 的进阶系列文章,这是该系列的第一篇文章,介绍进程和线程的知识,刚好上一篇文章就介绍了采用 concurrent.futures
模块实现多进程和多线程的操作,本文则介绍下进程和线程的概念,多进程和多线程各自的实现方法和优缺点,以及分别在哪些情况采用多进程,或者是多线程。
概念
并发编程就是实现让程序同时执行多个任务,而如何实现并发编程呢,这里就涉及到进程和线程这两个概念。
对于操作系统来说,一个任务(或者程序)就是一个进程(Process),比如打开一个浏览器是开启一个浏览器进程,打开微信就启动了一个微信的进程,打开两个记事本,就启动两个记事本进程。
进程的特点有:
- 操作系统以进程为单位分配存储空间, 每个进程有自己的地址空间、数据栈以及其他用于跟踪进程执行的辅助数据;
- 进程可以通过
fork
或者spawn
方式创建新的进程来执行其他任务 - 进程都有自己独立的内存空间,所以进程需要通过进程间通信机制(IPC,Inter-Process Communication)来实现数据共享,具体的方式包括管道、信号、套接字、共享内存区等
一个进程还可以同时做多件事情,比如在 Word 里面同时进行打字、拼音检查、打印等事情,也就是一个任务分为多个子任务同时进行,这些进程内的子任务被称为线程(Thread)。
因为每个进程至少需要完成一件事情,也就是一个进程至少有一个线程。当要实现并发编程,也就是同时执行多任务时,有以下三种解决方案:
- 多进程,每个进程只有一个线程,但多个进程一起执行多个任务;
- 多线程,只启动一个进程,但一个进程内开启多个线程;
- 多进程+多线程,即启动多个进程,每个进程又启动多个线程,但这种方法非常复杂,实际很少使用
注意:真正的并行执行多任务只有在多核 CPU 上才可以实现,单核 CPU 系统中,真正的并发是不可能的,因为在某个时刻能够获得CPU的只有唯一的一个线程,多个线程共享了CPU的执行时间。
Python 是同时支持多进程和多线程的,下面就分别介绍多进程和多线程。
多进程
在 Unix/Linux
系统中,提供了一个 fork()
系统调用,它是一个特殊的函数,普通函数调用是调用一次,返回一次,但 fork
函数调用一次,返回两次,因为调用该函数的是父进程,然后复制出一份子进程了,最后同时在父进程和子进程内返回,所以会返回两次。
子进程返回的永远是 0
,而父进程会返回子进程的 ID,因为父进程可以复制多个子进程,所以需要记录每个子进程的 ID,而子进程可以通过调用 getpid()
获取父进程的 ID。
Python 中 os
模块封装了常见的系统调用,这就包括了 fork
,代码示例如下:
import os
print('Process (%s) start...' % os.getpid())
# Only works on Unix/Linux/Mac:
pid = os.fork()
if pid == 0:
print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
else:
print('I (%s) just created a child process (%s).' % (os.getpid(), pid))
运行结果:
Process (876) start...
I (876) just created a child process (877).
I am child process (877) and my parent is 876.
由于 windows 系统中是不存在 fork
,所以上述函数无法调用,但 Python 是跨平台的,所以也还是有其他模块可以实现多进程的功能,比如 multiprocessing
模块。
multiprocess
multiprocessing
模块中提供了 Process
类来代表一个进程对象,接下来用一个下载文件的例子来说明采用多进程和不用多进程的差别。
首先是不采用多进程的例子:
def download_task(filename):
'''模拟下载文件'''
print('开始下载%s...' % filename)
time_to_download = randint(5, 10)
sleep(time_to_download)
print('%s下载完成! 耗费了%d秒' % (filename, time_to_download))
def download_without_multiprocess():
'''不采用多进程'''
start = time()
download_task('Python.pdf')
download_task('nazha.mkv')
end = time()
print('总共耗费了%.2f秒.' % (end - start))
if __name__ == '__main__':
download_without_multiprocess()
运行结果如下,这里用 randint
函数来随机输出当前下载文件的耗时,从结果看,程序运行时间等于两个下载文件的任务时间总和。
开始下载Python.pdf...
Python.pdf下载完成! 耗费了9秒
开始下载nazha.mkv...
nazha.mkv下载完成! 耗费了9秒
总共耗费了18.00秒.
如果是采用多进程,例子如下所示:
def download_task(filename):
'''模拟下载文件'''
print('开始下载%s...' % filename)
time_to_download = randint(5, 10)
sleep(time_to_download)
print('%s下载完成! 耗费了%d秒' % (filename, time_to_download))
def download_multiprocess():
'''采用多进程'''
start = time()
p1 = Process(target=download_task, args=('Python.pdf',))
p1.start()
p2 = Process(target=download_task, args=('nazha.mkv',))
p2.start()
p1.join()
p2.join()
end = time()
print('总共耗费了%.2f秒.' % (end - start))
if __name__ == '__main__':
download_multiprocess()
这里多进程例子中,我们通过 Process
类创建了进程对象,通过 target
参数传入一个函数表示进程需要执行的任务,args
是一个元组,表示传递给函数的参数,然后采用 start
来启动进程,而 join
方法表示等待进程执行结束。
运行结果如下所示,耗时就不是两个任务执行时间总和,速度上也是大大的提升了。
开始下载Python.pdf...
开始下载nazha.mkv...
Python.pdf下载完成! 耗费了5秒
nazha.mkv下载完成! 耗费了9秒
总共耗费了9.36秒.
Pool
上述例子是开启了两个进程,但如果需要开启大量的子进程,上述代码的写法就不合适了,应该采用进程池的方式批量创建子进程,还是用下载文件的例子,但执行下部分的代码如下所示:
import os
from multiprocessing import Process, Pool
from random import randint
from time import time, sleep
def download_multiprocess_pool():
'''采用多进程,并用 pool 管理进程池'''
start = time()
filenames = ['Python.pdf', 'nazha.mkv', 'something.mp4', 'lena.png', 'lol.avi']
# 进程池
p = Pool(5)
for i in range(5):
p.apply_async(download_task, args=(filenames[i], ))
print('Waiting for all subprocesses done...')
# 关闭进程池
p.close()
# 等待所有进程完成任务
p.join()
end = time()
print('总共耗费了%.2f秒.' % (end - start))
if __name__ == '__main__':
download_multiprocess_pool()
代码中 Pool
对象先创建了 5 个进程,然后 apply_async
方法就是并行启动进程执行任务了,调用 join()
方法之前必须先调用 close
() ,close
() 主要是关闭进程池,所以执行该方法后就不能再添加新的进程对象了。然后 join()
就是等待所有进程执行完任务。
运行结果如下所示:
Waiting for all subprocesses done...
开始下载Python.pdf...
开始下载nazha.mkv...
开始下载something.mp4...
开始下载lena.png...
开始下载lol.avi...
nazha.mkv下载完成! 耗费了5秒
lena.png下载完成! 耗费了6秒
something.mp4下载完成! 耗费了7秒
Python.pdf下载完成! 耗费了8秒
lol.avi下载完成! 耗费了9秒
总共耗费了9.80秒.
子进程
大多数情况,子进程是一个外部进程,而非自身。在创建子进程后,我们还需要控制子进程的输入和输出。
subprocess
模块可以让我们很好地开启子进程以及管理子进程的输入和输出。
下面是演示如何用 Python 演示命令 nslookup www.python.org
,代码如下所示:
import subprocess
print('$ nslookup www.python.org')
r = subprocess.call(['nslookup', 'www.python.org'])
print('Exit code:', r)
运行结果:
$ nslookup www.python.org
Server: 192.168.19.4
Address: 192.168.19.4#53
Non-authoritative answer:
www.python.org canonical name = python.map.fastly.net.
Name: python.map.fastly.net
Address: 199.27.79.223
Exit code: 0
如果子进程需要输入,可以通过 communicate()
进行输入,代码如下所示:
import subprocess
print('$ nslookup')
p = subprocess.Popen(['nslookup'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = p.communicate(b'set q=mx\npython.org\nexit\n')
print(output.decode('utf-8'))
print('Exit code:', p.returncode)
这段代码就是执行命令 nslookup
时,输入:
set q=mx
python.org
exit
运行结果:
$ nslookup
Server: 192.168.19.4
Address: 192.168.19.4#53
Non-authoritative answer:
python.org mail exchanger = 50 mail.python.org.
Authoritative answers can be found from:
mail.python.org internet address = 82.94.164.166
mail.python.org has AAAA address 2001:888:2000:d::a6
Exit code: 0
进程间通信
进程之间是需要通信的,multiprocess
模块中也提供了 Queue
、Pipes
等多种方式来交换数据。
这里以 Queue
为例,在父进程创建两个子进程,一个往 Queue
写入数据,另一个从 Queue
读取数据。代码如下:
import os
from multiprocessing import Process, Queue
import random
from time import time, sleep
# 写数据进程执行的代码:
def write(q):
print('Process to write: %s' % os.getpid())
for value in ['A', 'B', 'C']:
print('Put %s to queue...' % value)
q.put(value)
sleep(random.random())
# 读数据进程执行的代码:
def read(q):
print('Process to read: %s' % os.getpid())
while True:
value = q.get(True)
print('Get %s from queue.' % value)
def ipc_queue():
'''
采用 Queue 实现进程间通信
:return:
'''
# 父进程创建Queue,并传给各个子进程:
q = Queue()
pw = Process(target=write, args=(q,))
pr = Process(target=read, args=(q,))
# 启动子进程pw,写入:
pw.start()
# 启动子进程pr,读取:
pr.start()
# 等待pw结束:
pw.join()
# pr进程里是死循环,无法等待其结束,只能强行终止:
pr.terminate()
if __name__ == '__main__':
ipc_queue()
运行结果如下所示:
Process to write: 24992
Put A to queue...
Process to read: 22836
Get A from queue.
Put B to queue...
Get B from queue.
Put C to queue...
Get C from queue.
多线程
前面也提到了一个进程至少包含一个线程,其实进程就是由若干个线程组成的。线程是操作系统直接支持的执行单元,因此高级语言通常都内置多线程的支持,Python 也不例外,而且 Python 的线程是真正的 Posix Thread
,而不是模拟出来的线程。
多线程的运行有如下优点:
- 使用线程可以把占据长时间的程序中的任务放到后台去处理。
- 用户界面可以更加吸引人,比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度。
- 程序的运行速度可能加快。
- 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。
线程可以分为:
- **内核线程:**由操作系统内核创建和撤销。
- **用户线程:**不需要内核支持而在用户程序中实现的线程。
Python 的标准库提供了两个模块:_thread
和 threading
,前者是低级模块,后者是高级模块,对 _thread
进行了封装。大多数情况只需要采用 threading
模块即可,并且也推荐采用这个模块。
这里再次以下载文件作为例子,用多线程的方式来实现一遍:
from random import randint
from threading import Thread, current_thread
from time import time, sleep
def download(filename):
print('thread %s is running...' % current_thread().name)
print('开始下载%s...' % filename)
time_to_download = randint(5, 10)
sleep(time_to_download)
print('%s下载完成! 耗费了%d秒' % (filename, time_to_download))
def download_multi_threading():
print('thread %s is running...' % current_thread().name)
start = time()
t1 = Thread(target=download, args=('Python.pdf',), name='subthread-1')
t1.start()
t2 = Thread(target=download, args=('nazha.mkv',), name='subthread-2')
t2.start()
t1.join()
t2.join()
end = time()
print('总共耗费了%.3f秒' % (end - start))
print('thread %s is running...' % current_thread().name)
if __name__ == '__main__':
download_multi_threading()
实现多线程的方式和多进程类似,也是通过 Thread
类创建线程对象,target
参数表示传入需要执行的函数,args
参数是表示传给函数的参数,然后 name
是给当前线程进行命名,默认命名是如 Thread-
1、Thread-2
等等。
此外,任何进程默认会启动一个线程,我们将它称为主线程,主线程又可以启动新的线程,在 threading
模块中有一个函数 current_thread()
,可以返回当前线程的实例。主线程实例的名字叫 MainThread
,子线程的名字是在创建的时候指定,也就是 name
参数。
运行结果:
thread MainThread is running...
thread subthread-1 is running...
开始下载Python.pdf...
thread subthread-2 is running...
开始下载nazha.mkv...
nazha.mkv下载完成! 耗费了5秒
Python.pdf下载完成! 耗费了7秒
总共耗费了7.001秒
thread MainThread is running...
Lock
多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。
下面是一个例子,演示了多线程同时操作一个变量,如何把内存给改乱了:
from threading import Thread
from time import time, sleep
# 假定这是你的银行存款:
balance = 0
def change_it(n):
# 先存后取,结果应该为0:
global balance
balance = balance + n
balance = balance - n
def run_thread(n):
for i in range(100000):
change_it(n)
def nolock_multi_thread():
t1 = Thread(target=run_thread, args=(5,))
t2 = Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)
if __name__ == '__main__':
nolock_multi_thread()
运行结果:
-8
代码中定义了一个共享变量 balance
,然后启动两个线程,先存后取,理论上结果应该是 0
。但是,由于线程的调度是由操作系统决定的,当 t1、t2 交替执行时,只要循环次数足够多,balance
的结果就不一定是0
了。
原因就是下面这条语句:
balance = balance + n
这条语句的执行分为两步的:
- 先计算
balance + n
,保存结果到一个临时变量 - 将临时变量的值赋给
balance
也就是可以看成:
x = balance+n
balance=x
正常运行如下所示:
初始值 balance = 0
t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t1: balance = x1 # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1 # balance = 0
t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2 # balance = 8
t2: x2 = balance - 8 # x2 = 8 - 8 = 0
t2: balance = x2 # balance = 0
结果 balance = 0
但实际上两个线程是交替运行的,也就是:
初始值 balance = 0
t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2 # balance = 8
t1: balance = x1 # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1 # balance = 0
t2: x2 = balance - 8 # x2 = 0 - 8 = -8
t2: balance = x2 # balance = -8
结果 balance = -8
简单说,就是因为对 balance
的修改需要多条语句,而执行这几条语句的时候,线程可能中断,导致多个线程把同个对象的内容该乱了。
要保证计算正确,需要给 change_it()
添加一个锁,添加锁后,其他线程就必须等待当前线程执行完并释放锁,才可以执行该函数。并且锁是只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁。通过 threading
模块的 Lock
实现。
因此代码修改为:
from threading import Thread, Lock
from time import time, sleep
# 假定这是你的银行存款:
balance = 0
lock = Lock()
def change_it(n):
# 先存后取,结果应该为0:
global balance
balance = balance + n
balance = balance - n
def run_thread_lock(n):
for i in range(100000):
# 先要获取锁:
lock.acquire()
try:
# 放心地改吧:
change_it(n)
finally:
# 改完了一定要释放锁:
lock.release()
def nolock_multi_thread():
t1 = Thread(target=run_thread_lock, args=(5,))
t2 = Thread(target=run_thread_lock, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)
if __name__ == '__main__':
nolock_multi_thread()
但遗憾的是 Python 并不能完全发挥多线程的作用,这里可以通过写一个死循环,然后通过任务管理器查看进程的 CPU 使用率。
正常来说,如果有两个死循环线程,在多核CPU中,可以监控到会占用200%的CPU,也就是占用两个CPU核心。
要想把 N 核CPU的核心全部跑满,就必须启动 N 个死循环线程。
死循环代码如下所示:
import threading, multiprocessing
def loop():
x = 0
while True:
x = x ^ 1
for i in range(multiprocessing.cpu_count()):
t = threading.Thread(target=loop)
t.start()
在 4 核CPU上可以监控到 CPU 占用率仅有102%,也就是仅使用了一核。
但是用其他编程语言,比如C、C++或 Java来改写相同的死循环,直接可以把全部核心跑满,4核就跑到400%,8核就跑到800%,为什么Python不行呢?
因为 Python 的线程虽然是真正的线程,但解释器执行代码时,有一个 GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个 GIL 全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
GIL是 Python 解释器设计的历史遗留问题,通常我们用的解释器是官方实现的 CPython,要真正利用多核,除非重写一个不带GIL的解释器。
尽管多线程不能完全利用多核,但对于程序的运行效率提升还是很大的,如果想实现多核任务,可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
ThreadLocal
采用多线程的时候,一个线程采用自己的局部变量会比全局变量更好,原因前面也介绍了,如果不加锁,多个线程可能会乱改某个全局变量的数值,而局部变量是只有每个线程自己可见,不会影响其他线程。
不过,局部变量的使用也有问题,就是函数调用时候,传递起来会比较麻烦,即如下所示:
def process_student(name):
std = Student(name)
# std是局部变量,但是每个函数都要用它,因此必须传进去:
do_task_1(std)
do_task_2(std)
def do_task_1(std):
do_subtask_1(std)
do_subtask_2(std)
def do_task_2(std):
do_subtask_2(std)
do_subtask_2(std)
局部变量需要一层层传递给每个函数,比较麻烦,有没有更好的办法呢?
一个思路是用一个全局的 dict
,然后用每个线程作为 key
,代码例子如下所示:
global_dict = {}
def std_thread(name):
std = Student(name)
# 把std放到全局变量global_dict中:
global_dict[threading.current_thread()] = std
do_task_1()
do_task_2()
def do_task_1():
# 不传入std,而是根据当前线程查找:
std = global_dict[threading.current_thread()]
...
def do_task_2():
# 任何函数都可以查找出当前线程的std变量:
std = global_dict[threading.current_thread()]
这种方式理论上是可行的,它可以避免局部变量在每层函数中传递,只是获取局部变量的代码不够优雅,在 threading
模块中提供了 local
函数,可以自动完成这件事情,代码如下所示:
import threading
# 创建全局ThreadLocal对象:
local_school = threading.local()
def process_student():
# 获取当前线程关联的student:
std = local_school.student
print('Hello, %s (in %s)' % (std, threading.current_thread().name))
def process_thread(name):
# 绑定ThreadLocal的student:
local_school.student = name
process_student()
t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()
运行结果:
Hello, Alice (in Thread-A)
Hello, Bob (in Thread-B)
在代码中定义了一个全局变量 local_school
,它是一个 ThreadLocal
对象,每个线程都可以对它读写 student
属性,但又不会互相影响,也不需要管理锁的问题,这是 ThreadLocal
内部会处理。
ThreadLocal
最常用的是为每个线程绑定一个数据库连接,HTTP 请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
进程 vs 线程
我们已经分别介绍了多进程和多线程的实现方式,那么究竟应该选择哪种方法来实现并发编程呢,这两者有什么优缺点呢?
通常多任务的实现,我们都是设计 Master-Worker
,Master
负责分配任务,Worker
负责执行任务,因此多任务环境下,通常是一个 Master
和多个 Worker
。
如果用多进程实现 Master-Worker
,主进程就是 Master
,其他进程就是 Worker
。
如果用多线程实现 Master-Worker
,主线程就是 Master
,其他线程就是 Worker
。
对于多进程,最大的优点就是稳定性高,因为一个子进程挂了,不会影响主进程和其他子进程。当然主进程挂了,所有进程自然也就挂,但主进程只是负责分配任务,挂掉概率非常低。著名的 Apache 最早就是采用多进程模式。
缺点有:
- 创建进程代价大,特别是在 windows 系统,开销巨大,而
Unix/ Linux
系统因为可以调用fork()
,所以开销还行; - 操作系统可以同时运行的进程数量有限,会受到内存和 CPU 的限制。
对于多线程,通常会快过多进程,但也不会快太多;缺点就是稳定性不好,因为所有线程共享进程的内存,一个线程挂断都可能直接造成整个进程崩溃。比如在Windows上,如果一个线程执行的代码出了问题,你经常可以看到这样的提示:“该程序执行了非法操作,即将关闭”,其实往往是某个线程出了问题,但是操作系统会强制结束整个进程。
进程/线程切换
是否采用多任务模式,第一点需要注意的就是,一旦任务数量过多,效率肯定上不去,这主要是切换进程或者线程是有代价的。
操作系统在切换进程或者线程时的流程是这样的:
- 先保存当前执行的现场环境(CPU寄存器状态、内存页等)
- 然后把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等)
- 开始执行任务
这个切换过程虽然很快,但是也需要耗费时间,如果任务数量有上千个,操作系统可能就忙着切换任务,而没有时间执行任务,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。
计算密集型vsI/O密集型
采用多任务的第二个考虑就是任务的类型,可以将任务分为计算密集型和 I/O 密集型。
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如对视频进行编码解码或者格式转换等等,这种任务全靠 CPU 的运算能力,虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU 执行任务的效率就越低。计算密集型任务由于主要消耗CPU资源,这类任务用 Python这样的脚本语言去执行效率通常很低,最能胜任这类任务的是C语言,我们之前提到了 Python 中有嵌入 C/C++ 代码的机制。不过,如果必须用 Python 来处理,那最佳的就是采用多进程,而且任务数量最好是等同于 CPU 的核心数。
除了计算密集型任务,其他的涉及到网络、存储介质 I/O 的任务都可以视为 I/O 密集型任务,这类任务的特点是 CPU 消耗很少,任务的大部分时间都在等待 I/O 操作完成(因为 I/O 的速度远远低于 CPU 和内存的速度)。对于 I/O 密集型任务,如果启动多任务,就可以减少 I/O 等待时间从而让 CPU 高效率的运转。一般会采用多线程来处理 I/O 密集型任务。
异步 I/O
现代操作系统对 I/O 操作的改进中最为重要的就是支持异步 I/O。如果充分利用操作系统提供的异步 I/O 支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型。Nginx 就是支持异步 I/O的 Web 服务器,它在单核 CPU 上采用单进程模型就可以高效地支持多任务。在多核 CPU 上,可以运行多个进程(数量与CPU核心数相同),充分利用多核 CPU。用 Node.js 开发的服务器端程序也使用了这种工作模式,这也是当下实现多任务编程的一种趋势。
在 Python 中,单线程+异步 I/O 的编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。协程最大的优势就是极高的执行效率,因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销。协程的第二个优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不用加锁,只需要判断状态就好了,所以执行效率比多线程高很多。如果想要充分利用CPU的多核特性,最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
参考
以上就是本次教程的所有内容,代码已经上传到:
欢迎关注我的微信公众号--算法猿的成长,或者扫描下方的二维码,大家一起交流,学习和进步!