Go语言之并发资源竞争

并发本身并不复杂,但是因为有了资源竞争的问题,就使得我们开发出好的并发程序变得复杂起来,因为会引起很多莫名其妙的问题。

package main

import (
    "fmt"
    "runtime"
    "sync"
)

var (
    count int32
    wg    sync.WaitGroup
)

func main() {
    wg.Add(2)
    go incCount()
    go incCount()
    wg.Wait()
    fmt.Println(count)
}

func incCount() {
    defer wg.Done()
    for i := 0; i < 2; i++ {
        value := count
        runtime.Gosched()
        value++
        count = value
    }
}

这是一个资源竞争的例子。我们可以多运行几次这个程序,会发现结果可能是 2 ,也可以是 3 ,也可能是 4 。因为共享资源count变量没有任何同步保护,所以两个goroutine都会对其进行读写,会导致对已经计算好的结果覆盖,以至于产生错误结果。这里我们演示一种可能,两个goroutine我们暂时称之为g1和g2。

g1读取到count为 0 。

然后g1暂停了,切换到g2运行,g2读取到count也为 0 。

g2暂停,切换到g1,g1对count+1,count变为 1 。

g1暂停,切换到g2,g2刚刚已经获取到值 0 ,对其+1,最后赋值给count还是 1 。

有没有注意到,刚刚g1对count+1的结果被g2给覆盖了,两个goroutine都+1还是 1 。

不再继续演示下去了,到这里结果已经错了,两个goroutine相互覆盖结果。我们这里的runtime.Gosched()是让当前goroutine暂停的意思。退回执行队列,让其他等待的goroutine运行,目的是让我们演示资源竞争的结果更明显。注意,这里还会牵涉到CPU问题,多核会并行,那么资源竞争的效果更明显。

所以我们对于同一个资源的读写必须是原子化的,也就是说,同一时间只能有一个goroutine对共享资源进行读写操作。

共享资源竞争的问题,非常复杂,并且难以察觉,好在Go提供了一个工具来帮助我们检查,这个就是go build -race命令。我们在当前项目目录下执行这个命令,生成一个可以执行文件,然后再运行这个可执行文件,就可以看到打印出的检测信息。

go build -race

多加了一个-race标志,这样生成的可执行程序就自带了检测资源竞争的功能。下面我们运行,也是在终端运行。

./hello

我这里示例生成的可执行文件名是hello,所以是这么运行的。这时候,我们看终端输出的检测结果。

  hello ./hello       
==================
WARNING: DATA RACE
Read at 0x0000011a5118 by goroutine 7:
  main.incCount()
      /Users/xxx/code/go/src/flysnow.org/hello/main.go:25 +0x76

Previous write at 0x0000011a5118 by goroutine 6:
  main.incCount()
      /Users/xxx/code/go/src/flysnow.org/hello/main.go:28 +0x9a

Goroutine 7 (running) created at:
  main.main()
      /Users/xxx/code/go/src/flysnow.org/hello/main.go:17 +0x77

Goroutine 6 (finished) created at:
  main.main()
      /Users/xxx/code/go/src/flysnow.org/hello/main.go:16 +0x5f
==================
4
Found 1 data race(s)

看,找到一个资源竞争,连在那一行代码出了问题,都标示出来了。goroutine 7在代码 25 行读取共享资源value := count,而这时goroutine 6正在代码 28 行修改共享资源count = value,而这两个goroutine都是从main函数启动的,在 16、17 行,通过go关键字。

既然我们已经知道共享资源竞争的问题,是因为同时有两个或者多个goroutine对其进行了读写,那么我们只要保证,同时只有一个goroutine读写不就可以了。现在我们就看下传统解决资源竞争的办法——对资源加锁。

Go语言提供了atomic包和sync包里的一些函数对共享资源同步枷锁,我们先看下atomic包。

package main

import (
    "fmt"
    "runtime"
    "sync"
    "sync/atomic"
)

var (
    count int32
    wg    sync.WaitGroup
)

func main() {
    wg.Add(2)
    go incCount()
    go incCount()
    wg.Wait()
    fmt.Println(count)
}

func incCount() {
    defer wg.Done()
    for i := 0; i < 2; i++ {
        value := atomic.LoadInt32(&count)
        runtime.Gosched()
        value++
        atomic.StoreInt32(&count,value)
    }
}

留意这里atomic.LoadInt32atomic.StoreInt32两个函数:一个读取int32类型变量的值,一个是修改int32类型变量的值。这两个都是原子性的操作,Go已经帮助我们在底层使用加锁机制,保证了共享资源的同步和安全,所以我们可以得到正确的结果。这时候我们再使用资源竞争检测工具go build -race检查,也不会提示有问题了。

atomic包里还有很多原子化的函数可以保证并发下资源同步访问修改的问题。比如函数atomic.AddInt32可以直接对一个int32类型的变量进行修改,在原值的基础上再增加多少的功能,也是原子性的。这里不再举例,大家自己可以试试。

atomic虽然可以解决资源竞争问题,但是比较都是比较简单的,支持的数据类型也有限,所以Go语言还提供了一个sync包。这个sync包里提供了一种互斥型的锁,可以让我们自己灵活地控制那些代码,同时只能有一个goroutine访问,被sync互斥锁控制的这段代码范围,被称之为临界区。临界区的代码,同一时间,只能又一个goroutine访问。刚刚那个例子,我们还可以这么改造。

package main

import (
    "fmt"
    "runtime"
    "sync"
)

var (
    count int32
    wg    sync.WaitGroup
    mutex sync.Mutex
)

func main() {
    wg.Add(2)
    go incCount()
    go incCount()
    wg.Wait()
    fmt.Println(count)
}

func incCount() {
    defer wg.Done()
    for i := 0; i < 2; i++ {
        mutex.Lock()
        value := count
        runtime.Gosched()
        value++
        count = value
        mutex.Unlock()
    }
}

实例中,新声明了一个互斥锁mutex sync.Mutex。这个互斥锁有两个方法,一个是mutex.Lock(),一个是mutex.Unlock()。这两个之间的区域就是临界区,临界区的代码是安全的。

示例中我们先调用mutex.Lock()对有竞争资源的代码加锁,这样当一个goroutine进入这个区域的时候,其他goroutine就进不来了,只能等待,一直到调用mutex.Unlock() 释放这个锁为止。

这种方式比较灵活,可以让代码编写者任意定义需要保护的代码范围,也就是临界区。除了原子函数和互斥锁,Go还为我们提供了更容易在多个goroutine同步的功能,这就是通道chan,我们会在下次继续讲解。

时间: 2024-10-12 21:43:23

Go语言之并发资源竞争的相关文章

Go语言之并发

Go语言直接支持内置支持并发.当一个函数创建为goroutine时,Go会将其视为一个独立的工作单元.这个单元会被调度到可用的逻辑处理器上执行. Go语言运行时的调度器是一个复杂的软件,这个调度器在操作系统之上.操作系统的线程与语言运行时的逻辑处理器绑定,并在逻辑处理器上运行goroutine. Go语言的并发同步逻辑来自一个叫做通信顺讯进程(CSP)的范型.CSP是一种消息传递模型,通过在goroutine之间传递数据来传递消息,而不是通过对数据进行加锁来实现同步访问.这种数据的类型叫做通道(

java并发编程学习:用 Semaphore (信号量)控制并发资源

并发编程这方面以前关注得比较少,恶补一下,推荐一个好的网站:并发编程网 - ifeve.com,上面全是各种大牛原创或编译的并发编程文章. 今天先来学习Semaphore(信号量),字面上看,根本不知道这东西是干啥的,借用 并发工具类(三)控制并发线程数的Semaphore一文中的交通红绿信号灯的例子来理解一下: 一条4车道的主干道,假设100米长,每辆车假设占用的长度为10米(考虑到前后车距),也就是说这条道上满负载运行的话,最多只能容纳4*(100/10)=40辆车,如果有120辆车要通过的

java线程共享受限资源 解决资源竞争 thinking in java4 21.3

java线程共享受限资源 解决资源竞争  详细介绍请参阅:thinking in java4 21.3 thinking in java 4免费下载:http://download.csdn.net/detail/liangrui1988/7580155 package org.rui.thread.res; /** * 不正确的访问 资源 * @author lenovo * */ public abstract class IntGenerator { private volatile bo

C++11线程指南(七)--资源竞争条件

1. 接口设计1 下面例子使用vector实现了一个栈.两个线程轮流从中弹出元素. #include <iostream> #include <thread> #include <mutex> #include <string> #include <vector> std::mutex myMutex; class Stack { public: Stack() {}; ~Stack() {}; void pop(); int top() { r

多线程下解决资源竞争的7种方法

前言 一般情况下,只要涉及到多线程编程,程序的复杂性就会显著上升,性能显著下降,BUG出现的概率大大提升. 多线程编程本意是将一段程序并行运行,提升数据处理能力,但是由于大部分情况下都涉及到共有资源的竞争,所以修改资源 对象时必须加锁处理.但是锁的实现有很多种方法,下面就来一起了解一下在C#语言中几种锁的实现与其性能表现. 一.c#下的几种锁的运用方式 1.临界区,通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问. 1 private static object obj =

Android - 系统语言(language)定制资源值(values)

系统语言(language)定制资源值(values) 本文地址: http://blog.csdn.net/caroline_wendy 语言代码参考: http://www.loc.gov/standards/iso639-2/php/code_list.php 创建资源值文件夹(values), 添加语言标记, 常用英文和繁体中文: values-en: 英文 values-zh-rTW: 繁体中文

苹果Swift语言中文教程资源汇总

苹果swift语言中文教程(零)搭配环境以及代码执行成功http://vjiazhi.com/kaifa/1014.html 苹果Swift语言中文教程(一)基础数据类型 http://vjiazhi.com/kaifa/1027.html 苹果Swift语言中文教程(二)基本运算符 http://vjiazhi.com/kaifa/1033.html 苹果Swift语言中文教程(三) 字符串和字符 http://vjiazhi.com/kaifa/1035.html 苹果Swift语言中文教程

好记性不如烂笔头75-多线程-并行访问下的资源竞争和样例

在实际业务常见中,很多的时候我们都需要访问一些共同的资源,比如一个序列号,比如某一个文件.如果多个线程一起访问这个序列或者文件,而我们没有做足够多的处理,就很容易造成脏数据或者数据丢失等各种问题. 这种场景特别常见,写一个简单的例子.以免自己的团队在实际开发中,犯这种小错误. 当然,这种错误知道了,要预先处理还是相当简单:但是如果要真的出现了错误,在一大堆代码中找这个坑,那是相当的要命. 样例的场景 我们的业务需要获取一个序列号.下面是一个取得序列号的单例模式的例子,但调用get()时,可能会产

go语音之进阶篇多任务资源竞争问题

1.多任务资源竞争问题 示例: package main import ( "fmt" "time" ) //定义一个打印机,参数为字符串,按每个字符打印 //打印机属于公共资源 func Printer(str string) { for _, data := range str { fmt.Printf("%c", data) time.Sleep(time.Second) } fmt.Printf("\n") } fun