并发(1)
<p>go语言的并发属于go语言中一大亮点,其他语言创建并发是通过线程,而go语言则通过协程,协程是一个轻量级的线程。进程或者线程在一台电脑中最多不能超过一万个,而协程可以在一台电脑中创建上百万个也不会影响到电脑资源。学习之前先知道一些并发与并行的一些概念。</p>
<ul>
<li><code>并发</code> 是指在同一个时间点上只能执行同一个任务,但是因为速度非常快,所以就像同时进行一样。</li>
<li><code>并行</code> 是指在一个时间点上同时处理多个任务。真正的并行,是需要电脑硬件的支持,单核的CPU是无法达到并行的。并行,他不一定快因为并行运行时是需要通信的,这种通信的成本还是很高的,而并发的程序成本很低。</li>
</ul>
<p><img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/15/172b65d9a53aa5cf~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp" alt="" /></p>
<ul>
<li><code>进程</code> 就是一个独立功能的程序,在一个数据集中的一次动态执行过程,可以认为他是一个正在执行的程序,比如打开一个QQ就是在运行一个进程。</li>
<li><code>线程</code> 线程是被包含在进程之中的,它是比进程更小的能独立运行的基本单位 一个进程可以包含多个线程。例如、打开文档在你输入文字的时候他还在后台检测你输入的文字的大小写,还有拼写是否正确 ,这就是一个线程来检测的。</li>
<li><code>协程</code> 协程属于一种轻量级的线程,英文名 Goroutine 协程之间的调度由 Go运行时(runtime)管理。</li>
</ul>
<h2>什么是Goroutine</h2>
<p>goroutine 协程。是go语言中特有的名词,他不同于进程Process,以及线程Thread。Go语言创造者认为和他们还是有区别的,所以创造为goroutine。goroutine与线程相比创建成本非常小,可以认为goroutine就是一小段代码,我们使用goroutine往往是执行某一个特定的任务,也就是函数或者方法。</p>
<p><img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/22/172dc29c25af5453~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp" alt="" /></p>
<p>使用go关键字调用这个函数开启一个goroutine时候,即使这个函数有返回值也会忽略的。所以不需要接收这个函数的返回值。</p>
<h2>如何创建Goroutine</h2>
<p>在函数或者方法前面加上关键字go,就会同时运行一个新的goroutine。</p>
<p><img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/22/172dbe049628a37a~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp" alt="" /></p>
<pre><code>package main
import(
&quot;fmt&quot;
)
func main(){
go testgo() //使用关键字go调用函数或者方法 开启一个goroutine
for i:=0;i&lt;10;i++{
fmt.Println(i)
}
fmt.Println(&quot;main 函数结束&quot;)
}
//自定义函数
func testgo(){
for i:=0;i&lt;10;i++{
fmt.Println(&quot;测试goroutine&quot;,i)
}
}</code></pre>
<h2>Goroutine是如何执行的</h2>
<p>与函数不同的是goroutine调用之后会立即返回,不会等待goroutine的执行结果,所以goroutine不会接收返回值。 把封装main函数的goroutine叫做主goroutine,main函数作为主goroutine执行,如果main函数中goroutine终止了,程序也将终止,其他的goroutine都不会再执行。</p>
<p><img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/22/172dbfd7d9833fa8~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp" alt="" /></p>
<pre><code>package main
import (
&quot;fmt&quot;
)
func main() {
go testgo1()
go testgo2()
for i := 0; i &lt;= 5; i++ {
fmt.Println(&quot;main函数执行&quot;, i)
}
fmt.Println(&quot;main 函数结束&quot;)
}
func testgo1() {
for i := 0; i &lt;= 10; i++ {
fmt.Println(&quot;测试子goroutine1&quot;, i)
}
}
func testgo2() {
for i := 0; i &lt;= 10; i++ {
fmt.Println(&quot;测试子goroutine2&quot;, i)
}
}</code></pre>
<p>上面代码执行结果为:</p>
<pre><code>main函数执行 0
main函数执行 1
main函数执行 2
main函数执行 3
main函数执行 4
main函数执行 5
main函数结束
测试子goroutine1: 0</code></pre>
<p>由结果可以看出,当主函数main执行完成后,子goroutine执行了一次整个程序就执行结束了,main函数并不会等待子goroutine执行结束。一个goroutine的执行速度是非常快的,并且是主goroutine和子goroutine进行资源竞争,谁抢到资源多,谁就先执行。main函数是不会让着子goroutine的。我们可以在主goroutine中加上时间休眠,可以看每一个goroutine执行过程。</p>
<p><img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/22/172dc43d7c56e1b5~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp" alt="" /></p>
<pre><code>time.Sleep(1000 * time.Millisecond) //让程序休眠1秒
package main
import (
&quot;fmt&quot;
&quot;time&quot;
)
func main() {
go testgo1()
go testgo2()
for i := 0; i &lt;= 5; i++ {
fmt.Println(&quot;main函数执行&quot;, i)
}
time.Sleep(3000 * time.Millisecond)//加上休眠让主程序休眠3秒钟。
fmt.Println(&quot;main函数结束&quot;)
}
func testgo1() {
for i := 0; i &lt;= 10; i++ {
fmt.Println(&quot;测试子goroutine1:&quot;, i)
}
}
func testgo2() {
for i := 0; i &lt;= 10; i++ {
fmt.Println(&quot;测试子goroutine2:&quot;, i)
}
}</code></pre>
<p>这时候程序就会等待所有的子goroutine执行结束后再结束。</p>
<h2>使用匿名函数创建Goroutine</h2>
<p>使用匿名函数创建goroutine时候在匿名函数后加上(),直接调用。</p>
<pre><code>package main
import (
&quot;fmt&quot;
)
func main() {
go func() {
fmt.Println(&quot;匿名函数创建goroutine执行&quot;)
}()
fmt.Println(&quot;主函数执行&quot;)
}</code></pre>
<h2>runtime包</h2>
<p>虽然说Go编译器将Go的代码编译成本地可执行代码。不需要像java或者.net那样的语言需要一个虚拟机来运行,但其实go是运行在runtime调度器上的,它主要负责内存管理、垃圾回收、栈处理等等。也包含了Go运行时系统交互的操作,控制goroutine的操作,Go程序的调度器可以很合理的分配CPU资源给每一个任务。</p>
<p>Go1.5版本之前默认是单核执行的。从1.5之后使用可以通过<code>runtime.GOMAXPROCS()</code>来设置让程序并发执行,提高CPU的利用率。</p>
<pre><code>package main
import (
&quot;fmt&quot;
&quot;runtime&quot;
&quot;time&quot;
)
func main() {
//获取当前GOROOT目录
fmt.Println(&quot;GOROOT:&quot;, runtime.GOROOT())
//获取当前操作系统
fmt.Println(&quot;操作系统:&quot;, runtime.GOOS)
//获取当前逻辑CPU数量
fmt.Println(&quot;逻辑CPU数量:&quot;, runtime.NumCPU())
//设置最大的可同时使用的CPU核数 取逻辑cpu数量
n := runtime.GOMAXPROCS(runtime.NumCPU())
fmt.Println(n) //一般在使用之前就将cpu数量设置好 所以最好放在init函数内执行
//goexit 终止当前goroutine
//创建一个goroutine
go func() {
fmt.Println(&quot;start...&quot;)
runtime.Goexit() //终止当前goroutine
fmt.Println(&quot;end...&quot;)
}()
time.Sleep(3 * time.Second) //主goroutine 休眠3秒 让子goroutine执行完
fmt.Println(&quot;main_end...&quot;)
}</code></pre>
<p>如果调用<code>runtime.Goexit()</code>函数之后,会立即停止当前goroutine,其他的goroutine不会受影响。并且当前goroutine如果有未执行的defer 还是会执行完defer 操作。需要注意的是 <strong>不能</strong> 将<code>runtime.goexit()</code> 放在主goroutine也就是<code>main</code>函数中执行,否则会发生运行时恐慌。</p>
<h2>Go语言临界资源安全</h2>
<h3>什么是临界资源</h3>
<p>指并发环境中多个协程之间的共享资源,如果对临界资源处理不当,往往会导致数据不一致的情况。例如:多个goroutine在访问同一个数据资源的时候,其中一个修改了数据,另一个goroutine在使用的时候就不对了。</p>
<p><img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/22/172dc6bc45204748~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp" alt="" /></p>
<pre><code>package main
import (
&quot;fmt&quot;
&quot;math/rand&quot;
&quot;time&quot;
)
//定义全局变量 表示救济粮食总量
var food = 10
func main() {
//开启4个协程抢粮食
go Relief(&quot;灾民好家伙1&quot;)
go Relief(&quot;灾民好家伙2&quot;)
go Relief(&quot;灾民老李头1&quot;)
go Relief(&quot;灾民老李头2&quot;)
//让程序休息5秒等待所有子协程执行完毕
time.Sleep(5 * time.Second)
}
//定义一个发放的方法
func Relief(name string) {
for {
if food &gt; 0 { //此时有可能第二个goroutine访问的时候 第一个goroutine还未执行完 所以条件也成立
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond) //随机休眠时间
food--
fmt.Println(name, &quot;抢到救济粮 ,还剩下&quot;, food, &quot;个&quot;)
} else {
fmt.Println(name, &quot;别抢了 没有粮食了。&quot;)
break
}
}
}
//结果
//灾民好家伙1 抢到救济粮 ,还剩下 8 个
//灾民老李头1 抢到救济粮 ,还剩下 7 个
//灾民好家伙1 抢到救济粮 ,还剩下 6 个
//灾民老李头1 抢到救济粮 ,还剩下 5 个
//灾民老李头2 抢到救济粮 ,还剩下 4 个
//灾民好家伙2 抢到救济粮 ,还剩下 3 个
//灾民好家伙1 抢到救济粮 ,还剩下 2 个
//灾民老李头1 抢到救济粮 ,还剩下 1 个
//灾民老李头2 抢到救济粮 ,还剩下 0 个
//灾民老李头2 别抢了 没有粮食了。
//灾民老李头1 抢到救济粮 ,还剩下 -1 个
//灾民老李头1 别抢了 没有粮食了。
//灾民好家伙1 抢到救济粮 ,还剩下 -2 个
//灾民好家伙1 别抢了 没有粮食了。
//灾民好家伙2 抢到救济粮 ,还剩下 -3 个
//灾民好家伙2 别抢了 没有粮食了。</code></pre>
<p>以上代码出现负数的情况,也是因为Go语言的并发走的太快了,当有一个协程进入执行的时候还没来得及取出数据,另外一个协程也进来了,所以会出现负数的情况,那么如何解决这样的问题,我们不能用休眠的方法让程序等待,因为你并不知道程序会多久执行结束,到底应该让程序休眠多长时间。下面看看如何控制goroutine协程在执行过程中保证数据的安全。</p>