通过图片学习多线程Go编程


您很可能已经听说过Go编程语言,其流行程度正在不断提高,这是很合理的。 这种语言简单,快速,并且依赖于强大的社区。 该语言最令人好奇的方面之一是多线程编程模型。 它的基础原语使您可以轻松,简单地创建多线程程序。 本文适用于想要学习这些原语的人:goroutines和channel。 并且,通过插图,我将展示如何使用它们。 希望这对您的进一步学习有很好的帮助。

单线程和多线程程序


您很可能已经编写了单线程程序。 通常看起来像这样:有一组用于执行各种任务的函数,仅当上一个函数准备好数据时才调用每个函数。 因此,程序顺序运行。

那将是我们的第一个例子-矿石开采计划。 我们的职能将寻找,开采和加工矿石。 在我们的示例中,矿山中的矿石由字符串列表表示,函数将其用作参数并返回“已处理”字符串列表。 对于单线程程序,我们的应用程序将设计如下:



在此示例中,所有工作均由一个线程(Gary的地鼠)完成。 三个主要功能:搜索,生产和处理依次执行。

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

如果我们打印每个函数的结果,则会得到以下结果:

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

简单的设计和实现是单线程方法的优点。 但是,如果您想彼此独立地运行和执行功能,该怎么办? 在这里,多线程编程将为您提供帮助。


这种矿石开采方法效率更高。 现在,几个线程(地鼠)可以独立工作,而Gary只做一部分工作。 一个地鼠搜索矿石,另一个地鼠搜索矿石,第三次熔融,所有这些可能是同时发生的。 为了实现这种方法,我们在代码中需要做两件事:创建彼此独立的地鼠处理器,并在它们之间转移矿石。 Go具有为此的goroutines和渠道。

古鲁丁


Goroutines可以被认为是“轻量级线程”,要创建goroutines,只需将go关键字放在函数调用代码之前。 为了演示它的简单性,让我们创建两个搜索功能,使用go关键字调用它们,并在他们每次在矿山中发现“矿石”时打印一条消息。


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

我们程序的输出如下:

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

如您所见,函数首先“查找矿石”没有顺序; 搜索功能同时工作。 如果您多次运行该示例,则顺序将有所不同。 现在我们可以运行多线程(多领域)程序,这是一个重要的进步。 但是,当我们需要在独立的goroutine之间建立连接时该怎么办? 渠道神奇的时刻到了。

频道



通道允许goroutines交换数据。 这是一种管道,goroutin可以通过该管道从其他goroutine发送和接收信息。

使用箭头运算符(<-)进行通道的读取和写入,该操作符指示数据移动的方向。

 myFirstChannel := make(chan string) myFirstChannel <- "hello" //    myVariable := <- myFirstChannel //    

现在我们的地鼠侦察兵不需要积累矿石,他可以立即使用渠道将其进一步转移。

我更新了示例,现在找矿者和矿工的代码是匿名函数。 如果您以前从未遇到过它们,请不要太在意,只需记住,它们每个都使用关键字go调用,因此,它将在自己的goroutine中执行。 这里最重要的是,goroutines使用oreChan通道在它们之间传输数据。 我们将在最后处理匿名函数。

 func main() { theMine := [5]string{“ore1”, “ore2”, “ore3”} oreChan := make(chan string) //   go func(mine [5]string) { for _, item := range mine { oreChan <- item // } }(theMine) //   go func() { for i := 0; i < 3; i++ { foundOre := <-oreChan // fmt.Println(“Miner: Received “ + foundOre + “ from finder”) } }() <-time.After(time.Second * 5) //     } 

以下结论清楚地表明,我们的矿工一次从通道矿石中获得了三份一份。

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

因此,现在我们可以在不同的goroutines(密码器)之间传输数据,但是在开始编写复杂程序之前,让我们看一下通道的一些重要属性。

锁具


在某些情况下,使用通道时,goroutin可能会被阻塞。 这是必需的,以便goroutine在开始或继续工作之前可以彼此同步。

写锁




当goroutine(gopher)将数据发送到通道时,它将被阻塞,直到另一个goroutine从该通道读取数据为止。

读锁




类似于在写入通道时锁定,从通道读取时,goroutin可以被锁定,直到没有内容写入为止。
如果乍一看这些锁对您来说似乎很复杂,您可以将其想象为两个goroutine(密码)之间的“金钱转移”。 当一位地鼠想要转账或收款时,他必须等待交易的第二位参与者。

处理了通道上的goroutine锁之后,让我们讨论两种不同类型的通道:缓冲通道和非缓冲通道。 选择这种或那种类型,我们在很大程度上决定了程序的行为。

无缓冲通道




在前面的所有示例中,我们仅使用了此类通道。 在这样的信道上,一次只能发送一个数据(如上所述,有阻塞)。

缓冲通道




程序中的流不能总是完全同步。 假设在我们的示例中,发生了一名地鼠侦查员发现了三部分矿石,而一名地鼠矿工设法同时提取了一部分已发现储量的情况。 在这里,为了避免地鼠侦查花费大部分时间,等待矿工完成工作,我们将使用缓冲通道。 首先创建一个容量为3的频道。

 bufferedChan := make(chan string, 3) 

我们可以将一些数据发送到缓冲通道,而无需使用另一个goroutine读取它们。 这是与非缓冲通道的主要区别。


 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) }() 

这样的程序的输出顺序如下:

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

为避免不必要的复杂性,我们不会在程序中使用缓冲通道。 但是请务必记住,也可以使用这些类型的渠道。
同样重要的是要注意,缓冲的通道并不总是使您免于阻塞。 例如,如果一个地鼠侦察器比一个地鼠矿工快十倍,并且它们通过容量为2的缓冲通道连接,那么如果该地鼠侦查器在通道中已经有两个数据,则每次发送时都会被阻塞。

全部放在一起


因此,有了goroutine和通道,我们可以使用Go中多线程编程的所有优点来编写程序。


 theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} oreChannel := make(chan string) minedOreChan := make(chan string) //  go func(mine [5]string) { for _, item := range mine { if item == "ore" { oreChannel <- item //   oreChannel } } }(theMine) //  go func() { for i := 0; i < 3; i++ { foundOre := <-oreChannel //   oreChannel fmt.Println("From Finder: ", foundOre) minedOreChan <- "minedOre" //   minedOreChan } }() //  go func() { for i := 0; i < 3; i++ { minedOre := <-minedOreChan //   minedOreChan fmt.Println("From Miner: ", minedOre) fmt.Println("From Smelter: Ore is smelted") } }() <-time.After(time.Second * 5) //     

这样的程序将输出以下内容:

 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 

与我们的第一个示例相比,这是一个重大改进,现在所有功能都是独立执行的,每个功能都在自己的goroutine中。 另外,我们从通道中获得了一条输送机,矿石在加工后立即通过该输送机进行输送。 为了保持对通道和goroutine的操作的基本了解,我省略了一些要点,这可能会导致启动程序时遇到困难。 最后,我想详细介绍语言的这些功能,因为它们有助于使用goroutine和通道。

匿名猩猩




正如我们在goroutine中运行常规函数一样,我们可以在go关键字之后立即声明一个匿名函数,并使用以下语法对其进行调用:

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

因此,如果只需要在一个地方调用一个函数,就可以在一个单独的goroutine中运行它,而不必担心事先声明它。

主要功能是goroutine。




是的, main函数确实可以在自己的goroutine中工作。 而且,更重要的是,在其完成之后,所有其他goroutine也将结束。 因此,我们在函数的末尾放置了一个计时器调用。 该调用将创建一个通道,并在5秒钟后向其发送数据。

 <-time.After(time.Second * 5) //       

还记得在从通道读取消息时,goroutine将被阻止,直到有东西发送给它了吗? 这就是添加指定代码时发生的情况。 主goroutine将被阻止,使其他goroutias有5秒钟的工作时间。 该方法效果很好,但是通常使用不同的方法来验证所有goroutine已完成其工作。 为了发送有关工作完成的信号,创建了一个特殊通道,阻止了主goroutine对其进行读取,并且一旦子goroutine完成其工作,它将立即写入该通道。 主goroutine被解锁,程序结束。



 func main() { doneChan := make(chan string) go func() { //  -  doneChan <- “I'm all done!” }() <-doneChan //        } 

在范围循环中从管道读取


在我们的示例中,在goffer-getter函数中,我们使用了for循环从通道中选择了三个元素。 但是,如果事先不知道该通道中可以有多少数据,该怎么办? 在这种情况下,就像集合一样,您可以将channel用作for-range循环的参数。 更新后的函数可能如下所示:

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

因此,矿工将读取侦察员发送给他的所有内容;在周期中使用通道将确保这一点。 请注意,处理完通道中的所有数据后,循环将锁定读取; 为了避免阻塞,您需要通过调用close(channel)关闭通道

无阻塞频道读取


使用select-case构造,可以避免阻塞来自管道的读取。 以下是使用此构造的示例: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! 

无阻塞频道录制


使用相同的select-case结构可以避免在写入通道时发生锁定。 让我们对上一个示例进行一些小的编辑:

 select { case myChan <- “message”: fmt.Println(“sent the message”) default: fmt.Println(“no message sent”) } 

需要进一步研究




有大量文章和报告更详细地介绍了通道和goroutine的工作。 现在,借助代码,您对如何以及如何使用这些工具有了清晰的了解,您可以从以下材料中获得最大收益:



感谢您抽出宝贵的时间阅读。 希望我能帮助您了解频道,goroutines以及多线程程序为您带来的好处。

Source: https://habr.com/ru/post/zh-CN412715/


All Articles