并发与并行一直两个容易搞混的概念:

  • 并发:同一个时间段,共同运行的任务。任务在这个时间段内,启动和结束的时间是可以有先后之分的。举例来说:公司的食堂在中午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自动处理。