array(2) { ["docs"]=> array(10) { [0]=> array(10) { ["id"]=> string(3) "428" ["text"]=> string(77) "Visual Studio 2017 单独启动MSDN帮助(Microsoft Help Viewer)的方法" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(8) "DonetRen" ["tagsname"]=> string(55) "Visual Studio 2017|MSDN帮助|C#程序|.NET|Help Viewer" ["tagsid"]=> string(23) "[401,402,403,"300",404]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511400964" ["_id"]=> string(3) "428" } [1]=> array(10) { ["id"]=> string(3) "427" ["text"]=> string(42) "npm -v;报错 cannot find module "wrapp"" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(4) "zzty" ["tagsname"]=> string(50) "node.js|npm|cannot find module "wrapp“|node" ["tagsid"]=> string(19) "[398,"239",399,400]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511400760" ["_id"]=> string(3) "427" } [2]=> array(10) { ["id"]=> string(3) "426" ["text"]=> string(54) "说说css中pt、px、em、rem都扮演了什么角色" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(12) "zhengqiaoyin" ["tagsname"]=> string(0) "" ["tagsid"]=> string(2) "[]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511400640" ["_id"]=> string(3) "426" } [3]=> array(10) { ["id"]=> string(3) "425" ["text"]=> string(83) "深入学习JS执行--创建执行上下文(变量对象,作用域链,this)" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(7) "Ry-yuan" ["tagsname"]=> string(33) "Javascript|Javascript执行过程" ["tagsid"]=> string(13) "["169","191"]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511399901" ["_id"]=> string(3) "425" } [4]=> array(10) { ["id"]=> string(3) "424" ["text"]=> string(30) "C# 排序技术研究与对比" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(9) "vveiliang" ["tagsname"]=> string(0) "" ["tagsid"]=> string(2) "[]" ["catesname"]=> string(8) ".Net Dev" ["catesid"]=> string(5) "[199]" ["createtime"]=> string(10) "1511399150" ["_id"]=> string(3) "424" } [5]=> array(10) { ["id"]=> string(3) "423" ["text"]=> string(72) "【算法】小白的算法笔记:快速排序算法的编码和优化" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(9) "penghuwan" ["tagsname"]=> string(6) "算法" ["tagsid"]=> string(7) "["344"]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511398109" ["_id"]=> string(3) "423" } [6]=> array(10) { ["id"]=> string(3) "422" ["text"]=> string(64) "JavaScript数据可视化编程学习(二)Flotr2,雷达图" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(7) "chengxs" ["tagsname"]=> string(28) "数据可视化|前端学习" ["tagsid"]=> string(9) "[396,397]" ["catesname"]=> string(18) "前端基本知识" ["catesid"]=> string(5) "[198]" ["createtime"]=> string(10) "1511397800" ["_id"]=> string(3) "422" } [7]=> array(10) { ["id"]=> string(3) "421" ["text"]=> string(36) "C#表达式目录树(Expression)" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(4) "wwym" ["tagsname"]=> string(0) "" ["tagsid"]=> string(2) "[]" ["catesname"]=> string(4) ".NET" ["catesid"]=> string(7) "["119"]" ["createtime"]=> string(10) "1511397474" ["_id"]=> string(3) "421" } [8]=> array(10) { ["id"]=> string(3) "420" ["text"]=> string(47) "数据结构 队列_队列实例:事件处理" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(7) "idreamo" ["tagsname"]=> string(40) "C语言|数据结构|队列|事件处理" ["tagsid"]=> string(23) "["246","247","248",395]" ["catesname"]=> string(12) "数据结构" ["catesid"]=> string(7) "["133"]" ["createtime"]=> string(10) "1511397279" ["_id"]=> string(3) "420" } [9]=> array(10) { ["id"]=> string(3) "419" ["text"]=> string(47) "久等了,博客园官方Android客户端发布" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(3) "cmt" ["tagsname"]=> string(0) "" ["tagsid"]=> string(2) "[]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511396549" ["_id"]=> string(3) "419" } } ["count"]=> int(200) } 222 Part25 Mutex - 爱码网

Golang 系列教程 第 25 部分 - Mutex


在该教程,我们学习互斥锁,也学习如何使用 channels 和 互斥锁解决竞态条件。

临界区

在学习互斥锁之前,理解并发程序的临界区的概念是很重要的。当程序并发地运行,修改共享资源的部分代码不应该被多个 协程(Goroutines)同时访问。修改共享资源的这部分代码就被叫做临界区。例如,我们假设有一段将变量 x 增加 1 的代码。

x = x +1

只要上面的一段代码被单个协程访问,不应该有任何问题。

我们来看看为什么这段代码在多个协程并发地运行时会失败。为了简单,我们假设有 2 个协程并发地运行上面的一行代码。

上面的一行代码内部将被系统以下面的三步执行(有更多的技术细节如寄存器,如如何添加工作等等,本教程为了简单,假设有三步):

  1. 获取 x 的当前值。
  2. 计算 x+1
  3. 将第2步计算的值赋值给 2

当这三个步骤仅被一个协程执行,一切正常。

我们讨论当2个协程并发地运行这段代码将会发生什么。下面的这幅图片描绘了一个当两个协程并发地访问 x=x+1 将会发行什么的情况。
Part25 Mutex
我们已经假设 x 的初始值是 0 。Goroutine 1获取了 x 的初始值,计算 x + 1 ,在它将计算结果赋值给 x 之前,系统上下文切换到 Goroutine 2。现在 Goroutine 2获取 x 的初始值,它仍然是 0,计算 x + 1,在这之后,系统上下文再次切换到 Goroutine 1。现在 Goroutine 1 将它的计算值 1赋值给 x,因此 x 的值成为 1。然后 Goroutine 2 再将开始执行,然后将它的计算值赋值,它又是 1,给 x。因此 x 在两个协程执行后是 1

现在我们来看一个可能发生不同情况
Part25 Mutex
在上面的情况,Goroutine 1开始执行,完成它的所有三个步骤,因此 x 的值为 1。然后 Goroutine 2开始执行,现在 x 的值是 1,当 Goroutine 2完成执行,x 的值是 2

所以从两个例子中你可以看出,x 的最终值是 12 取决于上下文切换的发生。这个程序的输出取决于协程执行的顺序的不良情况被称为竞态条件

在上面的场景中,如果在任何时间点,只允许一个协程访问临界区代码,可以避免竞态条件。这可以通过使用互斥锁实现。

互斥锁(Mutex)

Mutex用于提供锁定机制,以确保在任何时间点只有一个Goroutine运行代码的临界区,以防止发生竞争条件。

Mutex 可以从 sync包中获得。Mutex定义了两个名为 LockUnlock的方法。任何出现在调用 LockUnlock 之间的代码仅被一个协程执行,这样避免了竞态条件。

mutex.Lock()
x = x + 1
mutex.Unlock()

在上面代码中,x = x + 1 在任何时间点仅被一个协程执行,这样避免了竞态条件。

如果一个协程已经拥有了锁,如果一个新的务程试图请求一个锁,则新的协程将被阻塞直到互斥锁被释放。

含有竞态条件的程序

在这个部分,我们将写一个拥有竞态条件的程序,在接下来的程序中我们将修复这个竞态条件。

package main  
import (  
    "fmt"
    "sync"
    )
var x  = 0  
func increment(wg *sync.WaitGroup) {  
    x = x + 1
    wg.Done()
}
func main() {  
    var w sync.WaitGroup
    for i := 0; i < 1000; i++ {
        w.Add(1)        
        go increment(&w)
    }
    w.Wait()
    fmt.Println("final value of x", x)
}

在上面的程序中,第 7 行的 increment 函数将 x 的值增加 1,然后在WaitGroup上调用 Done() 通知它的完成。

在第 15 行我们生成了 1000 个 increment 协程,这个协程中的每个并发地运行,在第 8 行试图增加 x 时,多个协程并发地尝试访问 x 发生竞态条件。

请在你本地运行这个程序,由于playground 是确定的,在 playground 不会发生竞态条件。在你本地机器上多次运行这个程序,你会看到,由于竞态条件,每次运行的输出是不同的。

使用互斥锁解决竞态条件

在上面程序中,我们发出了 1000 个协程,如果每个将 x 值增加 1,最后 x 的期望值应该是 1000。在这个部分,我们在程序中使用互斥锁解决竞态条件。

package main  
import (  
    "fmt"
    "sync"
    )
var x  = 0  
func increment(wg *sync.WaitGroup, m *sync.Mutex) {  
    m.Lock()
    x = x + 1
    m.Unlock()
    wg.Done()   
}
func main() {  
    var w sync.WaitGroup
    var m sync.Mutex
    for i := 0; i < 1000; i++ {
        w.Add(1)        
        go increment(&w, &m)
    }
    w.Wait()
    fmt.Println("final value of x", x)
}

Mutex是一个结构体类型,我们在第 15 行创建一个空的类型为 Mutex 的变量 m。在上面的程序中我们修改了 increment 函数,这样增加 x 的代码 x = x + 1m.Lock()m.Unlock() 之间,现在在任何时间,史允许一个协程执行这段代码 ,避免了竞态条件。

如果该程序运行,将输出

final value of x 1000

在第 18 行传递 mutex 的地址是非常重要的。如果 mutex 通过值传递代替地址传递,第个协程将拥有 mutex 的一个副本,竞态条件依然会出现。

使用通道解决竞态条件

我们也可以使用通道来解决竞态条件

package main  
import (  
    "fmt"
    "sync"
    )
var x  = 0  
func increment(wg *sync.WaitGroup, ch chan bool) {  
    ch <- true
    x = x + 1
    <- ch
    wg.Done()   
}
func main() {  
    var w sync.WaitGroup
    ch := make(chan bool, 1)
    for i := 0; i < 1000; i++ {
        w.Add(1)        
        go increment(&w, ch)
    }
    w.Wait()
    fmt.Println("final value of x", x)
}

在上面的程序,我们创建一个容量为1带缓冲的通道,在第 18 行将它传递给 increment 协程。这个带缓冲的通道被用来确保仅有一个协程访问临界区代码 - 增加 x 。这在第 8 行在增加 x 之前通过向带缓冲的通道传递 true 完成。由于带缓冲的通道容量为 1,其他所有试图写该通道的协程被阻塞,直到第 10 行增加 x 之后该值被从该通道上读走。这有效地允许一个通道访问临界区。

这个程序也打印

final value of x 1000

Mutex vs Channels

我们使用 Mutex 和 通道都解决了竞态条件。那么我们如何决定何时用哪个。答案在于你所要解决的问题。如果你尝试解决的问题对于互斥更容易解决,那就使用互斥锁吧。如果需要,毫不犹豫地使用互斥锁。如果问题看上去使用通道更好地解决,那就使用它。

很多Go菜鸟尝试使用通道解决所有的并发问题,通道确实是语言的一个非常酷的特性。这是错的,语言给我们提供了使用互斥锁或通道的选项供选择,这并没有错。

一般地当协程需要互相通道使用通道,互斥仅当一个协程应该临界区代码。

我们解决问题的示例中,我更期望使用互斥锁,因为这个问题并不需要与任何协程通信。因此互斥是一个更自然的选择。

我的建议是为问题选择工具,而不是为工具尝试解决问题。

**下一教程 - 结构体代替类 **

相关文章: