go语言学习备忘(竞态与锁)

        基于调度器的内部算法,一个正运行的 goroutine 在工作结束前,可以被停止并重新调度。调度器这样做的目的是防止某个 goroutine 长时间占用逻辑处理器。当 goroutine 占用时间过长时,调度器会停止当前正运行的 goroutine,并给其他可运行的 goroutine 运行的机会。

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

        如果两个或者多个 goroutine 在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态,这种情况被称作竞争状态(race candition)。竞争状态的存在是让并发程序变得复杂的地方,十分容易引起潜在问题。对一个共享资源的读和写操作必须是原子化的,换句话说,同一时刻只能有一个 goroutine 对共享资源进行读和写操作。

       

 Go 语言有一个特别的工具,可以在代码里检测竞争状态:

go build -race // 用竞争检测器标志来编译程序

./example // 运行程序

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

原子函数 :

        原子函数能够以很底层的加锁机制来同步访问整型变量和指针。

// 这个示例程序展示如何使用 atomic 包来提供
// 对数值类型的安全访问
package main
import ( 
	"fmt"
	"runtime"
	"sync"
	"sync/atomic"
) 

var ( 
	// counter 是所有 goroutine 都要增加其值的变量
	counter int64

	// wg 用来等待程序结束
	wg sync.WaitGroup
) 

// main 是所有 Go 程序的入口
func main() { 
	// 计数加 2,表示要等待两个 goroutine
	wg.Add(2)

	// 创建两个 goroutine
	go incCounter(1)
	go incCounter(2)

	// 等待 goroutine 结束
	wg.Wait()

	// 显示最终的值
	fmt.Println("Final Counter:", counter)
} 

// incCounter 增加包里 counter 变量的值
func incCounter(id int) { 

	// 在函数退出时调用 Done 来通知 main 函数工作已经完成
	defer wg.Done()

	for count := 0; count < 2; count++ { 
		// 安全地对 counter 加 1 
		atomic.AddInt64(&counter, 1)
		
		// 当前 goroutine 从线程退出,并放回到队列
		runtime.Gosched()
	} 
} 

        程序的第 43 行使用了 atmoic 包的 AddInt64 函数。这个函数会同步整型值的加法,方法是强制同一时刻只能有一个 goroutine 运行并完成这个加法操作。当 goroutine 试图去调用任何原子函数时,这些 goroutine 都会自动根据所引用的变量做同步处理。另外两个有用的原子函数是 LoadInt64 和 StoreInt64。这两个函数提供了一种安全地读和写一个整型值的方式。

 

互斥锁

        另一种同步访问共享资源的方式是使用互斥锁(mutex)。互斥锁这个名字来自互斥(mutual exclusion)的概念。互斥锁用于在代码上创建一个临界区,保证同一时间只有一个 goroutine 可以执行这个临界区代码。

// 定义一段需要同步访问的代码临界区
// 资源的同步访问

package main

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

var ( 
	// counter 是所有 goroutine 都要增加其值的变量
	counter int

	// wg 用来等待程序结束
	wg sync.WaitGroup

	// mutex 用来定义一段代码临界区
	mutex sync.Mutex
) 

// main 是所有 Go 程序的入口
func main() { 
	// 计数加 2,表示要等待两个 goroutine
	wg.Add(2)

	// 创建两个 goroutine
	go incCounter(1)
	go incCounter(2)

	// 等待 goroutine 结束
	wg.Wait()
	fmt.Printf("Final Counter: %d\\n", counter)
} 

// incCounter 使用互斥锁来同步并保证安全访问,
// 增加包里 counter 变量的值
func incCounter(id int) { 
	// 在函数退出时调用 Done 来通知 main 函数工作已经完成
	defer wg.Done()

	for count := 0; count < 2; count++ { 
		// 同一时刻只允许一个 goroutine 进入
		// 这个临界区
		mutex.Lock()
		{ 
			// 捕获 counter 的值
			value := counter

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

			// 增加本地 value 变量的值
			value++

			// 将该值保存回 counter
			counter = value
		} 

		mutex.Unlock()

		// 释放锁,允许其他正在等待的 goroutine
		// 进入临界区
	} 
} 

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

 

本文内容来自《go语言实战》

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值