golang中map并发读写问题及解决方法

一、map并发读写问题

如果map由多协程同时读和写就会出现 fatal error:concurrent map read and map write的错误

如下代码很容易就出现map并发读写问题

func main(){

c := make(map[string]int)
       go func() {//开一个协程写map
            for j := 0; j < 1000000; j++ {
              c[fmt.Sprintf("%d", j)] = j
            }
       }()
       go func() {    //开一个协程读map
             for j := 0; j < 1000000; j++ {
                 fmt.Println(c[fmt.Sprintf("%d",j)])
             }
       }()

 time.Sleep(time.Second*20)

}

多个协程同时写也会出现fatal error: concurrent map writes的错误

如下代码很容易出现map并发写的问题

package main

import (
	"fmt"
	"time"
)

func main() {
	c := make(map[string]int)
	for i := 0; i < 100; i++ {
		go func() { //开100个协程并发写map
			for j := 0; j < 1000000; j++ {
				c[fmt.Sprintf("%d", j)] = j
			}
		}()
	}
	time.Sleep(time.Second * 20) //让执行main函数的主协成等待20s,不然不会执行上面的并发操作
}

二、出现问题的原因

因为map为引用类型,所以即使函数传值调用,参数副本依然指向映射m, 所以多个goroutine并发写同一个映射m, 写过多线程程序的同学都知道,对于共享变量,资源,并发读写会产生竞争的, 故共享资源遭到破坏

三、解决方法

1、加锁

(1)通用锁

type Demo struct {
  Data map[string]string 
  Lock sync.Mutex
}

func (d Demo) Get(k string) string{
  d.Lock.Lock()
  defer d.Lock.UnLock()

  return d.Data[k]
}


func (d Demo) Set(k,v string) {

  d.Lock.Lock()

  defer d.Lock.UnLock()

  d.Data[k]=v
}

(2)读写锁

type Demo struct {
  Data map[string]string 
  Lock sync.RwMutex
}

func (d Demo) Get(k string) string{
  d.Lock.RLock()

  defer d.Lock.RUnlock()

  return d.Data[k]
}

func (d Demo) Set(k,v string) {

  d.Lock.Lock()

  defer d.Lock.UnLock()

  d.Data[k]=v
}

2、利用channel串行化处理

[go语言]吐槽:怎么样实现支持并发访问的数据集合更好?

在go语言里,提倡用信道通讯的方式来替代显式的同步机制。但是我发现有的时候用信道通讯方式实现的似乎也不是很好(暂不考虑效率问题)。

假设有一个帐号的集合,需要在这个集合上实现一些操作,比如查找修改等。这个集合的操作必须是支持并发的。

如果用锁的方式(方案1)实现大概是这样:

import "sync"

type Info struct {
	age int
}
type AccountMap struct {
	accounts map[string]*Info
	mutex    sync.Mutex
}

func NewAccountMap() *AccountMap {
	return &AccountMap{
		accounts: make(map[string]*Info),
	}
}
func (p *AccountMap) add(name string, age int) {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	p.accounts[name] = &Info{age}
}
func (p *AccountMap) del(name string) {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	delete(p.accounts, name)
}
func (p *AccountMap) find(name string) *Info {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	res, ok := p.accounts[name]
	if !ok {
		return nil
	}
	inf := *res
	return &inf
}
用信道来实现试试(方案2):

type Info struct {
	age int
}
type AccountMap struct {
	accounts map[string]*Info
	ch chan func()
}
func NewAccountMap() *AccountMap {
	p := &AccountMap{
		accounts: make(map[string]*Info),
		ch: make(chan func()),
	}
	go func() {
		for {(<-p.ch)()}
	}()
	return p
}
func (p *AccountMap) add(name string, age int) {
	p.ch <- func() {
		p.accounts[name] = &Info{age}
	}
}
func (p *AccountMap) del(name string) {
	p.ch <- func() {
		delete(p.accounts, name)
	}
}
func (p *AccountMap) find(name string) *Info {
	// 每次查询都要创建一个信道
	c := make(chan *Info)
	p.ch <- func() {
		res, ok := p.accounts[name]
		if !ok {
			c <- nil
		} else {
			inf := *res
			c <- &inf
		}
	}
	return <-c
}
这里有个问题,每次调用find都要创建一个信道。
那么试试把信道作为参数(方案3),只需要修改find函数的实现:
// 信道对象作为参数,暴露了实现机制
func (p *AccountMap) find(name string, c chan *Info) *Info {
	p.ch <- func() {
		res, ok := p.accounts[name]
		if !ok {
			c <- nil
		} else {
			inf := *res
			c <- &inf
		}
	}
	return <-c
}
总结一下,现在的问题就是三种方案都有不尽如人意之处:
方案1:使用锁机制,不太符合go解决问题的方式。
方案2:对于需要返回结果的查询,每次查询都要创建一个信道,比较浪费资源。
方案3:需要在函数参数中指定信道对象,把实现机制暴露了。
那么有没有什么更好的方案呢?
 
2012.12.14:
方案2还有一个改进版本:利用预分配以及可回收的channel来提高资源利用率。这个技术在多个goroutine等待一个主动对象返回自己的数据时会比较有用。例如网游服务器中登录服务器里每个玩家的连接用一个goroutine来处理;另外一个主动对象代表帐号服务器连接用于验证帐号合法性。玩家goroutine会把各自的输入的玩家帐号密码发送给这个主动对象,并阻塞等待主动对象返回验证结果。因为有多个玩家同时发起帐号验证请求,所以主动对象需要把返回结果进行分发,因此可以在发送请求的时候申请一个信道并等待这个信道。
代码如下:

type Info struct {
	age int
}
type AccountMap struct {
	accounts map[string]*Info
	ch chan func()
	tokens chan chan *Info
}
func NewAccountMap() *AccountMap {
	p := &AccountMap{
		accounts: make(map[string]*Info),
		ch: make(chan func()),
		tokens: make(chan chan *Info, 128),
	}
	for i := 0; i < cap(p.tokens); i++ {
		p.tokens <- make(chan *Info)
	}
	go func() {
		for {(<-p.ch)()}
	}()
	return p
}
func (p *AccountMap) add(name string, age int) {
	p.ch <- func() {
		p.accounts[name] = &Info{age}
	}
}
func (p *AccountMap) del(name string) {
	p.ch <- func() {
		delete(p.accounts, name)
	}
}
func (p *AccountMap) find(name string) *Info {
	// 每次查询都要获取一个信道
	c := <-p.tokens
	p.ch <- func() {
		res, ok := p.accounts[name]
		if !ok {
			c <- nil
		} else {
			inf := *res
			c <- &inf
		}
	}
	inf := <-c
	// 回收信道
	p.tokens <- c
	return inf
}
补充一下golang-china上的评论:
xushiwei
在你的方式里面,用 channel 其实把所有请求串行化。
另外,从成本上来说,channel 远大于锁。因为 channel 本身显然是用锁 + 信号唤醒机制实现的。
 
steve wang
是不是可以这样总结:
1.对于共享给各个goroutine的数据对象的并发访问,使用锁来控制
2.对于goroutine之间的通信,使用信道
 
longshanksmo
 
单就性能来看,现在下这种结论有些草率。并发和性能问题错宗复杂,不同的场景可能会产生完全相反的结论。
还有众多因素需要考虑:
首先,不同的用况下,锁粒度不同。在你的案例中是map操作,锁粒度很小。但如果是某种重载操作,或者存在阻塞,锁粒度会很大。那时用锁就不划算。
其次,chan的锁粒度很小,基本固定,可预测。在实际业务中,性能可预测非常重要,决定了部署时的资源投入和调配。
最重要一点,如果进程内的所有goroutine是在单个线程内运行,那么chan的锁是不需要的。这样才能真正发挥coroutine的优势。现在的go编译器似乎还没有对这个做优化,不知将来是否会进化。
总之,并发方面还没有一改而论

猜你喜欢

转载自my.oschina.net/mickelfeng/blog/1630419