返回顶部
首页 > 资讯 > 精选 >GO如何使用Mutex确保并发程序正确性
  • 767
分享到

GO如何使用Mutex确保并发程序正确性

2023-07-05 08:07:11 767人浏览 薄情痞子
摘要

这篇“Go如何使用Mutex确保并发程序正确性”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“GO如何使用Mutex确保并发程

这篇“Go如何使用Mutex确保并发程序正确性”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“GO如何使用Mutex确保并发程序正确性”文章吧。

    1. 简介

    本文的主要内容是介绍Go中Mutex并发原语。包含Mutex的基本使用,使用的注意事项以及一些实践建议。

    2. 基本使用

    2.1 基本定义

      Mutex是Go语言中的一种同步原语,全称为Mutual Exclusion,即互斥。它可以在并发编程中实现对共享资源的互斥访问,保证同一时刻只有一个协程可以访问共享资源。Mutex通常用于控制对临界区的访问,以避免竞态条件的出现。

    2.2 使用方式

      使用Mutex的基本方法非常简单,可以通过调用Mutex的Lock方法来获取锁,然后通过Unlock方法释放锁,示例代码如下:

    import "sync"var mutex sync.Mutexfunc main() {  mutex.Lock()    // 获取锁  // 执行需要同步的操作  mutex.Unlock()  // 释放锁}

    2.3 使用例子

    2.3.1 未使用mutex同步代码示例

    下面是一个使用goroutine访问共享资源,但没有使用Mutex进行同步的代码示例:

    package mainimport (    "fmt"    "time")var count intfunc 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 mainimport (    "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 mainimport (    "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 mainimport (    "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.gotype Transport struct {   lk       sync.Mutex   idleConn map[string][]*persistConn   altProto map[string]RoundTripper // nil or map of URI scheme =&gt; 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.gotype 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.gotype 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.gofunc (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}

    以上就是关于“GO如何使用Mutex确保并发程序正确性”这篇文章的内容,相信大家都有了一定的了解,希望小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注编程网精选频道。

    --结束END--

    本文标题: GO如何使用Mutex确保并发程序正确性

    本文链接: https://lsjlt.com/news/350670.html(转载时请注明来源链接)

    有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

    猜你喜欢
    • GO如何使用Mutex确保并发程序正确性
      这篇“GO如何使用Mutex确保并发程序正确性”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“GO如何使用Mutex确保并发程...
      99+
      2023-07-05
    • GO使用Mutex确保并发程序正确性详解
      目录1. 简介2. 基本使用2.1 基本定义2.2 使用方式2.3 使用例子2.3.1 未使用mutex同步代码示例2.3.2 使用mutex解决上述问题3. 使用注意事项3.1 L...
      99+
      2023-03-03
      GO Mutex并发正确性 Mutex确保并发正确性
    • Go语言如何利用Mutex保障数据读写正确
      目录1、实现机制2、基本用法3、race detector4、总结5、思考问题Go 并发场景下如何保障数据读写正确?本文聊聊 Mutex 的用法。 Go 语言作为一个原生支持用户态进...
      99+
      2023-05-20
      Go语言Mutex保障数据读写正确 Go语言Mutex数据读写 Go语言Mutex
    • Go并发编程之goroutine使用正确方法
      目录1. 对创建的gorouting负载1.1 不要创建一个你不知道何时退出的 goroutine1.2 不要帮别人做选择1.3 不要作为一个旁观者1.4 不要创建不知道什么时候退出...
      99+
      2024-04-02
    • 如何让python程序正确高效地并发
      目录python线程何时需要拥有GIL?认知模型1:同一时刻只有一个线程运行python代码模型2:不保证每 5 毫秒释放一次 GIL模型3:非 Python 代码可以显式释放 GI...
      99+
      2024-04-02
    • 如何正确使用Go Map
      本篇内容主要讲解“如何正确使用Go Map”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“如何正确使用Go Map”吧!前言例子如下:func main() { &n...
      99+
      2023-06-15
    • 如何正确使用Go defer
      这篇文章主要介绍“如何正确使用Go defer”,在日常操作中,相信很多人在如何正确使用Go defer问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”如何正确使用Go defer”的疑惑有所帮助!接下来,请跟...
      99+
      2023-06-15
    • C++中如何确保多个并发进程正确的访问共享资源
      在C++中,可以使用互斥量(mutex)来确保多个并发进程正确地访问共享资源。互斥量是一种同步原语,它可以确保在任意时刻只有一个进程...
      99+
      2024-04-02
    • 怎么让python程序正确高效地并发
      这篇文章主要介绍“怎么让python程序正确高效地并发”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“怎么让python程序正确高效地并发”文章能帮助大家解决问题。python线程何时需要拥有GIL?...
      99+
      2023-07-02
    • Windows7-如何正确卸载程序
      要正确卸载程序,您可以按照以下步骤操作:1. 打开控制面板:点击开始菜单,然后选择“控制面板”。2. 在控制面板中找到“程序”或“程...
      99+
      2023-09-08
      Windows7
    • 如何正确使用MVCC
      这篇文章主要讲解了“如何正确使用MVCC”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“如何正确使用MVCC”吧! 简单理解版以下先引用我之前写过的...
      99+
      2024-04-02
    • 如何正确使用@property
      本篇内容主要讲解“如何正确使用@property”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“如何正确使用@property”吧!他们是这样说的:class People: ...
      99+
      2023-06-15
    • 如何正确使用WideCharToMultiByte
      要正确使用WideCharToMultiByte函数,需要按照以下步骤操作:1. 确定要转换的宽字符编码方式。WideCharToM...
      99+
      2023-09-26
      使用
    • NPM 索引中的 PHP 和 NumPy:如何确保数据的正确性?
      随着数据分析和处理的需求越来越多,许多开发者选择使用各种语言和工具来处理和管理数据。其中,PHP 和 NumPy 是两种常见的工具,它们在处理数据方面都有着很好的表现。然而,在使用这些工具时,我们需要保证数据的正确性和准确性。在本文中,我...
      99+
      2023-10-16
      numpy npm 索引
    • Python 并发编程中的异常处理:确保应用程序的稳定性
      Python 并发 异常处理 多线程 多进程 协程 多线程 在多线程环境中,每个线程都有自己的执行流和栈。异常发生时,通常只会影响该特定线程。为了处理线程中的异常,可以使用 threading.Thread() 的 join() 方法...
      99+
      2024-02-18
      并发编程中 异常处理对于确保应用程序的稳定性和正确性至关重要。本文将深入探讨 Python 并发中的异常处理 涵盖多线程 多进程和协程 提供实用代码示例和最佳实践。
    • 如何正确的使用MySQL触发器
      如何正确的使用MySQL触发器?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。1.触发器是什么?一类特殊的数据库程序,可以监视某种数据的操作(insert/updat ...
      99+
      2023-06-06
    • 如何在Go语言中正确使用包
      如何在Go语言中正确使用包,需要具体代码示例 在Go语言中,包(package)是组织和管理代码的基本单元。正确使用包可以帮助我们更好地组织代码,提高代码的复用性和可维护性。本文将介绍...
      99+
      2024-03-12
      使用 导入 包管理 go语言 标准库
    • ASP IDE并发文件:如何确保代码的安全性?
      在多人协作的软件开发过程中,经常会遇到并发修改文件的情况。这种情况下,如何保证代码的安全性,避免出现冲突和错误?本文将介绍一些常用的方法和工具。 版本控制工具 版本控制工具是保证代码安全性的常用工具。它可以记录文件的历史版本,并且能够...
      99+
      2023-06-18
      ide 并发 文件
    • PHP中的数组、并发和缓存:如何正确使用?
      在PHP开发中,数组、并发和缓存是非常常见的概念。它们都可以用来提高程序的效率和性能,但是如果使用不当,也会给程序带来严重的问题。本文将探讨如何正确使用PHP中的数组、并发和缓存。 一、数组 数组是PHP中最常用的数据结构之一。它可以存储...
      99+
      2023-09-21
      数组 并发 缓存
    • Golang cli 应用程序 - 如何正确使用上下文?
      Golang cli 应用程序 - 如何正确使用上下文?php小编鱼仔将为您介绍如何在Golang的cli应用程序中正确使用上下文。在开发cli应用程序时,上下文是非常重要的,它可以帮...
      99+
      2024-02-09
    软考高级职称资格查询
    编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
    • 官方手机版

    • 微信公众号

    • 商务合作