GO语言实战九 goroutine、并发

什么是 goroutine

goroutine是golang中的coroutine,也叫协程,微软大法称之纤程(Fiber)。
Goroutine是Go里的一种轻量级线程——协程。相对线程,协程的优势就在于它非常轻量级,进行上下文切换的代价非常的小。对于一个goroutine ,每个结构体G中有一个sched的属性就是用来保存它上下文的。这样,goroutine 就可以很轻易的来回切换。由于其上下文切换在用户态下发生,根本不必进入内核态,所以速度很快。而且只有当前goroutine 的 PC, SP等少量信息需要保存。

在Go语言中,每一个并发的执行单元为一个goroutine。当我们开始运行一个Go程序时,它的入口函数 main 实际上就是运行在一个goroutine 里

协程是一种更细粒度的调度,可以满足多个不同处理逻辑的协程共享一个线程资源。

package main

import (
	"fmt"
	"runtime"
	"sync"
)

func main(){
	// 分配一个逻辑处理器给 调度器使用
	runtime.GOMAXPROCS(1)
	//wg 用来等待程序完成
	// 计数加2  表示要等待两个goroutine
	WaitGroup 是一个计数信号量,可以用来记录并维护运行的 goroutine。如果 WaitGroup 的值大
	于 0,Wait 方法就会阻塞。在第 18 行,创建了一个 WaitGroup 类型的变量,之后在 第 19 行,
	将这个 WaitGroup 的值设置为 2,表示有两个正在运行的 goroutine。为了减小 WaitGroup 的值
	并最、终释放 main 函数,要在第 2639 行,使用 defer 声明在函数退出时 调用 Done 法。
	var wg  sync.WaitGroup
	wg.Add(2)
	fmt.Println("开始 groutine了---")
	
	//声明一个匿名函数 创建goroutine
	go func(){
		//函数退出时调用Done()来通知main函数工作已经完成
		键字 defer 会修改函数调用时机,在正在执行的函数返回时才真正调用 defer 声明的函 数。对
		这里的示例程序来说,我们使用关键字 defer 保证,每个 goroutine 一旦完成其工作就调 用 
		Done 方法
		defer wg.Done()
		for i :=0; i<3;i++{
			for word := 'a';word<'a'+26;word++{
				fmt.Printf("%c",word)
			}
			fmt.Println()
		}

	}()

	go func(){
		defer wg.Done()
		for i :=0; i<3;i++{
			for word := 'A';word<'A'+26;word++{
				fmt.Printf("%c",word)
			}
			fmt.Println()
		}
	}()
	
	//等待goroutine结束
	fmt.Println("等待结束------")
	wg.Wait()
	fmt.Println("结束了------")

}

输出

开始 groutine了---
等待结束------
ABCDEFGHIJKLMNOPQRSTUVWXYZ
ABCDEFGHIJKLMNOPQRSTUVWXYZ
ABCDEFGHIJKLMNOPQRSTUVWXYZ
abcdefghijklmnopqrstuvwxyz
abcdefghijklmnopqrstuvwxyz
abcdefghijklmnopqrstuvwxyz
结束了------

下图图 从逻辑处理器的角度展示了这一场景。在第 1 步,调度器开始运行 goroutine A,而 goroutine B 在运行队列里等待调度。之后,在第 2 步,调度器交换了 goroutine A 和 goroutine B。 由于 goroutine A 并没有完成工作,因此被放回到运行队列。之后,在第 3 步,goroutine B 完成 了它的工作并被系统销毁。这也让 goroutine A 继续之前的工作

竞争状态

如果两个或者多个 goroutine 在没有互相同步的情况下,访问某个共享的资源,并试图同时 读和写这个资源,就处于相互竞争的状态,这种情况被称作竞争状态(race candition)

package main

import (
	"fmt"
	"runtime"
	"sync"
)

/**
 * 竞争状态
 */
var (
	//wg 用来等待程序结束
	wg sync.WaitGroup

	//所有goroutine 需要增加的值
	counter int
)
func main(){
	wg.Add(2)
	go incCount(1)
	go incCount(2)
	wg.Wait()
	fmt.Println("结束了=====",counter)
}

func incCount(id int){
	defer wg.Done()

	for count :=0;count<2;count++{
		value := counter

		//从当前goroutine 线程退出,并放回队列
		runtime.Gosched()
		value++

		//将值保存回 counter
		counter = value
		fmt.Println(id,"value====",counter)
	}
}

输出

有时候是:2 value==== 1   1 value==== 1   2  value==== 2   1  value==== 2    结束了===== 2
有时候是:2 value==== 1   2 value==== 3   1 value==== 2    1 value==== 4    结束了===== 4

变量 counter 会进行 4 次读和写操作,每个 goroutine 执行两次。但是,程序终止时,counter 变量的值为 2。提供了为什么会这样的线索
每个 goroutine 都会覆盖另一个 goroutine 的工作。这种覆盖发生在 goroutine 切换的时候。每 个 goroutine 创造了一个 counter 变量的副本,之后就切换到另一个 goroutine。当这个 goroutine 再次运行的时候,counter 变量的值已经改变了,但是 goroutine 并没有更新自己的那个副本的 值,而是继续使用这个副本的值,用这个值递增,并存回 counter 变量,结果覆盖了另一个 goroutine 完成的工作。
在这里插入图片描述
使用 go build -race ./goroutine3.go检测竞争状态

xMacBook-Air 0106$ go build -race ./goroutine3.go 
xMacBook-Air 0106$ ./goroutine3 
1 value==== 1
1 value==== 2
==================
WARNING: DATA RACE
Read at 0x000001212840 by goroutine 7:
  main.incCount()
      /Users/xiaochai/work/study/go/go基础/2020/0106/goroutine3.go:31 +0x76

Previous write at 0x000001212840 by goroutine 6:
  main.incCount()
      /Users/xiaochai/work/study/go/go基础/2020/0106/goroutine3.go:38 +0x97

Goroutine 7 (running) created at:
  main.main()
      /Users/xiaochai/work/study/go/go基础/2020/0106/goroutine3.go:22 +0x89

Goroutine 6 (running) created at:
  main.main()
      /Users/xiaochai/work/study/go/go基础/2020/0106/goroutine3.go:21 +0x68
==================
2 value==== 3
2 value==== 4
结束了===== 4
Found 1 data race(s)

一种修正代码、消除竞争状态的办法是,使用 Go 语言提供的锁机制,来锁住共享资源,从 而保证 goroutine 的同步状态

锁住共享资源
  1. 原子函数
    原子函数能够以很底层的加锁机制来同步访问整型变量和指针。我们可以用原子函数来修正
package main

import (
	"fmt"
	"runtime"
	"sync"
	"sync/atomic"
)

/**
 * 竞争状态
 */
var (
	//wg 用来等待程序结束
	wg sync.WaitGroup

	//所有goroutine 需要增加的值
	counter int64
)
func main(){
	wg.Add(2)
	go incCount(1)
	go incCount(2)
	wg.Wait()
	fmt.Println("结束了=====",counter)
}

func incCount(id int){
	defer wg.Done()

	for count :=0;count<2;count++{
		//value++
		//改用原子方式
		atomic.AddInt64(&counter, 1)

		//从当前goroutine 线程退出,并放回队列
		runtime.Gosched()

		fmt.Println(id,"value====",counter)
	}
}
始终输出
1 value==== 1
1 value==== 3
2 value==== 2
2 value==== 4
结束了===== 4
atmoic 包的 AddInt64 函数。这个函数会同步整型值的加法, 方法是强制同一时刻只能有一个 goroutine 运行
并完成这个加法操作。

另外两个有用的原子函数是 LoadInt64 和 StoreInt64。这两个函数提供了一种安全地读 和写一个整型值的方式。代码清单 6-15 中的示例程序使用 LoadInt64 和 StoreInt64 来创建 一个同步标志,这个标志可以向程序里多个 goroutine 通知某个特殊状态。

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

/**
 * 竞争状态
 */
var (
	//wg 用来等待程序结束
	wg sync.WaitGroup

	//通知正在工作的goroutine 停止工作的标识
	shutdown int64
)
func main(){
	wg.Add(2)
	go doWork("AAA")
	go doWork("BBB")

	time.Sleep(1*time.Second)
	fmt.Println("准备结束了=====")
	atomic.StoreInt64(&shutdown,1)

	wg.Wait()

}

//模拟goroutine 工作
func doWork(name string){
	defer wg.Done()

	for {
		fmt.Println(name,"开始工作了====")
		time.Sleep(250*time.Millisecond)
		if atomic.LoadInt64(&shutdown) == 1{
			fmt.Println(name,"结束啦===")
			break
		}
	}
}

输出:
AAA 开始工作了====
BBB 开始工作了====
AAA 开始工作了====
BBB 开始工作了====
BBB 开始工作了====
AAA 开始工作了====
BBB 开始工作了====
AAA 开始工作了====
准备结束了=====
BBB 结束啦===
AAA 结束啦===
  1. 互斥锁
    另一种同步访问共享资源的方式是使用互斥锁(mutex)。互斥锁这个名字来自互斥(mutual exclusion)的概念。互斥锁用于在代码上创建一个临界区,保证同一时间只有一个 goroutine 可以 执行这个临界区代码。我们还可以用互斥锁来修正竞争状态
package main

import (
	"fmt"
	"runtime"
	"sync"
)

/**
 * 竞争状态解决方案 atomic.AddInt64
 */
var (
	//wg 用来等待程序结束
	wg sync.WaitGroup

	//所有goroutine 需要增加的值
	counter int64

	//设置临界区
	mutex  sync.Mutex
)
func main(){
	wg.Add(2)
	go incCount(1)
	go incCount(2)
	wg.Wait()
	fmt.Println("结束了1=====",counter)
}

func incCount(id int){
	defer wg.Done()

	for count :=0;count<2;count++{
		// 同一时刻只允许一个goroutine进入
		// 这个临界区
		mutex.Lock()
		{
			// 捕获counter的值
			value := counter
			// 当前goroutine从线程退出,并放回到队列
			runtime.Gosched()
			value++
			counter = value
		}
		mutex.Unlock()
		// 释放锁,允许其他正在等待的goroutine
		// 进入临界区
	}
}

输出
结束了===== 4   固定不变 

对 counter 变量的操作在第 46 行和第 60 行的 Lock()和 Unlock()函数调用定义的临界 区里被保护起来。使用大括号只是为了让临界区看起来更清晰,并不是必需的。同一时刻只有一 个 goroutine 可以进入临界区。之后,直到调用 Unlock()函数之后,其他 goroutine 才能进入临 界区。当第 52 行强制将当前 goroutine 退出当前线程后,调度器会再次分配这个 goroutine 继续运 行。当程序结束时,我们得到正确的值 4,竞争状态不再存在

  1. 通道 解决 下一篇分析

还参考了

简书浅谈goroutine https://www.jianshu.com/p/7ebf732b6e1f

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值