Go语言标准库学习之sync一(go语言中的锁)

在go语言多线程编程的过程中,我们会遇到多线程进行资源读写的问题,在GO语言中我们可以使用channel进行控制,但是除了channel我们还可以通过sync库进行资源的读写控制,这也就是我们常说的锁。锁的作用就是某个协程(线程)在访问某个资源时先锁住,防止其它协程的访问,等访问完毕解锁后其他协程再来加锁进行访问。本文向记录了学习sync标准库的学习笔记,希望对你有帮助。

一、互斥锁

1.什么是互斥锁

这里摘录百度百科的解释 :

互斥锁是用来保证共享数据操作的完整性的。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

2.sync.Mutex

在sync库中Mutex对象实现了两个方法,Lock和UnLock,从字面意思就可以理解,一个是锁另一个是释放锁。

// A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex.
//
// A Mutex must not be copied after first use.
type Mutex struct {
	state int32
	sema  uint32
}

在源码定义中我们可以看出,Mutex在使用后不能被复制,因此这里我们要注意。

3.互斥锁的使用

接下来我们看一下如何使用Mutex进行资源锁定和释放。

package main

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

// 定义一个锁
var m = new(sync.Mutex)

func StdOut(s string) {
	// 创建一个互斥锁
	//m := new(sync.Mutex)
	m.Lock()
	// 当main函数执行完成后,释放锁
	defer m.Unlock()
	for _, data := range s {
		fmt.Printf("%c", data)
	}
	fmt.Println()
}

func Person1(s string) {
	StdOut(s)
}

func main() {
	go Person1("Random_w1")
	go Person1("Random_w2")
	Person1("Random_w3")
	// 等待两秒,让goroutine运行完成
	time.Sleep(time.Millisecond * 100)
}

Output:

$ go run main.go
Random_w3
Random_w1
Random_w2

如果将StdOut中的Lock删除掉,那么输出就会混乱:

$ go run main.go
RandomRandom_w1
Random_w2
_w3

通过比对两种情况大家应该理解了互斥锁的使用了。

二、读写锁

1.什么是读写锁

同样这里我引用百度百科的解释:

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。

互斥锁的本质是当一个goroutine访问的时候,其他goroutine都不能访问。这样在资源同步,避免竞争的同时也降低了程序的并发性能。程序由原来的并行执行变成了串行执行。

2. sync.RWMutex

type RWMutex struct {
	w           Mutex  // held if there are pending writers
	writerSem   uint32 // semaphore for writers to wait for completing readers
	readerSem   uint32 // semaphore for readers to wait for completing writers
	readerCount int32  // number of pending readers
	readerWait  int32  // number of departing readers
}

sync.RWMutex 结构体实现了五种方法:

  • func (rw *RWMutex) Lock() 写锁定
  • func (rw *RWMutex) UnLock() 写解锁
  • func (rw *RWMutex) RLock() 读锁定
  • func (rw *RWMutex) RUnLock() 读解锁
  • func (rw *RWMutex) RLocker() Locker

RWMutex的使用主要事项

  • 读锁的时候无需等待读锁的结束
  • 读锁的时候要等待写锁的结束
  • 写锁的时候要等待读锁的结束
  • 写锁的时候要等待写锁的结束

3. 读写锁的使用

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

// 定义一个锁
var m = new(sync.RWMutex)
var count int

// Write 对count进行写操作
func Write(n int) {
	rand.Seed(time.Now().UnixNano())
	fmt.Printf("写 goroutine %d 正在写数据...\n", n)
	m.Lock()
	num := rand.Intn(500)
	count = num
	fmt.Printf("写 goroutine %d 写数据结束,写入新值 %d\n", n, num)
	m.Unlock()
}
// Read 对count进行读操作
func Read(n int) {
	m.RLock()
	fmt.Printf("读 goroutine %d 正在读取数据...\n", n)
	num := count
	fmt.Printf("读 goroutine %d 读取数据结束,读到 %d\n", n, num)
	m.RUnlock()
}

func main() {
	// 创建goroutine,进行读写操作
	for i := 0; i < 3; i++ {
		go Read(i)
		go Write(i)
	}
	time.Sleep(time.Second)
}

Output:

$ go run main.go
读 goroutine 1 正在读取数据...
读 goroutine 1 读取数据结束,读到 0
读 goroutine 0 正在读取数据...
读 goroutine 0 读取数据结束,读到 0
写 goroutine 2 正在写数据...
写 goroutine 0 正在写数据...
写 goroutine 1 正在写数据...
读 goroutine 2 正在读取数据...
读 goroutine 2 读取数据结束,读到 0
写 goroutine 2 写数据结束,写入新值 337
写 goroutine 0 写数据结束,写入新值 16
写 goroutine 1 写数据结束,写入新值 134

从Output中我们可以看到,当读锁被锁定时,写锁时阻塞状态,只有当读锁解除后,count才能写入新值。

三、Cond的使用

Cond是一个比较冷门的结构体,sync.Cond用于goroutine之间的协作,用于协程的挂起和唤醒。

1. Cond结构体

从下面的结构体我么可以看出Cond在被创建后是不能复制的,和互斥锁类似。

// A Cond must not be copied after first use.
type Cond struct {
	noCopy noCopy  // noCopy可以嵌入到结构中,在第一次使用后不可复制,使用go vet作为检测使用
 
	L Locker // 根据需求初始化不同的锁,如*Mutex 和 *RWMutex
 
	notify  notifyList  // 通知列表,调用Wait()方法的goroutine会被放入list中,每次唤醒,从这里取出
	checker copyChecker // 复制检查,检查cond实例是否被复制
}

2. Cond结构体实现的方法

Cond结构体实现了四个方法,分别是:

  • func (c *Cond) Wait() 必须获取该锁之后才能调用Wait()方法,Wait方法在调用时会释放底层锁Locker,并且将当前goroutine挂起,直到另一个goroutine执行Signal或者Broadcase,该goroutine才有机会重新唤醒,并尝试获取Locker,完成后续逻辑。也就是在等待被唤醒的过程中是不占用锁Locker的,这样就可以有多个goroutine可以同时处于Wait(等待被唤醒的状态)
  • func (c *Cond) Signal() 唤醒等待队列中的一个goroutine,一般都是任意唤醒队列中的一个goroutine。
  • func (c *Cond) Broadcast()唤醒等待队列中的所有goroutine。
  • func NewCond(l Locker) *Cond ,使用Locker创建一个Cond对象。

3. Cond的使用

下面的示例代码中我们使用cond.Wait让goroutine进入等待状态,在main函数中,我们分别测试了使用Siginal和Broadcast将goroutine唤醒,为了表示Siginal一次只能唤醒一个goroutine因此加入了时间,正常情况下实例中的goroutine在不到一微秒的时间就可以执行完成,但是我们延时了一秒,除了被唤醒的goroutine运行外,其他goroutine并没有执行。使用Broadcast我们可以看到剩下的两个goroutine快速执行完成。

package main

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

// 定义一个锁
var mutex = new(sync.Mutex)
// 初始化一个cond
var cond = sync.NewCond(mutex)

func CondTest() {
	// 5个goroutine正常情况下一微秒时间都可以运行完
	for i := 0; i < 5; i++ {
		id := i
		go func() {
			mutex.Lock()
			defer mutex.Unlock()
			// 让所有goroutine等待
			cond.Wait()
			fmt.Printf("goroutine %d 运行完成\n", id)
		}()
	}
}
// 输出时间
func PrintTime() {
	fmt.Println(time.Now().Format("15:04:05"))
}
func main() {
	CondTest()
	// 运行三个goroutine
	for i := 0; i < 3; i++ {
		PrintTime()
		cond.Signal()
		time.Sleep(time.Second)
	}
	// 通过Broadcast唤醒所有的goroutine
	PrintTime()
	cond.Broadcast()
	time.Sleep(time.Millisecond)
}

Output:

$ go run main.go
14:40:29
goroutine 1 运行完成
14:40:30
goroutine 0 运行完成
14:40:31
goroutine 2 运行完成
14:40:32
goroutine 4 运行完成
goroutine 3 运行完成

四、更优雅的等待goroutine结束(sync.WaitGroup)

平时我们在测试或者创建goroutine后,往往通过延时的方式等待goroutine退出,这种方式是比较耗费时间的,在sync中我们可以使用WaitGroup的方法更优雅的等待goroutine结束。

1. WaitGroup的使用

package main

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

func main() {
    // 新建一个WaitGroup对象
	wg := sync.WaitGroup{}
	// WaitGroup的数量为3
	wg.Add(3)
	for i := 0; i < 3; i++ {
		id := i
		go func() {
			fmt.Printf("goroutine %d 运行完成\n", id)
			// goroutine运行完成,通知wg
			wg.Done()
		}()
	}
	//wg程序阻塞,等待所有goroutine运行完成
	wg.Wait()
}

Output:

$ go run main.go
goroutine 2 运行完成
goroutine 0 运行完成
goroutine 1 运行完成

2. WaitGroup的注意事项

  1. 我们不能使用Add() 给wg 设置一个负值,否则代码将会报错。
$ go run main.go
panic: sync: negative WaitGroup counter

goroutine 1 [running]:
sync.(*WaitGroup).Add(0xc000070070, 0xffffffffffffffff)
        c:/Go/src/sync/waitgroup.go:74 +0x13c
main.main()
        D:/GOCODE/Test/main.go:12 +0x54
exit status 2
  1. WaitGroup对象不是一个引用类型,在通过函数传值的时候需要使用地址。
package main

import (
   "fmt"
   "math/rand"
   "sync"
   "time"
)

func main() {
   wg := sync.WaitGroup{}
   wg.Add(3)
   for i := 0; i < 3; i++ {
   	id := i
   	go func(wg *sync.WaitGroup) {
   		fmt.Printf("goroutine %d 运行完成\n", id)
   		wg.Done()
   	}(&wg)
   }
   wg.Wait()
}

Output:

$ go run main.go
goroutine 2 运行完成
goroutine 0 运行完成
goroutine 1 运行完成

上面的main函数如果改成这样:

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

func main() {
	wg := sync.WaitGroup{}
	wg.Add(3)
	for i := 0; i < 3; i++ {
		id := i
		go func(wg sync.WaitGroup) {
			fmt.Printf("goroutine %d 运行完成\n", id)
			wg.Done()
		}(wg)
	}
	wg.Wait()
}

Output:

$ go run main.go
goroutine 2 运行完成
goroutine 0 运行完成
goroutine 1 运行完成
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc000070078)
        c:/Go/src/runtime/sema.go:56 +0x40
sync.(*WaitGroup).Wait(0xc000070070)
        c:/Go/src/sync/waitgroup.go:130 +0x6c
main.main()
        D:/GOCODE/Test/main.go:20 +0xbc
exit status 2

可以看到,不使用地址传递参数会在goroutine运行完成之后触发panic报错。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章