go 通道 并发 顺序_并发Go中的锁定与通道

go 通道 并发 顺序

Go普及了口头禅: 不要通过共享内存来交流; 通过通信共享内存。 该语言确实具有传统的互斥体(互斥结构)来协调对共享内存的访问,但它偏爱使用通道在goroutine之间共享信息。

在本文中,对goroutines,线程和竞争条件的简要介绍为我们介绍了两个Go程序奠定了基础。 在第一个程序中,goroutine通过同步的共享内存进行通信,第二个程序出于相同目的使用通道。 可从我的网站以包含自述文件的.zip文件形式获得该代码。

线程和竞争条件

线程是一系列可执行指令,并且同一进程中的线程共享一个地址空间:多线程进程中的每个线程都具有对相同内存位置的读/写访问权限。 如果两个或多个线程(其中至少一个线程执行操作)对同一内存位置进行不协调的访问,则会发生基于内存的竞争情况

考虑一下对整数变量n描述,其值为777,并且有两个线程试图更改其内容:


   
   
        n = n + 10   + ---- -+  n = n - 10
Thread1 ------------ > | 777 | < ------------ Thread2
                    + ---- -+
                       n

在多处理器机器上,这两个线程可以同时执行。 这样,对变量n的影响就不确定了。 请务必注意,每次尝试进行的更新都包括两个机器级操作:对n的当前值进行算术运算(加或减10),以及随后的将n设置为新值的赋值操作(787或767) )。

在两个线程中执行的配对操作可能以各种不适当的方式交错。 请考虑以下情形,其中每个编号的项目在计算机级别都是单个操作。 为简单起见,假设每个操作占用系统时钟的一个刻度:

  1. 线程1对计算787进行加法运算,该运算被保存在临时位置(在堆栈中或在CPU寄存器中)。
  2. Thread2进行减法运算以计算767,该运算也保存在一个临时位置。
  3. Thread2执行分配; n的值现在是767。
  4. 线程1执行分配; n的值现在是787。

通过排在最后,Thread1赢得了与Thread2的比赛。 很明显,发生了不正确的交织。 Thread1执行加法运算,延迟两个滴答,然后执行分配。 相比之下,Thread2在不中断的情况下执行减法和后续赋值操作。 解决方法很明确:算术和赋值操作应该像单个原子操作一样进行。 互斥锁之类的构造提供了所需的修复,而Go具有互斥锁。

Go程序通常是多线程的,尽管线程发生在表面之下。 表面上是goroutines。 goroutine是绿色线程-Go运行时控件下的线程。 相比之下, 本机线程直接受OS控制。 但是goroutines可以多路复用到OS调度的本机线程上,这意味着Go中可能有基于内存的竞争条件。 两个示例程序中的第一个说明了这一点。

MiserSpendthrift1

MiserSpendthrift1程序模拟对银行帐户的共享访问。 除了main ,还有两个其他goroutine:

  • 守财奴够程在一个时间反复增加了平衡,一个货币单位。
  • 节俭的 goroutine反复从余额中减去,一次也减去一个货币单位。

每个goroutine执行其操作的次数取决于命令行参数,该参数应足够大以至于令人感兴趣(例如,100,000到几百万)。 帐户余额被初始化为零,并且应该结为零,因为存款和取款的金额相同且数量相同。

示例1.使用互斥锁协调对共享内存的访问


   
   
package main

import (
    "os"
    "fmt"
    "runtime"
    "strconv"
    "sync"
)

var accountBalance = 0     // balance for shared bank account
var mutex = & sync. Mutex {} // mutual-exclusion lock

// critical-section code with explicit locking/unlocking
func updateBalance ( amt int ) {
   mutex . Lock ()
   accountBalance += amt   // two operations: update and assignment
   mutex . Unlock ()
}

func reportAndExit ( msg string ) {
   fmt . Println ( msg )
   os . Exit ( - 1 ) // all 1s in binary
}

func main () {
    if len ( os . Args ) < 2 {
      reportAndExit ( " \n Usage: go ms1.go <number of updates per thread>" )
    }
   iterations , err := strconv . Atoi ( os . Args [ 1 ])
    if err != nil {
      reportAndExit ( "Bad command-line argument: " + os . Args [ 1 ]);
    }

    var wg sync . WaitGroup   // wait group to ensure goroutine coordination

    // miser increments the balance
   wg . Add ( 1 )           // increment WaitGroup counter
    go func () {
      defer wg . Done ()   // invoke Done on the WaitGroup when finished
      for i := 0 ; i < iterations ; i ++ {
         updateBalance ( 1 )
         runtime . Gosched ()   // yield to another goroutine
      }
    }()

    // spendthrift decrements the balance
   wg . Add ( 1 )           // increment WaitGroup counter
    go func () {
      defer wg . Done ()
      for i := 0 ; i < iterations ; i ++ {
         updateBalance ( - 1 )
         runtime . Gosched ()   // be nice--yield
      }
    }()

   wg . Wait ()   // await completion of miser and spendthrift
   fmt . Println ( "Final balance: " , accountBalance )   // confirm final balance is zero
}

MiserSpendthrift1程序(参见上文)中的控制流可以描述如下:

  • 该程序首先尝试读取并验证一个命令行参数,该参数指定了失败者和储蓄者每次更新帐户余额的次数(例如,一百万次)。
  • main goroutine通过调用启动另外两个:
     go func () { // either the miser or the spendthrift  
    这两个启动的goroutine中的第一个代表了失败者 ,第二个代表了节俭
  • 该程序使用sync.WaitGroup来确保main goroutine直到sync.WaitGroup者和节俭的goroutine完成工作并终止后才打印最终余额。

MiserSpendthrift1程序声明了两个全局变量,一个全局变量代表共享的银行帐户,另一个则是互斥体以确保对goroutine的协调访问。


   
   
var accountBalance = 0     // balance for shared bank account
var mutex = & sync. Mutex {} // mutual-exclusion lock

互斥代码出现在updateBalance函数中以保护关键部分 ,该部分是必须以单线程方式执行的代码段,程序才能正常运行:


   
   
func updateBalance ( amt int ) {
   mutex . Lock ()
   accountBalance += amt   // critical section
   mutex . Unlock ()
}

关键部分是Lock()Unlock()调用之间的语句。 尽管在Go源代码中只有一行,但是此语句涉及两个不同的操作:算术运算后跟赋值。 这两个操作必须一起执行,一次只能执行一个线程,互斥体代码可以确保此操作。 使用锁定代码后, accountBalance在末尾为零,因为加1减1的次数相同。

如果删除了互斥代码,则accountBalance的最终值是不可预测的。 在两次删除了锁码的示例运行中,最终余额在第一次运行中为249,在第二次运行中为-87,从而确认发生了基于内存的竞争情况。

互斥代码的行为值得仔细研究:

  • 要执行关键部分代码,goroutine必须首先通过执行mutex.Lock()调用来获取锁。 如果已持有该锁,则goroutine会阻塞,直到该锁可用为止;否则,它将继续运行。 否则,goroutine执行互斥保护的关键部分。
  • 互斥锁保证相互排斥 ,因为一次只能有一个goroutine可以执行锁定的代码段。 互斥锁确保关键部分的单线程执行:算术运算后跟赋值运算。
  • 调用Unlock()会释放一个持有的锁,以便某些goroutine(也许刚释放该锁的goroutine)可以重新获取该锁。

在MiserSpendthrift1程序中,三个goroutines(守财奴,spreadthrift和main )通过名为accountBalance的共享内存位置进行accountBalance 。 互斥锁协调错误者和挥霍者对这个变量的访问,并且main仅在错误者和挥霍者都终止之后才尝试访问该变量。 即使使用相对较大的命令行参数(例如,五到一千万),程序accountBalance运行相对较快,并为accountBalance产生预期的最终值为零。

sync/atomic具有诸如AddInt32功能,其中AddInt32了同步。例如,如果accountBalance类型从int更改为int32 ,则updateBalance函数可以简化如下:


   
   
func updateBalance ( amt int32 ) {           // argument must be int32 as well
   atomic . AddInt32 ( &accountBalance , amt ) // no explicit locking required
}

MiserSpendthrift1程序使用显式锁定突出显示关键部分代码,并强调需要进行线程同步以防止出现竞争状况。 在生产级示例中,关键部分可能包含几行源代码。 在任何情况下,关键部分都应尽可能短,以使程序尽可能保持并行。

MiserSpendthrift2

MiserSpendthrift2程序再次将全局变量accountBalance初始化为零,并且还存在争吵者和节俭的goroutines accountBalance更新余额。 但是,此程序不使用互斥量来防止出现竞争情况。 取而代之的是,现在有一个银行程序 ,可以响应守财奴和accountBalance请求访问accountBalance 。 这两个goroutine不再直接更新accountBalance 。 这是体系结构的草图:


   
   
                  requests         updates
miser / spendthrift ---------- > banker -------- -> balance

这种体系结构在线程安全的Go通道的支持下可以序列化来自accountBalance请求,从而可以防止accountBalance出现竞争情况。

例子2.使用线程安全通道来协调对共享内存的访问


   
   
package main

import (
    "os"
    "fmt"
    "runtime"
    "strconv"
    "sync"
)

type bankOp struct { // bank operation: deposit or withdraw
   howMuch int       // amount
   confirm chan int   // confirmation channel
}

var accountBalance = 0           // shared account
var bankRequests chan * bankOp   // channel to banker

func updateBalance ( amt int ) int {
   update := &bankOp { howMuch : amt , confirm : make ( chan int )}
   bankRequests < - update
   newBalance := < - update . confirm
    return newBalance
}

// For now a no-op, but could save balance to a file with a timestamp.
func logBalance ( current int ) { }

func reportAndExit ( msg string ) {
   fmt . Println ( msg )
   os . Exit ( - 1 ) // all 1s in binary
}

func main () {
    if len ( os . Args ) < 2 {
      reportAndExit ( " \n Usage: go ms1.go <number of updates per thread>" )
    }
   iterations , err := strconv . Atoi ( os . Args [ 1 ])
    if err != nil {
      reportAndExit ( "Bad command-line argument: " + os . Args [ 1 ]);
    }

   bankRequests = make ( chan * bankOp , 8 ) // 8 is channel buffer size

    var wg sync . WaitGroup
    // The banker: handles all requests for deposits and withdrawals through a channel.
    go func () {
      for {
          /* The select construct is non-blocking:
            -- if there's something to read from a channel, do so
            -- otherwise, fall through to the next case, if any */

          select {
          case request := < - bankRequests :
            accountBalance += request . howMuch   // update account
            request . confirm <- accountBalance   // confirm with current balance
          }
      }
    }()

    // miser increments the balance
   wg . Add ( 1 )           // increment WaitGroup counter
    go func () {
      defer wg . Done ()   // invoke Done on the WaitGroup when finished
      for i := 0 ; i < iterations ; i ++ {
         newBalance := updateBalance ( 1 )
         logBalance ( newBalance )
         runtime . Gosched ()   // yield to another goroutine
      }
    }()

    // spendthrift decrements the balance
   wg . Add ( 1 )           // increment WaitGroup counter
    go func () {
      defer wg . Done ()
      for i := 0 ; i < iterations ; i ++ {
         newBalance := updateBalance ( - 1 )
         logBalance ( newBalance )
         runtime . Gosched ()   // be nice--yield
      }
    }()

   wg . Wait ()   // await completion of miser and spendthrift
   fmt . Println ( "Final balance: " , accountBalance ) // confirm the balance is zero
}

MiserSpendthrift2程序中的更改可以总结如下。 有一个BankOp结构:


   
   
type bankOp struct { // bank operation: deposit or withdraw
   howMuch int       // amount
   confirm chan int   // confirmation channel
}

苦难者和节俭的goroutine用来发出更新请求。 howMuch字段是更新量,为1( howMuch )或-1(节约)。 confirm字段是银行行长程序用于响应错误或节俭请求的渠道; 该渠道会将新余额返还给请求者作为确认。 为了提高效率, bankOp结构的地址而不是其副本通过bankRequests通道发送,该通道声明如下:

 var bankRequests chan * bankOp // channel of pointers to a bankOp 

默认情况下,通道是同步的(即线程安全的)。

updateBalanceupdateBalance再次调用updateBalance函数以更改帐户余额。 此函数不再具有任何显式线程同步:


   
   
func updateBalance ( amt int ) int {   // request structure
   update := &bankOp { howMuch : amt ,
                     confirm : make ( chan int )}
   bankRequests < - update           // send request
   newBalance := <- update . confirm   // await confirmation
    return newBalance                 // perhaps to be logged
}

bankRequests通道的缓冲区大小为八,以最大程度地减少阻塞。 在阻止进一步尝试添加另一个bankOp指针之前,该通道最多可容纳八个未读请求。 同时,银行程序应在请求到达时对其进行处理。 银行家读取请求后,该请求会自动从渠道中删除。 但是, confirm通道未缓冲。 请求者将阻止直到确认消息(本地存储在newBalanace变量中的更新余额)从银行家到达。

局部变量和参数在updateBalance功能( updatenewBalance ,和amt )由此线程安全的,因为每一个够程让他们自身的副本。 通道也是线程安全的,因此updateBalance函数的主体不再需要显式锁定。 程序员真是放心!

银行家goroutine无限循环,等待来自守财奴和挥霍无度的goroutine的请求:


   
   
for {
    select {
    case request := < - bankRequests :       // Is there a request?
      accountBalance += request . howMuch // If so, update balance and
      request . confirm <- accountBalance // confirm to requester
    }
    // other cases could be added (e.g., golf outings)
}

当守财奴和节俭的goroutine仍处于活动状态时,只有银行家goroutine可以访问accountBalance ,这意味着不会在此存储位置上出现争用条件。 只有在accountBalanceaccountBalance完成工作并终止后, main goroutine才会打印accountBalance的最终值并退出。 当main终止时,银行家goroutine也终止。

锁或通道?

MiserSpendthrift2程序遵循Go的口头禅,在同步共享内存上偏爱通道。 可以肯定的是,锁定的内存可能很棘手。 互斥锁API是低级的,因此容易发生诸如锁定但忘记解锁之类的错误-可能导致死锁。 更细微的错误包括仅锁定关键部分的一部分(Unlocking)和锁定不属于关键部分的代码(Overlock)。 线程安全的函数(例如atomic.AddInt32减少这些风险,因为锁定和解锁会自动发生。 然而,挑战仍然在于如何推理复杂程序中的低级内存锁定。

Go口头禅带来了自己的挑战。 如果两个恶意程序/节俭程序使用足够大的命令行参数运行,则性能上的对比是值得注意的。 互斥锁可能是低级的,但性能良好。 Go频道之所以吸引人,是因为它们提供了内置的线程安全性,并鼓励单线程访问共享的关键资源,例如两个示例程序中的accountBalance 。 但是,与互斥锁相比,通道会导致性能下降。

在编程中很少有一种工具可以满足所有任务。 Go相应地提供了线程安全性选项,从低级锁定到高级通道。

翻译自: https://opensource.com/article/18/7/locks-versus-channels-concurrent-go

go 通道 并发 顺序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值