简单整两句

Go语言内置的map数据结构是线程不安全的,如果存在多个线程同时读或写同一个map时,常常会导致难以预料的问题。
所以,有很多程序员在使用Go语言内置的map时,会在外面包装一层,即封装一层读写接口,并在接口实现内部引入读锁或写锁,以此来保证多线程安全访问map。

在上一篇博客中,我们知道,即使是使用读锁,也会严重降低程序性能。因此针对map的优化,也存在很多方案。

sync.map方案

sync.map是Golang在1.9版本中引入的线程安全的高性能map数据结构,在实现中引入了缓存机制:

程序在读map时,会首先到Read内存区读数据,这个Read内存区是不需要锁机制来访问的,如果Read内存区中不存在想要的值,会到Dirty区域找数据,这个Dirty区域的访问是需要加锁的。
由此可见,sync.map适合在读多写少的情况优化程序性能。如果写入map操作很多,同样会导致各种锁引发的性能问题。

concurrent_map方案

除了sync.map这种通过缓存的方式提升map访问性能的方式外,也存在另一种优化方式,即concurrent_map(Java中有类似的实现)。
sync.map的实现中,对Dirty区的访问使用的锁是锁住整个map。但如果两次写或读是针对同一个map的不同区域的访问,那锁住整个map就显得不那么合理了。
所以,concurrent_map的实现原理大致是这样的:
左图是使用RWLock或sync.map的锁机制,即对整块map内存加锁。而右图是concurrent_map的锁机制,即对map内存细粒度的加锁。这样能够最大限度的减少锁冲突,进而提高性能。

上代码

这里有几段代码,分别通过RWLock、sync.map、concurrent_map三种方式访问map,然后通过测试程序对比不同方案的性能差异。

先看测试代码:(map_benchmark_test.go)

package maps

import (
	"strconv"
	"sync"
	"testing"
)

const (
	NumOfReader = 100
	NumOfWriter = 10
)

type Map interface {
	Set(key interface{}, val interface{})
	Get(key interface{}) (interface{}, bool)
	Del(key interface{})
}

func benchmarkMap(b *testing.B, hm Map) {
	for i := 0; i < b.N; i++ {
		var wg sync.WaitGroup
		for i := 0; i < NumOfWriter; i++ {
			wg.Add(1)
			go func() {
				for i := 0; i < 100; i++ {
					hm.Set(strconv.Itoa(i), i*i)
					hm.Set(strconv.Itoa(i), i*i)
					hm.Del(strconv.Itoa(i))
				}
				wg.Done()
			}()
		}
		for i := 0; i < NumOfReader; i++ {
			wg.Add(1)
			go func() {
				for i := 0; i < 100; i++ {
					hm.Get(strconv.Itoa(i))
				}
				wg.Done()
			}()
		}
		wg.Wait()
	}
}

func BenchmarkSyncmap(b *testing.B) {
	b.Run("map with RWLock", func(b *testing.B) {
		hm := CreateRWLockMap()
		benchmarkMap(b, hm)
	})

	b.Run("sync.map", func(b *testing.B) {
		hm := CreateSyncMapBenchmarkAdapter()
		benchmarkMap(b, hm)
	})

	b.Run("concurrent map", func(b *testing.B) {
		superman := CreateConcurrentMapBenchmarkAdapter(199)
		benchmarkMap(b, superman)
	})
}

首先定义了一个map接口(type Map interface ),接口函数有Set()、Get()、Del()。
然后封装了一个工具函数benchmarkMap,来针对一个map实例(对象)进行读和写操作。
最后是BenchmarkSyncmap函数,它调用benchmarkMap函数,并传入三种不同的map实现方案实现的map实例进行性能测试。
BenchmarkSyncmap函数中调用的CreateRWLockMap、CreateSyncMapBenchmarkAdapter、CreateConcurrentMapBenchmarkAdapter函数分别存在于另外三个go源码中。

这段测试用例代码还可以通过调整常量的值NumOfReader 、NumOfWriter来控制读多写少还是读少写多。

使用RWLock实现的map代码:(rw_map.go)

package maps

import "sync"

type RWLockMap struct {
	m    map[interface{}]interface{}
	lock sync.RWMutex
}

func (m *RWLockMap) Get(key interface{}) (interface{}, bool) {
	m.lock.RLock()
	v, ok := m.m[key]
	m.lock.RUnlock()
	return v, ok
}

func (m *RWLockMap) Set(key interface{}, value interface{}) {
	m.lock.Lock()
	m.m[key] = value
	m.lock.Unlock()
}

func (m *RWLockMap) Del(key interface{}) {
	m.lock.Lock()
	delete(m.m, key)
	m.lock.Unlock()
}

func CreateRWLockMap() *RWLockMap {
	m := make(map[interface{}]interface{}, 0)
	return &RWLockMap{m: m}
}

代码末尾的CreateRWLockMap函数用来对外提供创建RWLockMap实例服务(在map_benchmark_test.go中已经用到)。

使用sync.map实现的map:(sync_map_benchmark_adapter.go)

package maps

import "sync"

func CreateSyncMapBenchmarkAdapter() *SyncMapBenchmarkAdapter {
	return &SyncMapBenchmarkAdapter{}
}

type SyncMapBenchmarkAdapter struct {
	m sync.Map
}

func (m *SyncMapBenchmarkAdapter) Set(key interface{}, val interface{}) {
	m.m.Store(key, val)
}

func (m *SyncMapBenchmarkAdapter) Get(key interface{}) (interface{}, bool) {
	return m.m.Load(key)
}

func (m *SyncMapBenchmarkAdapter) Del(key interface{}) {
	m.m.Delete(key)
}

concurrent_map:(concurrent_map_benchmark_adapter.go)

package maps

import "github.com/easierway/concurrent_map"

type ConcurrentMapBenchmarkAdapter struct {
	cm *concurrent_map.ConcurrentMap
}

func (m *ConcurrentMapBenchmarkAdapter) Set(key interface{}, value interface{}) {
	m.cm.Set(concurrent_map.StrKey(key.(string)), value)
}

func (m *ConcurrentMapBenchmarkAdapter) Get(key interface{}) (interface{}, bool) {
	return m.cm.Get(concurrent_map.StrKey(key.(string)))
}

func (m *ConcurrentMapBenchmarkAdapter) Del(key interface{}) {
	m.cm.Del(concurrent_map.StrKey(key.(string)))
}

func CreateConcurrentMapBenchmarkAdapter(numOfPartitions int) *ConcurrentMapBenchmarkAdapter {
	conMap := concurrent_map.CreateConcurrentMap(numOfPartitions)
	return &ConcurrentMapBenchmarkAdapter{conMap}
}

这里使用了github中的开源方案:https://github.com/easierway/concurrent_map
需要先安装一下:
若访问github或Google较慢,可使用加速器:
http://91tianlu.date/aff.php?aff=3468

$ go get github.com/easierway/concurrent_map

在读次数10倍于写时,运行测试用例:

#const (
#	NumOfReader = 100
#	NumOfWriter = 10
#)

$ go test -bench=.
goos: windows
goarch: amd64
pkg: map
BenchmarkSyncmap/map_with_RWLock-4                   430           2643960 ns/op
BenchmarkSyncmap/sync.map-4                          525           2268836 ns/op
BenchmarkSyncmap/concurrent_map-4                    745           1570330 ns/op
PASS
ok      map     4.464s

读写次数各占比50%时,运行测试用例:

#const (
#	NumOfReader = 100
#	NumOfWriter = 100
#)

$ go test -bench=.
goos: windows
goarch: amd64
pkg: map
BenchmarkSyncmap/map_with_RWLock-4                    48          22778590 ns/op
BenchmarkSyncmap/sync.map-4                           48          41247146 ns/op
BenchmarkSyncmap/concurrent_map-4                     99          11579204 ns/op
PASS
ok      map     5.025s

写次数2倍于读时,运行测试用例:

#const (
#	NumOfReader = 100
#	NumOfWriter = 200
#)

$ go test -bench=.
goos: windows
goarch: amd64
pkg: map
BenchmarkSyncmap/map_with_RWLock-4                    48          22778590 ns/op
BenchmarkSyncmap/sync.map-4                           48          41247146 ns/op
BenchmarkSyncmap/concurrent_map-4                     99          11579204 ns/op
PASS
ok      map     5.025s

从测试结果上看,无论是读多写少、读少写多、还是各占50%的情况,concurrent_map的性能都优于前两者。
(原作者录制视频课程中时,在极端的读多写少的情况sync.map性能较优秀,可能是原作者后期对concurrent_map进行了优化)。

注:

博客内容为极客时间视频课《Go语言从入门到实战》学习笔记。
参考课程链接:
https://time.geekbang.org/course/intro/160?code=NHxez89MnqwIfa%2FvqTiTIaYof1kxYhaEs6o2kf3ZxhU%3D&utm_term=SPoster
博客参考代码:
https://github.com/geektime-geekbang/go_learning/tree/master/code/ch48/maps

若访问github或Google较慢,可使用加速器:
http://91tianlu.date/aff.php?aff=3468