首页 > 其他分享 >GO语言sync包与锁:限制线程对变量的访问

GO语言sync包与锁:限制线程对变量的访问

时间:2022-10-16 14:11:06浏览次数:77  
标签:包与锁 协程 sync 互斥 线程 func time 锁定

原文:http://c.biancheng.net/view/4307.html
Go语言中 sync 包里提供了互斥锁 Mutex 和读写锁 RWMutex 用于处理并发过程中可能出现同时两个或多个协程(或线程)读或写同一个变量的情况。

为什么需要锁

锁是 sync 包中的核心,它主要有两个方法,分别是加锁(Lock)和解锁(Unlock)。
在并发的情况下,多个线程或协程同时其修改一个变量,使用锁能保证在某一时间内,只有一个协程或线程修改这一变量。
不使用锁时,在并发的情况下可能无法得到想要的结果,如下所示:

package main

import (
	"fmt"
	"time"
)

func main() {
	var a = 0
	for i := 0; i < 1000; i++ {
		go func(idx int) {
			a += 1
			fmt.Println(a)
		}(i)
	}
	time.Sleep(time.Second)
}

从理论上来说,上面的程序会将 a 的值依次递增输出,然而实际结果却是下面这样子的。

通过运行结果可以看出 a 的值并不是按顺序递增输出的,这是为什么呢?

协程的执行顺序大致如下所示:

  • 从寄存器读取 a 的值;
  • 然后做加法运算;
  • 最后写到寄存器。
    按照上面的顺序,假如有一个协程取得 a 的值为 3,然后执行加法运算,此时又有一个协程对 a 进行取值,得到的值同样是 3,最终两个协程的返回结果是相同的。

而锁的概念就是,当一个协程正在处理 a 时将 a 锁定,其它协程需要等待该协程处理完成并将 a 解锁后才能再进行操作,也就是说同时处理 a 的协程只能有一个,从而避免上面示例中的情况出现。

互斥锁 Mutex

上面的示例中出现的问题怎么解决呢?加一个互斥锁 Mutex 就可以了。那什么是互斥锁呢 ?互斥锁中其有两个方法可以调用,如下所示:

func(m *Mutex) Lock()
func(m *Mutex) Unlock()

将上面的代码略作修改,如下所示:

package main

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

func main() {
	var lock sync.Mutex

	var a = 0
	for i := 0; i < 1000; i++ {
		go func(idx int) {
			lock.Lock()
			defer lock.Unlock()
			a += 1
			fmt.Printf("goroutine %d,a=%d\n", idx, a)
		}(i)
	}
	// 等待 1s 结束主程序
	// 确保所有协程执行完
	time.Sleep(time.Second)
}

执行结果

需要注意的是一个互斥锁只能同时被一个 goroutine 锁定,其它 goroutine 将阻塞直到互斥锁被解锁(重新争抢对互斥锁的锁定),示例代码如下:

package main

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

func main() {
	ch := make(chan struct{}, 2)
	var l sync.Mutex
	go func() {
		l.Lock()
		defer l.Unlock()
		fmt.Println("goroutine1: 我会锁定大概 2s")
		time.Sleep(time.Second * 2)
		fmt.Println("goroutine1: 我解锁了,你们去抢吧")
		ch <- struct{}{}
	}()
	go func() {
		fmt.Println("goroutine2: 等待解锁")
		l.Lock()
		defer l.Unlock()
		fmt.Println("goroutine2: 欧耶,我也解锁了")
		ch <- struct{}{}
	}()
	// 等待 goroutine 执行结束
	for i := 0; i < 2; i++ {
		<-ch
	}
}

读写锁

读写锁有如下四个方法:
写操作的锁定和解锁分别是func (RWMutex) Lock和func (RWMutex) Unlock;
读操作的锁定和解锁分别是func (RWMutex) Rlock和func (RWMutex) RUnlock。

读写锁的区别在于:
当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;
当有一个 goroutine 获得读锁定,其它读锁定仍然可以继续;
当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定。

所以说这里的读锁定(RLock)目的其实是告诉写锁定,有很多协程或者进程正在读取数据,写操作需要等它们读(读解锁)完才能进行写(写锁定)。

我们可以将其总结为如下三条:
同时只能有一个 goroutine 能够获得写锁定;
同时可以有任意多个 gorouinte 获得读锁定;
同时只能存在写锁定或读锁定(读和写互斥)。
示例代码如下所示:

package main

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

var count int
var rw sync.RWMutex

func main() {
	ch := make(chan struct{}, 10)
	for i := 0; i < 5; i++ {
		go read(i, ch)
	}
	for i := 0; i < 5; i++ {
		go write(i, ch)
	}
	for i := 0; i < 10; i++ {
		<-ch
	}
}

func read(n int, ch chan struct{}) {
	rw.RLock()
	fmt.Printf("goroutine %d进入读操作...\n", n)
	v := count
	fmt.Printf("goroutine %d 读取结束,值为:%d\n", n, v)
	rw.RUnlock()
	ch <- struct{}{}
}

func write(n int, ch chan struct{}) {
	rw.Lock()
	fmt.Printf("goroutine %d 进入x写操作...\n", n)
	v := rand.Intn(1000)
	count = v
	fmt.Printf("goroutine %d 写入结束,新值为:%d\n", n, v)
	rw.Unlock()
	ch <- struct{}{}
}


下面再来看两个示例。
【示例 1】多个读操作同时读取一个变量时,虽然加了锁,但是读操作是不受影响的。(读和写是互斥的,读和读不互斥)

package main

import (
	"sync"
	"time"
)

var m *sync.RWMutex

func main() {
	m = new(sync.RWMutex)
	// 多个同时读
	go read(1)
	go read(2)
	time.Sleep(2 * time.Second)
}
func read(i int) {
	println(i, "read start")
	m.RLock()
	println(i, "reading")
	time.Sleep(1 * time.Second)
	m.RUnlock()
	println(i, "read over")
}

【示例 2】由于读写互斥,所以写操作开始的时候,读操作必须要等写操作进行完才能继续,不然读操作只能继续等待。

package main
import (
    "sync"
    "time"
)
var m *sync.RWMutex
func main() {
    m = new(sync.RWMutex)
    // 写的时候啥也不能干
    go write(1)
    go read(2)
    go write(3)
    time.Sleep(2*time.Second)
}
func read(i int) {
    println(i,"read start")
    m.RLock()
    println(i,"reading")
    time.Sleep(1*time.Second)
    m.RUnlock()
    println(i,"read over")
}
func write(i int) {
    println(i,"write start")
    m.Lock()
    println(i,"writing")
    time.Sleep(1*time.Second)
    m.Unlock()
    println(i,"write over")
}

标签:包与锁,协程,sync,互斥,线程,func,time,锁定
From: https://www.cnblogs.com/jianjiana/p/16796129.html

相关文章

  • 多线程并行与分布式执行
    假如项目中有测试用例1000条,一条测试用例需要执行1分钟,一个测试人员需要1000分钟才能完成一个轮回测试。通常会用人力成本换取时间成本,加几个人的一起执行,时间就会缩短。如......
  • 线程,线程池和锁
    线程相关的面试题线程线程和进程有什么区别和联系?联系:进程是线程的【上级】和【容器】,一个进程中可以有一个或多个线程(至少一个)。线程概念是进程概念的轻量化,很多线......
  • 面试题:JAVA多线程交替打印ABC
    JAVA实现,3个线程交替A,B,C,一共完成10次“ABC”打印,结束后打印“END”。打印示例:abcabcabcabcabcabcabcabcabcabcEND 分析:打印10次ABC,3个线程分别打印A,B......
  • go sync.Once
        packagemainimport"fmt"import"sync"import"time"varocsync.Oncevaraint=5;funcmain(){gofunc(){oc.Do(func(){......
  • 2022.10.15线程状态观测
    线程状态观测线程优先级packagecom.fei.stata;​publicclassTestPriorityextendsThread{  publicstaticvoidmain(String[]args){    System.out......
  • 高并发和多线程
    高并发是一种状态。如果大量的访问网络中的某个接口,就会出现大量的操作,如数据库操作,资源的请求,硬件的占用等。这就需要对接口进行优化,对sql语句进行优化,而多线程是处理高并......
  • 线程创建的方式
    说说线程的创建方式继承thread类,重写run()方法;实现Runnable接口,实现run()方法;实现callable接口,实现call方法;​ 使用Runnable接口和Callable接口的方式,区别在于前......
  • 【WPF】Tabcontrol的IsSynchronizedWithCurrentItem属性
    如果两个控件都绑定到同一个源(ObservableCollection)集合视图时,该对象会自动绑定到该视图的CurrentItem。请注意,CollectionViewSource对象会自动同步货币与所选内容。如......
  • 如何在 JavaScript中将axios与async/await一起使用?
    Axios是一个基于promise的处理异步HTTP请求的客户端。本指南将演示如何通过async/await处理这些请求。安装和使用要使用Axios,您需要使用npmnpminstallaxios......
  • Keeping Multiple Databases in Sync Using Kafka Connect and CDC
    BRIJESHJAGGI SEP20,2022Microservicesarchitectureshavenowbeenwidelyadoptedamongdevelopers,andwithagreatdegreeofsuccess.However,......