首页 > 其他分享 >Go 快速入门指南 - 互斥锁和定时器

Go 快速入门指南 - 互斥锁和定时器

时间:2022-12-22 16:56:40浏览次数:64  
标签:Map 定时器 并发 互斥 func time Go main

互斥锁

对于任一共享资源,同一时间保证只有一个操作者,这种方法称为 互斥机制

关键字 Mutex 表示互斥锁类型,它的 Lock 方法用于获取锁,Unlock 方法用于释放锁。在 Lock 和 Unlock 之间的代码,可以读取和修改共享资源,这部分区域称为 临界区

错误的并发操作

先来看一个错误的示例。

在 Map 小节中讲到, Map 不是并发安全的, 也就是说,如果在多个线程中,同时对一个 Map 进行读写,会报错。现在来验证一下, 通过启动 100 个 goroutine 来模拟并发调用,每个 goroutine 都对 Map 的 key 进行设置。

package main

import "sync"

func main() {
    m := make(map[int]bool)

    var wg sync.WaitGroup

    for j := 0; j < 100; j++ {
        wg.Add(1)

        go func(key int) {
            defer func() {
                wg.Done()
            }()

            m[key] = true // 对 Map 进行并发写入
        }(j)
    }

    wg.Wait()
}

// $ go run main.go
// 输出如下,报错信息
/**
  fatal error: concurrent map writes
  fatal error: concurrent map writes

  goroutine 104 [running]:
  main.main.func1(0x0?)
          /home/codes/Go-examples-for-beginners/main.go:18 +0x66
  created by main.main
          /home/codes/Go-examples-for-beginners/main.go:13 +0x45

  goroutine 1 [semacquire]:
  sync.runtime_Semacquire(0xc0000112c0?)
          /usr/local/go/src/runtime/sema.go:62 +0x25
  sync.(*WaitGroup).Wait(0x60?)
          /usr/local/go/src/sync/waitgroup.go:139 +0x52
  main.main()
          /home/codes/Go-examples-for-beginners/main.go:22 +0x105

  ...
  ...
  ...
*/

通过输出信息 fatal error: concurrent map writes 可以看到,并发写入 Map 确实会报错。

正确的并发操作

Map 并发写入如何正确地实现呢?

一种简单的方案是在并发临界区域 (也就是设置 Map key 的地方) 进行加互斥锁操作, 互斥锁保证了同一时刻 只有一个 goroutine 获得锁,其他 goroutine 全部处于等待状态,这样就把并发写入变成了串行写入, 从而消除了报错问题。

package main

import (
    "fmt"
    "sync"
)

func main() {
    var mu sync.Mutex
    m := make(map[int]bool)

    var wg sync.WaitGroup

    for j := 0; j < 100; j++ {
        wg.Add(1)

        go func(key int) {
            defer func() {
                wg.Done()
            }()

            mu.Lock()     // 写入前加锁
            m[key] = true // 对 Map 进行并发写入
            mu.Unlock()   // 写入完成解锁
        }(j)
    }

    wg.Wait()

    fmt.Printf("Map size = %d\n", len(m))
}

// $ go run main.go
// 输出如下
/**
  Map size = 100
*/

超时控制

利用 channel (通道) 和 time.After() 方法实现超时控制。

例子

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan bool)

    go func() {
        defer func() {
            ch <- true
        }()

        time.Sleep(2 * time.Second) // 模拟超时操作
    }()

    select {
    case <-ch:
        fmt.Println("ok")
    case <-time.After(time.Second):
        fmt.Println("timeout!")
    }
}

// $ go run main.go
// 输出如下
/**
  timeout!
*/

定时器

调用 time.NewTicker 方法即可。

例子

package main

import (
    "fmt"
    "time"
)

func main() {
    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()

    done := make(chan bool)
    go func() {
        time.Sleep(5 * time.Second) // 模拟耗时操作
        done <- true
    }()

    for {
        select {
        case <-done:
            fmt.Println("Done!")
            return
        case <-ticker.C:
            fmt.Println(time.Now().Format("2006-01-02 15:04:05"))
        }
    }
}

// $ go run main.go
// 输出如下,你的输出可能和这里的不一样
/**
  2021-01-03 15:40:21
  2021-01-03 15:40:22
  2021-01-03 15:40:23
  2021-01-03 15:40:24
  2021-01-03 15:40:25
  Done!
*/

扩展阅读

  1. 1. 互斥锁 - 维基百科 (https://zh.wikipedia.org/wiki/互斥锁)

  2. 2. 临界区 - 百度百科 (https://baike.baidu.com/item/临界区/8942134)

联系我

公众号

标签:Map,定时器,并发,互斥,func,time,Go,main
From: https://www.cnblogs.com/duanbiaowu/p/16998881.html

相关文章

  • Go 快速入门指南 - 遍历通道
    概述建议先阅读 range, 阻塞通道, 非阻塞通道 等小节。range 除了可以遍历字符串、切片、数组等数据结构外,还可以遍历通道。语法规则和遍历其他数据结构不同,遍历......
  • Go 快速入门指南 - 结构体
    概述结构体 是将零个或多个字段(变量)组合在一起的复合数据类型,类似于面向对象语言中的 对象。结构体以及其字段都使用 可见性 规则。语法规则type 结构体名称......
  • Go 快速入门指南 - 嵌套结构体
    概述Go支持将多个结构体通过嵌套的方式,组成一个大的结构体,降低了单个结构体复杂度,同时提高了结构体之间组合的灵活性。例子为了省略篇幅,本小节只使用 字面量 方式初......
  • Go 快速入门指南 - 函数
    概述函数 是将一个或者一类问题包装为一个代码块,可以被多次调用,提高代码重用性。Go函数中声明、定义、参数、返回值这些基础概念,和其他编程语言中的一致,这里不再赘述。......
  • Go 快速入门指南 - 接口和方法
    接口Go接口是隐式实现。 对于一个数据类型,无需声明它实现了哪些接口,只需要实现接口必需的方法即可。当然了,存在一个小问题就是:我们可能无意间实现了某个接口:),所以 ......
  • Go 快速入门指南 - init 函数
    概述init()函数 是一个特殊的函数,一般称为初始化函数,不能被调用。 在每个文件里面,当程序启动或者文件被作为包引用的时候,init()函数就会自动执行,一般用来做一些包的......
  • Go 快速入门指南 - 零值和错误
    零值当一个变量使用 var 进行声明后并未进行初始化(变量后面没有赋值符 =)操作,会默认分配一个零值(zerovalue)。不同类型对应的零值类型零值boolfalse......
  • Go 快速入门指南 - 判断是否实现接口
    判断是否实现接口Go里面没有 implements 关键字来判断一个结构体(对象)是否实现了某个接口, 要实现类似的功能,需要用到 类型断言。类型断言// 判断 v 是否实......
  • Go 快速入门指南 - 字符切片
    概述建议先阅读 字符串, 切片 两个小节。由于字符串不可变,如果每次以 重新赋值 的方式改变字符串,效率会非常低,这时应该使用 []byte 类型,[]byte元素可以被修改。......
  • Go 快速入门指南 - 切片
    概述阅读本小节之前,建议先阅读 数组 小节。切片 是对数组的一个连续片段的引用。片段可以是整个数组,也可以是数组的一部分(例如数组的第3个元素到第8个元素)。所......