Go并发编程-读写锁

一、前言

go语言类似Java JUC包也提供了一些列用于多线程之间进行同步的措施,比如低级的同步措施有 锁、CAS、原子变量操作类。本节我们先来看看go中读写锁,读写锁相比互斥锁来说并发度更高,在读多写少情况下比较实用。

二、读写锁

在go中可以使用sync.RWMutex获取一个读写锁,读写锁是读写互斥锁,读锁可以被任意多的读goroutine持有,但是写锁只能被某一个goroutine持有。当一个goroutine获取读锁后,其他goroutine也可以获取到读锁,但是如果这时候有goroutine尝试获取写锁,则获取写锁的线程将会被阻塞,这时候如果再有goroutine尝试获取读锁,则其也会被阻塞。

当某个goroutine获取到写锁后,其后尝试获取读锁的goroutine都会被阻塞。

另外读锁是可重入锁,也就是同一个goroutine在可以在持有读锁的情况下再次获取读锁。

在go中获取读锁和写锁方式如下:

var rwLock sync.RWMutex
rwLock.RLock()//获取读锁
rwLock.RUnlock()//释放读锁
rwLock.Lock()//获取写锁
rwLock.Unlock()//释放写锁

三、读写锁例子

3.1 验证多个goroutine可以同时获取读锁

首先我们来验证下多个goroutine可以同时获取读锁:

package main
import (
    "fmt"
    "sync"
)
var (
    wg      sync.WaitGroup //信号量
)
var rwlock sync.RWMutex //读写锁
func main() {
    wg.Add(1)
    //1.
    rwlock.RLock()
    fmt.Println("--main goroutine get rlock---")
    //2.
    go func() {
        rwlock.RLock()//2.1
        fmt.Println("--new goroutine get rlock---")
        rwlock.RUnlock()//2.2
        wg.Done()//2.3
    }()
    wg.Wait()//2.4
    rwlock.RUnlock()//2.5
}
  • 如上代码首先创建了一个读写锁rwlock,和一个同步用的wg对象

  • main函数所在goroutine内代码(1)获取到了读锁,然后开启了一个新的goroutine,新goroutine内首先获取读锁,然后在释放,最后让信号量减去1.

  • main函数所在goroutine在代码2.4等待新goroutine运行结束然后在释放读锁

  • 这里假设同时只有一个goroutine可以获取读锁,则由于main所在goroutine已经在代码1获取到了读锁,所以新goroutine的代码2.1必然被阻塞,所以代码2.3得不到执行,所以main所在gouroutine会一直阻塞到步骤2.4,这两个goroutine就产生了死锁状态。

  • 而实际运行上面代码程序是可正常结束的,这验证了在main所在goroutine获取读锁后释放读锁前,新goroutine也获取到了读锁,也就是多个goroutine可以同时获取读锁。

3.2 读写锁是互斥锁

本节我们来验证当一个goroutine获取到写锁后,其他获取读锁的线程将会被阻塞;当一个goroutine获取到读锁后,另外一个goroutine获取写锁将会被阻塞,如果这时候其他goroutine获取读锁,也会被阻塞

package main
import (
    "fmt"
    "sync"
)
var (
    wg      sync.WaitGroup //信号量
)
var rwlock sync.RWMutex //读写锁
func main() {
    wg.Add(1)
    //1.
    rwlock.Lock()
    fmt.Println("--main goroutine get wlock---")
    //2.
    go func() {
        rwlock.RLock() //2.1
        fmt.Println("--new goroutine get rlock---")
        rwlock.RUnlock() //2.2
        wg.Done() //2.3
    }()
    wg.Wait()       //2.4
    rwlock.Unlock() //2.5
}
  • 如上代码我们修改上面的例子让代码1获取写锁,然后代码2.5释放写锁,新goroutine内还是先获取读锁然后释放读锁。运行上面代码大家猜会输出什么?

  • 首先main所在goroutine获取了写锁,然后执行代码2.4等待新goroutine运行完毕后,释放写锁。

  • 新goroutine则是先尝试获取读锁,由于读写是互斥锁,而现在写锁已经被main所在goroutine持有了,所以新goroutine会阻塞到获取读锁的地方,而main所在goroutine会阻塞到代码2.4,这时候就达到了循环等待的条件,两个goroutine就陷入了死锁状态。运行上面代码会输出:

fatal error: all goroutines are asleep - deadlock!
--main goroutine get wlock---
goroutine 1 [semacquire]:
sync.runtime_Semacquire(0x117ffd0)
    /usr/local/go/src/runtime/sema.go:56 +0x39
sync.(*WaitGroup).Wait(0x117ffc8)
    /usr/local/go/src/sync/waitgroup.go:130 +0x64
main.main()
    /Users/luxu.zlx/workspace/learn/go/workspace/lock/src/main/rwlock.go:29 +0xd4
goroutine 5 [semacquire]:
sync.runtime_SemacquireMutex(0x118001c, 0x0)
    /usr/local/go/src/runtime/sema.go:71 +0x3d
sync.(*RWMutex).RLock(0x1180010)
    /usr/local/go/src/sync/rwmutex.go:50 +0x4e
main.main.func1()
    /Users/luxu.zlx/workspace/learn/go/workspace/lock/src/main/rwlock.go:22 +0x31
created by main.main
    /Users/luxu.zlx/workspace/learn/go/workspace/lock/src/main/rwlock.go:21 +0xc4

上面例子我们验证了本节提的第一个问题,下面看第二个:

package main
import (
    "fmt"
    "sync"
    "time"
)
var (
    wg      sync.WaitGroup //信号量
)
var rwlock sync.RWMutex //读写锁
func main() {
    wg.Add(1)
    rwlock.RLock() //1
    //2
    go func() {
        fmt.Println("--new goroutine1 try get wlock---")
        rwlock.Lock() //2.1
        fmt.Println("--new goroutine1  got wlock---")
        rwlock.Unlock() //2.2
        fmt.Println("--new goroutine1  release wlock---")
    }()
    time.Sleep(3 * time.Second) //3
    //4
    go func() {
        rwlock.RLock() //4.1
        fmt.Println("--new goroutine2 get rlock---")
        rwlock.RUnlock() //4.2
        wg.Done() //4.3
    }()
    wg.Wait()        //5
    rwlock.RUnlock() //6
}

运行上面代码输出:

--new goroutine1 try get wlock---
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [semacquire]:
sync.runtime_Semacquire(0x1180fd0)
    /usr/local/go/src/runtime/sema.go:56 +0x39
sync.(*WaitGroup).Wait(0x1180fc8)
    /usr/local/go/src/sync/waitgroup.go:130 +0x64
main.main()
    /Users/luxu.zlx/workspace/learn/go/workspace/lock/src/main/rwlock.go:39 +0x98
goroutine 5 [semacquire]:
sync.runtime_SemacquireMutex(0x1181018, 0x0)
    /usr/local/go/src/runtime/sema.go:71 +0x3d
sync.(*RWMutex).Lock(0x1181010)
    /usr/local/go/src/sync/rwmutex.go:98 +0x74
main.main.func1()
    /Users/luxu.zlx/workspace/learn/go/workspace/lock/src/main/rwlock.go:22 +0xaa
created by main.main
    /Users/luxu.zlx/workspace/learn/go/workspace/lock/src/main/rwlock.go:20 +0x62
goroutine 7 [semacquire]:
sync.runtime_SemacquireMutex(0x118101c, 0x0)
    /usr/local/go/src/runtime/sema.go:71 +0x3d
sync.(*RWMutex).RLock(0x1181010)
    /usr/local/go/src/sync/rwmutex.go:50 +0x4e
main.main.func2()
    /Users/luxu.zlx/workspace/learn/go/workspace/lock/src/main/rwlock.go:32 +0x31
created by main.main
    /Users/luxu.zlx/workspace/learn/go/workspace/lock/src/main/rwlock.go:31 +0x88
  • 代码1main goroutine获取了读锁,然后代码2创建了goroutine1尝试获取写锁,代码3让main goroutine休眠3s以便让goroutine1阻塞到获取写锁后在继续向下运行,然后代码4创建了goroutine2尝试获取读锁,假设获取成功后则会释放读锁然后信号量减去1,然后代码5就会返回,但是这里运行结果出现了死锁说明代码4.1获取读锁阻塞。

3.3 读锁是可重入锁

读锁是可重入锁,同一个gorotine可以多次获取读锁

package main
import (
    "fmt"
)
var rwlock sync.RWMutex //读写锁
func main() {
    rwlock.RLock() //1
    rwlock.RLock() //
    fmt.Printf("i got read lock twice")
    rwlock.RUnlock() //2
    rwlock.RUnlock()
}
  • 如上代码main所在goroutine先两次获取读锁,然后打印输出,然后两次释放读锁,运行上面代码可以正常打印出i got read lock twice说明读锁是可重入锁。

使用下面代码可以验证同一个线程的读锁,不能晋升为写锁:

    lock.RLock()
    lock.Lock()
    fmt.Println("test")
    lock.Unlock()
    lock.RUnlock()

上面代码执行会报错:同理当一个线程获取到了写锁后尝试获取读锁也会造成deadlock错误

四、总结

本节我们介绍了sync包中的读写锁,读写锁相比互斥锁来说,锁的粒度有所减少,这是因为读写锁,可以让多个读取共享资源的goroutine同时获取读锁,而对互斥锁来说即使是多个读取共享资源的goroutine也只有一个可以获取读锁,其他的都会被阻塞。

假期在家无聊?那就免费学习下Go语言吧!!!

Go并发编程-并发与并行

Go并发编程-并发编程难在哪里

Go并发编程-线程模型

Go并发编程-内存模型

Go并发编程-goroutine轻量级的线程

Go并发编程-runtime包

Go并发编程-互斥锁

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
提供的源码资源涵盖了Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!
提供的源码资源涵盖了小程序应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!
### 回答1: 学习Go语言的并发编程,需要掌握以下几个方面: 1. 学习Goroutine:Goroutine是Go语言并发编程的核心,它是一种轻量级的线程,可以在程序中创建多个Goroutine并发执行,以实现高效的并发编程。需要学习如何创建和管理Goroutine。 2. 学习Channel:Channel是Goroutine之间进行通信的重要手段,可以用于数据传输和同步等操作。需要学习如何创建和使用Channel。 3. 学习Select语句:Select语句是Goroutine之间进行多路复用的重要语法,可以同时监听多个Channel,从而实现高效的并发处理。需要学习如何使用Select语句。 4. 学习Mutex和WaitGroup:Mutex和WaitGroup是Go语言中实现同步和互斥的重要机制,可以用于保护共享资源和协调Goroutine的执行。需要学习如何使用Mutex和WaitGroup。 5. 学习并发编程的设计模式:并发编程的设计模式是一些常用的并发编程思想和模式,可以用于解决并发编程中的常见问题。需要学习如何应用并发编程的设计模式。 为了学习并发编程,可以参考一些优秀的Go语言并发编程书籍,比如《Go并发编程实战》、《Go语言高并发与微服务实战》等。同时也可以参考一些优秀的开源项目,如etcd、Docker等,深入了解Go语言并发编程的应用场景和实现方式。 ### 回答2: 学习Go语言的并发编程可以按照以下步骤进行。 1. 学习并理解Go语言的并发模型:Go语言的并发编程基于goroutine和channel。首先,需要了解goroutine的概念,它是Go语言中的一种轻量级线程。然后学习如何使用channel进行通信和同步。 2. 掌握goroutine的创建和管理:学习如何创建和管理goroutine,可以通过使用go关键字来创建一个新的goroutine,以便并发地执行任务。 3. 理解channel的使用:掌握channel的使用,了解如何创建、发送和接收数据。学习不同类型的channel以及它们在并发编程中的应用场景。 4. 学习互斥锁和读写锁:Go语言提供了互斥锁和读写锁来实现资源的安全访问。深入理解锁的概念和使用方法,学习如何避免并发访问导致的数据竞争。 5. 掌握并发编程的常见模式:学习并发编程中的常见模式,例如生产者-消费者模式、多路复用模式、线程池等。熟悉这些模式可以帮助我们更好地设计并发程序。 6. 阅读优秀的并发编程代码和文档:阅读优秀的并发编程代码可以提供实际的应用示例和启发。同时,阅读官方文档和相关书籍也是学习的重要途径。 7. 实践和调试:编写自己的并发代码,利用调试工具对程序进行调试,观察并发执行的过程和结果。通过实践来提高对并发编程的理解和应用能力。 总之,学习Go语言的并发编程需要理解并发模型、掌握goroutine和channel的使用、了解锁的概念和使用方法,并通过实践来提高自己的技能。 ### 回答3: 学习Go语言的并发编程可以通过以下几个步骤来进行。 首先,了解并发编程的概念和原则。并发编程是指同时进行多个任务,使用多个线程或协程来提高程序的效率和性能。了解并发编程的基本概念,如协程、锁、原子操作等,对学习Go语言的并发编程非常重要。 其次,学习Go语言的并发特性。Go语言内置了丰富的并发编程工具和特性,如goroutine、channel、select等。通过学习这些特性,可以更好地理解Go语言中的并发编程模型,并能够正确地使用它们。 然后,阅读相关的书籍和文章。有很多经典的书籍和文章涉及到Go语言的并发编程,这些资源可以帮助你更深入地理解并发编程的原理和实践。值得推荐的书籍有《Go语言实战》和《Go并发编程实战》。 接着,进行实践和项目练习。通过编写一些小型的并发程序和项目,可以将理论知识应用到实践中,加深对并发编程的理解和掌握。可以尝试使用goroutine和channel来实现一些并发任务,如爬虫、并发请求等。 最后,参与社区和交流。加入Go语言的社区,如论坛、聊天群等,与其他开发者交流和分享经验。通过与他人的交流,可以学习到更多实践中的经验和技巧,不断提升自己的并发编程能力。 总之,学习Go语言的并发编程需要系统地学习基本概念和原则,掌握语言特性,进行实践和项目练习,并积极参与社区和交流。只有不断实践和学习,才能在并发编程领域不断进步。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值