理解Go的Goroutine和channel

进程,线程的概念在操作系统的书上已经有详细的介绍。进程是内存资源管理和cpu调度的执行单元。为了有效利用多核处理器的优势,将进程进一步细分,允许一个进程里存在多个线程,这多个线程还是共享同一片内存空间,但cpu调度的最小单元变成了线程。

那协程又是什么东西,以及与线程的差异性??

协程,可以看作是轻量级的线程。但与线程不同的是,线程的切换是由操作系统控制的,而协程的切换则是由用户控制的。

最早支持协程的程序语言应该是lisp方言scheme里的continuation(续延),续延允许scheme保存任意函数调用的现场,保存起来并重新执行。Lua,C#,python等语言也有自己的协程实现。

go的goroutinue

今天要讲的goroutinue,本质上就是协程。但有两点不同:
1. goroutinue可以实现并行,也就是说,多个协程可以在多个处理器同时跑。而协程同一时刻只能在一个处理器上跑(把宿主语言想象成单线程的就好了)。

2. goroutine之间的通信是通过channel,而协程的通信是通过yield和resume()操作。


在Go里实现goroutine非常简单,只需要在函数的调用前面加关键字go即可,

[plain]  view plain copy print ?
  1. go doSth()  

下面的例子演示,启动10个goroutines分别打印索引。

[plain]  view plain copy print ?
  1. package main  
  2. import (  
  3. "fmt"  
  4. "time"    
  5. )  
  6.   
  7. func main() {  
  8.     for i:=1;i<10;i++ {  
  9.         go func(i int) {  
  10.             fmt.Println(i)  
  11.         }(i)  
  12.     }  
  13.     //暂停一会,保证打印全部结束  
  14.     time.Sleep(1e9)  
  15. }  

在分析goroutine执行的随机性和并发性,把goroutine看作是java的守护线程是完全可以的。上面的例子中,启动了10个goroutine,再加上main函数的主goroutine,总共有11个goroutines。由于goroutine类似于”守护线程“,如果主goroutine不等待片刻,可能程序就没有输出打印了。上面的例子输出如下:(输出的索引是完全随机的)



go的channel

在java的世界里,并发主要是靠锁住临界资源(共享内存)来保证同步的。而channel则是goroutinues之间进行通信的利器。

channel可以形象比喻为工厂里的传送带,一头的生产者goroutine往传输带放东西,另一头的消费者goroutinue则从输送带取东西。channel实际上是一个有类型的消息队列,遵循先进先出的特点。

1. channel的操作符号

ch <- ele 表示ele被发送给channel ch;

ele2 <- ch 表示从channel ch取一个值,然后赋给ele2

2. 阻塞式channel

channel默认是没有缓冲区的,也就是说,通信是阻塞的。send操作必须等到有消费者accept才算完成。

举个栗子

[plain]  view plain copy print ?
  1. package main  
  2. import "fmt"  
  3.   
  4. func main() {  
  5.     ch1 := make(chan int)  
  6.     go pump(ch1) // pump hangs  
  7.     fmt.Println(<-ch1) // prints only 0  
  8. }  
  9.   
  10. func pump(ch chan int) {  
  11.     for i:= 0; ; i++ {  
  12.         ch <- i  
  13.     }  
  14. }  

上面代码pump()里的channel在接受到第一个元素后就被阻塞了,直到主goroutinue拿走了数据。最终channel阻塞在接受第二个元素,程序只打印 0

3 带有buff的channel

没有buff的channel只能容纳一个元素,而带有buff的channel则可以非阻塞容纳N个元素。发送数据到buffed channel不会被阻塞,除非channel已满;同样的,从buffed channel取数据也不会被阻塞,除非channel空了。这有点像java的ConcurrentLinkedQueue。


goroutine和channel的应用

结合goroutine和channel,可以模拟出java处理并发情况的若干情景

1. 实现future

[plain]  view plain copy print ?
  1. package main  
  2. import "fmt"  
  3. import "time"  
  4.   
  5. func main() {  
  6.     future := heavyCalculation()  
  7.     fmt.Println(<-future)  
  8. }  
  9.   
  10. func heavyCalculation() (chan int) {  
  11.       
  12.     future := make(chan int)  
  13.     go func() {  
  14.         //模拟耗时计算  
  15.         time.Sleep(1e9)  
  16.         future <- 666  
  17.     }()  
  18.       
  19.     return future  
  20. }  

2. 实现CountDownLatch

[plain]  view plain copy print ?
  1. package main  
  2. import "fmt"  
  3.   
  4. func main() {  
  5.     nTask := 5  
  6.     ch := make(chan int)  
  7.     for i:=1;i<=nTask;i++ {  
  8.         go doTask(ch)  
  9.     }  
  10.     for i:=1;i<=nTask;i++ {  
  11.         <-ch  
  12.     }  
  13.     fmt.Println("finished all tasks")  
  14. }  
  15.   
  16. func doTask(ch chan<- int) {  
  17.     //doSth...  
  18.     ch<- 0  
  19. }  


3. 并发访问对象

Hashtable是线程安全的,意味着多条线程同时操作hashtable对象是不会引起状态不一致的。查看Hashtable源代码可知,其几乎全部方法都添加synchronized关键字,例如put(),remove()操作。在go里,我们可以在对象内部保存一个函数类型的channel,涉及对象状态的操作都放入channel里,对象初始化的时候开启一条goroutinue,不停地执行匿名函数。

[plain]  view plain copy print ?
  1. package main  
  2. import (  
  3. "fmt"  
  4. "strconv"  
  5. "time"    
  6. )  
  7.   
  8. type Person struct {  
  9.     Name string  
  10.     salary float64  
  11.     chF chan func()  
  12. }  
  13. func NewPerson(name string, salary float64) *Person {  
  14.     p := &Person{name, salary, make(chan func())}  
  15.     go p.backend()  
  16.     return p  
  17. }  
  18. func (p *Person) backend() {  
  19.     for f := range p.chF {  
  20.         f()  
  21.     }  
  22. }  
  23.   
  24. func (p *Person) AddSalary(sal float64) {  
  25.     p.chF <- func() { p.salary += sal }  // (ThreadSafe)  
  26.       
  27.     // p.salary += sal (NotThreadSafe)  
  28. }  
  29.   
  30. func (p *Person) ReduceSalary(sal float64) {  
  31.     p.chF <- func() { p.salary -= sal }  // (ThreadSafe)  
  32.       
  33.     // p.salary -= sal (NotThreadSafe)  
  34. }  
  35.   
  36. func (p *Person) Salary() float64 {  
  37.     fChan := make(chan float64)  
  38.     p.chF <- func() { fChan <- p.salary }  
  39.     return <-fChan  
  40. }  
  41. func (p *Person) String() string {  
  42.     return p.Name + " - salary is: " +   
  43.     strconv.FormatFloat(p.Salary(), 'f', 2, 64)  
  44. }  
  45.   
  46. func main() {  
  47.     p := NewPerson("Kingston", 8888.8)  
  48.     fmt.Println(p)  
  49.     for i:=1;i<=500;i++ {  
  50.         go func() {  
  51.             p.AddSalary(1);  
  52.         }()  
  53.     }  
  54.     for i:=1;i<=500;i++ {  
  55.         go func() {  
  56.             p.ReduceSalary(1);  
  57.         }()  
  58.     }  
  59.     time.Sleep(3e9)  
  60.     fmt.Println("After changed:")  
  61.     fmt.Println(p)  
  62. }  


4. 生产者消费者模式

每次涉及到并发情景,都喜欢用生产者消费者模式,因为它太经典啦

2个面包师同时生产面包,5个顾客同时取面包(尽管以下例子的打印不能说明并发的真实情况,因为channel的操作和打印的组合不是原子操作,但不影响程序的逻辑)

[plain]  view plain copy print ?
  1. package main  
  2. import (  
  3. "fmt"  
  4. "time"  
  5. )  
  6.   
  7.   
  8. func main() {  
  9.     bread := make(chan int,3)  
  10.     for i:=1;i<=2;i++ {  
  11.         go produce(bread)  
  12.     }  
  13.     for i:=1;i<=5;i++ {  
  14.         go consume(bread)  
  15.     }  
  16.     time.Sleep(1e9)  
  17. }  
  18.   
  19. func produce(ch chan<- int) {  
  20.     for {  
  21.         ch <- 1  
  22.         fmt.Println("produce bread")  
  23.         time.Sleep(100 * time.Millisecond)  
  24.     }  
  25. }  
  26.   
  27. func consume(ch <-chan int) {  
  28.     for {  
  29.         <-ch  
  30.         fmt.Println("take bread")  
  31.         time.Sleep(200 * time.Millisecond)  
  32.     }  
  33. }  
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值