选择channel和互斥量

我们一般根据以下原则选择:

Y
N
Y
N
Y
N
Y
N
性能要求很高的临界区
使用传统的锁
转让数据所有权
使用channel
保护结构的内部状态
协调多个逻辑片段

协程简介

golang的协程模型,是基于M->N的绿色协程映射的。意思是有M个编程语言级别的绿色线程,运行在N个操作系统的线程上,操作系统的线程调度绿色线程,所以M≥NM\ge NM≥N。之后,协程运行在这些绿色线程上,协程是不可抢占的,每个协程都有自己的时间片,但是不能被抢占。

goroutine的代价非常小,一般来说,我们不需要考虑它的代价。需要注意,GC不回收协程,因此我们需要注意协程泄露的情况。

启动协程需要时间,Go语言经常利用闭包作为协程启动的基本单元,但是需要注意,闭包直接捕获外部循环变量时,可能存在不准确的情况,改进的做法是,使用把外部变量作为参数传入闭包:

package mainimport ("fmt""time"
)func main() {for i := 0; i < 10; i++ {go func() {fmt.Printf("%v ", i)}()}time.Sleep(time.Second)fmt.Println("\n============")for i := 0; i < 10; i++ {go func(n int) {fmt.Printf("%v ", n)}(i)}time.Sleep(time.Second)
}
/*
输出结果
10 10 10 10 10 10 2 10 10 10
============
0 1 2 6 5 7 3 8 4 9
*/

WaitGroup

该类型作为协程之间同步使用的,一般来说,我们需要等待一组并发协程归并时,需要借助这个工具。Wait()是等待归并,Add(n)是增加n个数值,Done()减少一个数据,当数据是0时,Wait()停止阻塞。给出代码示例:

package mainimport ("fmt""sync""time"
)func main() {var wg sync.WaitGroupN := 5wg.Add(N)  // 添加数据fmt.Println("Start")for i := 0; i < N; i++ {go func() {defer wg.Done()  // 一般在defer中Done操作fmt.Println("hello world")time.Sleep(time.Second)}()}fmt.Println("Waiting")wg.Wait()  // 这里阻塞fmt.Println("Done !")
}/*
Start
Waiting
hello world
hello world
hello world
hello world
hello world
Done !
*/

互斥锁和读写锁

互斥锁

互斥锁用于读写不确定的情况,而读写锁用于已知读写的情况。前者速度比后者慢,后者是有写锁时互斥的。

package mainimport ("fmt""sync"
)func main() {N := 0var wg sync.WaitGroupwg.Add(20)for i := 0; i < 20; i++ {go func() {defer wg.Done()for j := 0; j < 100; j++ {N += 1}}()}wg.Wait()fmt.Printf("Without lock, N=%d\n", N)M := 0wg.Add(20)var lock sync.Mutexfor i := 0; i < 20; i++ {go func(){defer wg.Done()for j := 0; j < 100; j++ {lock.Lock()M += 1lock.Unlock()}}()}wg.Wait()fmt.Printf("With lock, M=%d\n", M)
}

可能的结果输出:

Without lock, N=1802
With lock, M=2000

读写锁

效率更高的一种锁,给出代码示例:

package mainimport ("fmt""sync""time"
)func main() {var mu sync.RWMutexvar wg sync.WaitGroupM := 0wg.Add(3)go func() {defer wg.Done()mu.RLock() # 读锁fmt.Println("1", time.Now(), "M=", M)time.Sleep(time.Second * 3)mu.RUnlock()}()go func() {defer wg.Done()mu.RLock()fmt.Println("2", time.Now(), "M=", M)mu.RUnlock()}()go func() {defer wg.Done()mu.Lock()M ++mu.UnLock()}wg.Wait()
}

条件变量Cond

条件变量用于wait & signal原语操作,给出代码示例:

package mainimport ("fmt""sync""time"
)func main() {c := sync.NewCond(&sync.Mutex{})queue := make([]interface{}, 0, 10)removeFromQueue := func(delay time.Duration) {time.Sleep(delay)c.L.Lock()queue = queue[1:]  // 利用切片移动,模拟出队fmt.Println("Removed from queue")c.L.Unlock()c.Signal()}for i := 0; i < 10; i++ {c.L.Lock()for len(queue) == 2 { c.Wait()  // 超过两个立刻阻塞}fmt.Println("Adding to queue")queue = append(queue, struct{}{})go removeFromQueue(time.Second * 1)c.L.Unlock()}
}

再给出一个广播的例子,可以向一个按键点击行为注册不同的事件,借助sync.Cond实现,代码如下:

package mainimport ("fmt""sync"
)type Button struct {  // 模拟按键Clicked *sync.Cond
}func main() {button := Button{ Clicked: sync.NewCond(&sync.Mutex{}) }subscribe := func(c *sync.Cond, fn func()) { // 订阅,收到c的消息就执行对应的函数var goroutineRunning sync.WaitGroupgoroutineRunning.Add(1)go func() {goroutineRunning.Done()c.L.Lock()defer c.L.Unlock()c.Wait()fn()}()goroutineRunning.Wait()}// 订阅按键点击,响应不同的行为var clickRegistered sync.WaitGroupclickRegistered.Add(3)  // 只是为了阻塞用的subscribe(button.Clicked, func() {fmt.Println("Maximizing window")clickRegistered.Done()})subscribe(button.Clicked, func() {fmt.Println("Displaying annoying dialog box !")clickRegistered.Done()})subscribe(button.Clicked, func() {fmt.Println("Mouse clicked")clickRegistered.Done()})button.Clicked.Broadcast()clickRegistered.Wait()
}

相对于channel,该方式的优势在于,Boardcast()方法可以调用多次,行为可以响应多次。注意一点,使用sync.Cond时,最好在一个紧凑的范围内部,否则容易造成混乱。上述方式提供了一个消息注册的基本思路。

once

该方式保证函数只在全局调用一次,可以用作单例模式,给出代码示例:

package mainimport ("fmt""sync"
)var once sync.Once  // 注意需要声明func foo() {fmt.Println("foo")
}func test() {fmt.Println("test")once.Do(foo)  // 全局唯一执行
}func main() {for i := 0; i < 3; i++ {test()}
}
/*
输出结果:
test
foo
test
test
*/

注意一点,sync.Once指计算调用Do方法的次数,而不是多少次唯一调用Do方法,举个例子:

package mainimport ("fmt""sync"
)var once sync.Once
var count intfunc Inc() { count++ }
func Dec() { count-- }func main() {once.Do(Inc)once.Do(Dec)fmt.Println(count)
}

上述代码输出1,因为once变量的Do只记录自身调用的次数,不是记录某个函数调用的次数!!

单例模式可以利用Do方式唯一初始化。

Pool

这里是指资源池。首先,应该明确什么是资源池,为什么用资源池。某些资源申请需要花费较长的时间,为了可以快速获取这些资源,我们可以提前申请一定数量的资源放在“资源池”中,当需要资源时,可以从资源池中快速获取已经存在的资源;当使用完资源后,会把资源放回池中。

注意,一般来说资源池,不会限制申请资源的数量。比如资源池的容量是10,我们同时申请了15个资源,则有10个会快速从资源池中获取,有5个会重新创建。同样的,当放回的资源数超过10的时候,多的资源会被释放掉。

给出golang中资源池的使用方式:

package mainimport ("fmt""sync"
)func main() {var numCalsCreated intcalcPool := &sync.Pool{New: func() interface{} {numCalsCreated += 1mem := make([]byte, 1024)return &mem  // 注意这里返回的地址},}for i := 0; i < 4; i++ {calcPool.Put(calcPool.New())}const numWorkers = 1024 * 1024var wg sync.WaitGroupwg.Add(numWorkers)for i := numWorkers; i > 0; i-- {go func() {defer wg.Done()mem := calcPool.Get().(*[]byte)defer calcPool.Put(mem)  // 用完立刻放回!!!}()}wg.Wait()fmt.Println(numCalsCreated)  // 8
}

虽然启动了1024 * 1024个协程,但是因为存在资源池,所以实际也就用到了8 * 1024字节的内存。

Map

并发安全的map结构,这里不再赘述,直接参考文档即可。

https://golang.org/pkg/sync/#Map

Golang并发编程组件相关推荐

  1. golang并发编程goroutine+channel(一)

    go语言的设计初衷除了在不影响程序性能的情况下减少复杂度,另一个目的是在当今互联网大量运算下,如何让程序的并发性能和代码可读性达到极致.go语言的并发关键词 "go" go dos ...

  2. ​Golang 并发编程指南

    分享 Golang 并发基础库,扩展以及三方库的一些常见问题.使用介绍和技巧,以及对一些并发库的选择和优化探讨. go 原生/扩展库 提倡的原则 不要通过共享内存进行通信;相反,通过通信来共享内存. ...

  3. Golang 并发编程之同步原语

    当提到并发编程.多线程编程时,我们往往都离不开『锁』这一概念,Go 语言作为一个原生支持用户态进程 Goroutine 的语言,也一定会为开发者提供这一功能,锁的主要作用就是保证多个线程或者 Goro ...

  4. golang 并发编程

    文章目录 知识点 并发 并行 进程 线程 协程 通信模型 CSP communicating sequential process share memory 线程模型 1. 用户级线程模型M:1 2. ...

  5. Golang并发编程-GPM协程调度模型原理及组成分析

    文章目录 一.操作系统的进程和线程模型 1.1.基础知识 1.2.KST/ULT 二.Golang的GPM协程调度模型 三.M的结构及对应关系 四.P的结构及状态转换 五.G的结构及状态转换 六.GP ...

  6. Golang并发编程进程通信channel了解及简单使用

    概念及作用 channel是一个数据类型,用于实现同步,用于两个协程之间交换数据.goroutine奉行通过通信来共享内存,而不是共享内存来通信. 引用类型channel是CSP模式的具体实现,用于多 ...

  7. Golang并发编程入门教程

    时间单位 1S = 1000ms 1ms = 1000us 1us = 1000ns 并发与并行 并行: 借助多核 cpu 实现. (真 并行) 并发: 宏观:用户体验上,程序在并行执行. 微观:多个 ...

  8. Go语言自学系列 | golang并发编程之原子变量的引入

    视频来源:B站<golang入门到项目实战 [2021最新Go语言教程,没有废话,纯干货!持续更新中...]> 一边学习一边整理老师的课程内容及试验笔记,并与大家分享,侵权即删,谢谢支持! ...

  9. golang并发编程-04-通道-02-定时器、断续器

    文章目录 1. 定时器 1.1 time.NewTimer 1.2 <-time.After() 1.3 停止 1.4 定时器重置 2 断续器 2.1 断续器使用 2.2 断续器中断 1. 定时 ...

最新文章

  1. 超融合服务器品牌型号,蓝盾发布云平台!强势加入云计算IT行列
  2. 9.逆向-函数调用约定
  3. String : string的长度?string的子串?
  4. Ganglia集群监控系统搭建
  5. C++实现基数排序(附完整源码)
  6. Bug貌似发现了centos系统配置host主机的问题
  7. ORACLE基础应用学习-- 各种故障的恢复方法总结
  8. android收入管理系统,毕业设计(论文)-基于Android系统的家庭理财通软件的设计——收入管理模块.docx...
  9. IplImage* cvmat* mat 释放
  10. 全国省份、城市、地区全数据(SQL版与XML版)包括各城市邮编
  11. java相关求助---Java泛型的理解与等价实现---------没有完全明白啊...请大神指导...
  12. win7家庭版更改桌面图标
  13. 【华为OJ】【算法总篇章】
  14. Python解析mat文件
  15. 微信公众号爬虫开发-常见问题汇总
  16. 苹果CMSv10系统标签,仿站必备
  17. c#Form未能加载文件或程序集的解决方法
  18. 《星际迷航 超越星辰》
  19. 在线免费完整PDF转PPT格式
  20. 计算机单片机实训报告,计算器单片机实训报告.doc

热门文章

  1. mc有什么红石机器人_我的世界:MC黑科技!毁图仅需一秒 红石机器人真能移动!...
  2. 关于四元数的个人理解
  3. 计算机算法设计与分析 最大子段和问题
  4. Python之定义默认参数
  5. CUDA——调试“ImportError: libcudart.so.9.2: cannot open shared object file: No such file or directory”
  6. 【转】关于Class.forName(“com.mysql.jdbc.Driver”)
  7. TLE5012B ESP32驱动程序、硬件电路设计、4线SPI通信,驱动完美兼容4线SPI不用改MOSI开漏推挽输出
  8. 第k小的数(二分、partition)
  9. ConcurrentHashMap 底层原理,你真的理解了吗?
  10. 敏捷开发生态系统系列之三:计划跟踪II(需求优先级排序-迭代期内无变更-团队承诺)...