作为服务端的开发,最关注的就是并发编程,每个从java接触到golang的小伙伴应该都会有一些共同的困惑,做个总结
-
从java转到golang的一些类比:
- atomic.Value的用途和volatile类似
- sync.Mutex、sync.RWMutex和ReentrantLock、ReentrantReadWriteLock类似
- sync.WaitGroup和CountDownLauch类似
- channel和future或者阻塞队列类似
- sync.Map和ConcurrentHashMap类似
- atomic包对应java的原子类
- select特性没想到在java中有对应的东西,select加channel使用很广,配合default可以实现不阻塞,有很多妙用
- sync.Once可以很方便的实现单例模式,效果相当于java中的双重synchronized的懒加载
在官方文档golang的内存模型描述中没有对可见性有明确的说明,官方文档对保证并发安全的说明就一句话:use explicit synchronization
golang中经常会看到结构体的属性中有sync.mutex,就是同步处理结构体对象用的
-
golang中没有类似java的volatile关键字,但是有atomic.Value可以保证读写的原子性。
- 举个例子:假设我们有一个结构体存储着应用运行中的各种配置,有一个协程专门负责更新Config,用一个新的DynamicConfig对象赋值给Config(copy on write),其他的协程从Config中读取配置。如果我们不做同步处理的话,很有可能Config中的数据有的是新的有的是老的
- 聪明的小伙伴为了避免上面那种情况可能会考虑使用结构体的指针作为全局变量ConfigPtr进行操作,因为指针的读写按照道理应该是原子操作。有大神看了编译后的汇编发现确实是原子操作就这么用了,但是这样其实是依赖的golang编译机制,golang从语言层面并不提供原子保证。加入-race参数的话会看到data race警告。但是有很多开源项目都有在并发情况下用一个新map去替换老map的操作,使用起来也没啥问题,就是会有race警告,这就很尴尬了。。。
- 最好的做法应该是ConfigAtom这样使用atomic.Value,很多开源库里都有这种做法,其实使用读写锁也可以,就是性能差了点
type DynamicConfig struct {
MysqlUrl string
MysqlMaxIdleConns int
MysqlMaxOpenConns int
MysqlMaxLifetime int
}
var Config DynamicConfig
var ConfigPtr *DynamicConfig
var ConfigAtom atomic.Value
- golang中的channel粗略的可以看做java中的future或者阻塞队列,虽然golang中老生常谈的就是要用通信来共享内存,不要通过共享内存来通信,这句话的意思被很多人曲解成了在golang中保证并发安全最好都用channel。但其实这句话的意思是如果多个协程间有数据传递交互的话确实是用channel更好,但是就像上面那样的全局配置对象,肯定是使用共享内存更加合适。
- golang中atomic包中的原子操作只有整数类型,其他的类型如果要原子的load、store、cas操作需要转换成*unsafe.Pointer,但是golang的unsafe包和java的unsafe包一样是不推荐使用的,后续版本可能会有变化或者移除,但是现在已经有很多地方都在使用unsafe了,所以我猜测后续golang应该不会对这块有什么变化,或者是提供类似python2迁移python3的工具
- golang为了提醒大家map不是并发安全的,当并发写入同一个map时会panic而且不会被recover到!!!使用sync.Map保证并发安全
- channel的性能比sync.mutex低,这点有点违反使用者的第一感觉
有疑问加站长微信联系(非本文作者)