當(dāng)前位置:首頁 > 嵌入式培訓(xùn) > 嵌入式學(xué)習(xí) > 講師博文 > 為什么需要同步?
1. 為什么需要同步?
上面的圖是從《高級編程》中截的圖,雖然它是針對線程的,但是這里要說明,不僅僅線程要考慮這個(gè)問題,只要涉及到并發(fā)的程序,都要考慮同步。比如多進(jìn)程共享內(nèi)存,比如某個(gè)驅(qū)動會同時(shí)被打開,而且會被幾個(gè)進(jìn)程同時(shí)修改驅(qū)動中的值或者寄存器......
原理上都是一樣的,多線程并發(fā)訪問是一定要注意的,因?yàn)橥贿M(jìn)程的多個(gè)線程本身就共享進(jìn)程資源或者說變量的內(nèi)存。就拿上圖來說,我們對i變量的值+1操作,那么這個(gè)簡簡單單的+1操作真正到了CPU上會怎么執(zhí)行呢?通常分為3步:
(1) 從內(nèi)存單元讀入寄存器
(2) 在寄存器上進(jìn)行變量值增加
(3) 把新的值寫回內(nèi)存單元
這就導(dǎo)致了上圖的問題,A線程在把i從內(nèi)存讀入寄存器改變過程中(還沒寫回到內(nèi)存),B線程也對i做了同樣操作,以至于后結(jié)果就是讀入的都是5,寫入的都是6,那么本來我們是要對i增加2次的,實(shí)際卻增加了1次。這種操作時(shí)間問題可能發(fā)生在ns級別,但是以當(dāng)今處理器動輒幾GHZ的速度來說,發(fā)生這種情況概率還是很大的。
2.驗(yàn)證試驗(yàn)
下面我們就做實(shí)驗(yàn)來實(shí)際看看這種情況。
看下程序:
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. }
程序很簡單,就是創(chuàng)建兩個(gè)線程,然后每個(gè)線程分別對count增加40000000 值,這個(gè)值是我隨便選的,只要大一點(diǎn)就行,但是別超了2^32。而count1和count2分別來記錄兩個(gè)線程對count分別增加了多少次,其實(shí)有NUM控制就好了,不過為了對比,我們加入這兩個(gè)變量。主進(jìn)程創(chuàng)建兩個(gè)線程后我們用pthread_join函數(shù)來等待兩個(gè)線程執(zhí)行完畢,并打印三個(gè)值比較得出結(jié)果。
首先在PC機(jī)上看下結(jié)果,CPU是雙核2.6GHZ的,運(yùn)行環(huán)境是ubuntu,順便用time命令查看下執(zhí)行時(shí)間:
從上圖可以看出,兩個(gè)線程對count進(jìn)行總共80000000次累加大概需要2ms多一點(diǎn),測了6次有2次是有問題的,即count != count1 + count2,概率還是比較大的。
然后我把相同的代碼重新編譯拿到AM335x(TI A8單核600MHZ)運(yùn)行,結(jié)果如下
這個(gè)時(shí)間程序耗時(shí)就明顯長了,需要大概4s,本來我以為單核處理器出錯(cuò)概率會小,沒想到運(yùn)行5次結(jié)果居然全是錯(cuò)的。具體為什么會這樣沒去深究,猜想應(yīng)該和SMP機(jī)制及操作系統(tǒng)線程調(diào)度有關(guān)。這個(gè)結(jié)果更證明了線程同步的重要性,尤其是在嵌入式系統(tǒng)中。
3.同步問題解決方案
既然問題都明白了,接下來當(dāng)然是解決方案了,解決這種同步問題經(jīng)典的方案就是鎖了,相信大部分人平時(shí)都用過。以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. }
只列出了部分代碼,其它的都一樣,其實(shí)思想很簡單,就是在并發(fā)訪問同一個(gè)變量時(shí)候,給這個(gè)共享變量加鎖,保證寫操作的原子性即可。那么為什么count1和count2不用加鎖呢,因?yàn)閮蓚(gè)變量本身就只在兩個(gè)線程中分別操作,所以沒必要加鎖。
后來看下結(jié)果,問題本身已經(jīng)解決了,但是這次重點(diǎn)不在結(jié)果上,而在程序執(zhí)行時(shí)間上。這是在PC上結(jié)果:
這是在ARM上的結(jié)果:
加了這個(gè)操作后,PC上同一程序運(yùn)行時(shí)間多了10倍,板子上多了6倍。所以加鎖操作在保證了并發(fā)訪問正確性同時(shí),大大增加了程序運(yùn)行時(shí)間。所以我們在多進(jìn)程共享資源并發(fā)訪問程序設(shè)計(jì)時(shí)候,需要綜合考慮程序的正確性和效率。