• Go语言 | 并发设计中的同步锁与waitgroup用法


    今天是golang专题的第16篇文章,我们一起来聊聊golang当中的并发相关的一些使用。

    虽然关于goroutine以及channel我们都已经介绍完了,但是关于并发的机制仍然没有介绍结束。只有goroutine以及channel有时候还是不足以完成我们的问题,比如多个goroutine同时访问一个变量的时候,我们怎么保证这些goroutine之间不会互相冲突或者是影响呢?这可能就需要我们对资源进行加锁或者是采取其他的操作了。

    同步锁

    golang当中提供了两种常用的锁,一种是sync.Mutex另外一种是sync.RWMutex。我们先说说Mutex,它就是最简单最基础的同步锁,当一个goroutine持有锁的时候,其他的goroutine只能等待到锁释放之后才可以尝试持有。而RWMutex是读写锁的意思,它支持一写多读,也就是说允许支持多个goroutine同时持有读锁,而只允许一个goroutine持有写锁。当有goroutine持有读锁的时候,会阻止写操作。当有goroutine持有写锁的时候,无论读写都会被堵塞。

    我们使用的时候需要根据我们场景的特性来决定,如果我们的场景是读操作多过写操作的场景,那么我们可以使用RWMutex。如果是写操作为主,那么使用哪个都差不多。

    我们来看下使用的案例,假设我们当前有多个goroutine,但是我们只希望持有锁的goroutine执行,我们可以这么写:

    var lock sync.Mutex

    for i := 0; i < 10; i++ {
        go func() {
            lock.Lock()
            defer lock.Unlock()
            // do something
        }()
    }

    虽然我们用for循环启动了10个goroutine,但是由于互斥锁的存在,同一时刻只能有一个goroutine在执行

    RWMutex区分了读写锁,所以我们一共会有4个api,分别是Lock, Unlock, RLock, RUnlock。Lock和Unlock是写锁的加锁以及解锁,而RLock和RUnlock自然就是读锁的加锁和解锁了。具体的用法和上面的代码一样,我就不多赘述了。

    全局操作一次

    在一些场景以及一些设计模式当中,会要求我们某一段代码只能执行一次。比如很著名的单例模式,就是将我们经常使用的工具设计成单例,无论运行的过程当中初始化多少次,得到的都是同一个实例。这样做的目的是减去创建实例的时间,尤其是像是数据库连接、hbase连接等这些实例创建的过程非常的耗时。

    那我们怎么在golang当中实现单例呢?

    有些同学可能会觉得这个很简单啊,我们只需要用一个bool型变量判断一下初始化是否有完成不就可以了吗?比如这样:

    type Test struct {}
    var test Test
    var flag = false

    func init() Test{
        if !flag {
            test = Test{}
            flag = true
        }
        return test
    }

    看起来好像没有问题,但是仔细琢磨就会发现不对的地方。因为if判断当中的语句并不是原子的,也就是说有可能同时被很多goroutine同时访问。这样的话有可能test这个变量会被多次初始化并且被多次覆盖,直到其中一个goroutine将flag置为true为止。这可能会导致一开始访问的goroutine获得的test都各不相同,而产生未知的风险。

    要想要实现单例其实很简单,sync库当中为我们提供了现成的工具once。它可以传入一个函数,只允许全局执行这个函数一次。在执行结束之前,其他goroutine执行到once语句的时候会被阻塞,保证只有一个goroutine在执行once。当once执行结束之后,再次执行到这里的时候,once语句的内容将会被跳过,我们来结合一下代码来理解一下,其实也非常简单。

    type Test struct {}
    var test Test

    func create() {
        test = Test{}
    }

    func init() Test{
        once.Do(create)
        return test
    }

    waitgroup

    最后给大家介绍一下waitgroup的用法,我们在使用goroutine的时候有一个问题是我们在主程序当中并不知道goroutine执行结束的时间。如果我们只是要依赖goroutine执行的结果,当然可以通过channel来实现。但假如我们明确地希望等到goroutine执行结束之后再执行下面的逻辑,这个时候我们又该怎么办呢?

    有人说可以用sleep,但问题是我们并不知道goroutine执行到底需要多少时间,怎么能事先知道需要sleep多久呢?

    为了解决这个问题,我们可以使用sync当中的另外一个工具,也就是waitgroup。

    waitgroup的用法非常简单,只有三个方法,一个是Add,一个是Done,最后一个是Wait。其实waitgroup内部存储了当前有多少个goroutine在执行,当调用一次Add x的时候,表示当下同时产生了x个新的goroutine。当这些goroutine执行完的时候, 我们让它调用一下Done,表示执行结束了一个goroutine。这样当所有goroutine都执行完Done之后,wait的阻塞会结束。

    我们来看一个例子:

    sample := Sample{}

    wg := sync.WaitGroup{}

    go func() {
        // 增加一个正在执行的goroutine
        wg.Add(1)
        // 执行完成之后Done一下
        defer wg.Done()
        sample.JoinUserFeature() 
    }()

    go func() {
        wg.Add(1)
        defer wg.Done()
        sample.JoinItemFeature() 
    }()

    wg.Wait()
    // do something

    总结

    上面介绍的这些工具和库都是我们日常在并发场景当中经常使用的,也是一个golang工程师必会的技能之一。到这里为止,关于golang这门语言的基本功能介绍就差不多了,后面将会介绍一些实际应用的内容,敬请期待吧。

    今天的文章到这里就结束了,如果喜欢本文的话,请来一波素质三连,给我一点支持吧(关注、转发、点赞)。

    - END -

    原文链接,求个关注

  • 相关阅读:
    C库函数笔记
    曼彻斯特及差分曼彻斯特编码
    VS2008 由于应用程序配置不正确,应用程序未能启动。重新安装应用程序可能会纠正这个问题。
    刚子扯谈:酒装狗熊胆
    poj1410
    新游戏《真·方块无双》发布-穿越混世过险境,运筹方块化无双
    hdu4267 A Simple Problem with Integers
    【转载】变量的存储类型
    【转载】硬盘存储原理和内部结构
    【转载】让windows使用linux系统的命令
  • 原文地址:https://www.cnblogs.com/techflow/p/13664951.html
Copyright © 2020-2023  润新知