
您很可能已经听说过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"
现在我们的地鼠侦察兵不需要积累矿石,他可以立即使用渠道将其进一步转移。

我更新了示例,现在找矿者和矿工的代码是匿名函数。 如果您以前从未遇到过它们,请不要太在意,只需记住,它们每个都使用关键字
go调用,因此,它将在自己的goroutine中执行。 这里最重要的是,goroutines使用
oreChan通道在它们之间传输数据。 我们将在最后处理匿名函数。
func main() { theMine := [5]string{“ore1”, “ore2”, “ore3”} oreChan := make(chan string)
以下结论清楚地表明,我们的矿工一次从通道矿石中获得了三份一份。
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)
这样的程序将输出以下内容:
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关键字之后立即声明一个匿名函数,并使用以下语法对其进行调用:
因此,如果只需要在一个地方调用一个函数,就可以在一个单独的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() {
在范围循环中从管道读取
在我们的示例中,在goffer-getter函数中,我们使用了
for循环从通道中选择了三个元素。 但是,如果事先不知道该通道中可以有多少数据,该怎么办? 在这种情况下,就像集合一样,您可以将channel用作
for-range循环的参数。 更新后的函数可能如下所示:
因此,矿工将读取侦察员发送给他的所有内容;在周期中使用通道将确保这一点。 请注意,处理完通道中的所有数据后,循环将锁定读取; 为了避免阻塞,您需要通过调用
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以及多线程程序为您带来的好处。