關於cpu在執行過程中爲了提高效率可能交換指令的情況
最近在看《程序員的自我修養》一書,看到線程安全的部分,發現cpu在執行過程中,爲了提高效率有可能交換指令的順序,比如下面的代碼:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
int x = 0, y = 0;
int a = 0, b = 0;
void* thread1(void* arg) {
x = 1;
a = y;
}
void* thread2(void* arg) {
y = 1;
b = x;
}
int main(int argc, char** argv) {
int times = 0;
for(; ;) {
x = 0, y = 0, a = 0, b = 0;
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, (void *)thread1, NULL);
pthread_create(&tid2, NULL, (void *)thread2, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
times++;
if (a==0 && b==0) {
printf("a=%d, b=%d times=%d\n", a, b, times);
break;
}
}
return 0;
}
從邏輯上講,這個代碼執行起來應該是一個死循環,因爲單個線程裏面代碼都是順序執行的,不可能出現a和b同時爲0的情況。但是實際執行的過程中,卻發現不是死循環,而且每次times的值都不相同,這就很詭異了。無論是加鎖還是怎麼弄都不行,都會出現a和b同時爲0的情況,而出現a和b都爲0的情況只可能是線程裏面的代碼交換了位置,比如:
void* thread1(void* arg) {
a = y;
x = 1;
}
說明了cpu在執行過程中,爲了提高效率是有可能交換指令順序的(不過該代碼在單核cpu中執行發現,仍然是一個死循環,說明需要並行執行才行)。
因此要保證線程安全,阻止cpu換序是必需的,pthread庫中提供了屏障(barrier)這種多線程並行工作的同步機制。其實pthread_join函數就是一種屏障,允許一個線程等待,直到另一個線程退出。
pthread庫中提供了pthread_barrier_init,pthread_barrier_wait,pthread_barrier_destroy這三個屏障使用的函數,調用pthread_barrier_init初始化時,需要傳入count,代表在允許所有線程繼續允許之前,必須到達的線程數目,調用pthread_barrier_wait的線程在屏障計數未滿足條件時,會進入休眠狀態。如果該線程時最後一個調用pthread_barrier_wait的線程,就滿足了屏障計數,所有的線程都被喚醒,具體代碼如下:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
int x = 0, y = 0;
int volatile a = 0;
int volatile b = 0;
pthread_barrier_t barrier;
void* thread1(void* arg) {
x = 1;
pthread_barrier_wait(&barrier);
a = y;
}
void* thread2(void* arg) {
y = 1;
pthread_barrier_wait(&barrier);
b = x;
}
int main(int argc, char** argv) {
int times = 0;
int rc = pthread_barrier_init(&barrier, NULL, 2);
if (rc) {
fprintf(stderr, "pthread_barrier_init: %s\n", strerror(rc));
exit(1);
}
for(; ;) {
x = 0, y = 0, a = 0, b = 0;
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, (void *)thread1, NULL);
pthread_create(&tid2, NULL, (void *)thread2, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
times++;
if (a==0 && b==0) {
printf("a=%d, b=%d times=%d\n", a, b, times);
break;
}
}
// pthread_barrier_destroy(&barrier);
return 0;
}
之前有"神牛"指出由於亂序的影響,單例模式下的DCL(double checked locking)也是靠不住的,可以看下這個文章 http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html,其實java使用volatile即可,因爲volatile就實現了jvm的內存屏障。