Go基础学习09-多协程资源竞争、sync.Mutex、sync.Cond、chan在多协程对共享变量的资源竞争中的使用

Go中协程基础小记

协程基础

在Go中,线程被程为goroutine,也有人称其为线程的线程(a thread of thread)。每个线程都有自己的单独的变量如:

PC程序计数器、函数调用栈Stack、一套寄存器Registers。

在Go中协程对应的primitive原语有:

  • start/go:启动/运行一个线程
  • exit:线程退出,一般从某个函数退出/结束执行后,会自动隐式退出
  • stop:停止一个线程,比如向一个没有读者的channel写数据,那么channel阻塞,go可能会运行时暂时停止这个线程
  • resume:恢复原本停止的线程重新执行,需要恢复程序计数器(program counter)、栈指针(stack pointer)、寄存器(register)状态,让处理器继续运行该线程。

为什么需要多协程

依靠多协程达到并发的效果:

  • I/O concurrency:I/O并发。
  • multi-core parallelism:多核并行,提高整体吞吐量。充分利用CPU。
  • convinience:方便,经常有需要异步执行or定时执行的任务,可以通过线程完成。

多协程面临的问题

  • race conditions:多线程会引入竞态条件的场景
    • avoid sharing:避免共享内存以防止竞态条件场景的产生(Go有一个竞态检测器race detector,能够辅助识别代码中的一些竞态条件场景)
    • use locks:让一系列指令变成原子操作
  • coordination:同步协调问题,比如一个线程的执行依赖另一个线程的执行结果等
    • channels:通道允许同时通信和协调
    • condition variables:配合互斥锁使用
  • deadlock:死锁问题,比如在go中简单的死锁场景,一个写线程往channel写数据,但是永远没有读线程从channel读数据,那么写线程被永久阻塞,即死锁,go会抓住这种场景,抛出运行时错误runtime error。

go中还存在一个协程泄漏的问题:参考下面代码示例第四个:channel的使用。

代码演示

存在共享变量的资源竞争

// 定义一个工具类,不具有实际意义。
package main

import "math/rand"

func requestVote() bool {
	intn := rand.Intn(10)
	if intn <= 5 {
		return true
	}
	return false
}

// 基础代码
package main

func main() {

	count := 0
	finished := 0

	for i := 0; i < 10; i++ {
		// 匿名函数,创建共 10 个线程
		go func() {
			vote := requestVote() // 一个内部sleep随机时间,最后返回true的函数,模拟投票
			if vote {
				count++
			}
			finished++
		}()
	}
	for count < 5 && finished != 10 {
		// wait
	}
	if count >= 5 {
		println("received 5+ votes!")
	} else {
		println("lost")
	}
}

使用go自带的资源竞争分析工具:

go run -race xxx.go

执行结果如下:

==================
WARNING: DATA RACE
Write at 0x00c00001c118 by goroutine 9:
  main.main.func1()
      /home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:20 +0x77

Previous read at 0x00c00001c118 by main goroutine:
  main.main()
      /home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:25 +0x147

Goroutine 9 (running) created at:
  main.main()
      /home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:17 +0x71
==================
==================
WARNING: DATA RACE
Write at 0x00c00001c128 by goroutine 9:
  main.main.func1()
      /home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:22 +0xa4

Previous read at 0x00c00001c128 by main goroutine:
  main.main()
      /home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:25 +0x164

Goroutine 9 (running) created at:
  main.main()
      /home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:17 +0x71
==================
received 5+ votes!
==================
WARNING: DATA RACE
Read at 0x00c00001c128 by goroutine 12:
  main.main.func1()
      /home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:22 +0x91

Previous write at 0x00c00001c128 by goroutine 11:
  main.main.func1()
      /home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:22 +0xa4

Goroutine 12 (running) created at:
  main.main()
      /home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:17 +0x71

Goroutine 11 (finished) created at:
  main.main()
      /home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:17 +0x71
==================
Found 3 data race(s)
exit status 66

通过上述代码运行结果,可以得知存在多个协程的情况下对共享变量count、finished存在资源竞争。

Mutex解决资源竞争

代码示例:

package main

import (
	"sync"
)

func main() {

	count := 0
	finished := 0
	var mu sync.Mutex

	for i := 0; i < 10; i++ {
		// 匿名函数,创建共 10 个线程
		go func() {
			vote := requestVote() // 一个内部sleep随机时间,最后返回true的函数,模拟投票
			// 临界区加锁
			mu.Lock()
			// 推迟到基本block结束后执行,这里即函数执行结束后 自动执行解锁操作。利用defer语言,一般在声明加锁后,立即defer声明推迟解锁
			defer mu.Unlock()
			if vote {
				count++
			}
			finished++
		}()
	}
	for {
		mu.Lock()
		if count > 5 || finished == 10 {
			// 不能在此处解锁,下面仍然需要对count变量进行判断
			//mu.Unlock()
			break
		}
		mu.Unlock()
		// wait
	}
	if count >= 5 {
		println("received 5+ votes!")
	} else {
		println("lost")
	}
	mu.Unlock()
}

运行结果:

wt@wt:~/Backend/go/goprojects/src/golearndetail/concurrency/learn01$ go run -race voteselect_mutex.go utils.go 
received 5+ votes!

使用Mutex后,及时加锁以及解锁,不存在资源竞争。

Mutex+Cond解决资源竞争和CPU空转

代码示例:

package main

import "sync"

func main() {

	count := 0
	finished := 0
	var mu sync.Mutex
	//cond := sync.NewCond(&mu)
	cond := sync.Cond{L: &mu}
	for i := 0; i < 10; i++ {
		// 匿名函数,创建共 10 个线程
		go func() {
			vote := requestVote() // 一个内部sleep随机时间,最后返回true的函数,模拟投票
			// 临界区加锁
			mu.Lock()
			// 推迟到基本block结束后执行,这里即函数执行结束后 自动执行解锁操作。利用defer语言,一般在声明加锁后,立即defer声明推迟解锁
			defer mu.Unlock()
			if vote {
				count++
			}
			finished++

			cond.Broadcast()
		}()
	}
	mu.Lock()
	for count < 5 || finished != 10 {
		// 如果条件不满足,则在制定的条件变量上wait。内部原子地进入sleep状态,并释放与条件变量关联的锁。当条件变量得到满足时,这里内部重新获取到条件变量关联的锁,函数返回。
		cond.Wait()
		// 使用cond.Wait的目的防止CPU空转,使用time.sleep()无法控制合适的休眠时间
	}
	if count >= 5 {
		println("received 5+ votes!")
	} else {
		println("lost")
	}
	mu.Unlock()
}

执行结果:

wt@wt:~/Backend/go/goprojects/src/golearndetail/concurrency/learn01$ go run -race voteselect_mutex_cond.go utils.go 
received 5+ votes!

chan解决资源竞争

代码示例:

package main

func main() {

	count := 0
	finished := 0
	ch := make(chan bool)
	for i := 0; i < 10; i++ {
		// 匿名函数,创建共 10 个线程
		go func() {
			ch <- requestVote()
		}()
	}
	// 这里实现并不完美,如果count >= 5了,主线程不会再监听channel,导致其他还在运行的子线程会阻塞在往channel写数据的步骤。
	// 但是这里主线程退出后子线程们也会被销毁,影响不大。但如果是在一个长期运行的大型工程中,这里就存在泄露线程leaking threads
	for count < 5 && finished != 10 {
		// 主线程在这里等待
		v := <-ch
		if v {
			count++
		}
		finished++
	}
	if count >= 5 {
		println("received 5+ votes!")
	} else {
		println("lost")
	}
}

上述代码中涉及到一个线程泄漏的问题:

在Go语言中,"泄露线程leaking threads"通常指的是goroutine泄漏。Goroutine是Go语言中实现并发的轻量级线程,它们应该是临时的和短暂的。然而,如果一个goroutine因为某些原因无法正常结束,它就会一直运行,从而导致资源无法释放,这就是所谓的goroutine泄漏。
上面代码如果长期运行,由于非缓冲通道没有数据接收者,会导致部分goroutine一直阻塞在向其写数据的操作中,造成线程泄漏。

对于非缓冲通道的理解可以参考我的另一篇博文:
https://blog.csdn.net/weixin_45863010/article/details/142618550?spm=1001.2014.3001.5501
执行结果:

wt@wt:~/Backend/go/goprojects/src/golearndetail/concurrency/learn01$ go run -race voteselect_channel.go utils.go 
received 5+ votes!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值