线程安全与锁使用-注意事项

线程安全的代码需要一些额外注意的点,总结一下。

增加注释

增加与线程安全相关的注释
在示例1,函数描述中说明了线程安全。同时说明了该函数存在的风险。

  • _renderMutex是递归锁
  • anchorP->SetInfoWindowImageData会回调AnchorLayer中的持_renderMutex的成员函数(反回调)
  • 子调用addAnchorPoint中持有_renderMutex锁
    示例1
		/**
             * @brief:导致使用[递归锁],反回调,thread-safe
             * @param
             * @return
             */
            void AnchorLayer::addAnchor(const string attrStr,
                                                         const unsigned char *infoWindowdata,
                                                         int width, int height) {
                lock_t lk(_renderMutex);
                AnchorPoint *anchorP = new AnchorPoint;
                anchorP->SetScene(m_scene);
                anchorP->SetLayer(this);
                anchorP->SetInfo(infoWindowdata, width, height); //反回调
                const string id = anchorP->parseJsonString(attrStr);
                addAnchorPoint(id, std::shared_ptr<AnchorPoint>(anchorP)); //hold _renderMutex
                UpdateAnchor(id,attrStr,infoWindowdata,width,height); //hold _renderMutex
                SetSomeAnchorZIndex(id, anchorP->_zindex); //hold _renderMutex
            }

持锁函数调用持锁函数

  • 直接调用
    示例1中,持锁函数addAnchor,直接调用了3个持锁函数addAnchorPoint,UpdateAnchor,SetSomeAnchorZIndex。这样你要么使用递归锁,要么要小心的控制解锁时机。

  • 序列调用
    示例1中, addAnchorPoint,UpdateAnchor,SetSomeAnchorZIndex这三个函数持有相同的锁,本来锁一次就可以了,但是重复加解锁3次,降低了效率。

  • 间接调用
    示例1中,AnchorLayer::addAnchor持有锁,其调用了anchorP->SetInfo,实际上SetInfo内部又回调了AnchorLayer的另一个持锁成员函数,这样你要么使用递归锁,要么要小心的控制解锁时机。

原子操作+原子操作!=线程安全

如下,Find和Add都是原子操作,即线程安全的。我还想实现一个方法,查找不到,就添加缓存。
Add_Not_Find = Find + Add,但Add_Not_Find是线程不安全的。Find + Add此时应该是一个原子操作,而不是两个原子操作。

class Cache
{
public:
	using mutex_t = std::mutex;
	using lock_t = std::unique_lock<mutex_t>;
	using map_t = std::map<int,int>;
	bool Find(int key)
	{
		lock_t lk(m_mutex);
		auto iter = m_cache.find(key);
		return iter != m_cache.end();

	}
	void Add(int key, int val)
	{
		lock_t lk(m_mutex);
		m_cache[key] = val;
	}
	void Add_Not_Find(int key, int val)
	{
		if (!Find(key)) //找不到就添加,线程不安全
		{
			//线程竞争点
			Add(key, val);
		}
	}
	void Add_Not_Find_Thread_safe(int key, int val)
	{
		lock_t lk(m_mutex);
		auto iter = m_cache.find(key);
		if (iter == m_cache.end()) //找不到就添加,线程安全
		{
			m_cache[key] = val;
		}
	}
private:
	map_t m_cache;
	mutex_t m_mutex;
};

返回值和锁

被调用的函数中持有锁,调用者中没有锁,那么返回值的获取是线程安全的。
如下,Find函数中有锁,但调用者无锁,返回值sptr的获取是线程安全的吗?
根据函数堆栈原理,返回值是在函数结束前进行了复制,所以返回值sptr的复制其实是在持有锁期间完成的,因此,这种情况下调用者不需要加锁。

class Cache
{
public:
	using sptr_t = std::shared_ptr<int>;
	using mutex_t = std::mutex;
	using lock_t = std::unique_lock<mutex_t>;
	using map_t = std::map<int, sptr_t>;
	sptr_t Find(int key)
	{
		lock_t lk(m_mutex);
		auto iter = m_cache.find(key);
		if (iter != m_cache.end())
			return iter->second;
		else
			return nullptr;

	}
	
private:
	map_t m_cache;
	mutex_t m_mutex;
};

int main(void)
{
	Cache c;
	std::thread t([&c](){
		auto sptr = c.Find(1);
	});
	auto sptr = c.Find(1);

}

外部可获取锁

不要提供锁的对外访问方式,容易出现bug。

public:
mutex_t &Mutex() { return m_mutex; }

锁的粒度不需要太细

粒度不需要太细
第一个函数中,先判断key是否为空,再加锁。
第二个函数中,开始便加锁。
虽然第一个函数的加锁粒度更细,但可读性降低,出错可能性增大

sptr_t Find(const std::string &key)
	{
		if (key.empty()) //先判断是否为空
			return nullptr;
		lock_t lk(m_mutex); //再加锁
		auto iter = m_cache.find(key);
		if (iter != m_cache.end())
			return iter->second;
		else
			return nullptr;

	}
	sptr_t Find(const std::string &key)
	{
		lock_t lk(m_mutex); //先加锁
		if (key.empty()) //再判单是否为空
			return nullptr;
		auto iter = m_cache.find(key);
		if (iter != m_cache.end())
			return iter->second;
		else
			return nullptr;

	}

你可能感兴趣的:(c++,线程安全,锁,多线程)