一組併發執行緒執行在一個程式的上下文中,每個執行緒都有它自己獨立的執行緒上下文,例如:棧、程式計數器、執行緒ID、條件碼等,每個執行緒和其它的執行緒一起共享除此之外的程式上下文的剩餘部分,包括整個使用者的虛擬地址空間,當然也共享同樣的開啟的檔案的集合。,這裡有一點要特別注意,就是暫存器是從不共享的,而虛擬儲存器總是共享的。
有了共享就要防止在對共享變數進行操作的過程中得到一個不可知的值,在Linux核心中有個原子型別與原子操作這麼個概念,因為使用者態下沒有這麼一個原子操作存在,那麼在我們使用者態下就需要要對操作這個共享變數的執行緒進行同步。為什麼要進行同步呢?
因為假設我們在一個程式中有一個全域性變數cnt,初始值為0,接下去我們建立了兩個執行緒,完成的功能都是在一個迴圈中對這個變數進行+1操作,想象一下在這兩個執行緒操作完成後會出現什麼狀況。
假設我們這裡的max為10000,那麼我們想要得到的結果的結果當然是20000,可是在執行之後結果並不是我們所期望的20000,而是一個小於20000的值。為什麼會出現這個現象呢?
這裡就是我們為什麼需要對執行緒進行同步了。
因為在C語言的層面來說,cnt++就是一條語句,實際上我們在心裡預設把它當作了一個原子操作,事實上,就這麼一條操作語句,在彙編程式碼中是分三步執行的:
1)、將這個值從記憶體中取出來,放入一個暫存器中;
2)、將暫存器中的值進行+1操作;
3)、將暫存器中的值放入記憶體中去。
因為對與多執行緒來說我們不知道何時會執行哪個執行緒,所以執行的順序是不可知的。我們所想的是先讓一邊執行完,然後再開始執行另外一邊。
現在我們不妨將這個問題極端化,也就是兩執行緒交叉執行,假設左邊的執行執行緒為A,右邊為B,假設A先執行,A從記憶體中取出cnt的值,那麼現在在R1裡的值為0,接下去,A執行緒被B執行緒打斷,A停止執行,B開始執行,B又從記憶體中取出cnt的值,現在在R2中的值也為0。然後又輪到A執行,進行加1操作,則R1為1,接下去輪到B執行,進行加1操作,則R2為1。然後A將值寫回到記憶體中,B也將值寫回到記憶體中。這次我們知道記憶體中的值為1而並非我們所期望的2。
那麼怎麼能讓它進行正確的執行順序呢?同步,可以用加鎖來完成同步操作。
1 2 3 4 5 6 |
for(int i=0;i<max;i++) { P(&mutex); cnt++; V(&mutex); } |
在對cnt加1的操作時,對這個操作加鎖,這就意味著當下只有這一個執行緒執行這個操作,其它的執行緒都得等在外面,等這個執行緒解鎖出來,其他的執行緒才可以有機會進去。
加鎖之後我們再來看看上面的那張圖的執行過程,也假設是在一個極端的情況:
A先加鎖,然後完成那三個步驟(因為此時只有它一個執行緒有操作的許可權),解鎖;現在記憶體中的值為1,A加鎖,然後一樣完成三個步驟,解鎖;現在記憶體中的值為2。與所期望的相同。當然了,對於加鎖的問題還要防止出現死鎖現象,這裡就不討論了。