如何使用Mutex确保并发程序的正确性
1. 简介
本文的主要内容是介绍Go中Mutex并发原语。包含Mutex的基本使用,使用的注意事项以及一些实践建议。
2. 基本使用
2.1 基本定义
Mutex是Go语言中的一种同步原语,全称为Mutual Exclusion,即互斥锁。它可以在并发编程中实现对共享资源的互斥访问,保证同一时刻只有一个协程可以访问共享资源。Mutex通常用于控制对临界区的访问,以避免竞态条件的出现。
2.2 使用方式
使用Mutex的基本方法非常简单,可以通过调用Mutex的Lock方法来获取锁,然后通过Unlock方法释放锁,示例代码如下:
import "sync"
var mutex sync.Mutex
func main() {
mutex.Lock() // 获取锁
// 执行需要同步的操作
mutex.Unlock() // 释放锁
}
2.3 使用例子
2.3.1 未使用mutex同步代码示例
下面是一个使用goroutine访问共享资源,但没有使用Mutex进行同步的代码示例:
package main
import (
"fmt"
"time"
)
var count int
func main() {
for i := 0; i < 1000; i++ {
go add()
}
time.Sleep(1 * time.Second)
fmt.Println("count:", count)
}
func add() {
count++
}
上述代码中,我们启动了1000个goroutine,每个goroutine都调用add()函数将count变量的值加1。由于count变量是共享资源,因此在多个goroutine同时访问的情况下会出现竞态条件。但是由于没有使用Mutex进行同步,所以会导致count的值无法正确累加,最终输出的结果也会出现错误。
在这个例子中,由于多个goroutine同时访问count变量,而不进行同步控制,导致每个goroutine都可能读取到同样的count值,进行相同的累加操作。这就会导致最终输出的count值不是期望的结果。如果我们使用Mutex进行同步控制,就可以避免这种竞态条件的出现。
2.3.2 使用mutex解决上述问题
下面是使用Mutex进行同步控制,解决上述代码中竞态条件问题的示例:
package main
import (
"fmt"
"sync"
"time"
)
var (
count int
mutex sync.Mutex
)
func main() {
for i := 0; i < 1000; i++ {
go add()
}
time.Sleep(1 * time.Second)
fmt.Println("count:", count)
}
func add() {
mutex.Lock()
count++
mutex.Unlock()
}
在上述代码中,我们在全局定义了一个sync.Mutex类型的变量mutex,用于进行同步控制。在add()函数中,我们首先调用mutex.Lock()方法获取mutex的锁,确保只有一个goroutine可以访问count变量。然后进行加1操作,最后调用mutex.Unlock()方法释放mutex的锁,使其他goroutine可以继续访问count变量。
通过使用Mutex进行同步控制,我们避免了竞态条件的出现,确保了count变量的正确累加。最终输出的结果也符合预期。
3. 使用注意事项
3.1 Lock/Unlock需要成对出现
下面是一个没有成对出现Lock和Unlock的代码例子:
package main
import (
"fmt"
"sync"
)
func main() {
var mutex sync.Mutex
go func() {
mutex.Lock()
fmt.Println("goroutine1 locked the mutex")
}()
go func() {
fmt.Println("goroutine2 trying to lock the mutex")
mutex.Lock()
fmt.Println("goroutine2 locked the mutex")
}()
}
在上述代码中,我们创建了一个sync.Mutex类型的变量mutex,然后在两个goroutine中使用了这个mutex。
在第一个goroutine中,我们调用了mutex.Lock()方法获取mutex的锁,但是没有调用相应的Unlock方法。在第二个goroutine中,我们首先打印了一条信息,然后调用了mutex.Lock()方法尝试获取mutex的锁。由于第一个goroutine没有释放mutex的锁,第二个goroutine就一直阻塞在Lock方法中,一直无法执行。
因此,在使用Mutex的过程中,一定要确保每个Lock方法都有对应的Unlock方法,确保Mutex的正常使用。
3.2 不能对已使用的Mutex作为参数进行传递
下面举一个已使用的Mutex作为参数进行传递的代码的例子:
type Counter struct {
sync.Mutex
Count int
}
func main(){
var c Counter
c.Lock()
defer c.Unlock()
c.Count++
foo(c)
fmt.println("done")
}
func foo(c Counter) {
c.Lock()
defer c.Unlock()
fmt.println("foo done")
}
当一个 mutex 被传递给一个函数时,预期的行为应该是该函数在访问受 mutex 保护的共享资源时,能够正确地获取和释放 mutex,以避免竞态条件的发生。
如果我们在Mutex未解锁的情况下拷贝这个Mutex,就会导致锁失效的问题。因为Mutex的状态信息被拷贝了,拷贝出来的Mutex还是处于锁定的状态。而在函数中,当要访问临界区数据时,首先肯定是先调用Mutex.Lock方法加锁,而传入Mutex其实是处于锁定状态的,此时函数将永远无法获取到锁。
因此,不能将已使用的Mutex直接作为参数进行传递。
3.3 不可重复调用Lock/UnLock方法
下面是一个例子,其中对同一个 Mutex 进行了重复加锁:
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
mu.Lock()
fmt.Println("First Lock")
// 重复加锁
mu.Lock()
fmt.Println("Second Lock")
mu.Unlock()
mu.Unlock()
}
在这个例子中,我们先对 Mutex 进行了一次加锁,然后在没有解锁的情况下,又进行了一次加锁操作.
这种情况下,程序会出现死锁,因为第二次加锁操作已经被阻塞,等待第一次加锁的解锁操作,而第一次加锁的解锁操作也被阻塞,等待第二次加锁的解锁操作,导致了互相等待的局面,无法继续执行下去。
Mutex实际上是通过一个int32类型的标志位来实现的。当这个标志位为0时,表示这个Mutex当前没有被任何goroutine获取;当标志位为1时,表示这个Mutex当前已经被某个goroutine获取了。
Mutex的Lock方法实际上就是将这个标志位从0改为1,表示获取了锁;Unlock方法则是将标志位从1改为0,表示释放了锁。当第二次调用Lock方法,此时标记位为1,代表有一个goroutine持有了这个锁,此时将会被阻塞,而持有该锁的其实就是当前的goroutine,此时该程序将会永远阻塞下去。
4. 实践建议
4.1 Mutex锁不要同时保护两份不相关数据
下面是一个例子,使用Mutex同时保护两份不相关的数据
// net/http transport.go
type Transport struct {
lk sync.Mutex
idleConn map[string][]*persistConn
altProto map[string]RoundTripper // nil or map of URI scheme => RoundTripper
}
func (t *Transport) CloseIdleConnections() {
t.lk.Lock()
defer t.lk.Unlock()
if t.idleConn == nil {
return
}
for _, conns := range t.idleConn {
for _, pconn := range conns {
pconn.close()
}
}
t.idleConn = nil
}
func (t *Transport) RegisterProtocol(scheme string, rt RoundTripper) {
if scheme == "http" || scheme == "https" {
panic("protocol " + scheme + " already registered")
}
t.lk.Lock()
defer t.lk.Unlock()
if t.altProto == nil {
t.altProto = make(map[string]RoundTripper)
}
if _, exists := t.altProto[scheme]; exists {
panic("protocol " + scheme + " already registered")
}
t.altProto[scheme] = rt
}
在这个例子中,idleConn是存储了空闲的连接,altProto是存储了协议的处理器,CloseIdleConnections方法是关闭所有空闲的连接,RegisterProtocol是用于注册协议处理的。
尽管ideConn和altProto这两部分数据并没有任何关联,但是却是使用同一个Mutex来保护的,这样子当调用RegisterProtocol方法时,便无法调用CloseIdleConnections方法,这会导致竞争过多,从而影响性能。
因此,为了提高并发性能,应该将 Mutex 的锁粒度尽量缩小,只保护需要保护的数据。
现代版本的 net/http 中已经对 Transport 进行了改进,分别使用了不同的 mutex 来保护 idleConn 和 altProto,以提高性能和代码的可维护性。
type Transport struct {
idleMu sync.Mutex
idleConn map[connectMethodKey][]*persistConn // most recently used at end
altMu sync.Mutex // guards changing altProto only
altProto atomic.Value // of nil or map[string]RoundTripper, key is URI scheme
}
4.2 Mutex嵌入结构体中位置放置建议
将 Mutex 嵌入到结构体中,如果只需要保护其中一些数据,可以将 Mutex 放在需要控制的字段上面,然后使用空格将被保护字段和其他字段进行分隔。这样可以实现更细粒度的锁定,也能更清晰地表达每个字段需要被互斥保护的意图,代码更易于维护和理解。下面举一些实际的例子:
Server结构体中reqLock是用来保护freeReq字段,respLock用来保护freeResp字段,都是将mutex放在被保护字段的上面
//net/rpc server.go
type Server struct {
serviceMap sync.Map // map[string]*service
reqLock sync.Mutex // protects freeReq
freeReq *Request
respLock sync.Mutex // protects freeResp
freeResp *Response
}
在Transport结构体中,idleMu锁会保护closeIdle等一系列字段,此时将锁放在被保护字段的最上面,然后用空格将被idleMu锁保护的字段和其他字段分隔开来。 实现更细粒度的锁定,也能更清晰地表达每个字段需要被互斥保护的意图。
// net/http transport.go
type Transport struct {
idleMu sync.Mutex
closeIdle bool // user has requested to close all idle conns
idleConn map[connectMethodKey][]*persistConn // most recently used at end
idleConnWait map[connectMethodKey]wantConnQueue // waiting getConns
idleLRU connLRU
reqMu sync.Mutex
reqCanceler map[cancelKey]func(error)
altMu sync.Mutex // guards changing altProto only
altProto atomic.Value // of nil or map[string]RoundTripper, key is URI scheme
connsPerHostMu sync.Mutex
connsPerHost map[connectMethodKey]int
connsPerHostWait map[connectMethodKey]wantConnQueue // waiting getConns
}
4.3 尽量减小锁的作用范围
在一个代码段里,尽量减小锁的作用范围可以提高并发性能,减少锁的等待时间,从而减少系统资源的浪费。
锁的作用范围越大,那么就有越多的代码需要等待锁,这样就会降低并发性能。因此,在编写代码时,应该尽可能减小锁的作用范围,只在需要保护的临界区内加锁。
如果锁的作用范围是整个函数,使用 defer
语句来释放锁是一种常见的做法,可以避免忘记手动释放锁而导致的死锁等问题。
func (t *Transport) CloseIdleConnections() {
t.lk.Lock()
defer t.lk.Unlock()
if t.idleConn == nil {
return
}
for _, conns := range t.idleConn {
for _, pconn := range conns {
pconn.close()
}
}
t.idleConn = nil
}
在使用锁时,注意避免在锁内执行长时间运行的代码或者IO操作,因为这样会阻塞锁的使用,导致锁的等待时间变长。如果确实需要在锁内执行长时间运行的代码或者IO操作,可以考虑将锁释放,让其他代码先执行,等待操作完成后再重新获取锁, 比如下面代码示例
// net/http/httputil persist.go
func (cc *ClientConn) Read(req *http.Request) (resp *http.Response, err error) {
// Retrieve the pipeline ID of this request/response pair
cc.mu.Lock()
id, ok := cc.pipereq[req]
delete(cc.pipereq, req)
if !ok {
cc.mu.Unlock()
return nil, ErrPipeline
}
cc.mu.Unlock()
// xxx 省略掉一些中间逻辑
// 从http连接中读取http响应数据, 这个IO操作,先解锁
resp, err = http.ReadResponse(r, req)
// 网络IO操作结束,再继续读取
cc.mu.Lock()
defer cc.mu.Unlock()
if err != nil {
cc.re = err
return resp, err
}
cc.lastbody = resp.Body
cc.nread++
if resp.Close {
cc.re = ErrPersistEOF // don't send any more requests
return resp, cc.re
}
return resp, err
}
5.总结
在并发编程中,Mutex是一种常见的同步机制,用来保护共享资源。为了提高并发性能,我们需要尽可能缩小Mutex的锁粒度,只保护需要保护的数据,同时在一个代码段里,尽量减小锁的作用范围。如果锁的作用范围是整个函数,可以使用defer来在函数退出时解锁。当Mutex嵌入到结构体中时,我们可以将Mutex放到要控制的字段上面,并使用空格将字段进行分隔,以便只保护需要保护的数据。
如何使用Mutex确保并发程序的正确性的更多相关文章
- java并发编程实战:第十二章---并发程序的测试
并发程序中潜在错误的发生并不具有确定性,而是随机的. 安全性测试:通常会采用测试不变性条件的形式,即判断某个类的行为是否与其规范保持一致 活跃性测试:进展测试和无进展测试两方面,这些都是很难量化的(性 ...
- java并发编程(5)并发程序测试
并发程序测试 一.正确性测试 如:对一个自定义缓存的测试 //自定义的缓存 public class SemaphoreBoundedBuffer <E> { private final ...
- java并发程序和共享对象实用策略
java并发程序和共享对象实用策略 在并发程序中使用和共享对象时,可以使用一些实用的策略,包括: 线程封闭 只读共享.共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它.共享的只读对象包括不 ...
- 编写并发程序 Inversion
做完了 scala parallel 课程作业后,觉得 scala 写并发程序的便捷性是 java 永远都追不上的.scala 的Future 和 Promise,java 里 Future 和 Co ...
- EBS获取并发程序Trace File
http://blog.itpub.net/16832682/viewspace-1249765/ 最近因为项目上出现了PL/SQL性能的问题,因此需要对已经开发好的并发程序进行调优的工作.调优有个很 ...
- EBS 并发程序运行信息
--并发程序运行信息SELECT REQUEST_ID, PROGRAM, actual_start_date 开始日期, ACTUAL_COMPLETION_DA ...
- 使用async进行结构化并发程序开发
异步风格的函数: 继续来学习async相关的东东,对于它其实可以用到函数上,也就是用它可以定义一个异步风格的函数,然后在该函数中再来调用普通的函数,下面来瞅一下: 其实“GlobalScope.asy ...
- 【Java并发基础】利用面向对象的思想写好并发程序
前言 下面简单总结学习Java并发的笔记,关于如何利用面向对象思想写好并发程序的建议.面向对象的思想和并发编程属于两个领域,但是在Java中这两个领域却可以融合到一起.在Java语言中,面向对象编程的 ...
- 并发刺客(False Sharing)——并发程序的隐藏杀手
并发刺客(False Sharing)--并发程序的隐藏杀手 前言 前段时间在各种社交平台"雪糕刺客"这个词比较火,简单的来说就是雪糕的价格非常高!其实在并发程序当中也有一个刺客, ...
- Java7提供的Fork/Join框架实现高并发程序,你会使用吗?
摘要:Fork/Join框架位于J.U.C(java.util.concurrent)中,是Java7中提供的用于执行并行任务的框架,其可以将大任务分割成若干个小任务,最终汇总每个小任务的结果后得到最 ...
随机推荐
- centos-7部署kafka-v2.13.3.0.1集群
1.部署测试机器规划 ip kafka 版本 zookeeper 版本 192.168.113.132 v2.13.3.0.1 v3.6.3 192.168.113. ...
- 记事本默认编码改为UTF-8
前端时间发现用记事本直接创建的记录目标信息的TXT文本再用sublime打开变成了乱码,才发现编码有问题,记事本直接创建的文本编码竟然是ANSI编码. 于是动手将记事本默认编码改为UTF-8. 记事本 ...
- 在开启hadoop时候报错:localhost: Permission denied (publickey,gssapi-keyex,gssapi-with-mic,password).
此时 ssh localhost也会失败 原因是秘钥没有给自己, 运行ssh-copy-id -i ~/.ssh/id_rsa.pub root@localhost 即可解决.
- Windows Powershell个性化设置
1. 设置Powershell prompt只显示当前目录: 2. 设置Powershell title为当前路径: 这个文件的路径为:echo $profile # file location:ec ...
- C代码调用C++动态库
最近在工作中遇到了修改C++代码嵌入到C代码中去,C肯定不能直接用C++代码,就需要自己去修改成C代码,所以我就决定在C中调用C++动态库(谁让我懒呢),话不多说,直接上步骤 第一步:编写C++代码 ...
- 估计人数【最小路径重复点覆盖】【直接在(i,j)建一个新点】
估计人数 题意 思路 用最少的人,走完这几条线.最小重复路径点覆盖问题 建图之后,跑一下二分图. 考虑建图:图中'1'连着完下.或者右走.我们把图中所有的1编号,然后建图,然后floly,然后匈牙利. ...
- C语言基础编程题
//1.ASCII码值 ->相应字符 #include <stdio.h> int main() { int asc; scanf("%d",&asc); ...
- Delphi注解(不是注释)
开发环境Delphi XE10 1 unit Unit1; 2 3 interface 4 5 uses 6 Winapi.Windows, Winapi.Messages, System.SysUt ...
- termux搭建服务器方式
pkg install vim apt update 安装debian系统apt install proot-distroproot-distro install debianproot-distro ...
- Eclipse's Patching Codes Automatically
如何把等号左边的赋值等式补齐? 想把queryRunner.query(conn, sql,new BeanListHandler<>(type), params); 的等号左边代码(返回 ...