a是全域變數,初值為0
兩條執行緒回圈執行
{
a++;
a--;
}
最后的結果非0
奇怪的是,當a++在前時,結果總是正的
a--在前時,結果總是負的
a++和a--都只有一潭訓編碼
即便執行緒切換,a++和a--執行的次數應該都是不變的,最后應該回到0才對呀
而且交換a++和a--的順序所造成的怪異現象又如何解釋?
值的修改都是直接在記憶體中進行的,沒有經過暫存器
我目前能想到的原因,執行a--后,資料還沒修改時,已經切換了執行緒并執行了a++,然后前面的a--作廢
哪位大佬給個真相?
uj5u.com熱心網友回復:
執行緒的話我記得好像是同時執行的,不加鎖的情況拿到的就是原來的變數值而不是先改變后再拿到改變后的值uj5u.com熱心網友回復:
沒做好同步吧,執行緒執行執行緒體執行的先后以及次數是隨機的。因此要么做好同步,要么確定好執行的先后uj5u.com熱心網友回復:
a++和a--都只有一潭訓編碼 ---> 這樣理解是錯誤的,從匯編角度上是三潭訓編碼實際上的例子
MOV EAX, [g_x] ; Thread 1: Move 0 into a register.
INC EAX ; Thread 1: Increment the register to 1.
MOV [g_x], EAX ; Thread 1: Store 1 back in g_x.
MOV EAX, [g_x] ; Thread 2: Move 1 into a register.
INC EAX ; Thread 2: Increment the register to 2.
MOV [g_x], EAX ; Thread 2: Store 2 back in g_x.
After both threads are done incrementing g_x, the value in g_x is 2. This is great and is exactly what we expect: take zero (0), increment it by 1 twice, and the answer is 2. Beautiful. But wait— Windows is a preemptive, multithreaded environment.
So a thread can be switched away from at any time and another thread might continue executing at any time. So the preceding code might not execute exactly as I've written it. Instead, it might execute as follows:
MOV EAX, [g_x] ; Thread 1: Move 0 into a register.
INC EAX ; Thread 1: Increment the register to 1.
MOV EAX, [g_x] ; Thread 2: Move 0 into a register.
INC EAX ; Thread 2: Increment the register to 1.
MOV [g_x], EAX ; Thread 2: Store 1 back in g_x.
MOV [g_x], EAX ; Thread 1: Store 1 back in g_x.
If the code executes this way, the final value in g_x is 1—not 2 as you expect!
----------
這個是反匯編的結果
a++
mov ecx, [ebp+var_4]
add ecx, 1
mov [ebp+var_4], ecx
uj5u.com熱心網友回復:
生成幾條指令取決于編譯器和編譯選項(debug、release,優化級別之類的),也可能是:add/sub [addr], 1
或者
inc/dec [addr]
之類的
不管編譯器生成幾條指令,在多處理器/多核系統中不是原子操作,這是在多執行緒下出現臟讀/臟寫的原因,要保證讀/改/寫的原子化要使用lock前綴+單指令
uj5u.com熱心網友回復:
我是除錯時查看的,這也是反編譯出來的吧,我的匯編碼是這樣的:
=> 0x000000000040153d <+13>: mov DWORD PTR [rbp-0x4],0x0
0x0000000000401544 <+20>: add DWORD PTR [rbp-0x4],0x1
第一個是初始化的0,第二個是a++
uj5u.com熱心網友回復:
是執行緒同步的問題吧?uj5u.com熱心網友回復:
話說我看匯編碼,只有a++這類運算才有lock,生成的匯編碼也很多,而a=1或a=b這類運算卻沒有lock,這是什么情況?
uj5u.com熱心網友回復:
a++、a--不是原子操作,不加鎖肯定有問題啊。就算只有一條指令,當兩個執行緒同時執行,也一樣會有競爭問題uj5u.com熱心網友回復:
#include <iostream>
#include <atomic>
#include <thread>
using namespace std;
atomic_llong a{ 0 };
void func1() {
for (int i = 0; i < 100000; i++) {
++a;
}
}
void func2() {
for (int i = 0; i < 100000; i++)
{
--a;
}
}
int main()
{
thread t1(func1);
thread t2(func2);
t1.join();
t2.join();
cout << a << endl;
}
uj5u.com熱心網友回復:
對變數a不使用a++而是使用a=a+1,我看匯編碼里沒有lock了,這時它還有原子操作的功能嗎?是不是對于原子變數的操作都應該使用原子操作庫?
uj5u.com熱心網友回復:
如果多執行緒寫同一個變數,肯定需要使用原子操作,如果多讀一寫,通常情況下不需要,除非該變數的位置跨越cache lineuj5u.com熱心網友回復:
我之前說的就是原子型別,a++生成的匯編碼有lock,但是a=1卻沒有,而且前者生成的匯編碼很多,后者很少。關鍵是我試了C11的原子操作庫來給變數賦值,生成的匯編碼竟然也沒有lock,這是不是說明實際上并沒有達到原子的效果?
uj5u.com熱心網友回復:
我是在探究問題出現的原因,而不是如何避免這類問題
uj5u.com熱心網友回復:
操作原子變數是否都應該使用原子操作庫?如果匯編碼中沒有lock指令是否就意味著沒有實作原子操作?
uj5u.com熱心網友回復:
不管生成多少條指令, 各個核心操作的資料是自己的高速快取, 如果不使用高速快取一致性協議, 對同一個變數, 不同核心讀到的同一個變數值本來就很可能不一致. 在 x86 上, 加 lock 前綴則使用高速快取一致性協議通知其他核心重繪快取狀態, 在 ARM 上用 ldrex/strex, RISCV 上 lr/sc ..大部分的U上, 只有寫的時候需要通知其他核心快取失效, 讀的時候就隨意啦 ...
uj5u.com熱心網友回復:
如果沒有lock是否就意為著不是原子操作?我看原子操作庫函式生成的匯編碼也沒有lock,只有a++這類自增自減運算才有
uj5u.com熱心網友回復:
單行a++的話,如果a是整數,應該是原子操作吧?按說編譯器應該能優化吧?不就是inc指令嗎?uj5u.com熱心網友回復:
.file "test.c".text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $0, -4(%rbp)
addl $1, -4(%rbp)
subl $1, -4(%rbp)
nop
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-36)"
.section .note.GNU-stack,"",@progbits
如果是整數資料,GCC就算不優化,也是單潭訓編指令,VS就不行了,搞了3條。。。。
uj5u.com熱心網友回復:
凡是涉及記憶體訪問的指令都不是原子操作(并非單指令就是原子操作),但是實作多執行緒條件下原子操作并非必須用lock前綴,使用lock前綴是輕量級、高性能,但是硬體依賴的方法。也可以用Semaphore、Multex、Critical Section之類的來實作一段代碼串行化。uj5u.com熱心網友回復:
“凡是涉及記憶體訪問的指令都不是原子操作”這句話是指所有情況,還是排除了lock等方案的情況?
uj5u.com熱心網友回復:
凡是涉及記憶體訪問的指令都不是原子操作,所以為了原子化需要加lock前綴使用Semaphore、Multex、Critical Section之類的來實作一段代碼串行化,在串行化代碼中訪問共享變數相當于單執行緒操作,沒有其他執行緒競爭,所以訪問共享變數的指令不需要加lock前綴
uj5u.com熱心網友回復:
lock 是比 CS PV 更基本的同步原語, 最基本的內核態的 SPINLOCK 也就關閉本核心任務調度, 回圈 CAS 設定個標志位, 如果 lock 不是原子的, 根本就無法實作 自旋鎖..uj5u.com熱心網友回復:
按照樓主的描述,如果代碼塊沒做同步,是不可能得到理想狀態的0的,在匯編層級上,a++比++a還多一條指令。百思不得其解的時候可能是犯了低級錯誤了,主執行緒是不是沒有join子執行緒?這樣的話主執行緒帶了runtime提供的退出行程代碼,不管其它執行緒什么狀態都一律通殺,代碼執行是不可能完整的uj5u.com熱心網友回復:
我前面都貼了匯編碼了,都是一條指令,a++和++a包括a+=1都被生成了同一條指令。
我有同步啊,不然哪來的結果
不過我又不是在探究同步、探究如何實作正確運算,我是在探究單條指令仍會出錯的原因,因為我之前猜測的是執行緒切換,應該不會出現單條命令的運行錯誤,現在知道了,是多個處理器在同時運行,即兩條執行緒在真正同時運行,不是切換造成的。
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/29362.html
標籤:C語言
上一篇:無法運行
