Go语言----sync.Mutex互斥锁

在Go语言的并发编程中,多个协程(goroutine)同时读写同一个变量,在并发度较高的情况下,可能会发生冲突。这时候就需要我们确保一次只有一个协程(goroutine)可以访问这个变量,从而避免冲突,这也称之为互斥

在Go语言中,希望使用channel信道的方式来处理并发情况。但在某些特殊情况下,依然需要使用到锁,为此Go语言给我们提供了互斥锁(sync.Mutex)与读写锁(sync.RWMutex),这是实现线程安全、保护共享资源免受竞态条件影响的核心工具。

并发例子

go 复制代码
package main

import "fmt"

var count int64

func main() {
    var ch = make(chan struct{}, 2)
    go func() {
        for i := 0; i < 10000; i++ {
            count++
        }
        ch <- struct{}{}
    }()

    go func() {
        for i := 0; i < 10000; i++ {
            count--
        }
        ch <- struct{}{}
    }()

    <-ch
    <-ch
    close(ch)
    fmt.Println(count)
}
  • 定义了一个全局变量 count
  • 开启了两个子协程,一个对 count 进行 10000 次自增、另一个对 count 进行 10000 次自减
  • 使用了 channel,保证两个 goroutine 执行完毕之后打印 count。
  • 在不考虑并发的的情况下,打印结果应该为count =0;
  • 但是实际的结果是:

实际上,对count的自增和自减并不是一个原子操作;它大致可以

  • 分为读取变量当前值;
  • 对这个值进行加减操作
  • 运算之后把结果再保存到变量中。
    因为不是原子操作,就可能有并发的问题。比如 goroutine1 和 goroutine2 读取的 count 都是 0,然后 goroutine1 对 count 执行自增,goroutine2 对 count 执行自减,此时运算之后的结果一个是 1、一个是 -1,然后再将结果保存到 count 的时候,也是要么得到 1 要么得到 -1,但很明显结果应该是 0 才对。

互斥锁sync.Mutex

互斥锁能够确保同一时刻只有一个goroutine能够访问受保护的资源。在Go中,sync.Mutex类型提供了锁定Lock()和解锁Unlock()方法来实现互斥访问。其实现如下:

go 复制代码
// A Locker represents an object that can be locked and unlocked.
type Locker interface {
	Lock()
	Unlock()
}

当一个goroutine获得互斥锁权限后,其他请求锁的goroutine会阻塞在Lock()方法的调用上,直到调用Unlock()方法被释放。

go 复制代码
package main

import (
	"fmt"
	"sync"
)

var count int64

func main() {
	var ch = make(chan struct{}, 2)
	var lock = new(sync.Mutex)
	go func() {
		for i := 0; i < 10000; i++ {
			lock.Lock()
			count++
			lock.Unlock()
		}
		ch <- struct{}{}
	}()

	go func() {
		for i := 0; i < 10000; i++ {
			lock.Lock()
			count--
			lock.Unlock()
		}
		ch <- struct{}{}
	}()

	<-ch
	<-ch
	close(ch)
	fmt.Println(count)
}
  • 在子协程修改count变量的时候,都会调用.Lock()去获取锁,而一旦锁已被获取,那么证明别的 goroutine 进入了临界区,当前 goroutine 会阻塞。直到另一个goroutine释放锁,这个goroutine才会获取锁对其进行操作,所以我们不管执行多少次,结果都是0,对count的操作保证了原子性。

常见的问题

  • 忘记解锁
    忘记调用Unlock()方法会导致其他goroutine永远阻塞,形成死锁。务必确保每个Lock()都有对应的Unlock()。
go 复制代码
..........
lock.Lock()
count--
lock.Unlock()
..........

或者我们可以利用延迟defer,可以确保解锁操作在函数返回前自动执行,避免忘记解锁:

go 复制代码
..........
lock.Lock()
defer lock.Unlock()
count--
..........
  • 重复解锁
    多次调用Unlock()可能导致数据竞争或panic。每个Lock()只能被解锁一次。我们应该确保每个Lock()只有一个对应的Unlock()。使用defer可以避免此类问题。

注意

  • 在一个 goroutine 获得 Mutex 后,其他 goroutine 只能等到这个 goroutine 释放该 Mutex
  • 使用 Lock() 加锁后,不能再继续对其加锁,直到利用 Unlock() 解锁后才能再加锁
  • 在 Lock() 之前使用 Unlock() 会导致 panic 异常
  • 已经锁定的 Mutex 并不与特定的 goroutine 相关联,这样可以利用一个 goroutine 对其加锁,再利用其他 goroutine 对其解锁
  • 在同一个 goroutine 中的 Mutex 解锁之前再次进行加锁,会导致死锁
  • 适用于读写不确定,并且只有一个读或者写的场景

参考:

这里只对锁的使用做一个笔记,如果想要深入了解的可以参考详解 Go 的并发原语 sync.Mutex,通过互斥锁来解决资源并发访问所带来的问题

相关推荐
AI原吾2 小时前
掌握Python-uinput:打造你的输入设备控制大师
开发语言·python·apython-uinput
机器视觉知识推荐、就业指导2 小时前
Qt/C++事件过滤器与控件响应重写的使用、场景的不同
开发语言·数据库·c++·qt
毕设木哥2 小时前
25届计算机专业毕设选题推荐-基于python的二手电子设备交易平台【源码+文档+讲解】
开发语言·python·计算机·django·毕业设计·课程设计·毕设
珞瑜·2 小时前
Matlab R2024B软件安装教程
开发语言·matlab
weixin_455446172 小时前
Python学习的主要知识框架
开发语言·python·学习
孤寂大仙v2 小时前
【C++】STL----list常见用法
开发语言·c++·list
她似晚风般温柔7893 小时前
Uniapp + Vue3 + Vite +Uview + Pinia 分商家实现购物车功能(最新附源码保姆级)
开发语言·javascript·uni-app
咩咩大主教3 小时前
C++基于select和epoll的TCP服务器
linux·服务器·c语言·开发语言·c++·tcp/ip·io多路复用
FuLLovers3 小时前
2024-09-13 冯诺依曼体系结构 OS管理 进程
linux·开发语言
王中阳Go3 小时前
字节跳动的微服务独家面经
微服务·面试·golang