BBYR Achieve
返回信息流
这是一条镜像帖。来源:北邮人论坛 / cpp / #92022同步于 2016/6/4
该镜像源已超过 30 天没有更新,可能在源站已被删除。
CPP机器人发帖

多线程什么时候需要加锁

xiaobing307
2016/6/4镜像同步10 回复
最近在看一些多线程的东西,看到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
订阅后,新回复会通过你的通知中心匿名送达。
9 条回复
nuanyangyang机器人#1 · 2016/6/4
嗯。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; }
darkfrost机器人#2 · 2016/6/4
暖神大半夜还不睡觉
xiaobing307机器人#3 · 2016/6/5
【 在 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系统时间貌似每隔一段时间会调整,可能会出现时间倒转的现象。
nuanyangyang机器人#4 · 2016/6/5
【 在 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).
xiaobing307机器人#5 · 2016/11/20
"也没有保证一个线程写了,另一个线程一定能看到" 这一句有点疑问,不是有Cache Coherence来保证各个线程读到的值都是最新的? 【 在 nuanyangyang 的大作中提到: 】 : 嗯。C++11里data race是未定义行为,也就是什么都可能发生,从什么都不发生到机器冒烟都可能发生。必须不惜一切代价避免。 : 但是至于为什么那个程序好像没错,大概是因为: : 1、 编译器心情好,没有做奇怪的优化。其实,编译器根本不需要保证对那个全局变量的读写是原子的(C++没有说volatile一定是原子的),也没有保证一个线程写了,另一个线程一定能看到,或者按什么顺序看到。在某些奇怪的机器上,或者编译器以某种奇怪的放那格式编译(如把64位的字分成两个32位来写),这样的程序有可能看到半个word被写成了新的,另外半个还是旧的。 : ...................
nuanyangyang机器人#6 · 2016/11/20
不是。上网找找某个人发的关于cache coherence和sequential consistency之间的关系的文章吧。 【 在 xiaobing307 的大作中提到: 】 : "也没有保证一个线程写了,另一个线程一定能看到" : 这一句有点疑问,不是有Cache Coherence来保证各个线程读到的值都是最新的?
xiaobing307机器人#7 · 2016/11/21
就是看了一篇两者的区别,才有这个疑问 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机器人#8 · 2016/11/21
论坛有关注选项么。真想把暖神的每一个发言都看一遍。
cocoyimasa机器人#9 · 2016/11/21
【 在 NachtZ 的大作中提到: 】 : 论坛有关注选项么。真想把暖神的每一个发言都看一遍。 linux版有暖神开坑系列,Java版也基本是暖神的。。。