图解 Go 并发

你极可能从某种途径据说过 Go 语言。它愈来愈受欢迎,而且有充分的理由能够证实。 Go 快速、简单,有强大的社区支持。学习这门语言最使人兴奋的一点是它的并发模型。 Go 的并发原语使建立多线程并发程序变得简单而有趣。我将经过插图介绍 Go 的并发原语,但愿能点透相关概念以方便后续学习。本文是写给 Go 语言编程新手以及准备开始学习 Go 并发原语 (goroutines 和 channels) 的同窗。编程

单线程程序 vs. 多线程程序

你可能已经写过一些单线程程序。一个经常使用的编程模式是组合多个函数来执行一个特定任务,而且只有前一个函数准备好数据,后面的才会被调用。多线程

single Gopher

首先咱们将用上述模式编写第一个例子的代码,一个描述挖矿的程序。它包含三个函数,分别负责执行寻矿、挖矿和练矿任务。在本例中,咱们用一组字符串表示 rock(矿山) 和 ore(矿石),每一个函数都以它们做为输入,并返回一组 “处理过的” 字符串。对于一个单线程的应用而言,该程序可能会按以下方式来设计:并发

ore mining single-threaded program

它有三个主要的函数:finderminer 和 smelter。该版本的程序的全部函数都在单一线程中运行,一个接着一个执行,而且这个线程 (名为 Gary 的 gopher) 须要处理所有工做。函数

func main() {
    theMine := [5]string{"rock", "ore", "ore", "rock", "ore"}
    foundOre := finder(theMine)
    minedOre := miner(foundOre)
    smelter(minedOre)
}

 

在每一个函数最后打印出 "ore" 处理后的结果,获得以下输出:学习

From Finder: [ore ore ore]
From Miner: [minedOre minedOre minedOre]
From Smelter: [smeltedOre smeltedOre smeltedOre]

  

这种编程风格具备易于设计的优势,可是当你想利用多个线程并执行彼此独立的函数时会发生什么呢?这就是并发程序设计发挥做用的地方。ui

ore mining concurrent program

这种设计使得 “挖矿” 更高效。如今多个线程 (gophers) 是独立运行的,从而 Gary 再也不承担所有工做。其中一个 gopher 负责寻矿,一个负责挖矿,另外一个负责练矿,这些工做可能同时进行。this

为了将这种并发特性引入咱们的代码,咱们须要建立独立运行的 gophers 的方法以及它们之间彼此通讯 (传送矿石) 的方法。这就须要用到 Go 的并发原语:goroutines 和 channels。spa

Goroutines

Goroutines 能够看做是轻量级线程。建立一个 goroutine 很是简单,只须要把 go 关键字放在函数调用语句前。为了说明这有多么简单,咱们建立两个 finder 函数,并用 go 调用,让它们每次找到 "ore" 就打印出来。线程

go myFunc()

func main() {
    theMine := [5]string{"rock", "ore", "ore", "rock", "ore"}
    go finder1(theMine)
    go finder2(theMine)
    <-time.After(time.Second * 5) //you can ignore this for now
}

  

程序的输出以下:设计

Finder 1 found ore!
Finder 2 found ore!
Finder 1 found ore!
Finder 1 found ore!
Finder 2 found ore!
Finder 2 found ore!

能够看出,两个 finder 是并发运行的。哪个先找到矿石没有肯定的顺序,当执行屡次程序时,这个顺序并不老是相同的。

这是一个很大的进步!如今咱们有一个简单的方法来建立多线程 (multi-gopher) 程序,可是当咱们须要独立的 goroutines 之间彼此通讯会发生什么呢?欢迎来到神奇的 channels 世界。

Channels

communication

Channels 容许 go routines 之间相互通讯。你能够把 channel 看做管道,goroutines 能够往里面发消息,也能够从中接收其它 go routines 的消息。

my first channel

myFirstChannel := make(chan string) 

Goroutines 能够往 channel 发送消息,也能够从中接收消息。这是经过箭头操做符 (<-) 完成的,它指示 channel 中的数据流向。

arrow

myFirstChannel <-"hello" // Send myVariable := <- myFirstChannel // Receive 

如今经过 channel 咱们可让寻矿 gopher 一找到矿石就当即传送给开矿 gopher ,而不用等发现全部矿石。

ore channel

我重写了挖矿程序,把寻矿和开矿函数改写成了未命名函数。若是你从未见过 lambda 函数,没必要过多关注这部分,只须要知道每一个函数将经过 go 关键字调用并运行在各自的 goroutine 中。重要的是,要注意 goroutine 之间是如何经过 channel oreChan 传递数据的。别担忧,我会在最后面解释未命名函数的。

func main() {
    theMine := [5]string{"ore1", "ore2", "ore3"}
    oreChan := make(chan string)

    // Finder
    go func(mine [5]string) {
        for _, item := range mine {
            oreChan <- item //send
        }
    }(theMine)

    // Ore Breaker
    go func() {
        for i := 0; i < 3; i++ {
            foundOre := <-oreChan //receive
            fmt.Println("Miner: Received " + foundOre + " from finder")
        }
    }()
    <-time.After(time.Second * 5) // Again, ignore this for now
}

  

从下面的输出,能够看到 Miner 从 oreChan 读取了三次,每次接收一块矿石。

Miner: Received ore1 from finder
Miner: Received ore2 from finder
Miner: Received ore3 from finder

太棒了,如今咱们能在程序的 goroutines(gophers) 之间发送数据了。在开始用 channels 写复杂的程序以前,咱们先来理解它的一些关键特性。

Channel Blocking

Channels 阻塞 goroutines 发生在各类情形下。这能在 goroutines 各自欢快地运行以前,实现彼此之间的短暂同步。

Blocking on a Send

blocking on send

一旦一个 goroutine(gopher) 向一个 channel 发送数据,它就被阻塞了,直到另外一个 goroutine 从该 channel 取走数据。

Blocking on a Receive

blocking on receive

和发送时情形相似,一个 goroutine 可能阻塞着等待从一个 channel 获取数据,若是尚未其余 goroutine 往该 channel 发送数据。

一开始接触阻塞的概念可能使人有些困惑,但你能够把它想象成两个 goroutines(gophers) 之间的交易。 其中一个 gopher 不管是等着收钱仍是送钱,都须要等待交易的另外一方出现。

既然已经了解 goroutine 经过 channel 通讯可能发生阻塞的不一样情形,让咱们讨论两种不一样类型的 channels: unbuffered 和 buffered 。选择使用哪种 channel 可能会改变程序的运行表现。

Unbuffered Channels

unbuffered channel

在前面的例子中咱们一直在用 unbuffered channels,它们不同凡响的地方在于每次只有一份数据能够经过。

Buffered Channels

buffered channel

在并发程序中,时间协调并不老是完美的。在挖矿的例子中,咱们可能遇到这样的情形:开矿 gopher 处理一块矿石所花的时间,寻矿 gohper 可能已经找到 3 块矿石了。为了避免让寻矿 gopher 浪费大量时间等着给开矿 gopher 传送矿石,咱们可使用 buffered channel。咱们先建立一个容量为 3 的 buffered channel。

bufferedChan := make(chan string, 3) 

buffered 和 unbuffered channels 工做原理相似,但有一点不一样—在须要另外一个 gorountine 取走数据以前,咱们能够向 buffered channel 发送多份数据。

cap 3 buffered channel

bufferedChan := make(chan string, 3)

go func() {
    bufferedChan <-"first"
    fmt.Println("Sent 1st")
    bufferedChan <-"second"
    fmt.Println("Sent 2nd")
    bufferedChan <-"third"
    fmt.Println("Sent 3rd")
}()

<-time.After(time.Second * 1)

go func() {
    firstRead := <- bufferedChan
    fmt.Println("Receiving..")
    fmt.Println(firstRead)
    secondRead := <- bufferedChan
    fmt.Println(secondRead)
    thirdRead := <- bufferedChan
    fmt.Println(thirdRead)
}()

  

两个 goroutines 之间的打印顺序以下:

Sent 1st
Sent 2nd
Sent 3rd
Receiving..
first
second
third

为了简单起见,咱们在最终的程序中不使用 buffered channels。但知道该使用哪一种 channel 是很重要的。

注意: 使用 buffered channels 并不会避免阻塞发生。例如,若是寻矿 gopher 比开矿 gopher 执行速度快 10 倍,而且它们经过一个容量为 2 的 buffered channel 进行通讯,那么寻矿 gopher 仍会发生屡次阻塞。

把这些都放到一块儿

如今凭借 goroutines 和 channels 的强大功能,咱们可使用 Go 的并发原语编写一个充分发挥多线程优点的程序了。

putting it all together

theMine := [5]string{"rock", "ore", "ore", "rock", "ore"}
oreChannel := make(chan string)
minedOreChan := make(chan string)

// Finder
go func(mine [5]string) {
    for _, item := range mine {
        if item == "ore" {
            oreChannel <- item //send item on oreChannel
        }
    }
}(theMine)

// Ore Breaker
go func() {
    for i := 0; i < 3; i++ {
        foundOre := <-oreChannel //read from oreChannel
        fmt.Println("From Finder:", foundOre)
        minedOreChan <-"minedOre" //send to minedOreChan
    }
}()

// Smelter
go func() {
    for i := 0; i < 3; i++ {
        minedOre := <-minedOreChan //read from minedOreChan
        fmt.Println("From Miner:", minedOre)
        fmt.Println("From Smelter: Ore is smelted")
    }
}()

<-time.After(time.Second * 5) // Again, you can ignore this

  

程序输出以下:

From Finder:  ore
From Finder:  ore
From Miner:  minedOre
From Smelter: Ore is smelted
From Miner:  minedOre
From Smelter: Ore is smelted
From Finder:  ore
From Miner:  minedOre
From Smelter: Ore is smelted

相比最初的例子,已经有了很大改进!如今每一个函数都独立地运行在各自的 goroutines 中。此外,每次处理完一块矿石,它就会被带进挖矿流水线的下一个阶段。

为了专一于理解 goroutines 和 channel 的基本概念,上文有些重要的信息我没有提,若是不知道的话,当你开始编程时它们可能会形成一些麻烦。既然你已经理解了 goroutines 和 channel 的工做原理,在开始用它们编写代码以前,让咱们先了解一些你应该知道的其余信息。

在开始以前,你应该知道...

匿名的 Goroutines

anonymous goroutine

相似于如何利用 go 关键字使一个函数运行在本身的 goroutine 中,咱们能够用以下方式建立一个匿名函数并运行在它的 goroutine 中:

// Anonymous go routine
go func() {
    fmt.Println("I'm running in my own go routine")
}()

  

若是只须要调用一次函数,经过这种方式咱们可让它在本身的 goroutine 中运行,而不须要建立一个正式的函数声明。

main 函数是一个 goroutine

main func

main 函数确实运行在本身的 goroutine 中!更重要的是要知道,一旦 main 函数返回,它将关掉当前正在运行的其余 goroutines。这就是为何咱们在 main 函数的最后设置了一个定时器—它建立了一个 channel,并在 5 秒后发送一个值。

<-time.After(time.Second * 5) // Receiving from channel after 5 sec 

还记得 goroutine 从 channel 中读数据如何被阻塞直到有数据发送到里面吧?经过添加上面这行代码,main routine 将会发生这种状况。它会阻塞,以给其余 goroutines 5 秒的时间来运行。

如今有更好的方式阻塞 main 函数直到其余全部 goroutines 都运行完。一般的作法是建立一个 done channel, main 函数在等待读取它时被阻塞。一旦完成工做,向这个 channel 发送数据,程序就会结束了。

done chan

func main() {
    doneChan := make(chan string)

    go func() {
        // Do some work…
        doneChan <- "I'm all done!"
    }()

    <-doneChan // block until go routine signals work is done
}

  

你能够遍历 channel

在前面的例子中咱们让 miner 在 for 循环中迭代 3 次从 channel 中读取数据。若是咱们不能确切知道将从 finder 接收多少块矿石呢?

好吧,相似于对集合数据类型 (注: 如 slice) 进行遍历,你也能够遍历一个 channel。

更新前面的 miner 函数,咱们能够这样写:

// Ore Breaker
go func() {
    for foundOre := range oreChan {
        fmt.Println("Miner: Received " + foundOre + " from finder")
    }
}()

  

因为 miner 须要读取 finder 发送给它的全部数据,遍历 channel 能确保咱们接收到已经发送的全部数据。

遍历 channel 会阻塞,直到有新数据被发送到 channel。在全部数据发送完以后避免 go routine 阻塞的惟一方法就是用 "close(channel)" 关掉 channel。

对 channel 进行非阻塞读

但你刚刚告诉咱们 channel 如何阻塞 goroutine 的各类情形?!没错,不过还有一个技巧,利用 Go 的 select case 语句能够实现对 channel 的非阻塞读。经过使用这这种语句,若是 channel 有数据,goroutine 将会从中读取,不然就执行默认的分支。

myChan := make(chan string)

go func(){
    myChan <- "Message!"
}()

select {
    case msg := <- myChan:
        fmt.Println(msg)
    default:
        fmt.Println("No Msg")
}
<-time.After(time.Second * 1)

select {
    case msg := <- myChan:
        fmt.Println(msg)
    default:
        fmt.Println("No Msg")
}

  

程序输出以下:

No Msg
Message!

对 channel 进行非阻塞写

非阻塞写也是使用一样的 select case 语句来实现,惟一不一样的地方在于,case 语句看起来像是发送而不是接收。

select { case myChan <- "message": fmt.Println("sent the message") default: fmt.Println("no message sent") } 
相关文章
相关标签/搜索