我们在前面文章中已经分析了多线程VS多进程,也分析了线程的使用,现在我们来讲解一下linux多线程编程之同步与互斥。
现在,我们不管究竟是多线程好还是多进程好,先讲解一下,为什么要使用多线程?
一、 为什么要用多线程技术?
1、避免阻塞,大家知道,单个进程只有一个主线程,当主线程阻塞的时候,整个进程也就阻塞了,无法再去做其它的一些功能了。
2、避免CPU空转,应用程序经常会涉及到RPC,数据库访问,磁盘IO等操作,这些操作的速度比CPU慢很多,而在等待这些响应时,CPU却不能去处理新的请求,导致这种单线程的应用程序性能很差。
3、提升效率,一个进程要独立拥有4GB的虚拟地址空间,而多个线程可以共享同一地址空间,线程的切换比进程的切换要快得多。
二、 如何使用多线程技术进行编程?
下面给出个多线程程序,一个最简单的模拟售票系统,代码如下:
#include <stdio.h> #include <pthread.h> void *ticketsell1(void *); void *ticketsell2(void *); int tickets = 20; int main() { pthread_t id1,id2; int error; error = pthread_create(&id1, NULL, ticketsell1, NULL); if(error != 0) { printf("pthread is not created!\n"); return -1; } error = pthread_create(&id2, NULL, ticketsell2, NULL); if(error != 0) { printf("pthread is not created!\n"); return -1; } pthread_join(id1,NULL); pthread_join(id2,NULL); return 0; } void *ticketsell1(void *arg) { while(1) { if(tickets > 0) { // usleep(1000); printf("ticketse1 sells ticket:%d\n",tickets--); } else { break; } } return (void *)0; } void *ticketsell2(void *arg) { while(1) { if(tickets > 0) { // usleep(1000); printf("ticketse2 sells ticket:%d\n",tickets--); } else { break; } } return (void *)0; }
执行结果如下:
fs@ubuntu:~/qiang/mthread$ ./mthread1 ticketse2 sells ticket:20 ticketse2 sells ticket:19 ticketse2 sells ticket:18 ticketse2 sells ticket:17 ticketse2 sells ticket:16 ticketse2 sells ticket:15 ticketse2 sells ticket:14 ticketse2 sells ticket:13 ticketse2 sells ticket:12 ticketse2 sells ticket:11 ticketse2 sells ticket:10 ticketse2 sells ticket:9 ticketse2 sells ticket:8 ticketse2 sells ticket:7 ticketse2 sells ticket:6 ticketse2 sells ticket:4 ticketse2 sells ticket:3 ticketse2 sells ticket:2 ticketse2 sells ticket:1 ticketse1 sells ticket:5
看到结果,我们发现时能正常卖票的,一部分连续是sel2,另一部分是ticketsel1;
此时,其实存在一个隐含的问题,就是线程间的切换,在单CPU系统中,CPU是有时间片时间,时间片到了,就要执行其它的线程,假设thread1执行到if里面,但在printf执行前发生了线程切换,那么会发生什么呢?我们在这里用usleep函数(放开程序中的usleep注释行)进行强制模拟切换;
我们看看结果:
fs@ubuntu:~/qiang/mthread$ gcc -o mthread1 mthread1.c -lpthread fs@ubuntu:~/qiang/mthread$ ./mthread1 ticketse2 sells ticket:20 ticketse1 sells ticket:19 ticketse2 sells ticket:18 ticketse1 sells ticket:17 ticketse2 sells ticket:16 ticketse1 sells ticket:15 ticketse2 sells ticket:14 ticketse1 sells ticket:13 ticketse2 sells ticket:12 ticketse1 sells ticket:11 ticketse2 sells ticket:10 ticketse1 sells ticket:9 ticketse2 sells ticket:8 ticketse1 sells ticket:7 ticketse2 sells ticket:6 ticketse1 sells ticket:5 ticketse2 sells ticket:4 ticketse1 sells ticket:3 ticketse1 sells ticket:2 ticketse2 sells ticket:1 ticketse1 sells ticket:0 fs@ubuntu:~/qiang/mthread$
运行程序发现竟然有0号票被卖出了,这显然是错误的!当thread1的if里面发生线程切换时,thread2得到运行,把最后一张票卖了,此时thread1恢复运行,结果卖出了0号票,这里我们需要的是火车票的票数数据对于所有线程而言是同步的,所以就要用到线程同步技术了。
三、 使用多线程的同步与互斥
1、多线程的同步方式有很多种,例如互斥锁,条件变量,信号量,读写锁。先看看互斥锁如何解决多线程之间的同步问题。程序用互斥锁后如下:
#include <stdio.h> #include <pthread.h> void *ticketsell1(void *); void *ticketsell2(void *); int tickets = 20; pthread_mutex_t mutex; int main() { pthread_t id1,id2; pthread_mutex_init(&mutex, NULL);// int error; error = pthread_create(&id1, NULL, ticketsell1, NULL); if(error != 0) { printf("pthread is not created!\n"); return -1; } error = pthread_create(&id2, NULL, ticketsell2, NULL); if(error != 0) { printf("pthread is not created!\n"); return -1; } pthread_join(id1,NULL); pthread_join(id2,NULL); return 0; } void *ticketsell1(void *arg) { while(1) { pthread_mutex_lock(&mutex);//给互斥量上锁 if(tickets > 0) { usleep(1000); printf("ticketse1 sells ticket:%d\n",tickets--); pthread_mutex_unlock(&mutex);//给互斥量解锁 } else { pthread_mutex_unlock(&mutex);//给互斥量解锁 break; } pthread_yield();//线程调度函数,使每个线程都有执行机会 } return (void *)0; } void *ticketsell2(void *arg) { while(1) { pthread_mutex_lock(&mutex);//给互斥量上锁 if(tickets > 0) { usleep(1000); printf("ticketse2 sells ticket:%d\n",tickets--); pthread_mutex_unlock(&mutex);//给互斥量解锁 } else { pthread_mutex_unlock(&mutex);//给互斥量解锁 break; } pthread_yield();//线程调度函数,是两个线程都有执行机会 } return (void *)0; }
执行结果如下:
fs@ubuntu:~/qiang/mthread$ vi mthread1.c fs@ubuntu:~/qiang/mthread$ gcc -o mthread1 mthread1.c -lpthread fs@ubuntu:~/qiang/mthread$ ./mthread1 ticketse2 sells ticket:20 ticketse1 sells ticket:19 ticketse2 sells ticket:18 ticketse1 sells ticket:17 ticketse2 sells ticket:16 ticketse1 sells ticket:15 ticketse2 sells ticket:14 ticketse1 sells ticket:13 ticketse2 sells ticket:12 ticketse1 sells ticket:11 ticketse2 sells ticket:10 ticketse1 sells ticket:9 ticketse2 sells ticket:8 ticketse1 sells ticket:7 ticketse2 sells ticket:6 ticketse1 sells ticket:5 ticketse2 sells ticket:4 ticketse1 sells ticket:3 ticketse2 sells ticket:2 ticketse1 sells ticket:1
2、再看看用信号量来解决多线程的同步问题,程序代码如下:
#include <stdio.h> #include <pthread.h> #include <semaphore.h> void *ticketsell1(void *); void *ticketsell2(void *); int tickets = 20; sem_t mutex,full; int main() { pthread_t id1,id2; int error; int ret; ret = sem_init(&mutex, 0 ,1);//初始化mutex信号量为1 ret += sem_init(&full, 0 ,0);//初始化full信号量为0 if(ret != 0) { printf("sem_init fails!\n"); } error = pthread_create(&id1, NULL, ticketsell1, NULL); if(error != 0) { printf("pthread is not created!\n"); return -1; } error = pthread_create(&id2, NULL, ticketsell2, NULL); if(error != 0) { printf("pthread is not created!\n"); return -1; } pthread_join(id1,NULL); pthread_join(id2,NULL); return 0; } void *ticketsell1(void *arg) { while(1) { sem_wait(&mutex);//mutex信号量进行P操作 if(tickets > 0) { usleep(1000); printf("ticketse1 sells ticket:%d\n",tickets--); sem_post(&full);//full信号量进行V操作 } else { sem_post(&full);//full信号量进行V操作 break; } } return (void *)0; } void *ticketsell2(void *arg) { while(1) { sem_wait(&full);//full信号量进行P操作 if(tickets > 0) { usleep(1000); printf("ticketse2 sells ticket:%d\n",tickets--); sem_post(&mutex);//mutex信号量进行V操作 } else { sem_post(&mutex);//mutex信号量进行V操作 break; } } return (void *)0; }
执行结果:
fs@ubuntu:~/qiang/mthread$ vi mthread1.c fs@ubuntu:~/qiang/mthread$ gcc -o mthread1 mthread1.c -lpthread fs@ubuntu:~/qiang/mthread$ ./mthread1 ticketse1 sells ticket:20 ticketse2 sells ticket:19 ticketse1 sells ticket:18 ticketse2 sells ticket:17 ticketse1 sells ticket:16 ticketse2 sells ticket:15 ticketse1 sells ticket:14 ticketse2 sells ticket:13 ticketse1 sells ticket:12 ticketse2 sells ticket:11 ticketse1 sells ticket:10 ticketse2 sells ticket:9 ticketse1 sells ticket:8 ticketse2 sells ticket:7 ticketse1 sells ticket:6 ticketse2 sells ticket:5 ticketse1 sells ticket:4 ticketse2 sells ticket:3 ticketse1 sells ticket:2 ticketse2 sells ticket:1 fs@ubuntu:~/qiang/mthread$
上面的sem_init函数用来初始化两个信号量的初始化值,这里一个设为1,一个设为0,sem_wait类似于P操作,让信号量减1,如果小于结果小于0,线程阻塞,否则线程继续执行,sem_post类似于V操作,提升信号量的值,加1,通过这两个信号量之间的互相“救对方”,就可以实现这两个线程的同步执行。
我们编译运行以上程序,发现两个售票点交替卖票,两个纯程依次得到机会执行,并且不会有0号票卖出,实现了同步。
3、我们再用条件变量来解决同步问题,一般条件变量需要结合互斥量一起使用,代码如下
#include <stdio.h> #include <pthread.h> #include <semaphore.h> void *ticketsell1(void *); void *ticketsell2(void *); int tickets = 20; pthread_mutex_t mutex; pthread_cond_t qready = PTHREAD_COND_INITIALIZER;//静态初始化条件变量; int main() { pthread_t id1,id2; pthread_mutex_init(&mutex, NULL); int error; error = pthread_create(&id1, NULL, ticketsell1, NULL); if(error != 0) { printf("pthread is not created!\n"); return -1; } error = pthread_create(&id2, NULL, ticketsell2, NULL); if(error != 0) { printf("pthread is not created!\n"); return -1; } pthread_join(id1,NULL); pthread_join(id2,NULL); return 0; } void *ticketsell1(void *arg) { pthread_mutex_lock(&mutex); while(tickets > 0) { if(tickets%2 == 1) { usleep(1000); printf("ticketse1 sells ticket:%d\n",tickets--); pthread_cond_signal(&qready);//条件改变,发送信号,通知ticketse2 } else { pthread_cond_wait(&qready,&mutex);//解开Mutex,并等待qready改变 } pthread_mutex_unlock(&mutex);//给互斥量解锁 } return (void *)0; } void *ticketsell2(void *arg) { pthread_mutex_lock(&mutex); while(tickets > 0) { if(tickets%2 == 0) { usleep(1000); printf("ticketse2 sells ticket:%d\n",tickets--); pthread_cond_signal(&qready);//条件改变,发送信号,通知ticketse1 } else { pthread_cond_wait(&qready,&mutex);//解开mutex,并等待qready改变 } pthread_mutex_unlock(&mutex);//给互斥量解锁 } return (void *)0; }
执行结果如下:
fs@ubuntu:~/qiang/mthread$ vi mthread1.c fs@ubuntu:~/qiang/mthread$ gcc -o mthread1 mthread1.c -lpthread fs@ubuntu:~/qiang/mthread$ ./mthread1 ticketse2 sells ticket:20 ticketse1 sells ticket:19 ticketse2 sells ticket:18 ticketse1 sells ticket:17 ticketse2 sells ticket:16 ticketse1 sells ticket:15 ticketse2 sells ticket:14 ticketse1 sells ticket:13 ticketse2 sells ticket:12 ticketse1 sells ticket:11 ticketse2 sells ticket:10 ticketse1 sells ticket:9 ticketse2 sells ticket:8 ticketse1 sells ticket:7 ticketse2 sells ticket:6 ticketse1 sells ticket:5 ticketse2 sells ticket:4 ticketse1 sells ticket:3 ticketse2 sells ticket:2 ticketse1 sells ticket:1 fs@ubuntu:~/qiang/mthread$
条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件变量发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线程间的同步.
函数pthread_cond_wait使线程阻塞在一个条件变量上,而函数pthread_cond_signal是用来释放被阻塞在条件变量上的一个线程。但是要注意的是,条件变量只是起到阻塞和唤醒线程的作用,具体的判断条件还需用户给出,我这里给出的是tickets是否是偶数这个条件。