【发布时间】:2012-06-05 16:55:19
【问题描述】:
我想从 并行 运行线程访问基于 STL 的容器只读。不使用任何用户实现的锁定。以下代码的基础是 C++11,并正确实现了该标准。
http://gcc.gnu.org/onlinedocs/libstdc++/manual/using_concurrency.html
http://www.sgi.com/tech/stl/thread_safety.html
http://www.hpl.hp.com/personal/Hans_Boehm/c++mm/threadsintro.html
http://www.open-std.org/jtc1/sc22/wg21/(current draft 或 N3337,本质上是 C++11,修正了小错误和错别字)
23.2.2 容器数据竞争 [container.requirements.dataraces]
为了避免数据竞争 (17.6.5.9),实现应 将以下函数视为 const:begin、end、rbegin、 撕裂,前,后,数据,查找,下界,上界,相等范围, 在并且,除了在关联或无序关联容器中, 运算符[]。
尽管有 (17.6.5.9),但仍需要实现 当包含对象的内容在 同一序列中的不同元素,除了vector
,是 同时修改。 [ 注意:对于尺寸更大的向量
x 大于一,x[1] = 5 和 *x.begin() = 10 可以同时执行 没有数据竞争,但执行了 x[0] = 5 和 *x.begin() = 10 同时可能导致数据竞争。作为一般的例外 规则,对于 vector y,y[0] = true 可能与 y[1] 竞争 =真。 ——尾注]
和
17.6.5.9 避免数据竞争 [res.on.data.races] 1 本节规定了实现应满足的要求以防止数据竞争 比赛(1.10)。每个标准库函数都应满足每个 要求,除非另有规定。实施可能会阻止 除了下面指定的情况之外的数据竞争。
2 C++ 标准 库函数不得直接或间接访问对象 (1.10) 可由当前线程以外的线程访问,除非 对象通过函数的直接或间接访问 参数,包括这个。
3 C++ 标准库函数应 不直接或间接修改线程可访问的对象(1.10) 除了当前线程之外,除非直接访问对象 或间接通过函数的非常量参数,包括 这个。
4 [ 注意:例如,这意味着实现不能 将静态对象用于内部目的而无需同步 因为即使在不支持的程序中,它也可能导致数据竞争 在线程之间显式共享对象。 ——尾注]
5 C++ 标准库函数不得间接访问对象 可通过其参数或通过其容器的元素访问 参数,除非通过调用其规范所需的函数 在那些容器元素上。
6 获得的迭代器上的操作 调用标准库容器或字符串成员函数可能
访问底层容器,但不得修改它。 [注:在 特别是使迭代器无效的容器操作冲突 对与该容器关联的迭代器进行操作。 - 结尾 注意]7 实现可以在它们之间共享它们自己的内部对象 如果对象对用户不可见且受保护,则线程 反对数据竞争。
8 除非另有说明,C++ 标准库 功能应仅在当前执行所有操作 如果这些操作具有可见的效果(1.10),则线程 用户。
9 [注意:这允许实现并行化操作 如果没有明显的副作用。 ——尾注]
结论
容器不是线程安全的!但是从多个并行线程对容器调用 const 函数 是安全的。因此可以在没有锁定的情况下从并行线程执行只读操作。
我说的对吗?
我假设他们不存在任何错误的实现,并且 C++11 标准的每个实现都是正确的。
示例:
// concurrent thread access to a stl container
// g++ -std=gnu++11 -o p_read p_read.cpp -pthread -Wall -pedantic && ./p_read
#include <iostream>
#include <iomanip>
#include <string>
#include <unistd.h>
#include <thread>
#include <mutex>
#include <map>
#include <cstdlib>
#include <ctime>
using namespace std;
// new in C++11
using str_map = map<string, string>;
// thread is new in C++11
// to_string() is new in C++11
mutex m;
const unsigned int MAP_SIZE = 10000;
void fill_map(str_map& store) {
int key_nr;
string mapped_value;
string key;
while (store.size() < MAP_SIZE) {
// 0 - 9999
key_nr = rand() % MAP_SIZE;
// convert number to string
mapped_value = to_string(key_nr);
key = "key_" + mapped_value;
pair<string, string> value(key, mapped_value);
store.insert(value);
}
}
void print_map(const str_map& store) {
str_map::const_iterator it = store.begin();
while (it != store.end()) {
pair<string, string> value = *it;
cout << left << setw(10) << value.first << right << setw(5) << value.second << "\n";
it++;
}
}
void search_map(const str_map& store, int thread_nr) {
m.lock();
cout << "thread(" << thread_nr << ") launched\n";
m.unlock();
// use a straight search or poke around random
bool straight = false;
if ((thread_nr % 2) == 0) {
straight = true;
}
int key_nr;
string mapped_value;
string key;
str_map::const_iterator it;
string first;
string second;
for (unsigned int i = 0; i < MAP_SIZE; i++) {
if (straight) {
key_nr = i;
} else {
// 0 - 9999, rand is not thread-safe, nrand48 is an alternative
m.lock();
key_nr = rand() % MAP_SIZE;
m.unlock();
}
// convert number to string
mapped_value = to_string(key_nr);
key = "key_" + mapped_value;
it = store.find(key);
// check result
if (it != store.end()) {
// pair
first = it->first;
second = it->second;
// m.lock();
// cout << "thread(" << thread_nr << ") " << key << ": "
// << right << setw(10) << first << setw(5) << second << "\n";
// m.unlock();
// check mismatch
if (key != first || mapped_value != second) {
m.lock();
cerr << key << ": " << first << second << "\n"
<< "Mismatch in thread(" << thread_nr << ")!\n";
exit(1);
// never reached
m.unlock();
}
} else {
m.lock();
cerr << "Warning: key(" << key << ") not found in thread("
<< thread_nr << ")\n";
exit(1);
// never reached
m.unlock();
}
}
}
int main() {
clock_t start, end;
start = clock();
str_map store;
srand(0);
fill_map(store);
cout << "fill_map finished\n";
// print_map(store);
// cout << "print_map finished\n";
// copy for check
str_map copy_store = store;
// launch threads
thread t[10];
for (int i = 0; i < 10; i++) {
t[i] = thread(search_map, store, i);
}
// wait for finish
for (int i = 0; i < 10; i++) {
t[i].join();
}
cout << "search_map threads finished\n";
if (store == copy_store) {
cout << "equal\n";
} else {
cout << "not equal\n";
}
end = clock();
cout << "CLOCKS_PER_SEC " << CLOCKS_PER_SEC << "\n";
cout << "CPU-TIME START " << start << "\n";
cout << "CPU-TIME END " << end << "\n";
cout << "CPU-TIME END - START " << end - start << "\n";
cout << "TIME(SEC) " << static_cast<double>(end - start) / CLOCKS_PER_SEC << "\n";
return 0;
}
这段代码可以用 GCC 4.7 编译并在我的机器上运行良好。
$ 回声 $?
$ 0
【问题讨论】:
-
简而言之:如果你从不改变容器本身,并且如果每个线程都访问一个 distinct 容器元素,那么你很好。如果您没有后一种保证,则需要为每个元素配备自己的同步机制。
-
仍然允许
const方法写入带有mutable关键字的字段,不是吗? -
@mkb 是的,但是 STL 容器背后的想法是不应该有任何这样的“状态”变量,如果有,那么就有一个明确的同步机制就地保护底层的
mutable变量。换句话说,最终用户应该能够将容器用作“黑盒”,并假设如果“黑盒”容器上没有数据竞争,那么容器中就没有数据竞争。容器本身。 -
@Jason 解释得很好,我只想补充两点:(1)要求不仅适用于容器,还适用于所有标准库类型; (2) 库不仅必须使用适当的同步来确保
const操作对mutable状态所做的任何修改都是无竞争的,而且还必须同步对static数据和不同对象之间共享的其他状态的任何修改,这样对不同对象的写入是无竞争的,即使它们碰巧在幕后共享状态。
标签: c++ multithreading stl c++11 containers