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倍。所以加锁操作在保证了并发访问正确性同时,大大增加了程序运行时间。所以我们在多进程共享资源并发访问程序设计时候,需要综合考虑程序的正确性和效率。