并发与并行一直两个容易搞混的概念:
- 并发:同一个时间段,共同运行的任务。任务在这个时间段内,启动和结束的时间是可以有先后之分的。举例来说:公司的食堂在中午12点-13点之间开放,累计接待了100人就餐。这100人的就餐任务就是并发的。
- 并行:同一个时刻,共同启动并运行的任务。任务的结束时间可能有先后之分,但是启动的时刻是一致的。举例来说:田径短跑比赛,10名选手站在起跑线上。一声枪响,所有运动员向终点发起冲刺。这10人的比赛任务就是并行的。
并行需要将多个进程绑定到多个CPU内核上来实现。并发就灵活很多,既可以在多进程多CPU条件下实现,也可以在单核多线程的环境下运行。
备注:进程\线程\协程的概念以及Python的多线程与Golang的groutine之间的区别,资料很多就不详述了,这里主要演示操作。
在Python中,并发通过threading库实现,代码如下:
import threading
import time
import datetime
def Calc(s):
for i in range(1, 6):
s = s + i
print("当前的值是{}".format(s))
# 每次循环等待1s
time.sleep(1)
def Say(x):
for i in range(1, 6):
print("这是第< {0} > 次说 < {1} >".format(i, x))
# 每次循环等待1s
time.sleep(1)
# 创建一个列表,用于存储要启动多线程的实例
threads = []
calc = threading.Thread(target=Calc, args=(5,))
# 线程任务追加至队列
threads.append(calc)
say = threading.Thread(target=Say, args=("哈哈",))
threads.append(say)
start = datetime.datetime.now()
for thr in threads:
#把列表中的实例遍历出来后,调用start()方法为每个实例分配一个线程
thr.start()
for thr in threads:
"""
让主线程等待线程结束之后最后再结束。
"""
if thr:
thr.join()
timerange = datetime.datetime.now() - start
print('程序执行耗时 {}'.format(timerange))
执行结果,正常串行运行两个函数需要耗时10s左右。当前耗时5s左右,说明2个函数通过thr.start()方法是并发的运行的。
% python main.py
当前的值是6
这是第< 1 > 次说 < 哈哈 >
当前的值是8
这是第< 2 > 次说 < 哈哈 >
这是第< 3 > 次说 < 哈哈 >
当前的值是11
这是第< 4 > 次说 < 哈哈 >
当前的值是15
这是第< 5 > 次说 < 哈哈 >
当前的值是20
程序执行耗时 0:00:05.020021
在Go中,并发是通过groutine实现的。
package main
import (
"fmt"
"sync"
"time"
)
// 声明WaitGroup,用于确保主groutine一定晚于子groutine结束
var wg sync.WaitGroup
func Calc(s int) {
// 告知 WaitGroup 执行这个函数的子groutine已结束
defer wg.Done()
for i := 1; i < 6; i++ {
s = s + i
fmt.Println("s当前的值是: ", s)
time.Sleep(time.Second * 1)
}
}
func Say(x string) {
defer wg.Done()
for i := 1; i < 6; i++ {
fmt.Printf("这是第< %d >次说<%s>\n", i, x)
time.Sleep(time.Second * 1)
}
}
func main() {
StartTime := time.Now()
// 告知WaitGroup 会有2个子groutine运行。每完成一个子任务,Add()中的值-1
wg.Add(2)
// 分配启动一个子groutine 运行绑定的函数
go Calc(1)
go Say("哈哈")
// 锁住主groutine的运行,直至声明的Add()中的值减为0
// 类似python中 join()的功能
wg.Wait()
fmt.Println("程序执行结束")
TimeRange := time.Since(StartTime)
fmt.Printf("程序执行耗时 %v", TimeRange)
}
运行结果与python中一致
% go run main.go
这是第< 1 >次说<哈哈>
s当前的值是: 2
s当前的值是: 4
这是第< 2 >次说<哈哈>
s当前的值是: 7
这是第< 3 >次说<哈哈>
s当前的值是: 11
这是第< 4 >次说<哈哈>
s当前的值是: 16
这是第< 5 >次说<哈哈>
程序执行结束
程序执行耗时 5.00587825s
总结:
- Go在启动并发的语法方面比Python要简单
- groutine是介于线程与协程之间的东西,比线程更轻量、比协程更自动
- Python受限于GIL,无论启动多少线程都只能运行在一个CPU内核上。在数据量比较小的情况多并行效率与Go差异不大。
- 在Go中,线程与groutine是一堆多的关系。多个groutine会被自动分配到多个CPU内核上运行。所以启动groutine并发程序,那么任务的处理并发和并行的场景都有可能,由golang自动处理。