线程安全的代码需要一些额外注意的点,总结一下。
增加注释
增加与线程安全相关的注释
在示例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;
}