教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 golang学习笔记(二)—— 深入golang中的协程

golang学习笔记(二)—— 深入golang中的协程

发布时间:2022-03-23   编辑:jiaochengji.com
教程集为您提供golang学习笔记(二)—— 深入golang中的协程等资源,欢迎您收藏本站,我们将为您提供最新的golang学习笔记(二)—— 深入golang中的协程资源

小白一枚,最近在研究golang,记录自己学习过程中的一些笔记,以及自己的理解。

<blockquote><ul><li>go中协程的实现</li> <li>go中协程的sync同步锁</li> <li>go中信道channel</li> <li>go中的range</li> <li>go中的select切换协程</li> <li>go中带缓存的channel</li> <li>go中协程调度</li> </ul></blockquote>

原文的地址为:https://github.com/forthealll...

欢迎star

介绍go中的协程之前,首先看以下go中的defer函数,defer函数不是普通的函数,defer函数会在普通函数返回之后执行。defer函数中可以释放函数内部变量、关闭数据库连接等等操作,举例来说:

<pre><code>func print(){ fmt.Println(2); } func main() { defer print(); fmt.Println(1); } </code></pre>

上述的例子中先输出1后输出2,说明defer确实是在普通函数调用结束之后执行的。

go中使用协程的方式来处理并发,协程可以理解成更小的线程,占用空间小且线程上下文切换的成本少。

可以再为具体的描述以下协程的好处,协程比线程更加轻量,使用4K栈的内存就可以创建它们,可以用很小的内存占用就可以处理大量的任务。

在go中,携程是通过go关键字来调用,从关键字可以看出,golang的一个十分重要的特点就是协程,有句话叫“协程在手,说go就go”。

<h3>1、go中协程的实现</h3>

下面我们来看一个例子:

<pre><code>func printOne(){ fmt.Println(1); } func printTwo(){ fmt.Println(2); } func printThree(){ fmt.Println(3); } func main() { go printOne(); go printTwo(); go printThree(); }</code></pre>

执行上述的main函数,我们发现并没有像我们想的那样输出有123的输出,原因在于虽然协程是并发的,但是如果在协程调用前退出了调用协程的函数后,协程会随着程序的消亡而消亡。

因此我们可以在main函数中,将主函数挂起,增加等待协程调用的事件。

<pre><code>func main() { go printOne(); go printTwo(); go printThree(); time.Sleep(5 * 1e9); }</code></pre>

这样会有相应的go关键字修饰的协程函数的调用。我们来看分别执行3次的结果。

<ul><li>第一次
1
3
2</li> <li>第二次
3
2
1</li> <li>第三次
3
1
2</li> </ul>

我们发现因为协程是并发执行的,我们无法确定其调用的顺序,因此 每次的调用主函数的返回结果都是不确定的。

从协程的上述例子中,我们可以看出使用协程的时候必须还要考虑两个问题:

<ul><li>如何控制协程的调用顺序,特别是当不同的协程同时访问同一个资源。</li> <li>如何实现不同协程间的通信</li> </ul>

问题1,可以通过sync的同步锁来实现,问题2,go中提供了channel来实现不同协程间的通信。

<h3>2、go中协程的sync同步锁</h3>

go中sync包提供了2个锁,互斥锁sync.Mutex和读写锁sync.RWMutex.我们用互斥锁来解决上述的同步问题,改写上述的例子:

<pre><code>func printOne(m *sync.Mutex){ m.Lock(); fmt.Println(1); defer m.Unlock(); } func printTwo(m *sync.Mutex){ m.Lock(); fmt.Println(2); defer m.Unlock(); } func printThree(m *sync.Mutex){ m.Lock(); fmt.Println(3); defer m.Unlock(); } func main() { m:= new(sync.Mutex); go printOne(m); go printTwo(m); go printThree(m); time.Sleep(5 * 1e9); } </code></pre>

通过互斥锁,可以发现每次运行,确实都依次输出了1,2,3

<h3>3、go中信道channel</h3>

go中有一种特殊的类型通道channel,可以通过channel来发送类型化的数据,实现在协程之间的通信,通过通道的通信方式也保证了同步性。

channel的声明方式很简单:

<pre><code>var ch1 chan string ch1 = make(chan string) </code></pre>

我们用ch表示通道,通道的符号包括了流向通道(发送): ch <- int1 和从通道流出(接收) int2 = <- ch。

同时go中也支持声明单向通道:

<pre><code>var ch1 chan int //普通的channel var ch2 chan <- int //只用于写int数据 var ch3 <- chan int //只用于读int数据 </code></pre>

上述定义的都是不带缓存区,或者说长度为1的channel,这种channel的特点就是:

一旦有数据被放入channel,那么该数据必须被取走才能让另一条数据放入,这就是同步的channel,channel的发送者和接受者在同一时间只交流一条数据,然后必须等待另一边完成相应的发送和接受动作。

我们还是用上述的输出123的例子,用同步channel来实现同步的输出。

<pre><code>func printOne(cs chan int){ fmt.Println(1); cs <- 1 } func printTwo(cs chan int){ <-cs fmt.Println(2); defer close(cs); } func main() { cs := make(chan int); go printOne(cs); go printTwo(cs); time.Sleep(5 * 1e9); } </code></pre>

上述的例子中会依次输出12,这样我们通过同步channel的方式实现了同步的输出。

我们前面讲到用为了等待go协程执行完成,我们在main函数中用time.sleep来挂起主函数,其实main函数本身也可以看成一个协程,如果使用channel,就不用在main函数中用time.sleep来挂起。

我们改写上述的例子:

<pre><code>func printOne(cs chan int){ fmt.Println(1); cs <- 1 } func main() { cs := make(chan int); go printOne(cs); <-cs; close(cs); }</code></pre>

上述的例子中,会输出 1 ,我们并没有在主函数中通过time.sleep的方式来挂起,转而用一个等待写入的channel来代替。

<em>注意:通道可以被显式的关闭,当需要告诉接受者不会种子提供新的值的时候,就需要关闭通道。</em>

<h3>4、go中的range</h3>

上面我们也讲到要及时的关闭channel,但是持续的访问数据源并检查channel是否已经关闭,并不高效。go中提供了range关键字。

range关键字在使用channel的时候,会自动等待channel的动作一直到channel关闭。通俗点将就是可以channel可以自动开关。

同样的来举例:

<pre><code>func input(cs chan int,count int){ for i:=1;i<=count;i { cs <- i } } func output(cs chan int){ for s:= range cs { fmt.Println(s); } } func main() { cs := make(chan int); go input(cs,5); go output(cs); time.Sleep(3*1e9) } </code></pre>

上述的例子会依次的输出1,2,3,4,5. 通过使用range关键字,当channel被关闭时,接受者的for循环也就自动停止了。

<h3>5、go中的select切换协程</h3>

从不同的并发执行过程中获取值可以通过关键字select来完成,它和switch控制语句非常相似,也被称为通信开关。

首先要明确select做了什么??

select中存在着一种轮询机制,select监听进入通道的数据,也可以是通道发送值的时候,监听到相应的行为后就执行case里面的操作。

select的声明:

<pre><code>select { case u:= <- ch1: ... case v:= <- ch2; ... } </code></pre>

同样的来看一下具体使用select的例子:

<pre><code>func channel1(cs chan int,count int){ for i:=1;i<=count;i { cs <- i } } func channel2(cs chan int,count int){ for i:=1;i<=count;i { cs <- i } } func selectTest(cs1 ,cs2 chan int){ for i:=1;i<10;i { select { case u:=<-cs1: fmt.Println(u); case v:=<-cs2: fmt.Println(v); } } } func main() { cs1 := make(chan int); cs2 := make(chan int); go channel1(cs1,5); go channel2(cs2,3); go selectTest(cs1,cs2); time.Sleep(3*1e9) } 输出结果为:1,2,1,2,3,3,4,5 总共8个数据。且因为没有做同步控制,因此运行几次后的输出结果是不相同的。 </code></pre> <h3>6、go中带缓存的channel</h3>

前面讲到的都是不带缓存的channel或者说长度为1的channel,实际上channel也是可以带缓存的,我们可以在声明的时候执行channel的长度。

<pre><code>ch = make(chan string,3) </code></pre>

比如上述的例子中,指定了ch这个channel的长度为3,长度不为1的channel,就可以称之为带缓存的channel.

带缓存的channel可以连续写入,直到长度占满为止。

<pre><code>ch <- 1 ch <- 2 ch <- 3 </code></pre> <h3>7、go中协程调度</h3>

讲到并发,就要提到go中的协程调度。go中的runtime包,提供了调度器的功能。runtime包提供了以下几个方法:

<ul><li>Gosched:让当前线程让出 cpu 以让其它线程运行,它不会挂起当前线程,因此当前线程未来会继续执行</li> <li>NumCPU:返回当前系统的 CPU 核数量</li> <li>GOMAXPROCS:设置最大的可同时使用的 CPU 核数</li> <li>Goexit:退出当前 goroutine(但是defer语句会照常执行)</li> <li>NumGoroutine:返回正在执行和排队的任务总数</li> <li>GOOS:目标操作系统</li> </ul>

对于多核CPU的机器,go可以显示的指定编译器将go的协程调度到多个CPU上运行

<pre><code>import "runtime" ... cpuNum:=runtime.NumCPU; runtime.GOMAXPROCS(cpuNum) </code></pre>

来聊聊GO中的调度原理,首先定义以下模型的概念:

M:内核中的线程的数目
G:go中的协程,并发的最小单元,在go中通过go关键字来创建
P:处理器,即协程G的上下文,每个P会维护一个本地的协程队列。

接着来看解释GO中协程调度的经典图:

<span class="img-wrap"></span>

我们来解释上图:

<ul><li>P是处理器的个数,我们经常将调度器的GOMAXPROCS设置成CPU的个数,因此这里P一般来说是机器CPU的个数。</li> <li>M是线程,在P处理器上关联一个线程,P和M的一组配对组成了局部的协程队列</li> <li>G就是协程,需要被添加到由P和M组成的局部队列中依次处理</li> <li>除了局部的协程外,在全局还维护了一个协程队列。</li> <li>如果局部协程队列中处理完了所有队列,且没有新队列,那么M线程会取消对于CPU的占用,M线程进入休眠</li> </ul> 到此这篇关于“golang学习笔记(二)—— 深入golang中的协程”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
mysql导入导出数据时中文乱码的解决办法
专家教你如何有效的学习Drupal - Drupal问答
网页标题随机显示名言js代码
go语言学习笔记(第3章)—面向对象编程
golang学习笔记(二)—— 深入golang中的协程
MySQL中group_concat函数使用例子
有关 mysql count(*) 与 count(col) 查询效率的比较分析
想系统学习GO语言(Golang
php实现简单用户登录功能程序代码
Golang学习笔记3——常量与运算符

[关闭]
~ ~