协程
在 Go 中,应用程序并发处理的部分被称作 goroutines(协程) ,它可以进行更有效的并发运算。
操作系统会在物理处理器上调度线程来运行,而 Go 语言的运行时会在逻辑处理器上调度
goroutine来运行。每个逻辑处理器都分别绑定到单个操作系统线程。
协程是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程
最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。
这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
下面将通过具体例子来说明:
1.
2. import (
3. "fmt"
4. "time"
5. )
6.
7.
8. func main() {
9. fmt.Println("the main begin")
10. go longWait()
11. go shortWait()
12. fmt.Println("About to sleep in main()")
13. // sleep works with a Duration in nanoseconds (ns) !
14. time.Sleep(10 * 1e9)
15. fmt.Println("At the end of main()")
16. }
17.
18.
19. func longWait() {
20. fmt.Println("Beginning longWait()")
21. time.Sleep(5 * 1e9) // sleep for 5 seconds
22. fmt.Println("End of longWait()")
23. }
24. func shortWait() {
25. fmt.Println("Beginning shortWait()")
26. time.Sleep(2 * 1e9) // sleep for 2 seconds
27. fmt.Println("End of shortWait()")
main() , longWait() 和 shortWait() 三个函数作为独立的处理单元按顺序启动,然后开始并行运行。每一个函数都在运行的开始和结束阶段输出了消息。为了模拟他们运算的时间消耗,我们使用了time 包中的 Sleep 函数。 Sleep() 可以按照指定的时间来暂停函数或协程的执行,这里使用了纳秒(ns,符号 1e9 表示 1 乘 10 的 9 次方,e=指数)。他们按照我们期望的顺序打印出了消息,几乎都一样,可是我们明白这是模拟出来的,以并行的方式。我们让 main() 函数暂停 10 秒从而确定它会在另外两个协程之后结束。如果不这样(如果我们让main() 函数停止 4 秒), main() 会提前结束, longWait() 则无法完成。如果我们不在main() 中等待,协程会随着程序的结束而消亡。当 main() 函数返回的时候,程序退出:它不会等待任何其他非 main 协程的结束。这就是为什么在服务器程序中,每一个请求都会启动一个协程来处理, server() 函数必须保持运行状态。通常使用一个无限循环来达到这样的目的。另外,协程是独立的处理单元,一旦陆续启动一些协程,你无法确定他们是什么时候真正开始执行的。你的代码逻辑必须独立于协程调用的顺序。
如果把go关键字去掉,则不是协程 ,运行结果为
通道(channel)
Go有一个特殊的类型, 通道(channel) ,像是通道(管道),可以通过它们发送类型化的数据在协程之间通信,可以避开所有内存共享导致的坑;通道的通信方式保证了同步性。数据通过通道:同一时间只有一个协程可以访问数据:所以不会出现数据竞争,设计如此。数据的归属(可以读写数据的能力)被传递。
通道服务于通信的两个目的:值的交换,同步的,保证了两个计算(协程)任何时候都是可知状态
var name chan datatype
var ch1 chan int
ch1 = make(chan int)
或者 ch1 := make(chan int )
函数通道: funcChan := chan func()
所以通道只能传输一种类型的数据,比如 chan int 或者 chan string ,所有的类型都可以用于通道,空接口 interface{} 也可以。甚至可以(有时非常有用)创建通道的通道。通道实际上是类型化消息的队列:使数据得以传输。它是先进先出(FIFO)结构的所以可以保证发送给他们的元素的顺序(有些人知道,通道可以比作 Unix shells 中的双向管道(tw-way pipe))。通道也是引用类型,所以我们使用 make() 函数来给它分配内存。这里先声明了一个字符串通道 ch1,然后创建了它(实例化)这个操作符直观的标示了数据的传输:信息按照箭头的方向流动。
流向通道(发送)
ch <- int1 表示:用通道 ch 发送变量 int1(双目运算符,中缀 = 发送)
从通道流出(接收),三种方式:
int2 = <- ch 表示:变量 int2 从通道 ch(一元运算的前缀操作符,前缀 = 接收)接收数据(获取
新值);假设 int2 已经声明过了,如果没有的话可以写成: int2 := <- ch 。
<- ch 可以单独调用获取通道的(下一个)值,当前值会被丢弃,但是可以用来验证,下面是一个简单的例子来说明协程之间通过通道来进行通信的机制
1.
2. import (
3. "fmt"
4. "time"
5. )
6.
7.
8. func main() {
9. ch :=make(chan string)
10.
11. go sendData(ch)
12. go getData(ch)
13.
14. time.Sleep(1e9)
15. }
16.
17. func sendData(ch chan string){
18. ch <- "Beijing"
19. ch <- "Tokyo"
20. ch <- "Washington"
21. ch <- "London"
22. ch <- "Moscow"
23. }
24.
25. func getData(ch chan string){
26. // var input string
27. for input := range ch {
28.
29. fmt.Printf("%s\n",input)
30. }
31. }
一个传递数据,一个接收数据,传递数据通过通道,读取通过 "range ch“的方式可以遍历
Select等待机制
1.
2. import (
3. "fmt"
4. "time"
5. )
6.
7. func main() {
8. ch1 :=make(chan int)
9. ch2 :=make(chan int)
10.
11. go pump1(ch1)
12. go pump2(ch2)
13.
14. go suck(ch1,ch2)
15. time.Sleep(1e9)
16. }
17.
18. func pump1(ch chan int){
19. for i := 0;i<10; i++ {
20. ch <- i * 2
21. }
22. }
23.
24. func pump2(ch chan int){
25. for i :=0;i<10;i++{
26. ch <- i+5
27. }
28. }
29.
30. func suck(ch1,ch2 chan int) {
31. for {
32. select {
33. case v := <-ch1:
34. fmt.Printf("Received on channel 1: %d\n", v)
35. case v := <-ch2:
36. fmt.Printf("Received on channel 2: %d\n", v)
37. }
38. }
39. }
有 2 个通道 ch1 和 ch2 ,三个协程 pump1() 、 pump2() 和suck() 。这是一个典型的生产者消费者模式。在无限循环中, ch1 和 ch2 通过 pump1() 和pump2() 填充整数; suck() 也是在无限循环中轮询输入的,通过 select 语句获取 ch1 和ch2 的整数并输出。选择哪一个 case 取决于哪一个通道收到了信息。程序在 main 执行 1 秒后结束。