For The Best Thing In The World

Work Hard to Enjoy Them. bloom energy

写多线程的小技巧

  1. 可以使用golang的race-detector检查资源冲突, 但最重要还是全局考虑资源分配问题.

  2. 在debug多线程时,发现一个自己锁自己的bug.

func (p *RemoteConnPool) CloseMindsockConn(conn *Conn) error {
	p.mutex.RLock()
	defer p.mutex.RUnlock()
	for _, c := range p.Conns {
		if c.MindConn() == conn {
			p.Close(c)
		}
	}
	return nil
}

func (p *RemoteConnPool) Close(c RemoteConn) error {
	if c == nil {
		return nil
	}
	p.mutex.Lock()
	defer p.mutex.Unlock()
	if p.OnConnCloser != nil {
		p.OnConnClose(c.Id())
	}
	delete(p.Conns, c.Id())
	return c.Close()
}

在CloseMindsockConn里全局加了读锁遍历map,在==conn的条件时到Close函数里,Close函数里加了写锁删除map里的东西,结果就block了,死锁。

##解决方法: 不用全局读锁,在要使用close函数时把读锁去掉,用完又锁回来

即上述情况变成:

func (p *RemoteConnPool) CloseMindsockConn(conn *Conn) error {
	p.mutex.RLock()
	for _, c := range p.Conns {
		if c.MindConn() == conn {
			p.mutex.RUnlock()
			p.Close(c)
			p.mutex.RLock()
		}
	}
	p.mutex.RUnlock()
	return nil
}

朋友Googol Lee告诉我, 可以在写的时候有个约定,所有私有方法只考虑无竞争的情况,公开方法考虑如何在使用的时候消除竞争 基本的原则是分离责任,所有私有方法只考虑逻辑,不考虑并发,这样编写和测试都相对容易。公开函数考虑并发,实现的逻辑委托给对应的私有函数完成。 所以表现上,有可能会出现同时存在Close和close两个方法,其中公开的那个会保证外部调用时的锁维护,加锁后直接调用私有方法完成逻辑

在上面的情况下,就是close给CloseMindsockConn使用,不加锁也不会有冲突,也就不会自己锁自己 公开Close给外部使用,加锁,这样问题就好了


	for i := 0; i < connCount/2; i++ {
		go func() {
			conn := NewConn(&websocket.Conn{})
			mux.Lock()
			defer mux.Unlock()
			c := ConnPool.NewBaseRemoteConn(conn)
			remoteConns[c.id] = c
		}()
	}

	mux.RLock()
	defer mux.RUnlock()
	for ConnPool.ConnsCount() < connCount || len(remoteConns) < connCount {
		time.Sleep(time.Millisecond)
	}

此代码乍看没有问题,实际上由于开了很多线程都持有读锁改变remoteConns, 而下面for循环遍历读取remoteConns这个map的length, 所以条件不满足的时候,会一直循环,又由于持有读锁,所以上面的线程并不能写, 自然map也无法添加. 于是一直死循环.

##解决方法:

锁的范围太大了, 我们只是想对if里的条件加锁, 所以把条件抽象成函数,在里面加锁就好了.

	for isCreateConnsFinished() {
		time.Sleep(time.Millisecond)
	}

func isCreateConnsFinished() bool {
	mux.RLock()
	defer mux.RUnlock()
	return (ConnPool.ConnsCount() < connCount || len(remoteConns) < connCount)
}