初始Golang通道

文档翻译自Golang channels tutorial

Golag内置并发功能的特性。通过把标识符go放置在将要执行的函数前,被执行的代码可以在相同的地址空间内启动独立的并发线程。在Golang中,称为goroutine。这里我想特别强调下并发并不意味着并行。Goroutines是创建并发体系结构的手段,可以在硬件允许的情况下并行执行。并发并不意味着并行

我们先试试goroutine的示例代码:

func main() {
     // Start a goroutine and execute println concurrently
     go println("goroutine message")
     println("main function message")
}
复制代码

这段程序将会打印main function message,但是可能打印goroutine function message。在main方法中衍生goroutine协程,但是在main方法中并没有等待goroutine方法执行完,此时程序就只会打印main function message,反之会打印出goroutine message。

你想必会想golang一定会有解决这种不确定性的方案,这就是我将要介绍的关于Golang通道的知识点。

通道的基础知识

通道可以将并发执行的程序同步,并通过特定值类型实现并发进程相互通信。通道有以下几个组成:通过通道传递的数据类型、通道的缓存容量、标识数据方向的标识符<-。开发者可以通过内置函数make为通道分配内存。

i := make(chan int)
s := make(chan string, 3)

r := make(<-chan bool) // can only read from
w := make(chan<- []os.FileInfo) // can only write to
复制代码

通道是基础的变量,使用场景与其它基础类型一样。诸如:结构元素、函数变量、函数返回值、甚至可以作为其它通道的类型:

c := make(chan <- chan bool)

func readFromChannel(input <-chan string)

func getChannel() chan bool {
    b := make(chan bool)
    return b
}
复制代码

可以通过标识符<-向通道中读写数据,如何创建通道并对它做些基础操作,现在让我们回到本文的第一个例子程序中,探究通道可以帮助我们事先什么功能?

package main
import (
	"fmt"
)

func main() {
	done := make(chan bool)

	go func() {
		fmt.Println("Goroutine message")
		done <- true
	}()

	fmt.Println("Main message")
	<-done
}
复制代码

这段程序将会将程序中所有message信息都打印出来,并不存在其它可能性。为什么会这样?done通道没有任何缓冲(由于我们没有为通道定义容量)。所有没有缓冲的通道都将会阻塞程序的执行,除非通道的发送和接受数据的功能都已经为通信做好准备,这也是无缓冲通道也被称为同步的原因。在我们的示例代码main函数中,消费done通道的数据一直会阻塞main函数的执行,直至goroutine向main通道中写入数据。因此程序只有在成功读取done通道的数据后才会结束。

对于有缓冲的通道:当缓存不为空时,所有通过通道读数据的操作都不会阻塞程序的执行。当缓存没有满时,所有向通道中写数据的操作都不会阻塞程序的执行。这样的通道可以称为异步。下面的代码将展示同步通道与异步通道的差异:

package main

import (
	"fmt"
	"time"
)

func main() {
	message := make(chan string)
	count := 3

	go func () {
		for i := 1; i <= count; i ++ {
			fmt.Println("send message")
			message <- fmt.Sprintf("message %d", i)
		}
	}()

	time.Sleep(time.Second * 3)
	for i := 1; i <= count; i ++ {
		fmt.Println(<-message)
	}
}
复制代码

上面的例子中message通道是同步的,程序的输出如下:

send message
// wait for 3 seconds
message 1
send message
send message
message 2
message 3
复制代码

如你所见,当第一次向通道中写入数据后,其它的写入数据操作都将被阻塞,直到3秒后,通道中的数据被消费。

如果我们使用的是有缓存的通道, 例如向下面那样创建message通道: message := make(chan tring, 2) 这时,程序将是下面的输出:

send message
send message
send message

// wait for 3 seconds
message 1
message 2
message 3
复制代码

通过上面的输出我们可以看到,所有缓存通道的写操作并没有因为通道中的数据没有被消费而阻塞。通过更改通道的容量,开发者可以控制系统吞吐量。

死锁

现在我们先看下面这段向通道中读写数据的示例代码片段:

package main
import (
    "fmt"
)

func main() {
    c :=  make(chan int)
    c <- 42
    val := <-c
    fmt.Println(val)
}
复制代码

执行上面的代码,将会得到下面的输出结果:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
     /fullpathtofile/channelsio.go:5 +0x54
exit status 2
复制代码

上面的输出结果是典型的死锁信息。这是由于两个goroutines相互等待对方,以至于任何一方都不能完成执行操作。Golang可以在运行时发现错误,然后将错误信息打印出来。因此这个错误是由于通道的通信被阻塞所导致的。

代码可以在单线程中按照顺序一行行的执行,只有当接收方顺利读取通道中的数据后,(c <- 42)才可以以同步的方式向通道中写入数据。因此开发者要在向通道写数据的代码后,添加向通道中消费数据的功能代码。

为了使程序顺利运行,开发者可以参考下面的修改:

package main
import (
    "fmt"
)

func main() {
    c := make(chan int)
    
    go func() {
        c <- 42
    }()
    
    val := <-c
    fmt.Println(val)
}
复制代码

Range channels and closing(非常抱歉,真心不知道给如何优雅的翻译)

在上面的了例子中,实现多次通过通道读写数据的代码如下:

for i := 1; i <= count; i ++ {
    fmt.Println(<-message)
}
复制代码

由于通道被消费的数据个数不能多于写入通道的数据个数,为了顺利通过通道读数据而且不产生死锁,开发者必须准确知道已经向通道中写入多少条数据。

在Golang中,通过range表达式的语法可以实现对数组、字符串、切片、maps和通道的遍历。对于通道而言,在通道关闭时会触发遍历通道的代码。可以参照下面的代码(下面的代码并不能正常工作):

package main
import (
    "fmt"
)

func main() {
    message := make(chan string)
    count := 3
    
    go func() {
       for i := 1; i <= count; i ++ {
           messager <- fmt.Sptintf("message %d", i)
       } 
    }()
    
    for msg := range message {
        fmt.Println(msg)
    }
}
复制代码

非常抱歉上面的程序并不能正常工作。我在上文也已经强调了,当通道被关闭时,range表达式才会被触发。因此为了程序能够正常工作,开发者必须在程序中调用close函数。这样gotroutine的代码将会是下面的样子:

go func() {
    for i := 1; i <= count; i ++ {
        message <- fmt.Sprintf("message %d", i)
    }
    close(message)
}()
复制代码

关闭通道的函数还具有一个额外的特性 - 当调用关闭通道的函数后,然后向空通道中读取数据或是在同一个线程中读数据,此时并不会阻塞程序。

package main
import (
	"fmt"
)

func main() {
	done := make(chan bool)
	close(done)

	fmt.Println(<- done)  // false
	fmt.Println(<- done)  // false
}
复制代码

这个特性也可以使用在同步化goroutines的代码中。现在让我们在回顾下同步goroutine的代码:

func main() {
     done := make(chan bool)

     go func() {
          println("goroutine message")

          // We are only interested in the fact of sending itself, 
          // but not in data being sent.
          done <- true
     }()

     println("main function message")
     <-done 
} 
复制代码

上面done通道只是为了使得程序可以同步执行,并不需要通过通道传递数据。因此我们可以将上面的代码作如下修改:

func main() {
     // Data is irrelevant
     done := make(chan struct{})

     go func() {
          println("goroutine message")

          // Just send a signal "I'm done"
          close(done)
     }()

     println("main function message")
     <-done
} 
复制代码

当开发者关闭了goroutine的通道后,并不会阻塞向通道读数据的操作,因此main函数可以继续执行。

多通道和select

在真实的开发需求中,开发者往往需要面对多个goroutine和通道。独立运行的模块越多,越是需要高效率的同步。让我们来看一个更加复杂的例子:

func getMessagesChannel(msg string, delay time.Duration) <-chan string {
     c := make(chan string)
     go func() {
          for i := 1; i <= 3; i++ {
               c <- fmt.Sprintf("%s %d", msg, i)
               // Wait before sending next message
               time.Sleep(time.Millisecond * delay)
          }
     }()
     return c
}

func main() {
     c1 := getMessagesChannel("first", 300)
     c2 := getMessagesChannel("second", 150)
     c3 := getMessagesChannel("third", 10)

     for i := 1; i <= 3; i++ {
          println(<-c1)
          println(<-c2)
          println(<-c3)
     }
}
复制代码

上面的代码中,通过创建通道、衍生spawns的goroutine函数和休眠固定的时间后返回通道。我们可以明显发现c3通道的时间间隔是最短的,因此期望先打印出c3通道中消息。然而程序的输出却是下面的内容:

first 1
second 1
third 1
first 2
second 2
third 2
first 3
second 3
third 3
复制代码

显然易见,程序的输出是正确的。按顺序的调用getMessagesChannel函数会衍生出相应goroutine和新的无缓存通道,因此只有当无缓存通道中数据被消费后,相应goroutine的代码才会继续向通道中写入数据。如果我们期望程序的运行结果是那个goroutine更快的向通道写入数据,将会被优先消费。

在Golang中,select关键词可以用来在通信中处理多通道的消息传递。这很像其它语言的switch语法,但是select使用场景只能用来向通道中读写数据。因此为了高效的处理多通道问题,可以参照下面的代码:

for i := 1; i <= 9; i++ {
     select {
     case msg := <-c1:
          println(msg)
	 case msg := <-c2:
          println(msg)
     case msg := <-c3:
          println(msg)
     }
}
复制代码

请注意数字9:由于每个通道都会3次向通道中写入数据,因此需要9次循环使用select。 现在我们将会得到期望的输出结果,不同的读写操作间也不会相互阻塞。输出是:

first 1
second 1
third 1 // this channel does not wait for others
third 2
third 3
second 2
first 2
second 3
first 3
复制代码

结论

通道是Golang中非常强大也很有趣的功能。但是如果想高效的使用这个语法特性,必须理解它的实现原理。在这篇文章中我试图向读者介绍关于使用通道的最基本知识点。如果你想进行深入的研究,可以参考下面的连接:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值