并发状态中最容易发生的问题就是竞争问题,而Go以一种更天生的姿态来处理并发问题

目录


存在问题:两个线程之间竞争,共享资源会出错

package main

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

var (
	counter int
	wg      sync.WaitGroup
)

func main() {
	wg.Add(2)

	// 创建两个gorountine
	go inCounter(1)
	go inCounter(2)

	fmt.Println("waiting for final counter……")
	wg.Wait()
	fmt.Println("finish final counter! The final counter: ", counter)
}

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

	for count := 0; count < 2; count++ {
		value := counter  //捕获counter的值
		runtime.Gosched() //当前gorountine从线程中退出并放回到队列,但不会挂起go进程未来会恢复执行
		value++
		counter = value
	}
}

运行结果:期望结果是4,但是可能出现各种结果,比如 2、3等,为什么呢?因为在一个线程读取counter的时候,还没有进行写操作的时候,这个counter资源已经被另一个线程读取了!

解决方案之原子操作

import "sync/atomic"

atomic包提供了底层的原子级内存操作,对于同步算法的实现很有用。

这些函数必须谨慎地保证正确使用。除了某些特殊的底层应用,使用通道或者sync包的函数/类型实现同步更好。

应通过通信来共享内存,而不通过共享内存实现通信。

利用atomix.AddInt64()进行改进:

package main

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

var (
	counter int64
	wg      sync.WaitGroup
)

func main() {
	wg.Add(2)

	// 创建两个gorountine
	go inCounter(1)
	go inCounter(2)

	fmt.Println("waiting for process over……")
	wg.Wait()
	fmt.Println("finish final counter! The final counter: ", counter)
}

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

	for count := 0; count < 2; count++ {
		// 利用原子操作安全地对counter进行+1操作,避免了读后写前资源被其他线程竞争后抢夺
		atomic.AddInt64(&counter, 1)
		// 从当前gorountine中退出,并放回到队列中
		runtime.Gosched()
	}
}

 

解决方案之互斥锁创建临界区

何为临界区?

只能被串行化访问或者执行的某个资源/代码段称为临界区

下面用mutex互斥锁加锁临界区来处处理上面这个问题:

package main

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

var (
	counter int64
	wg      sync.WaitGroup
	mutex   sync.Mutex //定义一段代码临界区
)

func main() {
	wg.Add(2)

	// 创建两个gorountine
	go inCounter(1)
	go inCounter(2)

	fmt.Println("waiting for process over……")
	wg.Wait()
	fmt.Println("finish final counter! \n The final counter: ", counter)
}

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

	for count := 0; count < 2; count++ {
		// 加互斥锁创建临界区
		mutex.Lock()
		{
			value := counter
			// 从当前gorountine线程退出并放回到队列中
			runtime.Gosched()
			// 增加本地value并放回到counter中
			value++
			counter = value
		}
		// 解锁释放临界区
		mutex.Unlock()
	}
}

但是用互斥锁来保护一个数值型共享变量比较麻烦并且效率低下,所以这种情况之下还是使用原子操作比较好。

再举个栗子

加锁操作

package main

import (
	"fmt"
	"sync"
)

var total struct {
	sync.Mutex
	value int
}

func worker(wg *sync.WaitGroup) {
	defer wg.Done()
	for i := 0; i <= 100; i++ {
		// 保证原子操作,加锁-操作-解锁
		total.Lock()
		total.value += i
		total.Unlock()
	}
}

func main() {
	var wg sync.WaitGroup
	wg.Add(2)
	go worker(&wg)
	go worker(&wg)
	wg.Wait()

	fmt.Println(total.value)
}

原子操作

package main

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

var total uint64

func worker(wg *sync.WaitGroup) {
	defer wg.Done()

	var i uint64
	for i = 0; i <= 100; i++ {
		// 无锁方法,原子操作
		atomic.AddUint64(&total, i)
	}
}

func main() {
	var wg sync.WaitGroup
	wg.Add(2)

	go worker(&wg)
	go worker(&wg)
	wg.Wait()

	fmt.Println(total)
}

结语和参考

原子操作和互斥锁并不是Golang所特有的并发编程手段,下一篇将讨论一下Go特有的通道方式处理gorountine同步与竞争问题!