當(dāng)前位置:首頁(yè) > 嵌入式培訓(xùn) > 嵌入式學(xué)習(xí) > 講師博文 > 為什么需要同步?
為什么需要同步?
時(shí)間:2018-09-26 來(lái)源:未知
1. 為什么需要同步?

上面的圖是從《高級(jí)編程》中截的圖,雖然它是針對(duì)線程的,但是這里要說(shuō)明,不僅僅線程要考慮這個(gè)問(wèn)題,只要涉及到并發(fā)的程序,都要考慮同步。比如多進(jìn)程共享內(nèi)存,比如某個(gè)驅(qū)動(dòng)會(huì)同時(shí)被打開(kāi),而且會(huì)被幾個(gè)進(jìn)程同時(shí)修改驅(qū)動(dòng)中的值或者寄存器......
原理上都是一樣的,多線程并發(fā)訪問(wèn)是一定要注意的,因?yàn)橥贿M(jìn)程的多個(gè)線程本身就共享進(jìn)程資源或者說(shuō)變量的內(nèi)存。就拿上圖來(lái)說(shuō),我們對(duì)i變量的值+1操作,那么這個(gè)簡(jiǎn)簡(jiǎn)單單的+1操作真正到了CPU上會(huì)怎么執(zhí)行呢?通常分為3步:
(1) 從內(nèi)存單元讀入寄存器
(2) 在寄存器上進(jìn)行變量值增加
(3) 把新的值寫(xiě)回內(nèi)存單元
這就導(dǎo)致了上圖的問(wèn)題,A線程在把i從內(nèi)存讀入寄存器改變過(guò)程中(還沒(méi)寫(xiě)回到內(nèi)存),B線程也對(duì)i做了同樣操作,以至于后結(jié)果就是讀入的都是5,寫(xiě)入的都是6,那么本來(lái)我們是要對(duì)i增加2次的,實(shí)際卻增加了1次。這種操作時(shí)間問(wèn)題可能發(fā)生在ns級(jí)別,但是以當(dāng)今處理器動(dòng)輒幾GHZ的速度來(lái)說(shuō),發(fā)生這種情況概率還是很大的。
2.驗(yàn)證試驗(yàn)
下面我們就做實(shí)驗(yàn)來(lái)實(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. }
程序很簡(jiǎn)單,就是創(chuàng)建兩個(gè)線程,然后每個(gè)線程分別對(duì)count增加40000000 值,這個(gè)值是我隨便選的,只要大一點(diǎn)就行,但是別超了2^32。而count1和count2分別來(lái)記錄兩個(gè)線程對(duì)count分別增加了多少次,其實(shí)有NUM控制就好了,不過(guò)為了對(duì)比,我們加入這兩個(gè)變量。主進(jìn)程創(chuàng)建兩個(gè)線程后我們用pthread_join函數(shù)來(lái)等待兩個(gè)線程執(zhí)行完畢,并打印三個(gè)值比較得出結(jié)果。
首先在PC機(jī)上看下結(jié)果,CPU是雙核2.6GHZ的,運(yùn)行環(huán)境是ubuntu,順便用time命令查看下執(zhí)行時(shí)間:

從上圖可以看出,兩個(gè)線程對(duì)count進(jìn)行總共80000000次累加大概需要2ms多一點(diǎn),測(cè)了6次有2次是有問(wèn)題的,即count != count1 + count2,概率還是比較大的。
然后我把相同的代碼重新編譯拿到AM335x(TI A8單核600MHZ)運(yùn)行,結(jié)果如下

這個(gè)時(shí)間程序耗時(shí)就明顯長(zhǎng)了,需要大概4s,本來(lái)我以為單核處理器出錯(cuò)概率會(huì)小,沒(méi)想到運(yùn)行5次結(jié)果居然全是錯(cuò)的。具體為什么會(huì)這樣沒(méi)去深究,猜想應(yīng)該和SMP機(jī)制及操作系統(tǒng)線程調(diào)度有關(guān)。這個(gè)結(jié)果更證明了線程同步的重要性,尤其是在嵌入式系統(tǒng)中。
3.同步問(wèn)題解決方案
既然問(wèn)題都明白了,接下來(lái)當(dāng)然是解決方案了,解決這種同步問(wèn)題經(jīng)典的方案就是鎖了,相信大部分人平時(shí)都用過(guò)。以linux線程庫(kù)提供的接口,代碼改為下面形式。
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í)思想很簡(jiǎn)單,就是在并發(fā)訪問(wèn)同一個(gè)變量時(shí)候,給這個(gè)共享變量加鎖,保證寫(xiě)操作的原子性即可。那么為什么count1和count2不用加鎖呢,因?yàn)閮蓚(gè)變量本身就只在兩個(gè)線程中分別操作,所以沒(méi)必要加鎖。
后來(lái)看下結(jié)果,問(wèn)題本身已經(jīng)解決了,但是這次重點(diǎn)不在結(jié)果上,而在程序執(zhí)行時(shí)間上。這是在PC上結(jié)果:

這是在ARM上的結(jié)果:

加了這個(gè)操作后,PC上同一程序運(yùn)行時(shí)間多了10倍,板子上多了6倍。所以加鎖操作在保證了并發(fā)訪問(wèn)正確性同時(shí),大大增加了程序運(yùn)行時(shí)間。所以我們?cè)诙噙M(jìn)程共享資源并發(fā)訪問(wèn)程序設(shè)計(jì)時(shí)候,需要綜合考慮程序的正確性和效率。
華清遠(yuǎn)見(jiàn)90+項(xiàng)目獲批!教育部2021最新協(xié)同育人項(xiàng)目名
華清遠(yuǎn)見(jiàn)榮獲2021騰訊教育“年度口碑影響力職業(yè)教育品
華清遠(yuǎn)見(jiàn)受邀參加2021年武漢民辦高校信息學(xué)科合作聯(lián)盟
溫暖同行共創(chuàng)佳績(jī) 2019華清遠(yuǎn)見(jiàn)北京總部年會(huì)大曝光
助力高校AI人工智能學(xué)科建設(shè) 華清遠(yuǎn)見(jiàn)人工智能師資班
華清遠(yuǎn)見(jiàn)受邀參加四川省物聯(lián)網(wǎng)年會(huì),榮獲優(yōu)秀企業(yè)專家
