Go Book / 3 Go Seniors / Go 并发编程:利用通道创建并发安全的数据结构

Go 并发编程:利用通道创建并发安全的数据结构

利用通道创建并发安全的映射或切片

创建一个并发安全的映射或切片,不需要使用锁或者其他底层原语

我们之前讲过值类型的数据在函数参数传递时是拷贝传递,所以没有并发安全问题,我们并不用担心传递的值被多个协程操作的后果。然而当我们使用切片、映射等引用类型参数,甚至指针等参数时,多个并发协程对该参数的操作会引起混乱,这样的参数传递是数据不安全的。

我们知道对于引用或指针在协程内的操作,如果它同时被多个协程使用,一般我们需要加同步锁,以保证数据安全,但是加锁会损失一些计算资源,如果锁过多的话会影响整体效率。

有没有不加锁的方式来保证引用类型或指针类型的并发安全的实现方案呢?这就要利用通道了,我们知道,在Go中,通道通信是并发安全的。下面以映射Map和切片Slice为例,我们来创建并发安全的数据结构。

并发安全映射

安全映射的实现其实就是在一个协程里执行一个内部方法以操作一个普通的map,外界只能通过通道来操作这个内部映射,这样就能保证对这个映射的所有访问都是串行的。这种方法运行着一个无限循环,阻塞等待一个输入通道的命令(即insert、remove等)。

以下为具体实现:

// 定义一个并发安全的映射,此处声明该安全映射的接口
type SafeMap interface {
	Insert(string, interface{})      // 插入键值
	Delete(string)                   // 删除键
	Find(string) (interface{}, bool) // 查找键值
	Len() int                        // map元素长度
	Update(string, UpdateFunc)       // 更新键值
	Close() map[string]interface{}   // 关闭map,实质关闭通道
}

type UpdateFunc func(interface{}, bool) interface{}

// 操作安全映射的命令数据结构
type commandData struct {
	action  commandAction                 // 操作类型
	key     string                        // 操作的键
	value   interface{}                   // 操作的值
	result  chan<- interface{}            // 操作的结果,用于查找结果返回
	data    chan<- map[string]interface{} // map存储的数据,用于关闭通道时返回map数据
	updater UpdateFunc                    // 更新函数,用于更新时设置更新函数
}

// 定义安全映射的操作项
type commandAction int

const (
	remove commandAction = iota
	end
	find
	insert
	length
	update
)

// safeMap的实现基于一个可发送和接收的commandData类型值的通道
type safeMap chan commandData

// 安全映射的新增操作
func (sm safeMap) Insert(key string, value interface{}) {
	// 向通道发送新增操作命令
	sm <- commandData{action: insert, key: key, value: value}
}

// 安全映射的删除操作
func (sm safeMap) Delete(key string) {
	// 向通道发送删除操作命令
	sm <- commandData{action: remove, key: key}
}

// 定义查找的返回结果
type findResult struct {
	value interface{} // 查找的值
	found bool        // 查找是否存在
}

// 安全映射的查找操作
func (sm safeMap) Find(key string) (value interface{}, found bool) {
	// 响应通道,命令执行完成后会在此通道接收结果数据
	reply := make(chan interface{})
	sm <- commandData{action: find, key: key, result: reply}
	result := (<-reply).(findResult)
	return result.value, result.found
}

// 安全映射的长度计算操作
func (sm safeMap) Len() int {
	// 响应通道,命令执行完成后会在此通道接收结果数据
	reply := make(chan interface{})
	sm <- commandData{action: length, result: reply}
	return (<-reply).(int)
}

// 安全映射的更新操作
func (sm safeMap) Update(key string, updater UpdateFunc) {
	sm <- commandData{action: update, key: key, updater: updater}
}

// 安全映射的关闭操作,针对通道,返回map的底层数据
func (sm safeMap) Close() map[string]interface{} {
	reply := make(chan map[string]interface{})
	sm <- commandData{action: end, data: reply}
	return <-reply
}

// 运行于协程中的safeMap,其创建一个底层映射来保存实际数据
func (sm safeMap) run() {
	store := make(map[string]interface{})
	// 不断从safeMap通道接收操作命令
	for command := range sm {
		switch command.action {
		case insert: // 新增
			store[command.key] = command.value
		case remove: // 删除
			delete(store, command.key)
		case find: // 查找
			value, found := store[command.key]
			command.result <- findResult{value, found}
		case length: // 计算长度
			command.result <- len(store)
		case update: // 更新键值
			value, found := store[command.key]
			// 执行更新函数,注意,更新函数不能调用sm的其他方法,否则会导致死锁
			store[command.key] = command.updater(value, found)
		case end: // 关闭操作
			close(sm)
			command.data <- store
		}
	}
}

// safeMap的工厂方法
func NewSafeMap() SafeMap {
	sm := make(safeMap)
	go sm.run()
	return sm
}

并发测试:


func TestSafeMap(t *testing.T) {
	smap := NewSafeMap()

	wg := sync.WaitGroup{}

	// 并发新增
	for i := 0; i < 100; i++ {
		key := fmt.Sprintf("key%d", i)
		wg.Add(1)
		go func(sm SafeMap, k, v string) {
			sm.Insert(k, v)
			wg.Done()
		}(smap, key, key)
	}

	// 并发查找
	go func() {
		for i := 0; i < 40; i += 2 {
			key := fmt.Sprintf("key%d", i)
			wg.Add(1)
			go func(sm SafeMap, k string) {
				value, found := sm.Find(key)
				fmt.Printf("Find Key:%s,result:%s,%v\n", k, value, found)
				wg.Done()
			}(smap, key)
		}
	}()

	// 并发更新
	go func() {
		for i := 0; i < 60; i += 3 {
			key := fmt.Sprintf("key%d", i)
			wg.Add(1)
			go func(sm SafeMap, k string) {
				sm.Update(key, func(oldValue interface{}, found bool) (newValue interface{}) {
					if found {
						newValue = oldValue.(string) + "update"
						return
					}
					return oldValue
				})
				wg.Done()
			}(smap, key)
		}
	}()

	// 并发删除
	go func() {
		for i := 0; i < 80; i += 4 {
			key := fmt.Sprintf("key%d", i)
			wg.Add(1)
			go func(sm SafeMap, k string) {
				sm.Delete(k)
				wg.Done()
			}(smap, key)
		}
	}()

	// 并发计算长度
	wg.Add(1)
	go func(sm SafeMap) {
		l := sm.Len()
		fmt.Println("SafeMap Len:", l)
		wg.Done()
	}(smap)

	wg.Wait()

	// 关闭管到输出结果
	sm := smap.Close()
	fmt.Println("Print SafeMap:")
	for k, v := range sm {
		fmt.Printf("key:%s,value:%v\n", k, v)

	}

}

并发安全切片

与并发安全映射类似,以下实现的安全切片数据结构都是基于在协程通信中通道的同步属性:

package ss

// 安全切片接口
type SafeSlice interface {
	Append(interface{})     // 添加指定元素
	At(int) interface{}     // 返回指定位置的元素
	Close() []interface{}   // 关闭通道并返回切片
	Delete(int)             // 删除指定位置元素
	Len() int               // 返回元素个数
	Update(int, UpdateFunc) // 更新指定位置元素
}

// 更新函数
type UpdateFunc func(int, interface{}) interface{}

// 命令数据结构
type commandData struct {
	action  commandAction
	index   int
	value   interface{}
	result  chan<- interface{}
	data    chan<- []interface{}
	updater UpdateFunc
}

// 命令类型值
type commandAction int

const (
	insert commandAction = iota
	at
	remove
	update
	size
	end
)

// 安全切片实现,由通道作为外部输入,内部运行于一个协程中,协程函数维护一个底层切片
type safeSlice chan commandData

func (ss safeSlice) Append(value interface{}) {
	ss <- commandData{action: insert, value: value}
}

func (ss safeSlice) At(index int) interface{} {
	reply := make(chan interface{})
	ss <- commandData{action: at, index: index, result: reply}
	return <-reply
}

func (ss safeSlice) Close() []interface{} {
	reply := make(chan []interface{})
	ss <- commandData{action: end, data: reply}
	return <-reply
}

func (ss safeSlice) Delete(index int) {
	ss <- commandData{action: remove, index: index}
}

func (ss safeSlice) Len() int {
	reply := make(chan interface{})
	ss <- commandData{action: size, result: reply}
	return (<-reply).(int)
}

func (ss safeSlice) Update(index int, updater UpdateFunc) {
	ss <- commandData{action: update, index: index, updater: updater}
}

// 协程运行
func (ss safeSlice) run(length, cap int) {
	slice := make([]interface{}, length, cap)
	for command := range ss {
		switch command.action {
		case insert:
			slice = append(slice, command.value)
		case at:
			var value interface{}
			if len(slice)-1 > command.index {
				value = slice[command.index]
			}
			command.result <- value

		case remove:
			if len(slice)-1 > command.index {
				slice[command.index] = nil
			}

		case update:
			if len(slice)-1 > command.index {
				oldValue := slice[command.index]
				slice[command.index] = command.updater(command.index, oldValue)
			}
		case size:
			command.result <- len(slice)
		case end:
			close(ss)
			command.data <- slice
		}
	}

}

func NewSafeSlice(length, cap int) SafeSlice {
	ss := make(safeSlice)
	ss.run(length, cap)
	return ss
}

小结

可以看到,使用一个通道解决并发安全的数据结构比一个普通的映射会消耗更大的内存开销,每条命令都需要创建一个commandData,利用通道来达到多个协程串行访问一个SafeMap或SafeSlice的目的。对于引用类型或指针类型的并发安全解决方案有很多,最简单的就是同步锁sync.Mutex,在协程里操作map时记得加解锁便可;此外,你也可以创建一个包含同步锁的数据结构,当然这只是写法不同而已。

我们日常使用时最好避免传递引用类型或指针类型到不同协程并发执行,如需有这个需求,最好每个协程各维护一个map,然后在父协程合并即可。但如果特殊场景你仍需要一个并发安全的映射,那么本文的并发安全映射结构不失为一种解决方案。