1. 為什么需要同步?
上面的圖是從《高級編程》中截的圖,雖然它是針對線程的,但是這里要說明,不僅僅線程要考慮這個問題,只要涉及到并發的程序,都要考慮同步。比如多進程共享內存,比如某個驅動會同時被打開,而且會被幾個進程同時修改驅動中的值或者寄存器......
原理上都是一樣的,多線程并發訪問是一定要注意的,因為同一進程的多個線程本身就共享進程資源或者說變量的內存。就拿上圖來說,我們對i變量的值+1操作,那么這個簡簡單單的+1操作真正到了CPU上會怎么執行呢?通常分為3步:
(1) 從內存單元讀入寄存器
(2) 在寄存器上進行變量值增加
(3) 把新的值寫回內存單元
這就導致了上圖的問題,A線程在把i從內存讀入寄存器改變過程中(還沒寫回到內存),B線程也對i做了同樣操作,以至于后結果就是讀入的都是5,寫入的都是6,那么本來我們是要對i增加2次的,實際卻增加了1次。這種操作時間問題可能發生在ns級別,但是以當今處理器動輒幾GHZ的速度來說,發生這種情況概率還是很大的。
2.驗證試驗
下面我們就做實驗來實際看看這種情況。
看下程序:
1. #include
2. #include
3. #include
4. #include
5.
6. #define NUM 40000000
7.
8. pthread_t tid1;
9. pthread_t tid2;
10.
11. unsigned int count1 = 0;
12. unsigned int count2 = 0;
13. unsigned int count = 0;
14.
15. void * thr_fn1(void *arg)
16. {
17. while(count1
18. {
19. count++;
20. count1++;
21. }
22. }
23.
24. void * thr_fn2(void *arg)
25. {
26. while(count2
27. {
28. count++;
29. count2++;
30. }
31. }
32.
33. int main(void)
34. {
35. int err;
36.
37. err = pthread_create(&tid1, NULL, thr_fn1, NULL);
38. if (err != 0)
39. perror("can't create thread1");
40.
41. err = pthread_create(&tid2, NULL, thr_fn2, NULL);
42. if (err != 0)
43. perror("can't create thread2");
44.
45. pthread_join(tid1, NULL);
46. pthread_join(tid2, NULL);
47.
48. printf("count = %u, count1 = %u, count2 = %u\n", count, count1, count2);
49. exit(0);
50. }
程序很簡單,就是創建兩個線程,然后每個線程分別對count增加40000000 值,這個值是我隨便選的,只要大一點就行,但是別超了2^32。而count1和count2分別來記錄兩個線程對count分別增加了多少次,其實有NUM控制就好了,不過為了對比,我們加入這兩個變量。主進程創建兩個線程后我們用pthread_join函數來等待兩個線程執行完畢,并打印三個值比較得出結果。
首先在PC機上看下結果,CPU是雙核2.6GHZ的,運行環境是ubuntu,順便用time命令查看下執行時間:
從上圖可以看出,兩個線程對count進行總共80000000次累加大概需要2ms多一點,測了6次有2次是有問題的,即count != count1 + count2,概率還是比較大的。
然后我把相同的代碼重新編譯拿到AM335x(TI A8單核600MHZ)運行,結果如下
這個時間程序耗時就明顯長了,需要大概4s,本來我以為單核處理器出錯概率會小,沒想到運行5次結果居然全是錯的。具體為什么會這樣沒去深究,猜想應該和SMP機制及操作系統線程調度有關。這個結果更證明了線程同步的重要性,尤其是在嵌入式系統中。
3.同步問題解決方案
既然問題都明白了,接下來當然是解決方案了,解決這種同步問題經典的方案就是鎖了,相信大部分人平時都用過。以linux線程庫提供的接口,代碼改為下面形式。
1. #define NUM 40000000
2. pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
3.
4. pthread_t tid1;
5. pthread_t tid2;
6.
7. unsigned int count1 = 0;
8. unsigned int count2 = 0;
9. unsigned int count = 0;
10.
11. void * thr_fn1(void *arg)
12. {
13. while(count1
14. {
15. pthread_mutex_lock(&lock);
16. count++;
17. pthread_mutex_unlock(&lock);
18.
19. count1++;
20. }
21. }
22.
23. void * thr_fn2(void *arg)
24. {
25. while(count2
26. {
27. pthread_mutex_lock(&lock);
28. count++;
29. pthread_mutex_unlock(&lock);
30.
31. count2++;
32. }
33. }
只列出了部分代碼,其它的都一樣,其實思想很簡單,就是在并發訪問同一個變量時候,給這個共享變量加鎖,保證寫操作的原子性即可。那么為什么count1和count2不用加鎖呢,因為兩個變量本身就只在兩個線程中分別操作,所以沒必要加鎖。
后來看下結果,問題本身已經解決了,但是這次重點不在結果上,而在程序執行時間上。這是在PC上結果:
這是在ARM上的結果:
加了這個操作后,PC上同一程序運行時間多了10倍,板子上多了6倍。所以加鎖操作在保證了并發訪問正確性同時,大大增加了程序運行時間。所以我們在多進程共享資源并發訪問程序設計時候,需要綜合考慮程序的正確性和效率。