![b3f71535704db5f4b76d70bb72fca8bb.png](https://img-blog.csdnimg.cn/img_convert/b3f71535704db5f4b76d70bb72fca8bb.png)
总所周知:
- C++的STL中的容器都不是线程安全的
- 使用mutex保护数据可以使其线程安全
那么使用mutex + stl 一定可以组装出一个线程安全的容器。
如下所示,我们可以这么实现一个线程安全的stack
#include
我们可以在push,top, pop甚至empty的内部,使用mutex将stack s保护起来。使用std::lock_guard将mutex包起来,似乎一个线程安全的stack就完成了。
BUT!这样实现的stack并不是threadsafe的,魔鬼藏在细节中:
T& top();
top接口返回引用会使你精心保护的内部stack数据,泄漏到mutex的外部,从而导致在多线程访问时会出现问题,考虑如下情况:
![319ac60fb208f954dc5e7f69d0e0fd7d.png](https://img-blog.csdnimg.cn/img_convert/319ac60fb208f954dc5e7f69d0e0fd7d.png)
这样会导致额外的程序的崩溃,而对于下面这种情况,返回引用则会引起data race
![b998b888c8605258f11a03ef3df63982.png](https://img-blog.csdnimg.cn/img_convert/b998b888c8605258f11a03ef3df63982.png)
所以,在多线程时接口返回引用并不是一个好主意。
if (s.empty()) {
s.push(1);
}
上面的代码在单线程中work良好,当stack为空时,便向stack中push一个元素。但是在多线程中,上述代码可能会出现问题:
![d381cc66784c1e15b41b57737b5a9d74.png](https://img-blog.csdnimg.cn/img_convert/d381cc66784c1e15b41b57737b5a9d74.png)
可以看出由于empty+push非原子性操作,所以在多线程的情境下empty的结果并不可信。无法保证在push的时候empty的结果有效。
解决方案之一是在整个if 语句的外层增加一个mutex,将这部分代码保护起来。这种方案在某种程度上确实可以work,但是这这要求用户在使用的时候额外添加自己的锁,也不符合我们对于线程安全容器的定义。
在多线程的场景下,empty这种在没有更粗粒度的锁的情况下往往是无意义的。所以解决这一问题的另一方案就是提供语意多线程安全的接口,例如empty_push接口,如果stack为空,那么就push,否则就返回。这样我们通过在函数内部加锁可以使得这一操作多线程安全,而用户也无需增加额外的锁。
按照上述思路,我们可以组合出各种各样的接口,甚至丧心病狂的empty_push_pop_empty_push_pop()。当然这种方法在99.99999%的时候都是多余的。那么,如何才能设计出合理又简洁的接口呢?
hin遗憾,没有任何一个标准可以告诉你到底你应该提供哪些接口,如果非要又有一个,那就是看需求(手动狗头)。不过我们可以参考一下标准库的实现,看看他们提供了哪些接口。
第一个是C++的atomic,它是c++11中新加入的一个模板类型,可实例化为原子类型,它提供了如下的接口:
- store 替换atomic对象的值
- load 获取atomic对象的值
- exchange 交换并获取atomic对象先前的值
- compare-exchange-weak / compare-exchange-weak 比较原子与非原子的值,相等则交换,不相等则加载
- add-fetch 将参数加到atomic对象,并返回先前的值
第二个是golang的sync.map ,它提供了一个线程安全的map:
- load 获取key对应的value
- store 设置key对应的value
- loadOrStore key存在则load,不存在则store
- delete 删除k,v
- range 使用给定的func遍历map
通过比较,可以得出:
- 提供了store和load的方法,用于获取和设置数据
- 提供了功能相似的方法:compare-exchange vs. loadOrStore方法,提供了 加载-比较-操作-写回的功能
- 提供了各自个性化的方法:add-fetch vs. range
效仿这两个库,threadsafe_stack拟提供一下接口:
- push
- pop
- top-exchange
- empty_push
这样就能保证使用这不需要额外增加锁来保证接口的有效性。什么?你说有没有啥特有的方法?那你得去问问你得产品经理喽~
综上所述,对一个线程不安全的库进行线程安全的改造,仅仅使用mutex将原有的接口包裹起来是不足够的,有些在单线程下感觉良好的接口到了多线程中并不适合,需要重新设计。