• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

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

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

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

                                                作者:尹正杰

版权声明:原创作品,谢绝转载!否则将追究法律责任。

 

   看过我之前几篇博客小伙伴可能对Golang语言的语法上了解的差不多了,但是,如果想要你的代码和性能更高,那还得学点晋升的本来,这个时候我们就需要引入Golang的协成这个概念了,其实,你可能早就听说了Golang的优势就是处理大并发,我们可以用它来做日志收集系统,也可以用它做业务上的“秒杀系统”,当然我们还可以用它来做“监控系统”。好了,下面跟我一起来体会一下Golang的五味杂陈吧。

 

一.什么是协程;

  再说协成之前,我们需要了解两个概念,即用户态和内核态。

1.什么是用户态;

  官方解释:用户态(user mode)在计算机结构指两项类似的概念。在CPU的设计中,用户态指非特权状态。在此状态下,执行的代码被硬件限定,不能进行某些操作,比如写入其他进程的存储空间,以防止给操作系统带来安全隐患。在操作系统的设计中,用户态也类似,指非特权的执行状态。内核禁止此状态下的代码进行潜在危险的操作,比如写入系统配置文件、杀掉其他用户的进程、重启系统等。

   应用程序在用户态下运行,仅仅只能执行cpu整个指令集的一个子集,该子集中不包含操作硬件功能的部分,因此,一般情况下,在用户态中有关I/O和内存保护(操作系统占用的内存是受保护的,不能被别的程序占用)。

  如果感兴趣的朋友可以参考:https://baike.baidu.com/item/%E7%94%A8%E6%88%B7%E6%80%81/9548791?fr=aladdin

 

2.什么是内核态;

  内核态也叫和核心态。

  官方解释:在处理器的存储保护中,主要有两种权限状态,一种是核心态(管态),也被称为特权态;一种是用户态(目态)。核心态是操作系统内核所运行的模式,运行在该模式的代码,可以无限制地对系统存储、外部设备进行访问。

  操作系统在内核态运行情况下可以访问硬件上所有的内容。

  如果感兴趣的朋友可以参考:https://baike.baidu.com/item/%E6%A0%B8%E5%BF%83%E6%80%81/6845908?fr=aladdin

 

3.什么是协成;

  官方解释:一个程序可以包含多个协程,可以对比与一个进程包含多个线程,因而下面我们来比较协程和线程。我们知道多个线程相对独立,有自己的上下文,切换受系统控制;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其他协程由当前协程来控制。

  协程(coroutine)是Go语言中的轻量级线程实现,由Go运行时(runtime)管理。在一个函数调用前加上go关键字,这次调用就会在一个新的goroutine中并发执行。当被调用的函数返回时,这个goroutine也自动结束。需要注意的是,如果这个函数有返回值,那么这个返回值会被丢弃。协成工作在用户态,它类似于现场的运行方式可以并行处理任务。

   如果感兴趣的朋友可以参考:http://baike.baidu.com/link?url=MKRg3Ca7Bokkqv5EarU1JO7Oz6Wn8jMhmJcoivDG27dOFeznk3gVDSl5SfN6P191GSwGDInEfDPVkQ6j3pJNbICpy04U0F1TkLOZHVzs1-u

 

二.创建一个协程;

   在一个函数调用前加上go关键字,这次调用就会在一个新的goroutine中并发执行,下面我们一起来看段代码的执行结果:

 1 /*
 2 #!/usr/bin/env gorun
 3 @author :yinzhengjie
 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/
 5 EMAIL:[email protected]
 6 */
 7 
 8 package main
 9 
10 import (
11 "time"
12 "fmt"
13 )
14 
15 func sayhi(s string)  {
16     for i := 0; i < 5; i++ {
17         time.Sleep(100*time.Millisecond)  //表示每次循环后都要休息100毫秒。
18         fmt.Println(s)
19     }
20 }
21 
22 func main() {
23     go sayhi("尹正杰")  //在函数执行前加个go,表示单独起了一个协程,表示和当前主协程(main)并驾齐驱运行代码。
24     sayhi("Golang")
25 }
26 
27 
28 #以上代码执行结果如下:
29 Golang
30 尹正杰
31 尹正杰
32 Golang
33 Golang
34 尹正杰
35 尹正杰
36 Golang
37 Golang
38 尹正杰

   相信大家已经看到了一些端倪,我们先定义了一个“sayhi”函数,这个函数的功能就是将传入的字符串打印5遍,然后我们在主程序上调用的时候,发现在函数面前加"go"关键字时,就会打印输出,但是奇怪的是没有按照顺序打,它没有先打印五遍“尹正杰”然后在打印5遍“Golang”,而是函数执行结果是不规律的打印了两个字符串。这就是并发的效果,两个函数同事运行效果。这也是我们的并发之路的初体验。哈哈~

 

 

三.协程的局限性;

  发现让两端代码同事运行貌似听起来很高大上的样子,但是有个局限性,就是当主进程结束后,协成也会跟着结束,其实这个很好理解,我们通过一段代码就知道了:

 1 /*
 2 #!/usr/bin/env gorun
 3 @author :yinzhengjie
 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/
 5 EMAIL:[email protected]
 6 */
 7 
 8 package main
 9 
10 import (
11     "time"
12     "fmt"
13 )
14 
15 func main() {
16     s := []int{2,7,1,6,4,3,11,15,17,5,8,9,12}
17     for _,n := range s{
18         go func(n int) { //定义一个匿名函数,并对该函数开启协程,每次循环都会开启一个协成,也就是说它开启了13个协程。
19             time.Sleep(time.Duration(n) * time.Second) /*表示每循环一次就需要睡1s,睡的总时间是由n来控制的,
20             总长度是由s切片数组中最大的一个数字决定,也就是说这个协成最少需要17秒才会结束哟*/
21             fmt.Println(n)
22         }(n)  //由于这个函数是匿名函数,所以调用方式就直接:(n)调用,不用输入函数名。
23     }
24     time.Sleep(12*time.Second) //主进程要执行的时间是12秒
25 }
26 
27 
28 
29 #以上代码执行结果如下:
30 1
31 2
32 3
33 4
34 5
35 6
36 7
37 8
38 9
39 11
40 12

  是不是很神奇,直接就进行排序了,但是15和17没有在终端输出,为什么呢?这就是我所说的主进程结束,那么整个程序也终将结束,因为主程序的时间只有12s,如果你想让所有的数据都排序出来,就得把数字换成一个大于或等于17的,这样才会对所有的数字进行排序。因为协成13个协程都要执行完毕的话需要17s才好使。

 

四.锁;

1.为什么要引入锁;

  在线上生活中,我们为了避免多个用户操作同一个文件,就会定义一个锁,为什么我们需要一个锁呢?我们可以看以下的案例:

 1 package main
 2 
 3 import (
 4     "time"
 5     "fmt"
 6 )
 7 
 8 type Accout struct {
 9     money int
10 }
11 
12 func (a *Accout) Do_Prepare() {
13     time.Sleep(time.Second)
14 }
15 
16 func (a *Accout) Get_Gongzi(n int) {
17     a.money += n
18 }
19 
20 func (a *Accout) Give_Wife(n int) {
21     if a.money > n {
22         a.Do_Prepare()
23         a.money -= n
24     }
25 }
26 
27 func (a *Accout) Buy(n int) {
28     if a.money >n {
29         a.Do_Prepare()
30         a.money -= n
31     }
32 }
33 
34 func (a *Accout) Left()int {
35     return a.money
36 }
37 
38 func main() {
39     var   account Accout
40     account.Get_Gongzi(10000)
41     go account.Give_Wife(6000)
42     go account.Buy(5000)
43     time.Sleep(2 * time.Second) //不能让主程序结束掉,因为主进程一结束go的协程也就跟着结束啦。
44     fmt.Println(account.Left())
45 }
46 
47 
48 
49 #以上代码执行结果如下:
50 -1000

 

  估计大家都看出来端倪了,在让两个协程并发跑起来,发现你得到的10000块钱都让给花出去了,是不是老尴尬了,写的判断语句也是没有生效的,最终10000块钱的工资,它竟然花了11000,如果银行这么干早就倒闭了,哈哈~那么问题来了,如果解决这一种现象呢?目前有四种常见的处理方案即:互斥锁,读写锁和channel锁以及waitgroup等等。

 

2.互斥锁;

  互斥锁是传统的并发程序对共享资源进行访问控制的主要手段。它由标准库代码包sync中的Mutex结构体类型代表。sync.Mutex类型(确切地说,是*sync.Mutex类型)只有两个公开方法——Lock和Unlock。顾名思义,前者被用于锁定当前的互斥量,而后者则被用来对当前的互斥量进行解锁。

 1 /*
 2 #!/usr/bin/env gorun
 3 @author :yinzhengjie
 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/
 5 EMAIL:[email protected]
 6 */
 7 
 8 package main
 9 
10 import (
11     "time"
12     "fmt"
13     "sync"
14 )
15 
16 
17 type Accout struct {
18     flag sync.Mutex
19     money int
20 }
21 
22 func (a *Accout) Do_Prepare() {
23     time.Sleep(time.Second)
24 }
25 
26 func (a *Accout) Get_Salary(n int) { //定义发的工资
27     a.money += n
28 }
29 
30 func (a *Accout) Give_Wife(n int) {  //上交给妻子的工资
31     a.flag.Lock()
32     defer a.flag.Unlock()
33     if a.money > n {
34         a.Do_Prepare()
35         a.money -= n
36     }else {
37         fmt.Println("您的余额已不足!请及时充值~")
38     }
39 }
40 
41 func (a *Accout) Buy(n int) {  //自己买的工资
42     a.flag.Lock()
43     defer a.flag.Unlock()
44     if a.money >n {
45         a.Do_Prepare()
46         a.money -= n
47     }else {
48         fmt.Println("您的余额已不足!请及时充值~")
49     }
50 }
51 
52 func (a *Accout) Left()int {
53     return a.money
54 }
55 
56 func main() {
57     var   account Accout
58     account.Get_Salary(10000)
59     go account.Give_Wife(6000)
60     go account.Buy(5000)
61     time.Sleep(2 * time.Second) //不能让主程序结束掉,因为主进程一结束go的协程也就跟着结束啦。
62     fmt.Printf("您的剩余工资是\033[31;1m%d\033[0m",account.Left())
63 
64 }
65 
66 
67 
68 #以上代码解析如下:
69 您的余额已不足!请及时充值~
70 您的剩余工资是4000

 

 

3.读写锁;

  在Go语言中,读写锁由结构体类型sync.RWMutex代表。与互斥锁类似,sync.RWMutex类型的零值就已经是立即可用的读写锁了。

  读写锁即是针对于读写操作的互斥锁。它与普通的互斥锁最大的不同就是,它可以分别针对读操作和写操作进行锁定和解锁操作。读写锁控制下的多个写操作之间都是互斥的,并且写操作与读操作之间也都是互斥的。但是,多个读操作之间却不存在互斥关系。

 

A.多个读操作之间却不存在互斥关系;

  我们会发现读取操作没有存在互斥的关系。5个进程同事进行读取,最终都顺利并行跑完了。

 1 /*
 2 #!/usr/bin/env gorun
 3 @author :yinzhengjie
 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/
 5 EMAIL:[email protected]
 6 */
 7 
 8 package main
 9 
10 import (
11     "sync"
12     "time"
13 )
14 
15 var m *sync.RWMutex  //定义m的类型为读写锁。
16 
17 func main() {
18     m = new(sync.RWMutex)
19     go read("第一次读取:")  //开启5个协程进行读取操作。如果不能并发跑最少需要5秒钟时间。
20     go read("第二次读取:")
21     go read("第三次读取:")
22     go read("第四次读取:")
23     go read("第五次读取:")
24     time.Sleep(2* time.Second)  //主进程只有2s,意味着这个程序最多能运行2s的时间。
25 }
26 
27 func read(i string) {
28     println(i, "read start")
29     m.RLock()  //读锁定
30     println(i, "资料正在读取中....")
31     time.Sleep(1 * time.Second)  //改函数执行完毕最少需要睡1秒。
32     m.RUnlock() //读解锁
33     println(i, "read end")
34 }
35 
36 
37 
38 #以上代码执行结果如下:
39 第二次读取: read start
40 第二次读取: 资料正在读取中....
41 第一次读取: read start
42 第一次读取: 资料正在读取中....
43 第三次读取: read start
44 第三次读取: 资料正在读取中....
45 第四次读取: read start
46 第四次读取: 资料正在读取中....
47 第五次读取: read start
48 第五次读取: 资料正在读取中....
49 第一次读取: read end
50 第三次读取: read end
51 第二次读取: read end
52 第四次读取: read end
53 第五次读取: read end

 

 

B.读写锁控制下的多个写操作之间都是互斥的,并且写操作与读操作之间也都是互斥的;

  我们发现,读的时候不能写,写的时候不能读,我们将主程序设置的时间是10秒钟,一次读两次写花费的总时间是12秒钟,因此肯定有一个是无法完成度或是写的部分。

 1 /*
 2 #!/usr/bin/env gorun
 3 @author :yinzhengjie
 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/
 5 EMAIL:[email protected]
 6 */
 7 
 8 package main
 9 
10 import (
11     "sync"
12     "time"
13 )
14 
15 var m *sync.RWMutex
16 
17 func read(i string) {
18     println(i, "read start")
19     m.RLock()
20     println(i, "资料正在读取中.....")
21     time.Sleep(4 * time.Second)
22     m.RUnlock()
23     println(i, "read end")
24 }
25 
26 func write(i string) {
27     println(i, "write start")
28     m.Lock() //写操作锁定
29     println(i, "数据正在写入硬盘中.....")
30     time.Sleep(4 * time.Second)
31     m.Unlock()  //写操作解锁
32     println(i, "write end")
33 }
34 
35 func main(){
36     m = new(sync.RWMutex)
37     go write("第一次写入") //写的时候啥都不能干,即其他协程无法写入,也无法读取。
38     go read("第一次读取")
39     go write("第二次写入")
40     time.Sleep(10 * time.Second) //主进程只给出10秒的时间,但是整个进程跑完最少需要12秒的时间。
41 }
42 
43 
44 
45 
46 #以上代码直接结果如下:
47 第一次写入 write start
48 第一次写入 数据正在写入硬盘中.....
49 第一次读取 read start
50 第二次写入 write start
51 第一次写入 write end
52 第一次读取 资料正在读取中.....
53 第一次读取 read end
54 第二次写入 数据正在写入硬盘中.....

 

4.channel锁;

  channel锁的工作原理就是让2个协成互相通信,一会我们会详细介绍。

 1 /*
 2 #!/usr/bin/env gorun
 3 @author :yinzhengjie
 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/
 5 EMAIL:[email protected]
 6 */
 7 
 8 package main
 9 
10 import (
11     "time"
12     "fmt"
13     "sync"
14 )
15 
16 
17 type Accout struct {
18     flag sync.Mutex
19     money int
20 }
21 
22 func (a *Accout) Do_Prepare() {
23     time.Sleep(time.Second)
24 }
25 
26 func (a *Accout) Get_Gongzi(n int) {
27     a.money += n
28 }
29 
30 func (a *Accout) Give_Wife(n int) {
31     a.flag.Lock()
32     defer a.flag.Unlock()
33     if a.money > n {
34         a.Do_Prepare()
35         a.money -= n
36     }else {
37         fmt.Println("您的余额已不足,请及时充值!")
38     }
39 }
40 
41 func (a *Accout) Buy(n int) {
42     a.flag.Lock()
43     defer a.flag.Unlock()
44     if a.money >n {
45         a.Do_Prepare()
46         a.money -= n
47     }else {
48         fmt.Println("您的余额已不足,请及时充值!")
49     }
50 }
51 
52 func (a *Accout) Left()int {
53     return a.money
54 }
55 
56 func main() {
57     var   account Accout
58     account.Get_Gongzi(10000)
59 
60     var work_info  chan string
61     work_info = make(chan string ,2)  //定义一个channel
62 
63     go func() {
64         account.Give_Wife(6000)
65         work_info <- "I done"  //如果该进程执行完毕就发送一条数据给channel,下面的那个也一样
66     }()
67 
68     go func() {
69         account.Buy(50000)
70         work_info <- "I  have done too!"
71     }()
72 
73     cnt := 0
74     for i := range work_info {
75         fmt.Println(i)
76         cnt++ //每次循环都叠加1,当2个协程都工作完就让主程序结束。
77         if cnt >= 2 {
78             break
79         }
80 
81     }
82     defer close(work_info)
83     fmt.Printf("您的剩余工资是\033[31;1m%d\033[0m",account.Left())
84 }
85 
86 
87 
88 
89 #以上代码直接结果如下:
90 您的余额已不足,请及时充值!
91 I  have done too!
92 I done
93 您的剩余工资是4000

 

  其实channel还有一个好处就是解决超时问题,就是如果在规定的时间没有完成进程的内容,我们就返回给用户超时的状态。以下是示例代码:

 1 /*
 2 #!/usr/bin/env gorun
 3 @author :yinzhengjie
 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/
 5 EMAIL:[email protected]
 6 */
 7 
 8 package main
 9 
10 import (
11     "time"
12     "fmt"
13     "sync"
14 )
15 
16 
17 type Accout struct {
18     flag sync.Mutex
19     money int
20 }
21 
22 func (a *Accout) Do_Prepare() {
23     time.Sleep(time.Second)
24 }
25 
26 func (a *Accout) Get_Gongzi(n int) {
27     a.money += n
28 }
29 
30 func (a *Accout) Give_Wife(n int) {
31     a.flag.Lock()
32     defer a.flag.Unlock()
33     if a.money > n {
34         a.Do_Prepare()
35         a.money -= n
36     }else {
37         fmt.Println("您的余额已不足,请及时充值!")
38     }
39 }
40 
41 func (a *Accout) Buy(n int) {
42     a.flag.Lock()
 
                       
                    
                    

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
Go语言核心36讲(Go语言进阶技术十六)--学习笔记发布时间:2022-07-10
下一篇:
go中的string操作发布时间:2022-07-10
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap