时运不济,命途多舛,最近 Bug 有点多。大概是这样的:
#include <cstring>
#include <memory>
#include <vector>
class A {
public:
void Update(const std::shared_ptr<std::vector<std::string>> &value) {
paths_ = value;
}
bool InPaths(const std::string &input) const {
auto paths = paths_.load();
for (auto &path : paths) {
if (input.length() < path.length()) {
continue;
}
int same = strncmp(&input[0], &path[0], path.length());
if (same == 0 &&
(input.length() == path.length() || input[path.length()] == '/')) {
return true;
}
}
return false;
}
private:
std::atomic_shared_ptr<std::vector<std::string>> paths_;
}
这里会判断输入的 input
是不是在 paths_
表示的路径下面。代码在多线程环境下执行,paths
为原子共享指针(参考 folly 实现),运行过程中会调用 Update
动态更新。在 gcc 4.8.2 C++11 环境下会 core dump。
在循环 for (auto &path : paths)
中加上常量修饰符 const
可以修复该 Bug。有兴趣可以再看看代码,想想是哪里的问题,下面将公布答案。
path.length()
本身是 const
的方法,path
是否是 const
并不受影响,所以只剩下 &path[0]
比较可疑了。查看对应的 std::string
源码,会发现 operator[]
比 operator[] const
多调用一个 _M_leak
方法,最后会执行 _M_mutate
:
template <typename _CharT, typename _Traits, typename _Alloc>
void basic_string<_CharT, _Traits, _Alloc>::_M_mutate(size_type __pos,
size_type __len1,
size_type __len2) {
const size_type __old_size = this->size();
const size_type __new_size = __old_size + __len2 - __len1;
const size_type __how_much = __old_size - __pos - __len1;
if (__new_size > this->capacity() || _M_rep()->_M_is_shared()) {
// Must reallocate.
const allocator_type __a = get_allocator();
_Rep *__r = _Rep::_S_create(__new_size, this->capacity(), __a);
if (__pos)
_M_copy(__r->_M_refdata(), _M_data(), __pos);
if (__how_much)
_M_copy(__r->_M_refdata() + __pos + __len2, _M_data() + __pos + __len1,
__how_much);
_M_rep()->_M_dispose(__a);
_M_data(__r->_M_refdata());
} else if (__how_much && __len1 != __len2) {
// Work in-place.
_M_move(_M_data() + __pos + __len2, _M_data() + __pos + __len1, __how_much);
}
_M_rep()->_M_set_length_and_sharable(__new_size);
}
嗯会有修改操作。查询得知 gcc 4.8 的 std::string
是 copy-on-write 的,在修改时才会去执行复制,多线程环境下就挂了。按照 C++11 的规范已经不允许这种 COW 了,gcc 5 以上的版本就不会存在这样的问题,但 gcc 4.8 没有完全按照标准规范实现。
验证 COW 也很简单,点击在线执行:
#include <cassert>
#include <iostream>
#include <string>
int main() {
std::string a = "Hello World";
std::string b = a; // does not do a deep copy
*const_cast<char *>(b.data()) = 'W'; // modify string a in fact
puts(a == b ? "copy on write" : "C++11 std::string");
return 0;
}
复现 COW 导致的 core dump:
#include <atomic>
#include <chrono>
#include <cstdint>
#include <iostream>
#include <string>
#include <thread>
#include <vector>
int main() {
std::string a = "hello world";
std::string b = a;
std::atomic<bool> f{false};
constexpr uint32_t N = 8;
std::vector<std::thread> threads;
for (uint32_t i = 0; i < N; ++i) {
threads.emplace_back([&] {
while (f.load() == false) {
}
auto p = &b[0]; // `b.data()` is ok
printf("%p\n", (void *)p);
});
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
f = true;
for (auto &thread : threads) {
thread.join();
}
}
除了增加 const
,也可以将 &path[0]
替换为 path.data()
,后者也是 const
方法。C++ 标准库中 const
方法通常是线程安全的,能加上 const
就都加上吧。