返回信息流最近在看一些多线程的东西,看到data race是未定义行为,应当尽量避免。
如果对于一份数据,同时有线程在读和写,一定需要用同步机制(比如加锁)吗?
比如下面这个程序,实现一个简单的计数器,不加锁,是data race,我怎么感觉没什么问题呢???
#include <thread>
#include <windows.h>
#include <iostream>
volatile unsigned long long _g_counter = 0;
void thr1() {
while (1) {
Sleep(1000);
++_g_counter;
}
}
void do_something(int i) {
std::cout << i << std::endl;
Sleep(3000);
}
void thr2() {
for (int i = 0; i < 10; ++i) {
unsigned long long begin = _g_counter;
do_something(i);
unsigned long long end = _g_counter;
std::cout << "loop " << i << " used time: " << end - begin << std::endl;
}
}
int main(int argc, char *argv[]) {
std::thread t1(thr1);
std::thread t2(thr2);
t1.join();
t2.join();
return 0;
}
@nuanyangyang
这是一条镜像帖。来源:北邮人论坛 / cpp / #92022同步于 2016/6/4
该镜像源已超过 30 天没有更新,可能在源站已被删除。
CPP机器人发帖
多线程什么时候需要加锁
xiaobing307
2016/6/4镜像同步10 回复
订阅后,新回复会通过你的通知中心匿名送达。
9 条回复
嗯。C++11里data race是未定义行为,也就是什么都可能发生,从什么都不发生到机器冒烟都可能发生。必须不惜一切代价避免。
但是至于为什么那个程序好像没错,大概是因为:
1、 编译器心情好,没有做奇怪的优化。其实,编译器根本不需要保证对那个全局变量的读写是原子的(C++没有说volatile一定是原子的),也没有保证一个线程写了,另一个线程一定能看到,或者按什么顺序看到。在某些奇怪的机器上,或者编译器以某种奇怪的放那格式编译(如把64位的字分成两个32位来写),这样的程序有可能看到半个word被写成了新的,另外半个还是旧的。
2、 你在x86_64上运行。这个机器上,只要内存空间没有跨越16字节边界(内存似乎每16字节一个line),那么读和写就都是原子的。而且,x86_64绝对不会调换两个相邻的读操作,也不会调换两个相邻的写操作,也不会把先读后写的两个操作调换。而且保证cache coherence。在这么强的内存模型的机器上,很难重现某些并发bug。
3、 那个++运算不是原子的,但是你的整个进程里只有一个线程在进行写操作,而且那个读的线程并不在意自己看到了什么(就算看到的值“不是最新的”,thr2怎么知道?就算thr1被操作系统不小心暂停了1分钟,thr2又怎么知道读到的值“正确”不“正确”?这个程序,thr1和thr2之间其实没有同步的需要。这种用另一个线程来sleep然后写变量的方法,不是可靠的计时方法。),所以并不会发生两个++互相叠加,然后发现少加了一个1。
下面是你的程序的不会让机器冒烟的版本。需要支持c++11的编译器。在linux下的编译方法:
g++ -Wall -pedantic -O3 -pthread -o counter counter.cpp
#include <thread>
#include <atomic>
#include <iostream>
#include <chrono>
using namespace std::chrono_literals;
std::atomic_ullong _g_counter(0LL);
std::atomic_bool _g_thr1_should_stop(false);
void thr1() {
while (!_g_thr1_should_stop) { // 用_g_counter.load()也行
std::this_thread::sleep_for(100ms);
_g_counter++; // atomic型的++运算符是原子的。
// 用_g_counter+=1 也是原子的
// 用_g_counter.fetch_add(1LL)也是原子的
// 但long long x = _g_counter; _g_counter = x+1 不是
}
}
void do_something(int i) {
std::cout << i << std::endl;
std::this_thread::sleep_for(300ms);
}
void thr2() {
for (int i = 0; i < 10; ++i) {
unsigned long long begin, end;
begin = _g_counter; // 用_g_counter.load()也行
do_something(i);
end = _g_counter; // 用_g_counter.load()也行
std::cout << "loop " << i << " used time: " << end - begin << std::endl;
}
}
int main(int argc, char *argv[]) {
std::thread t1(thr1);
std::thread t2(thr2);
t2.join();
_g_thr1_should_stop = true; // 用_g_thr1_should_stop.store(true)也行
t1.join();
return 0;
}
下面这个程序有两个线程同时不断地加一。如果不用原子操作,到最后会发现总和不是两个线程分别加的结果。
#include <thread>
#include <atomic>
#include <iostream>
#include <chrono>
#include <string>
using namespace std::chrono_literals;
std::atomic_ullong _g_counter(0LL);
void adder(uint64_t num, bool atomically) {
unsigned long long i;
if (atomically) {
for (i = 0; i < num; i++) {
_g_counter.fetch_add(1LL, std::memory_order_relaxed);
}
} else {
for (i = 0; i < num; i++) {
unsigned long long tmp = _g_counter.load(std::memory_order_relaxed);
_g_counter.store(tmp + 1, std::memory_order_relaxed);
}
}
}
int main(int argc, char *argv[]) {
bool atomically = true;
if (argc == 2) {
std::string argv1 = argv[1];
if (argv1 == "--no-atomic") {
atomically = false;
}
}
auto time1 = std::chrono::steady_clock::now();
std::thread t1(adder, 100000000LL, atomically);
std::thread t2(adder, 200000000LL, atomically);
t1.join();
t2.join();
auto time2 = std::chrono::steady_clock::now();
auto diff = std::chrono::duration_cast<std::chrono::milliseconds>(time2 - time1);
unsigned long long result = _g_counter.load(std::memory_order_relaxed);
std::cout << "result: " << result << std::endl;
std::cout << "time: " << diff.count() << " ms" << std::endl;
return 0;
}
【 在 nuanyangyang 的大作中提到: 】
3、 那个++运算不是原子的,但是你的整个进程里只有一个线程在进行写操作,而且那个读的线程并不在意自己看到了什么(就算看到的值“不是最新的”,thr2怎么知道?就算thr1被操作系统不小心暂停了1分钟,thr2又怎么知道读到的值“正确”不“正确”?这个程序,thr1和thr2之间其实没有同步的需要。这种用另一个线程来sleep然后写变量的方法,不是可靠的计时方法。),所以并不会发生两个++互相叠加,然后发现少加了一个1。
"这种用另一个线程来sleep然后写变量的方法,不是可靠的计时方法。"
1. 为什么不可靠呢?
之前看过某个公司线上的代码,计时就是这么写的,既没有加锁,也没有用原子操作。一个线程每隔100ms写一次全局变量_g_counter,其他好几个线程在发包和收包的时候会读这个全局变量,计算网络时延。
linux环境,gcc编译。_g_counter类型是static volatile unsigned int,sleep是用的nanosleep 。
2. 如果不可靠,该用什么方法来计时呢?
gettimeofday ? linux系统时间貌似每隔一段时间会调整,可能会出现时间倒转的现象。
【 在 xiaobing307 的大作中提到: 】
: "这种用另一个线程来sleep然后写变量的方法,不是可靠的计时方法。"
: 1. 为什么不可靠呢?
因为操作系统的调度是很随机的。并不是说要自己sleep 1秒钟,它就会准确地在1秒钟后醒过来。任务比较重的时候调度可能会更不可靠。
: 之前看过某个公司线上的代码,计时就是这么写的,既没有加锁,也没有用原子操作。一个线程每隔100ms写一次全局变量_g_counter,其他好几个线程在发包和收包的时候会读这个全局变量,计算网络时延。
: linux环境,gcc编译。_g_counter类型是static volatile unsigned int,sleep是用的nanosleep 。
首先为这个公司默哀。
在C++11出现之前,C++语言里并没有原子类型。所以要实现原子类型和多线程,就必须利用编译器提供的私有扩展。比如gcc早就有__sync_fetch_and_add (https://gcc.gnu.org/onlinedocs/gcc-4.4.3/gcc/Atomic-Builtins.html) 在那个时候,假定x86_64上的64位读写是原子的,用volatile防治编译器把这个变量去掉,然后用gcc的扩展函数做fetch_add,也是没有办法的办法。但是那个_g_counter++是个硬伤,这个一定不是原子的。
: 2. 如果不可靠,该用什么方法来计时呢?
: gettimeofday ? linux系统时间貌似每隔一段时间会调整,可能会出现时间倒转的现象。
C++11有了chrono库(标准库之一),就像我写的那样就行。
如果要用操作系统的接口,linux上有clock_gettime,并选用CLOCK_MONOTONIC时钟类型即可(http://linux.die.net/man/3/clock_gettime)。
gettimeofday的男人页里有这么一段:
Notes
The time returned by gettimeofday() is affected by discontinuous jumps in the system time (e.g., if the system administrator manually changes the system time). If you need a monotonically increasing clock, see clock_gettime(2).
"也没有保证一个线程写了,另一个线程一定能看到"
这一句有点疑问,不是有Cache Coherence来保证各个线程读到的值都是最新的?
【 在 nuanyangyang 的大作中提到: 】
: 嗯。C++11里data race是未定义行为,也就是什么都可能发生,从什么都不发生到机器冒烟都可能发生。必须不惜一切代价避免。
: 但是至于为什么那个程序好像没错,大概是因为:
: 1、 编译器心情好,没有做奇怪的优化。其实,编译器根本不需要保证对那个全局变量的读写是原子的(C++没有说volatile一定是原子的),也没有保证一个线程写了,另一个线程一定能看到,或者按什么顺序看到。在某些奇怪的机器上,或者编译器以某种奇怪的放那格式编译(如把64位的字分成两个32位来写),这样的程序有可能看到半个word被写成了新的,另外半个还是旧的。
: ...................
不是。上网找找某个人发的关于cache coherence和sequential consistency之间的关系的文章吧。
【 在 xiaobing307 的大作中提到: 】
: "也没有保证一个线程写了,另一个线程一定能看到"
: 这一句有点疑问,不是有Cache Coherence来保证各个线程读到的值都是最新的?
就是看了一篇两者的区别,才有这个疑问
http://www.parallellabs.com/2010/03/06/why-should-programmer-care-about-sequential-consistency-rather-than-cache-coherence/
sc指每个线程内部的代码不会乱序执行,可不管怎么乱序执行,cc的语义不就是保证cpu在执行读指令的时候取到的值是最新的? 也就是说乱序以后,每个线程真正读的时候取到的值都是最新的,于是所有线程看到的值都是最新的
【 在 nuanyangyang 的大作中提到: 】
: 不是。上网找找某个人发的关于cache coherence和sequential consistency之间的关系的文章吧。
【 在 NachtZ 的大作中提到: 】
: 论坛有关注选项么。真想把暖神的每一个发言都看一遍。
linux版有暖神开坑系列,Java版也基本是暖神的。。。