协程(goroutine)

协程(goroutine)是Go中应用程序并发处理的部分,它可以进行高效的并发运算。

  • 协程是轻量的,比线程更廉价。使用4K的栈内存就可以在内存中创建。
  • 能够对栈进行分割,动态地增加或缩减内存的使用。栈的管理会在协程退出后自动释放。
  • 协程的栈会根据需要进行伸缩,不出现栈溢出。

协程的使用

package mainimport ("fmt""time"
)func main() {fmt.Println("In main()")go longWait()go shortWait()fmt.Println("About to sleep in main()")//time.Sleep(4 * 1e9)time.Sleep(10 * 1e9)fmt.Println("At the end of main()")
}func longWait() {fmt.Println("Beginning longWait()")time.Sleep(5 * 1e9)fmt.Println("End of longWait()")
}func shortWait() {fmt.Println("Beginning shortWait()")time.Sleep(2 * 1e9)fmt.Println("End of shortWait()")
}
复制代码

Go中用go关键字来开启一个协程,其中main函数也可以看做是一个协程。

不难理解,上述代码的输出为:

In main()
About to sleep in main()
Beginning shortWait()
Beginning longWait()
End of shortWait()
End of longWait()
At the end of main()
复制代码

但是,当我们将main的睡眠时间设置成4s时,输出发生了改变。

In main()
About to sleep in main()
Beginning shortWait()
Beginning longWait()
End of shortWait()
At the end of main()
复制代码

程序并没有输出End of longWait(),原因在于,longWait()main()运行在不同的协程中,两者是异步的。也就是说,早在longWait()结束之前,main已经退出,自然也就看不到输出了。

通道(channel)

通道(channel)是Go中一种特殊的数据类型,可以通过它们发送类型化的数据在协程之间通信,避开内存共享导致的问题。

通道的通信方式保证了同步性,并且同一时间只有一个协程能够访问数据,不会出现数据竞争

以工厂的传输带为例,一个机器放置物品(生产者协程),经过传送带,到达下一个机器打包装箱(消费者协程)。

通道的使用

在学习使用管道之前,我们先来看一个“悲剧”。

package mainimport ("fmt""time"
)func main() {fmt.Println("Reveal romantic feelings...")go sendLove()go responseLove()waitFor()fmt.Println("Leaving ☠️....")
}func waitFor() {for i := 0; i < 5; i++ {fmt.Println("Keep waiting...")time.Sleep(1 * 1e9)}
}func sendLove() {fmt.Println("Love you, mm ❤️")
}func responseLove() {time.Sleep(6 * 1e9)fmt.Println("Love you, too")
}
复制代码

用上面学习的知识,不难看出。。。真的惨啊

Reveal romantic feelings...
Love you, mm ❤️
Keep waiting...
Keep waiting...
Keep waiting...
Keep waiting...
Keep waiting...
Leaving ☠️....
复制代码

明明收到了暗恋女孩的回应,然而却以为对方不接受自己的情感,含泪离去。【TAT】

可见,协程之间没有互相通信将会引起多么大的误解。幸好,我们有了channel,现在就来一起改写故事的结局吧~

package mainimport ("fmt""time"
)func main() {ch := make(chan string)var answer stringfmt.Println("Reveal fomantic feelings...")go sendLove()go responseLove(ch)waitFor()answer = <-chif answer != "" {fmt.Println(answer)} else {fmt.Println("Dead ☠️....")}}func waitFor() {for i := 0; i < 5; i++ {fmt.Println("Keep waiting...")time.Sleep(1 * 1e9)}
}func sendLove() {fmt.Println("Love you, mm ❤️")
}func responseLove(ch chan string) {time.Sleep(6 * 1e9)ch <- "Love you, too"
}
复制代码

输出为:

Reveal fomantic feelings...
Love you, mm ❤️
Keep waiting...
Keep waiting...
Keep waiting...
Keep waiting...
Keep waiting...
Love you, too
复制代码

皆大欢喜。

这里我们用ch := make(chan string)创建了一个string类型的管道,当然我们还可以构建其他类型比如ch := make(chan int),甚至一个函数管道funcChan := chan func()

我们还用到了一个通信操作符<-

  • 流向通道:ch <- content,用管道ch发送变量content。

  • 从通道流出:answer := <- ch,变量answer从通道ch接收数据。

  • <- ch可以单独调用,以获取通道的下一个值,当前值会被丢弃,但是可以用来验证,比如:

    if <- ch != 100 {/* do something */
    }
    复制代码

通道阻塞

  • 对于同一通道,发送操作在接受者准备好之前是不会结束的。这就意味着,如果一个无缓冲通道在没有空间接收数据的时候,新的输入数据无法输入,即发送者处于阻塞状态。
  • 对于同一通道,接收操作是阻塞的,直到发送者可用。如果通道中没有数据,接收者会保持阻塞。

以上两条性质,反映了无缓冲通道的特性:同一时间只允许至多一个数据存在于通道中

我们通过例子来感受一下:

package mainimport "fmt"func main() {ch1 := make(chan int)go pump(ch1)fmt.Println(<-ch1)
}func pump(ch chan int) {for i := 0; ; i++ {ch <- i}
}
复制代码

程序输出:

0
复制代码

这里的pump()函数被称为生产者

解除通道阻塞

package mainimport "fmt"
import "time"func main() {ch1 := make(chan int)go pump(ch1)go suck(ch1)time.Sleep(1e9)
}func pump(ch chan int) {for i := 0; ; i++ {ch <- i}
}func suck(ch chan int) {for {fmt.Println(<-ch)}
}
复制代码

这里我们定义了一个suck函数,作为接收者,并给main协程一个1s的运行时间,于是,便产生了70W+的输出【TAT】。

通道死锁

通道两段互相阻塞对方,会形成死锁状态。Go运行时会检查并panic,停止程序。无缓冲通道会被阻塞。

package mainimport "fmt"func main() {out := make(chan int)out <- 2go f1(out)
}func f1(in chan int) {fmt.Println(<-in)
}
复制代码
fatal error: all goroutines are asleep - deadlock!
复制代码

显然在out <- 2的时候,由于没有接受者,主线程被阻塞。

同步通道

除了普通的无缓存通道外,还有一种特殊的带缓存通道——同步通道

buf := 100
ch1 := make(chan string, buf)
复制代码

buf是通道可以同时容纳的元素个数,即ch1的缓冲区大小,在buf满之前,通道都不会阻塞。

如果容量大于0,通道就是异步的:在缓冲满载或边控之前通信不会阻塞,元素会按照发送的顺序被接收。

同步:ch := make(chan type, value)

  • value ==0 --> synchronous, unbuffered(阻塞)
  • value > 0 --> asynchronous, buffered(非阻塞)取决于value元素

使用通道缓冲能使程序更具有伸缩性(scalable)。

尽量在首要位置使用无缓冲通道,只在不确定的情况下使用缓冲。

package mainimport "fmt"
import "time"func main() {c := make(chan int, 50)go func() {time.Sleep(15 * 1e9)x := <-cfmt.Println("received", x)}()fmt.Println("sending", 10)c <- 10fmt.Println("send", 10)
}复制代码

信号量模式

func compute(ch chan int) {ch <- someComputation()
}func main() {ch := make(chan int)go compute(ch)doSomethingElaseForAWhile()result := <-ch
}
复制代码

协程通过在通道ch中放置一个值来处理结束信号。main线程等待<-ch直到从中获取到值。

我们可以用它来处理切片排序:

done := make(chan bool)doSort := func(s []int) {sort(s)done <- true
}
i := pivot(s)
go doSort(s[:i])
go doSort(s[i:])
<-done
<-done
复制代码

带缓冲通道实现信号量

信号量时实现互斥锁的常用同步机制,限制对资源的访问,解决读写问题。

  • 带缓冲通道的容量要和同步的资源容量相同
  • 通道的长度(当前存放的元素个数)与当前资源被使用的数量相同
  • 容量减去通道的长度等于未处理的资源个数
//创建一个长度可变但容量为0的通道
type Empty interface {}
type semaphore chan Empty
复制代码

初始化信号量

sem = make(semaphore, N)
复制代码

对信号量进行操作,建立互斥锁

func (s semaphore) P (n int) {e := new(Empty)for i := 0; i < n; i++ {s <- e}
}func (a semaphore) V (n int) {for i := 0; i < n; i++ {<- s}
}/* mutexes */
func (s semaphore) Lock() {s.P(1)
}func (s semaphore) Unlock(){s.V(1)
}/* signal-wait */
func (s semaphore) Wait(n int) {s.P(n)
}func (s semaphore) Signal() {s.V(1)
}
复制代码

通道工厂模式

不将通道作为参数传递,而是在函数内生成一个通道,并返回。

package mainimport ("fmt""time"
)func main() {stream := pump()go suck(stream)time.Sleep(1e9)
}func pump() chan int {ch := make(chan int)go func() {for i := 0; ; i++ {ch <- i}}()return ch
}func suck(ch chan int) {for {fmt.Println(<-ch)}
}
复制代码

通道使用for循环

for循环可以从ch中持续获取值,直到通道关闭。(这意味着必须有另一个协程写入ch,并且在写入完成后关闭)

for v := range ch {fmt.Println("The value is", v)
}
复制代码
package mainimport ("fmt""time"
)func main() {suck(pump())time.Sleep(1e9)
}func pump() chan int {ch := make(chan int)go func() {for i := 0; ; i++ {ch <- i}}()return ch
}func suck(ch chan int) {go func() {for v := range ch {fmt.Println(v)}}()
}
复制代码

通道的方向

通道可以表示它只发送或者只接受:

var send_only chan<- int    // channel can only send data
var recv_only <-chan int    // channel can only receive data
复制代码

只接收的通道(<-chan T)无法关闭,因为关闭通道是发送者用来表示不再给通道发送值,所以对只接收通道是没有意义的。

管道和选择器模式

借鉴一个经典的例子筛法求素数来学习这一内容。

这个算法的主要思想是,引入筛法(一种时间复杂度为O(x * ln(lnx))的算法),对一个给定返回的正整数从大到小排序,然后从中筛选掉所有的非素数,那么剩下的数中最小的就是素数,再去掉该数的倍数,以此类推。

假设一个范围为1~30的正整数集,已经从大到小排序。

第一遍筛掉非素数1,然后剩余数中最小的是2。

由于2是一个素数,将其取出,然后去掉所有2的倍数,那么剩下的数为:

3 5 7 9 11 13 15 17 19 21 23 25 27 29

剩下的数中3最小,且为素数,取出并去除所有3的倍数,循环直至所有数都筛完。

代码如下:

// 一般写法
package mainimport ("fmt"
)func generate(ch chan int) {for i := 2; i < 100; i++ {ch <- i}
}func filter(in, out chan int, prime int) {for {i := <-inif i%prime != 0 {out <- i}}
}func main() {ch := make(chan int)go generate(ch)for {prime := <-chfmt.Print(prime, " ")ch1 := make(chan int)go filter(ch, ch1, prime)ch = ch1}
}
复制代码
// 习惯写法
package mainimport ("fmt"
)func generate() chan int {ch := make(chan int)go func() {for i := 2; ; i++ {ch <- i}}()return ch
}func filter(in chan int, prime int) chan int {out := make(chan int)go func() {for {if i := <-in; i%prime != 0 {out <- i}}}()return out
}func sieve() chan int {out := make(chan int)go func() {ch := generate()for {prime := <-chch = filter(ch, prime)out <- prime}}()return out
}func main() {primes := sieve()for {fmt.Println(<-primes)}
}
复制代码

Golang —— goroutine(协程)和channel(管道)相关推荐

  1. golang goroutine协程运行机制及使用详解

    Go(又称Golang)是Google开发的一种静态强类型.编译型.并发型,并具有垃圾回收功能的编程语言.Go于2009年正式推出,国内各大互联网公司都有使用,尤其是七牛云,基本都是golang写的, ...

  2. golang goroutine 协程原理

    一.goroutine简介 goroutine是go语言中最为NB的设计,也是其魅力所在,goroutine的本质是协程,是实现并行计算的核心.goroutine使用方式非常的简单,只需使用go关键字 ...

  3. golang goroutine 协程同步 sync.WaitGroup 简介

    介绍 经常会看到以下了代码: package mainimport ("fmt""time" )func main(){for i := 0; i < 1 ...

  4. golang goroutine协程概念及入门:轻量级线程(或用户态线程)

    import ("strconv""time""fmt" )

  5. Golang的协程调度

    调度的基础,模型关系的映射 GPM模型: G,Goroutinue 被调度器管理的轻量级线程,goroutine使用go关键字创建 调度系统的最基本单位goroutine,存储了goroutine的执 ...

  6. GO语言的进阶之路-协程和Channel

    GO语言的进阶之路-协程和Channel 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 看过我之前几篇博客小伙伴可能对Golang语言的语法上了解的差不多了,但是,如果想要你的代码 ...

  7. Golang的协程调度器原理及GMP设计思想

    一.Golang"调度器"的由来? (1) 单进程时代不需要调度器 我们知道,一切的软件都是跑在操作系统上,真正用来干活(计算)的是CPU.早期的操作系统每个程序就是一个进程,知道 ...

  8. Go 语言编程 — 并发 — Goroutine 协程

    目录 文章目录 目录 Golang 的协程 go 关键字 Golang 的协程 Golang 的协程被称为 Goroutine.因为操作系统内核是不感知协程的,也就是说 Golang 需要自己实现一个 ...

  9. Golang的协程池设计

    转载地址:https://studygolang.com/articles/15477 使用Go语言实现并发的协程调度池阉割版,本文主要介绍协程池的基本设计思路,目的为深入浅出快速了解协程池工作原理, ...

  10. go语言中的goroutine(协程)

    文章目录 goroutine(协程) 1.进程和线程说明: 2.并发和并行说明: 3.go协程和go主线程: 4.MPG 模式基本介绍 5.设置golang运行的cpu数 goroutine(协程) ...

最新文章

  1. Web Service 的工作原理
  2. 编程之美3.3 计算两个字符串的相似度
  3. linux下imwbr1进程,Linux服务器中木马wnTKYg
  4. fzu - 2164 Jason's problem
  5. tsinsen A1067. Fibonacci数列整除问题 dp
  6. Spring Security OAuth2——自定义OAuth2第三方登录(Gitee)
  7. php变量作用域(花括号、global、闭包)
  8. SpringBoot优缺点总结
  9. python爬虫之ip代理参数/动态加载数据抓取
  10. HTML5 高频面试题!!!
  11. vue html5 picker,详解VUE-地区选择器(V-Distpicker)组件使用心得
  12. python众数_169. 求众数(Python)
  13. 【Arduino】一天入门Arduino语言 教程
  14. 【C语言-11】Bingou! ~~~~三个数字从大到小排排坐~~
  15. webApp 之 常见问题
  16. WebBrowser怎么指定ie内核
  17. linux系统怎么数据恢复,linux系统数据恢复
  18. 输入两个已经按从小到大顺序排列好的字符串,编写一个合并两个字符串的函数,使合并后的字符串,仍然是从小到大排列。
  19. minikube国内安装之曲线救国
  20. (转)Linux系统下PDF文件的编辑

热门文章

  1. 解决打包软链接打包失败问题
  2. 2(3).选择排序_快排(线性表)
  3. 国产麒麟Linux安装体验
  4. Discuz! 6.0.0 安装图文教程
  5. 中交兴路完成7亿元A轮融资,携手蚂蚁金服共建小微物流科技服务生态
  6. 28个HTML5特征、窍门和技术
  7. web前端开发怎么学,web教程资源
  8. Debian8 远程登录Permission Denied,please try again
  9. ASP中的工具类函数收集
  10. 关于异常Microsoft.CSharp.RuntimeBinder.RuntimeBinderException