程序的并发实现方式,常见的无非如题所述的三种:
线程、进程和协程。本文将总结三者的区别,以及在 python 和 golang 等语言中的实际应用。
并发、并行、异步、同步#
- 并发 (Concurrency),是指在同一段时间内执行多个程序
- 并行(Parallelism),是指在同一刻有多个程序在同时执行
并发可以理解为两个(或多个)程序表现上能同时进行。
而真正意义上的同时,对于单核CPU,真正意义上的并行(在某一个时刻,同时运行多个程序)并不存在。
因此从直白的说,并发是一种程序的调度技术,使CPU的空闲时间降低,而大大提高效率。
CPU 本身也有并发技术,如中断机制、流水 CPU、DMA等等,这并非是本文的重点。
- 异步(Asynchrony),是指多个程序的运行并没有时间上的顺序关系
- 非阻塞(Non-blocking),是指程序的执行过程中不存在等待,是异步的实现
- 同步(Synchrony),和异步相对,程序的运行有时间上的顺序,通常会有阻塞
- 阻塞 (Blocking), 是指任务执行过程中的暂停或等待,通常是为了实现同步
在时序逻辑电路中也有同步和异步的概念,同步的时序逻辑电路通常引入一个统一的时钟信号。
并发安全#
程序的并发需要考虑的一个问题就是并发安全。
对于一段数据,由于并发的存在,可能存在脏读的情况。
一般的解决方式是通过加锁的方式,产生一定程度上的阻塞,从而避免并发导致的数据冲突。
能避免由于并发而造成的数据冲突的数据结构称之为并发安全的(或是线程安全的)
线程 (Thread) 是 CPU 调度的最小单位。也就是说子线程的执行先后顺序是由CPU决定调度的。
一个进程可以有多个线程。
Python 中多线程的实现#
1
2
3
4
5
6
7
8
9
10
11
12
13
| import threading
def sub_program(name: str) -> None:
for _ in range(5):
print(f'Hello {name}')
def main() -> None:
threads = [threading.Thread(target=sub_program, args=(str(i),)) for i in range(10)]
for thread in threads:
thread.start()
if __name__ == '__main__':
main()
|
进程 (Process) 是计算机资源分配的最小单位,是并行。
以下是 Python 实现多进程
Python 多进程的实现#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| import multiprocessing
def sub_program(name: str) -> None:
for _ in range(5):
print(f'Hello {name}')
def main() -> None:
pool = multiprocessing.Pool(5)
for i in range(5):
pool.apply_async(sub_program, args=(str(i),))
pool.close()
pool.join()
if __name__ == '__main__':
main()
|
协程 (Coroutine), 也叫微线程。
是在一个线程内异步执行,
需要程序自己进行调动(而不是CPU去调动)
资源消耗相比线程来说更小。
Python 协程的实现#
1
2
3
4
5
6
7
8
9
10
11
12
| import asyncio
async def sub_program(name: str) -> None:
for _ in range(5):
print(f'Hello {name}')
async def main() -> None:
await asyncio.gather(*[sub_program(str(i)) for i in range(5)])
if __name__ == '__main__':
asyncio.run(main())
|
并发开销#
总的来说,资源开销: 协程 < 线程 < 进程。
GIL 锁#
GIL (全局解释器锁)是指只能有一个线程解释执行 Python 的字节码。
这是因为一些 Python 的解释器(例如 CPython)的实现不是线程安全的,
通过 GIL 可以避免多个线程访问修改同一数据。
GIL的存在对于CPU密集型的任务来说,可能会影响多线程的性能。也是 Python 为人诟病的一点。
Goroutine#
Go 在语言层面支持并发操作得益于 Go 的 Goroutine.
Goroutine 是一种协程的实现。
而相比 Python 在协程上的繁琐程度,Go通过关键字 go
可以实现并发。
例如下面的一段样例,
该样例模拟的是两个输入设备,一个设备等待用户的输入,而另一个设备每隔1s进行一次输入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| import asyncio
async def func():
while True:
await asyncio.sleep(1)
print('ran')
async def func2():
print(await asyncio.get_running_loop().run_in_executor(None, input, '>'))
async def main():
await asyncio.gather(func(), func2())
asyncio.run(main())
|
而 Golang
的实现相比而言就简单很多
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| package main
import (
"fmt"
"time"
)
func func1() {
for {
time.Sleep(1 * time.Second)
fmt.Println("ran")
}
}
func func2() {
var input string
fmt.Print(">")
fmt.Scanln(&input)
fmt.Println(input)
}
func main() {
go func1()
for {
func2()
}
}
|
作为对比,上述代码的多线程版本(python)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| import threading
import time
def func():
while True:
time.sleep(1)
print('ran')
def func2():
print(input('>'))
x = threading.Thread(target=func)
x.start()
while True:
func2()
|