Go并发同步Mutex典型易错使用场景

目录
  • Mutex的4种易错使用场景
    • 1.Lock/Unlock 不成对出现
    • 2.Copy 已使用的 Mutex
    • 3.重入
    • 4.死锁
  • 解决策略

Mutex的4种易错使用场景

1.Lock/Unlock 不成对出现

Lock/Unlock 没有成对出现,就可能会出现死锁或者是因为Unlock一个未加锁的Mutex而导致 panic。

忘记Unlock的情形

  • 代码中有太多的 if-else 分支,可能在某个分支中漏写了 Unlock;
  • 在重构的时候把 Unlock 给删除了;
  • Unlock 误写成了 Lock。

忘记Lock的情形一般是误删除了或者注释掉了Lock。

eg:

func main() {
   var mu sync.Mutex
   defer mu.Unlock()
   fmt.Println("oh, missing Lock!")
}

error result:

2.Copy 已使用的 Mutex

实际上sync包下的同步原语在使用后都是不可复制的,原因在于Mutex是有状态的,其state的值时刻在变化,如果复制一个已经加锁的Metux对象给一个新的变量,可能这个变量刚初始化就显示被加锁了,这显然是不合理的。

eg:以下代码在调用 foo 函数的时候,调用者会复制 Mutex 变量 c 作为 foo 函数的参数,不幸的是,复制之前已经使用了这个锁,这就导致,复制的 Counter 是一个带状态 Counter,从而会导致死锁。

type Counter struct {
   sync.Mutex
   Count int
}
func main() {
   var c Counter
   c.Lock()
   defer c.Unlock()
   c.Count++
   foo(c) // 复制锁
}
// 这里Counter的参数是通过复制的方式传入的
func foo(c Counter) {
   c.Lock()
   defer c.Unlock()
   fmt.Println("in foo")
}

error result:还好有Go的协程死锁检查机制,程序运行后会快速失败而不是一直hang住。

Go Vet指令

我们当然不想程序运行了才发现死锁,我们可以通过go vet指令来在运行前检查我们的代码是否存在lock copy问题:

检查原理

检查是通过copylock分析器静态分析实现的。这个分析器会分析函数调用、range 遍历、复制、声明、函数返回值等位置,有没有锁的值 copy 的情景,以此来判断有没有问题。

通过源码我们可以看到实现了Lock或者Unlock接口的struct都支持copylock检查。

var lockerType *types.Interface
 // Construct a sync.Locker interface type.
 func init() {
     nullary := types.NewSignature(nil, nil, nil, false) // func()
     methods := []*types.Func{
     types.NewFunc(token.NoPos, nil, "Lock", nullary),
     types.NewFunc(token.NoPos, nil, "Unlock", nullary),
 }
 lockerType = types.NewInterface(methods, nil).Complete()
 }

3.重入

Mutex不像Java中的ReentrantLock拥有可重入的功能,主要是因为其实现中没有标记位记录哪个goroutine 拥有这把锁,所以Mutex是一个不可重入锁,而一旦误用Mutex的重入就会报错。

eg:

func foo(l sync.Locker) {
   fmt.Println("in foo")
   l.Lock()
   bar(l)
   l.Unlock()
}
func bar(l sync.Locker) {
   l.Lock()
   fmt.Println("in bar")
   l.Unlock()
}
func main() {
   l := &sync.Mutex{}
   foo(l)
}

error result:我们可以看到当在bar方法中尝试再次获取锁时,获取不到,触发了死锁。

4.死锁

两个或两个以上的进程(或线程,goroutine)

执行过程中,因争夺共享资源而处于一种互相等待的状态,如果没有外部干涉,它们都将无法推进下去,此时,我们称系统处于死锁状态或系统产生了死锁。

死锁产生的4个必要条件

如果想避免死锁,我们只要思考如何打破以下任意条件就可以。

  • 1.互斥: 至少一个资源是被排他性独享的,其他线程必须处于等待状态,直到资源被释放。
  • 2.持有和等待:goroutine 持有一个资源,并且还在请求其它 goroutine 持有的资源,也就是咱们常说的“吃着碗里,看着锅里”的意思。
  • 3. 不可剥夺:资源只能由持有它的 goroutine 来释放。
  • 4.环路等待:一般来说,存在一组等待进程,P={P1,P2,…,PN},P1 等待 P2 持有的

资源,P2 等待 P3 持有的资源,依此类推,最后是 PN 等待 P1 持有的资源,这就形成
了一个环路等待的死结。

eg:在这里我们以办理居住证业务,举一个简单的环路等待导致死锁的例子:

//办理居住证
func main() {
   // 网签中心证明
   var psCertificate sync.Mutex
   // 社区证明
   var propertyCertificate sync.Mutex
   var wg sync.WaitGroup
   wg.Add(2) // 需要网签中心和社区都处理
   // 网签中心处理goroutine
   go func() {
      defer wg.Done() // 网签中心处理完成
      psCertificate.Lock()
      defer psCertificate.Unlock()
      // 检查材料
      time.Sleep(5 * time.Second)
      // 请求社区的证明
      propertyCertificate.Lock()
      propertyCertificate.Unlock()
   }()
   // 社区处理goroutine
   go func() {
      defer wg.Done() // 社区处理完成
      propertyCertificate.Lock()
      defer propertyCertificate.Unlock()
      // 检查材料
      time.Sleep(5 * time.Second)
      // 请求网签中心的证明
      psCertificate.Lock()
      psCertificate.Unlock()
   }()
   wg.Wait()
   fmt.Println("成功完成")
}

error result:

解决策略

1.可以引入一个第三方的锁,大家都依赖这个锁进行业务处理,比如现在政府推行的一站式政务服务中心。

2.解决持有等待问题,比如社区不需要看到网签中心的证明才给开居住证明。

以上就是Go并发同步Mutex典型易错使用场景的详细内容,更多关于Go Mutex易错场景的资料请关注我们其它相关文章!

(0)

相关推荐

  • golang中sync.Mutex的实现方法

    目录 mutex 的实现思想 golang 中 mutex 的实现思想 mutex 的结构以及一些 const 常量值 Mutex 没有被锁住,第一个协程来拿锁 Mutex 仅被协程 A 锁住,没有其他协程抢锁,协程 A 释放锁 Mutex 已经被协程 A 锁住,协程 B 来拿锁 lockSlow() runtime_doSpin() runtime_canSpin() Mutex 被协程 A 锁住,协程 B 来抢锁但失败被放入等待队列,此时协程 A 释放锁 unlockSlow() Mutex

  • GO语言协程互斥锁Mutex和读写锁RWMutex用法实例详解

    sync.Mutex Go中使用sync.Mutex类型实现mutex(排他锁.互斥锁).在源代码的sync/mutex.go文件中,有如下定义: // A Mutex is a mutual exclusion lock. // The zero value for a Mutex is an unlocked mutex. // // A Mutex must not be copied after first use. type Mutex struct { state int32 sem

  • go RWMutex的实现示例

    目录 Overview RWMutex的结构 Lock Unlock RLock RUnlock Q1:多个协程并发拿读锁,如何保证这些读锁协程都不会被阻塞? Q2:多个协程并发拿写锁,如何保证只会有一个协程拿到写锁? Q3:在读锁被拿到的情况下,新协程拿写锁,如果保证写锁现成会被阻塞? Q4:在读锁被拿到的情况下,新协程拿写锁被阻塞,当旧有的读锁协程全部释放,如何唤醒等待的写锁协程 Q5:在写锁被拿到的情况下,新协程拿读锁,如何让新协程被阻塞? Q6:在写锁被拿到的情况下,新协程拿读锁,写锁协

  • 详解golang RWMutex读写互斥锁源码分析

    针对Golang 1.9的sync.RWMutex进行分析,与Golang 1.10基本一样除了将panic改为了throw之外其他的都一样. RWMutex是读写互斥锁.锁可以由任意数量的读取器或单个写入器来保持. RWMutex的零值是一个解锁的互斥锁. 以下代码均去除race竞态检测代码 源代码位置:sync\rwmutex.go 结构体 type RWMutex struct { w Mutex // 互斥锁 writerSem uint32 // 写锁信号量 readerSem uin

  • Go语言并发编程之互斥锁Mutex和读写锁RWMutex

    目录 一.互斥锁Mutex 1.Mutex介绍 2.Mutex使用实例 二.读写锁RWMutex 1.RWMutex介绍 2.RWMutex使用实例 在并发编程中,多个Goroutine访问同一块内存资源时可能会出现竞态条件,我们需要在临界区中使用适当的同步操作来以避免竞态条件.Go 语言中提供了很多同步工具,本文将介绍互斥锁Mutex和读写锁RWMutex的使用方法. 一.互斥锁Mutex 1.Mutex介绍 Go 语言的同步工具主要由 sync 包提供,互斥锁 (Mutex) 与读写锁 (R

  • Golang Mutex互斥锁深入理解

    目录 引言 Mutex结构 饥饿模式和正常模式 正常模式 饥饿模式 状态的切换 加锁和解锁 加锁 自旋 计算锁的新状态 更新锁状态 解锁 可能遇到的问题 锁拷贝 panic导致没有unlock 引言 Golang的并发编程令人着迷,使用轻量的协程.基于CSP的channel.简单的go func()就可以开始并发编程,在并发编程中,往往离不开锁的概念. 本文介绍了常用的同步原语 sync.Mutex,同时从源码剖析它的结构与实现原理,最后简单介绍了mutex在日常使用中可能遇到的问题,希望大家读

  • Go并发同步Mutex典型易错使用场景

    目录 Mutex的4种易错使用场景 1.Lock/Unlock 不成对出现 2.Copy 已使用的 Mutex 3.重入 4.死锁 解决策略 Mutex的4种易错使用场景 1.Lock/Unlock 不成对出现 Lock/Unlock 没有成对出现,就可能会出现死锁或者是因为Unlock一个未加锁的Mutex而导致 panic. 忘记Unlock的情形 代码中有太多的 if-else 分支,可能在某个分支中漏写了 Unlock: 在重构的时候把 Unlock 给删除了: Unlock 误写成了

  • Java 包装类型及易错陷阱详解

    目录 一.预备知识 1.1 Java内存管理 1.2 基本数据类型的包装类 1.3 包装类的构造方法 1.4 包装类的优缺点 1.5 包装类易错点 二.自动拆/装箱 三.整形池 四.优先选择基本数据类型 一.预备知识 1.Java把内存划分成两种:一种是栈内存,另一种是堆内存. 2.int是基本类型,直接存数值:而 Integer是类,产生对象时用一个引用指向这个对象. 3.包装器(wrapper)--这是<JAVA核心技术>一书中对Integer这类对象的称呼. 4.包装器位于java.la

  • Golang易错知识点汇总

    目录 类型转换和类型断言 1.类型转换示例代码 2.类型断言代码示例 全局变量 全局变量使用var,编译通过 全局变量不使用var,编译不通过 init函数 Go接口总结 Go字符串 如何修改字符串的内容 slice 1.删除单个元素 2.删除多个元素 总结 类型转换和类型断言 类型转换语法:Type(expression) 类型断言语法为:expression.(Type) 1.类型转换示例代码 package main import "fmt" //典型的类型转换示例 func m

  • JavaScript易错知识点整理

    前言 本文是我学习JavaScript过程中收集与整理的一些易错知识点,将分别从变量作用域,类型比较,this指向,函数参数,闭包问题及对象拷贝与赋值这6个方面进行由浅入深的介绍和讲解,其中也涉及了一些ES6的知识点. JavaScript知识点 1.变量作用域 var a = 1; function test() { var a = 2; console.log(a); // 2 } test(); 上方的函数作用域中声明并赋值了a,且在console之上,所以遵循就近原则输出a等于2. va

  • C/C++常用函数易错点分析

    本文简单分析了C/C++中常用函数的易错点,包括memset.sizeof.getchar等函数.分享给大家供大家参考之用.具体分析如下: 1.memset #include <string.h> void* memset( void* buffer, int ch, size_t count ); 将内存中buffer的前count个字节的内容全部设置为ch指定的ASCII值.经常用来初始化数组.复制时以字节为单位,如果buffer是int long,或者其他类型的指针时,需要注意不一定为数

  • 总结js中的一些兼容性易错的问题

    一.属性相关 我们通常把特征(attribute)和属性(property)统称为属性,但是他们确实是不同的概念, 特征(attribute)会表现在HTML文本中,对特征的修改一定会表现在元素的outerHTML中,并且特征只存在于元素节点中: 属性(property)是对于JS对象进行修改,除了浏览器内置的部分特征外,其它的属性操作并不会影响HTML文本. 1. IE6/7不区分属性和特征 其它浏览器会区分属性和特征,而IE67并不会区分它们,在IE67下我们只能用属性名来删除特征,虽然这两

  • Android悬浮窗的实现(易错点)

    0. 前言 现在很多应用都使用到悬浮窗,例如微信在视频的时候,点击Home键,视频小窗口仍然会在屏幕上显示.这个功能在很多情况下都非常有用.那么今天我们就来实现一下Android悬浮窗,以及探索一下实现悬浮窗时的易错点. 1. 实现原理 1.1 悬浮窗插入接口 在实现悬浮窗之前,我们需要知道通过什么接口,能够将一个控件放入到屏幕中去. Android的界面绘制,都是通过WindowMananger的服务来实现的.那么,既然要实现一个能够在自身应用以外的界面上的悬浮窗,我们就要利用WindowMa

  • javascript 易错知识点实例小结

    本文实例总结了javascript 易错知识点.分享给大家供大家参考,具体如下: 为什么 typeof null === 'object' 原理是这样的,不同的对象在底层都表示为二进制,在JavaScript中二进制前三位都为0的话会被判断为 object 类型, null 的二进制表示是全0,自然前三位也是0,所以执行 typeof 时会返回" object ". 对象属性的存在性 如 myObject.a 的属性访问返回值可能是 undefined ,但是这个值有可能是属性中存储的

  • 详解Java中方法next()和nextLine()的区别与易错点

    1.基本语法 1.1基本使用方法 本篇博客重点nextLine()会读取换行('\r'),但是不会进行输出. Java中Scanner类中的方法next()和nextLine()都是吸取输入台输入的字符,区别: next()不会吸取字符前/后的空格/Tab键,只吸取字符,开始吸取字符(字符前后不算)直到遇到空格/Tab键/回车截止吸取: nextLine()吸取字符前后的空格/Tab键,回车键截止. 输入两行字符串: 我爱学JAVA 我真的很爱爱学JAVA 我真的很爱很爱学JAVA 期望输出结果

  • MySQL null的一些易错点

    依据null-values,MySQL的值为null的意思只是代表没有数据,null值和某种类型的零值是两码事,比如int类型的零值为0,字符串的零值为"",但是它们依然是有数据的,不是null. 我们在保存数据的时候,习惯性的把暂时没有的数据记为null,表示当前我们无法提供有效的信息. 不过使用null但是时候,需要我们注意一些问题.对此MySQL文档说明如下: problems-with-null 使用null的易错点 下面我摘取MySQL官方给出的null的易错点做讲解. 对M

随机推荐