Golang并发:除了channel,你还有其余选择

咱们都知道Golang并发优选channel,但channel不是万能的,Golang为咱们提供了另外一种选择:sync。经过这篇文章,你会了解sync包最基础、最经常使用的方法,至于sync和channel之争留给下一篇文章。git

sync包提供了基础的异步操做方法,好比互斥锁(Mutex)、单次执行(Once)和等待组(WaitGroup),这些异步操做主要是为低级库提供,上层的异步/并发操做最好选用通道和通讯。github

sync包提供了:golang

  1. Mutex:互斥锁
  2. RWMutex:读写锁
  3. WaitGroup:等待组
  4. Once:单次执行
  5. Cond:信号量
  6. Pool:临时对象池
  7. Map:自带锁的map

这篇文章是sync包的入门文章,因此只介绍经常使用的结构和方法:MutexRWMutexWaitGroupOnce,而CondPoolMap留给你们自行探索,或有需求再介绍。bash

互斥锁

常作并发工做的朋友对互斥锁应该不陌生,Golang里互斥锁须要确保的是某段时间内,不能有多个协程同时访问一段代码(临界区)微信

互斥锁被称为Mutex,它有2个函数,Lock()Unlock()分别是获取锁和释放锁,以下:并发

type Mutex
func (m *Mutex) Lock(){}
func (m *Mutex) Unlock(){}

Mutex的初始值为未锁的状态,而且Mutex一般做为结构体的匿名成员存在less

通过了上面这么“官方”的介绍,举个例子:你在工商银行有100元存款,这张卡绑定了支付宝和微信,在中午12点你用支付宝支付外卖30元,你在微信发红包,抢到10块。银行须要按顺序执行上面两件事,先减30再加10或者先加10再减30,结果都是80,但若是同时执行,结果多是,只减了30或者只加了10,即你有70元或者你有110元。前一个结果是你赔了,后一个结果是银行赔了,银行可不但愿把这种事算错。异步

看看实际使用吧:建立一个银行,银行里存每一个帐户的钱,存储查询都加了锁操做,这样银行就不会算错帐了。
银行的定义:函数

type Bank struct {
    sync.Mutex
    saving map[string]int // 每帐户的存款金额
}

func NewBank() *Bank {
    b := &Bank{
        saving: make(map[string]int),
    }
    return b
}

银行的存取钱:测试

// Deposit 存款
func (b *Bank) Deposit(name string, amount int) {
    b.Lock()
    defer b.Unlock()

    if _, ok := b.saving[name]; !ok {
        b.saving[name] = 0
    }
    b.saving[name] += amount
}

// Withdraw 取款,返回实际取到的金额
func (b *Bank) Withdraw(name string, amount int) int {
    b.Lock()
    defer b.Unlock()

    if _, ok := b.saving[name]; !ok {
        return 0
    }
    if b.saving[name] < amount {
        amount = b.saving[name]
    }
    b.saving[name] -= amount

    return amount
}

// Query 查询余额
func (b *Bank) Query(name string) int {
    b.Lock()
    defer b.Unlock()

    if _, ok := b.saving[name]; !ok {
        return 0
    }

    return b.saving[name]
}

模拟操做:小米支付宝存了100,而且同时花了20。

func main() {
    b := NewBank()
    go b.Deposit("xiaoming", 100)
    go b.Withdraw("xiaoming", 20)
    go b.Deposit("xiaogang", 2000)

    time.Sleep(time.Second)
    fmt.Printf("xiaoming has: %d\n", b.Query("xiaoming"))
    fmt.Printf("xiaogang has: %d\n", b.Query("xiaogang"))
}

结果:先存后花。

➜  sync_pkg git:(master) ✗ go run mutex.go
xiaoming has: 80
xiaogang has: 2000

也多是:先花后存,由于先花20,由于小明没钱,因此没花出去。

➜  sync_pkg git:(master) ✗ go run mutex.go
xiaoming has: 100
xiaogang has: 2000

这个例子只是介绍了mutex的基本使用,若是你想多研究下mutex,那就去个人Github(阅读原文)下载下来代码,本身修改测试。Github中还提供了没有锁的例子,运行屡次总能碰到错误:

fatal error: concurrent map writes
这是因为并发访问map形成的。

读写锁

读写锁是互斥锁的特殊变种,若是是计算机基本知识扎实的朋友会知道,读写锁来自于读者和写者的问题,这个问题就不介绍了,介绍下咱们的重点:读写锁要达到的效果是同一时间能够容许多个协程读数据,但只能有且只有1个协程写数据

也就是说,读和写是互斥的,写和写也是互斥的,但读和读并不互斥。具体讲,当有至少1个协程读时,若是须要进行写,就必须等待全部已经在读的协程结束读操做,写操做的协程才得到锁进行写数据。当写数据的协程已经在进行时,有其余协程须要进行读或者写,就必须等待已经在写的协程结束写操做。

读写锁是RWMutex,它有5个函数,它须要为读操做和写操做分别提供锁操做,这样就4个了:

  • Lock()Unlock()是给写操做用的。
  • RLock()RUnlock()是给读操做用的。

RLocker()能获取读锁,而后传递给其余协程使用。使用较少

type RWMutex
func (rw *RWMutex) Lock(){}
func (rw *RWMutex) RLock(){}
func (rw *RWMutex) RLocker() Locker{}
func (rw *RWMutex) RUnlock(){}
func (rw *RWMutex) Unlock(){}

上面的银行实现不合理:你们都是拿手机APP查余额,能够同时几我的一块儿查呀,这根本不影响,银行的锁能够换成读写锁。存、取钱是写操做,查询金额是读操做,代码修改以下,其余不变:

type Bank struct {
    sync.RWMutex
    saving map[string]int // 每帐户的存款金额
}

// Query 查询余额
func (b *Bank) Query(name string) int {
    b.RLock()
    defer b.RUnlock()

    if _, ok := b.saving[name]; !ok {
        return 0
    }

    return b.saving[name]
}

func main() {
    b := NewBank()
    go b.Deposit("xiaoming", 100)
    go b.Withdraw("xiaoming", 20)
    go b.Deposit("xiaogang", 2000)

    time.Sleep(time.Second)
    print := func(name string) {
        fmt.Printf("%s has: %d\n", name, b.Query(name))
    }

    nameList := []string{"xiaoming", "xiaogang", "xiaohong", "xiaozhang"}
    for _, name := range nameList {
        go print(name)
    }

    time.Sleep(time.Second)
}

结果,可能不同,由于协程都是并发执行的,执行顺序不固定

➜  sync_pkg git:(master) ✗ go run rwmutex.go
xiaohong has: 0
xiaozhang has: 0
xiaogang has: 2000
xiaoming has: 100

等待组

互斥锁和读写锁大多数人可能比较熟悉,而对等待组(WaitGroup)可能就不那么熟悉,甚至有点陌生,因此先来介绍下等待组在现实中的例子。

大家团队有5我的,你做为队长要带领你们打开藏有宝藏的箱子,但这个箱子须要4把钥匙才能同时打开,你把寻找4把钥匙的任务,分配给4个队员,让他们分别去寻找,而你则守着宝箱,在这等待,等他们都找到回来后,一块儿插进钥匙打开宝箱。

这其中有个很重要的过程叫等待:等待一些工做完成后,再进行下一步的工做。若是使用Golang实现,就得使用等待组。

等待组是WaitGroup,它有3个函数:

  • Add():在被等待的协程启动前加1,表明要等待1个协程。
  • Done():被等待的协程执行Done,表明该协程已经完成任务,通知等待协程。
  • Wait(): 等待其余协程的协程,使用Wait进行等待。
type WaitGroup
func (wg *WaitGroup) Add(delta int){}
func (wg *WaitGroup) Done(){}
func (wg *WaitGroup) Wait(){}

来,一块儿看下怎么用WaitGroup实现上面的问题。

队长先建立一个WaitGroup对象wg,每一个队员都是1个协程, 队长让队员出发前,使用wg.Add(),队员出发寻找钥匙,队长使用wg.Wait()等待(阻塞)全部队员完成,某个队员完成时执行wg.Done(),等全部队员找到钥匙,wg.Wait()则返回,完成了等待的过程,接下来就是开箱。

结合以前的协程池的例子,修改为WG等待协程池协程退出,实例代码:

func leader() {
    var wg sync.WaitGroup
    wg.Add(4)
    for i := 0; i < 4; i++ {
        go follower(&wg, i)
    }
    wg.Wait()
    
    fmt.Println("open the box together")
}

func follower(wg *sync.WaitGroup, id int) {
    fmt.Printf("follwer %d find key\n", id)
    wg.Done()
}

结果:

➜  sync_pkg git:(master) ✗ go run waitgroup.go
follwer 3 find key
follwer 1 find key
follwer 0 find key
follwer 2 find key
open the box together

WaitGroup也经常使用在协程池的处理上,协程池等待全部协程退出,把上篇文章《Golang并发模型:轻松入门协程池》的例子改下:

func workerPool(n int, jobCh <-chan int, retCh chan<- string) {
    var wg sync.WaitGroup
    wg.Add(n)
    for i := 0; i < n; i++ {
        go worker(&wg, i, jobCh, retCh)
    }

    wg.Wait()
    close(retCh)
}

func worker(wg *sync.WaitGroup, id int, jobCh <-chan int, retCh chan<- string) {
    cnt := 0
    for job := range jobCh {
        cnt++
        ret := fmt.Sprintf("worker %d processed job: %d, it's the %dth processed by me.", id, job, cnt)
        retCh <- ret
    }

    wg.Done()
}

单次执行

在程序执行前,一般须要作一些初始化操做,但触发初始化操做的地方是有多处的,可是这个初始化又只能执行1次,怎么办呢?

使用Once就能轻松解决,once对象是用来存放1个无入参无返回值的函数,once能够确保这个函数只被执行1次

type Once
func (o *Once) Do(f func()){}

直接把官方代码给你们搬过来看下,once在10个协程中调用,但once中的函数onceBody()只执行了1次:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var once sync.Once
    onceBody := func() {
        fmt.Println("Only once")
    }
    done := make(chan bool)
    for i := 0; i < 10; i++ {
        go func() {
            once.Do(onceBody)
            done <- true
        }()
    }
    for i := 0; i < 10; i++ {
        <-done
    }
}

结果:

➜  sync_pkg git:(master) ✗ go run once.go
Only once

示例源码

本文全部示例源码,及历史文章、代码都存储在Github:https://github.com/Shitaibin/golang_step_by_step/tree/master/sync_pkg

下期预告

此次先介绍入门的知识,下次再介绍一些深刻思考、最佳实践,不能一口吃个胖子,我们慢慢来,顺序渐进。

下一篇我以这些主题进行介绍,欢迎关注:

  1. 哪一个协程先获取锁
  2. 必定要用锁吗
  3. 锁与通道的选择

文章推荐

  1. Golang并发模型:轻松入门流水线模型
  2. Golang并发模型:轻松入门流水线FAN模式
  3. Golang并发模型:并发协程的优雅退出
  4. Golang并发模型:轻松入门select
  5. Golang并发模型:select进阶
  6. Golang并发模型:轻松入门协程池
  7. Golang并发的次优选择:sync包
  1. 若是这篇文章对你有帮助,请点个赞/喜欢,感谢
  2. 本文做者:大彬
  3. 若是喜欢本文,随意转载,但请保留此原文连接:http://lessisbetter.site/2019/01/04/golang-pkg-sync/

一块儿学Golang-分享有料的Go语言技术

相关文章
相关标签/搜索