Golang 基础之并发基本同步原语(一)

大家好,今天将梳理出的 Go语言并发知识内容,分享给大家。 请多多指教,谢谢。

本章节内容

  • WaitGroup
  • Mutex
  • Locker
  • RWMutex

Go在内存访问同步基元的基础上构建了一组新的并发基元,并为使用者提供扩展的内容。 Go sync标准库,主要包含对低级别内存访问同步最有用的并发原语。 如果你使用的是主要通过内存访问同步处理并发的语言,那么这些类型是不错的选择。

WaitGroup

WaitGroup 类型原型

type WaitGroup struct {
	// contains filtered or unexported fields
}

func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Wait()

可以把 WaitGroup 视作一个安全的并发计数器:调用 Add() 增加计数,调用 Done() 减少计数。调用 Wait() 会阻塞并等待至计数器归零。

请注意,Add() 的调用是在 goroutine 之外完成的。 如果没有这样做,我们会引入一个数据竞争条件,因为我们没有对 goroutine 做任何调度顺序上的保证; 我们可能在任何一个 goroutine 开始前触发 Wait() 调用。 如果 Add() 的调用被放置在 goroutine 的闭包中,对 Wait() 的调用可能完全没有阻塞地返回,因为 Add() 没有被执行。

WaitGroup 使用

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func main() {
	hello := func(wg *sync.WaitGroup, id int) {
		defer wg.Done()
		fmt.Printf("id: %d\n", id)
	}
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go hello(&wg, i+1)
	}
	wg.Wait()
}

通常情况下,尽可能与要跟踪的 goroutine 就近且成对的调用 Add(),但有时候会一次性调用 Add() 来跟踪一组 goroutine。

Mutex

之前文章中已经简单介绍了 Mutex 类型,可以参考 Golang 基础之并发知识 (三) 文章。

Mutex 很容易理解,代表 “mutual exclusion(互斥)”。互斥提供了一种并发安全的方式来表示对共享资源访问的独占。

可以理解为在代码块设置临界区,在同一时刻只能由一个 goroutine 去操作。

Mutex 类型原型

type Mutex struct {
	// contains filtered or unexported fields
}
func (m *Mutex) Lock()
func (m *Mutex) TryLock() bool
func (m *Mutex) Unlock()

Lock()方法: 锁定

TryLock()方法: 尝试锁定并报告 (很少使用)

Unlock()方法: 解锁

Mutex 使用

举例:两个 goroutine,它们试图增加和减少一个公共值,并使用 Mutex 来同步访问。

// 并发修改一个公共值
package main

import (
	"fmt"
	"sync"
)

var Count int
var Lock sync.Mutex
var wg sync.WaitGroup

func main() {
	// 增加
	for i := 0; i <= 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			increment()
		}()
	} 

	// 减少
	for i := 0; i <= 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			decrement()
		}()
	} 
	wg.Wait()
}

func increment() {
	Lock.Lock()
	defer Lock.Unlock()
	Count++
	fmt.Printf("Incrementing: %d\n", Count)
}

func decrement() {
	Lock.Lock()
	defer Lock.Unlock()
	Count--
	fmt.Printf("Decrementing: %d\n", Count)
}

这里,count变量由互斥锁保护

输出

Decrementing: -1
Incrementing: 0
Incrementing: 1
Incrementing: 2
Incrementing: 3
Incrementing: 4
Incrementing: 5
Decrementing: 4
Decrementing: 3
Decrementing: 2
Decrementing: 1
Decrementing: 0

这里因为goroutine调度机制原因,在大家各自设备编码后结果会发生变化。

注意,被锁定部分是程序的性能瓶颈,进入和退出锁定的成本有点高,因此通常尽量减少锁定涉及的范围。

Locker

Locker 接口原型

type Locker interface {
	Lock()
	Unlock()
}

Locker接口中定义了锁定和解锁的方法。

RWMutex

RWMutex 是读写互斥锁,锁可以由任意数量的读或单个写持有。RWMutex 的零值是一个未锁定的mutex。

RWMutexMutex 在概念上是一样的:它保护对内存的访问;不过,RWMutex可以给你更多地控制方式。 你可以请求锁定进行读取,在这种情况下,你将被授予读取权限,除非锁定正在进行写入操作。 这意味着,只要没有别的东西占用写操作,任意数量的读取者就可以进行读取操作。

常见的服务对资源的读写比列会非常高,如果大多数的请求都是读请求,它们之间不会互相影响,那么就可以将资源的操作进行读和写分离,出于这样的考虑,可以使用RWMutex。

读写锁控制下的多个写操作之间都是互斥的,并且写操作与读操作之间也都是互斥的。但多个读操作之间不存在互斥关系。读写锁可以在大大降低因使用锁而造成的性能损耗,完成对共享资源的访问控制。

RWMutex 类型原型

type RWMutex struct {
	// contains filtered or unexported fields
}

func (rw *RWMutex) Lock()
func (rw *RWMutex) RLock()
func (rw *RWMutex) RLocker() Locker
func (rw *RWMutex) RUnlock()
func (rw *RWMutex) TryLock() bool
func (rw *RWMutex) TryRLock() bool
func (rw *RWMutex) Unlock()

Lock()方法: 用于写入的锁定;如果锁已被锁定用于读取或写入,则锁定会一直锁定,直到锁可用。

RLock()方法: 用于读取的锁定;它不应用于递归读取锁定;被阻止的锁调用会阻止新读取获取锁。

RLocker()方法: RLocker返回一个Locker接口,该接口通过调用rw来实现Lock和Unlock方法。

RUnlock()方法: RUnlock撤销一个RLock调用;它不会影响其他同时读取的goroutine。如果rw在进入RUnlock时未被锁定读取,则为运行时错误。

TryLock()方法: TryLock试图锁定rw进行写入,并报告是否成功。 (很少使用)

TryRLock()方法: TryRLock尝试锁定rw进行读取,并报告是否成功。 (很少使用)

Unlock()方法: 解锁用于写入的rw。如果rw未被锁定以写入要解锁的条目,则这是一个运行时错误。

与 Mutex 一样,RWMutex的互斥体与特定的 goroutine 没有关联。一个 goroutine 可以重新锁定(锁定),然后安排另一个 goroutine 运行锁定(解锁)。

RWMutex 使用

举例:读写锁的使用

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var rwm sync.RWMutex
	for i := 0; i < 3; i++ {
		go func(i int) {
			fmt.Printf("执行读锁: %d\n", i)
			rwm.RLock()
			fmt.Printf("读锁: %d\n", i)
			time.Sleep(time.Second * 2)
			fmt.Printf("执行取消读锁: %d\n", i)
			rwm.RUnlock()
			fmt.Printf("取消读锁: %d\n", i)
		}(i)
	}
	time.Sleep(time.Millisecond * 100)
	fmt.Println("执行写锁...")
	rwm.Lock()
	fmt.Println("写锁")
}

输出

执行读锁: 0
读锁: 0
执行读锁: 1
读锁: 1
执行读锁: 2
读锁: 2
执行写锁...
执行取消读锁: 1
取消读锁: 1
执行取消读锁: 0
取消读锁: 0
执行取消读锁: 2
取消读锁: 2
写锁
  1. 启用了 3 个 goroutine 用于读写锁 rwm 的读锁定和读解锁操作
  2. 读解锁操作会延迟 2s 进行模拟真是的情况
  3. 先让主 goroutine 睡眠 100ms,让 3个 goroutine先有足够时间执行
  4. 之后 rwm 的写锁定操作让主 goroutine 阻塞,因为此时 3个 goroutine读锁定还未进行读解锁操作
  5. 当 3个 goroutine读解锁完成后,main函数写锁定才会完成

可以通过这个例子看到 RWMutex 在大量级上相对于 Mutex 是有性能优势。 建议在逻辑上合理的情况下使用 RWMutex 而不是 Mutex

package main

import (
	"os"
	"fmt"
	"sync"
	"time"
	"math"
	"text/tabwriter"
)

var wg sync.WaitGroup

func main() {
	tw := tabwriter.NewWriter(os.Stdout, 0, 1, 2, ' ', 0)
	defer tw.Flush()

	var m sync.RWMutex
	fmt.Fprintf(tw, "Readers\tRWMutex\tMutex\n")
	for i := 0; i < 20; i++ {
		count := int(math.Pow(2, float64(i)))
		fmt.Fprintf(
			tw, "%d\t%v\t%v\n", count,
			test(count, &m, m.RLocker()),
      test(count, &m, &m),
		)
	}
}

func test(count int, mutex, rwMutex sync.Locker) time.Duration {
	wg.Add(count + 1)
	beginTime := time.Now()
	go producer(&wg, mutex)
	for i := count; i > 0; i-- {
		go observer(&wg, rwMutex)
	}
	wg.Wait()
	return time.Since(beginTime)
}

func producer(wg *sync.WaitGroup, l sync.Locker) { // 1
	defer wg.Done()
	for i := 5; i > 0; i-- {
		l.Lock()
		l.Unlock()
		time.Sleep(1) // 2
	}
}

func observer(wg *sync.WaitGroup, l sync.Locker) {
	defer wg.Done()
	l.Lock()
	defer l.Unlock()
}

输出

Readers  RWMutex       Mutex
1        68.662µs      10.743µs
2        55.434µs      30.323µs
4        7.104µs       6.948µs
8        51.248µs      28.52µs
16       14.832µs      20.174µs
32       81.398µs      82.892µs
64       138.191µs     76.251µs
128      130.363µs     82.062µs
256      87.907µs      58.945µs
512      173.526µs     159.093µs
1024     273.202µs     253.273µs
2048     540.341µs     505.23µs
4096     1.190994ms    963.283µs
8192     3.125891ms    2.015721ms
16384    4.364017ms    4.279084ms
32768    15.637706ms   12.776055ms
65536    17.786311ms   16.694232ms
131072   35.540288ms   39.605993ms
262144   61.371264ms   64.062957ms
524288   119.099709ms  131.583856ms
  1. producer 函数的第二个参数是 sync.Locker 类型。 该接口有两种方法,锁定和解锁,MutexRWMutex 类型都适用。
  2. 让 producer 休眠1秒

技术文章持续更新,请大家多多关注呀~~

搜索微信公众号,关注我【 帽儿山的枪手 】

参考材料

  • 《Go并发编程实战》、《Concurrency in Go》
  • sync标准库
  • 16
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值