12.5 同步
如何实现不同多线程中的切换?如何保护那些共享的变量?
1、信号量机制:使用信号量实现同步
两组函数接口用于信号量:1)、取自POSIX的实时扩展,用于线程;2)、系统V信号量,用于进程的同步。
荷兰计算机科学家Dijkstra首先提出信号量的概念,信号量是一个特殊类型的变量,他可以被增加或者减少,但对其的关键访问被保证是原子操作,即使在一个多线程的程序中也是如此。则意味着如果一个程序中两个或者多个线程视图改变同一个信号量的值,系统将保证执行的顺序都是依次执行。
1)、最简单的信号量:二进制信号量
它只有0和1两个取值。
2)、计数型信号量:
他可以有更大范围的取值。
信号量一般用来保护一段代码,使其每次只能被一个线程访问,此时需要二进制信号量。有时希望允许有限的线程同时访问执行一段代码,此时需要计数型信号量。
信号量函数的名字都以sem_开头,线程中使用的基本信号量函数有4个:
#include <semaphore.h> int sem_init(sem_t *sem, int pshared,unsigned int value);
这个函数初始化sem指向的信号量的值,设置它的共享选项,并给他一个初始的整数值。pshared参数控制信号量的类型;如果它的值为0,就表示这个信号量是当前进程的局部信号量,否则这个信号量就可以在多个进程间共享。
#include <semaphore.h> int sem_wait(sem_t * sem); int sem_post(sem_t* sem);
两个函数都以一个指针为参数,该指针指向的对象是由sem_init调用初始化的信号量。
sem_post函数的作用是以原子操作的方式给信号量的值加1。所谓原子操作是指,如果两个线程企图同时给一个信号量加1,它们之间不会互相干扰,而不像如果两个程序同时对同一个文件进行读取、增加、写入操作时可能会引起冲突。信号量的值总是会被正确地加2,因为有两个线程试图改变它。
sem_wait函数以原子操作的方式将信号量的值减1,但它会等待直到信号量有个非零值才会开始减法操作。因此,如果对值为2的信号量调用sem_wait,线程将继续执行,但信号量的值会减到1。如果对值为0的信号量调用sem_wait,这个函数就会等待,直到有其他线程增加了该信号量的值使其不再是0为止。如果两个线程同时在sem_wait调用上等待同一个信号量变为非零值,那么当该信号量被第三个线程增加1时,只有其中一个等待线程将开始对信号量减1,然后继续执行,另外一个线程还将继续等待。
最后一个信号量函数是sem_destroy。这个函数的作用是,用完信号量后对它进行清理。它的定义如下:
#include <semaphore.h>
int sem_destory(sem_t * sem);
与前几个函数一样,这个函数也以一个信号量指针为参数,并清理该信号量拥有的所有资源。如果企图清理的信号量正被一些线程等待,就会收到一个错误。
实验一:使用信号量进行同步:
1 #include <stdlib.h> 2 #include <stdio.h> 3 #include <unistd.h> 4 #include <semaphore.h> 5 #include <pthread.h> 6 #include <string.h> 7 void * thread_function(void* arg); 8 sem_t bin_sem; 9 #define WORK_SIZE 1024 10 char work_area[WORK_SIZE]; 11 int main(int argc, char const *argv[]) 12 { 13 int res; 14 pthread_t a_thread; 15 void* thread_result; 16 //初始化信号量 17 res=sem_init(&bin_sem,0,0); 18 if(res!=0) 19 { 20 perror("Semaphore init failed"); 21 exit(EXIT_FAILURE); 22 } 23 //创建一个线程 24 res=pthread_create(&a_thread,NULL,thread_function,NULL); 25 if(res!=0) 26 { 27 perror("Create thread failed"); 28 exit(EXIT_FAILURE); 29 } 30 printf("input some text. Enter 'end' to finish\n"); 31 while(strncmp(work_area,"end",3)!=0) 32 { 33 fgets(work_area,WORK_SIZE,stdin); 34 printf("创建者释放一个信号量\n"); 35 sem_post(&bin_sem);//释放一个信号量 36 } 37 printf("\nWaiting for thread to finish\n"); 38 res=pthread_join(a_thread,&thread_result); 39 if(res!=0) 40 { 41 perror("Thread join failed"); 42 exit(EXIT_FAILURE); 43 } 44 printf("Thread joined\n"); 45 sem_destroy(&bin_sem); 46 exit(EXIT_SUCCESS); 47 return 0; 48 } 49 void* thread_function(void* arg) 50 { 51 printf("线程申请信号量\n"); 52 sem_wait(&bin_sem); 53 printf("申请信号量成功\n"); 54 while(strncmp("end",work_area,3)!=0) 55 { 56 printf("You input %d :characters\n",(int)strlen(work_area)); 57 printf("线程申请信号量\n"); 58 sem_wait(&bin_sem); 59 printf("申请信号量成功\n"); 60 } 61 pthread_exit(NULL); 62 }
该程序有一个全局变量:work_area,用来接受用户的输入;创建一个线程用来输出用户输入的字符串的长度。
创建线程后,线程执行thread_function,申请信号量,此时,在主程序中,用户还没有书输入数据,线程进入阻塞状态,当用户输入数据后,主程序释放一个信号量,此时线程成功的得到了信号量,就可以执行后面的代码。
2、使用互斥量进行同步
互斥量允许程序员锁住某个对象,使得每次只能有一个线程访问。为了控制对关键代码的访问,必须在进入这段代码之前锁住一个互斥量,然后在完成操作之后解锁它。
函数定义:
#include <pthread.h> int pthread_mutex_init(pthread_mutex_t * mutex,const pthread_mutexattr_t* mutexattr); int pthread_mutex_lock(pthread_mutex_t* mutex); int pthread_mutex_unlock(pthread_mutex_t* mutex); int pthread_mutex_destory(pthread_mutex_t* mutex);
与其他函数一样,成功时返回0,失败时将返回错误代码,但这些函数并不设置errno,你必须对函数的返回代码进行检查。
与信号量类似,这些函数的参数都是一个先前声明过的对象的指针。对互斥量来说,这个对象的类型为pthread_mutex_t。pthread_mutex_init函数中的属性参数允许我们设置互斥量的属性,而属性控制着互斥量的行为。属性类型默认为fast,但它有一个小缺点:如果程序试图对一个已经加了锁的互斥量调用pthread_mutex_lock,程序就会被阻塞,而又因为拥有互斥量的这个线程正是现在被阻塞的线程,所以互斥量就永远也不会被解锁了,程序也就进入死锁状态。这个问题可以通过改变互斥量的属性来解决,我们可以让它检查这种情况并返回一个错误,或者让它递归的操作,给同一个线程加上多个锁,但必须注意在后面执行同等数量的解锁操作。
实验二:
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<unistd.h> 4 #include<string.h> 5 #include<semaphore.h> 6 #include<pthread.h> 7 void* thread_function(void*arg); 8 pthread_mutex_t work_mutex; 9 #define WORK_SIZE 1024 10 char work_area[WORK_SIZE]; 11 int time_to_exit=0; 12 int main() 13 { 14 int res; 15 pthread_t a_thread; 16 void* thread_res; 17 res=pthread_mutex_init(&work_mutex,NULL);//初始化互斥量 18 if(res!=0) 19 { 20 perror("Init mutex failed"); 21 exit(EXIT_FAILURE); 22 } 23 //创建线程 24 res=pthread_create(&a_thread,NULL,thread_function,NULL); 25 if(res!=0) 26 { 27 perror("Create thread failed"); 28 exit(EXIT_FAILURE); 29 } 30 //锁住互斥量 31 pthread_mutex_lock(&work_mutex); 32 printf("Input some text. Ente 'end' to finish\n"); 33 while(!time_to_exit) 34 { 35 fgets(work_area,WORK_SIZE,stdin); 36 pthread_mutex_unlock(&work_mutex);//解锁互斥量 37 while(1) 38 { 39 pthread_mutex_lock(&work_mutex); 40 if(work_area[0]!='\0') 41 { 42 pthread_mutex_unlock(&work_mutex); 43 sleep(1); 44 } 45 else 46 break; 47 } 48 } 49 pthread_mutex_unlock(&work_mutex); 50 printf("\nWaiting for thread to finish...\n"); 51 res=pthread_join(a_thread,&thread_res); 52 if(res!=0) 53 { 54 perror("Thread join error"); 55 exit(EXIT_FAILURE); 56 } 57 printf("Thread joined\n"); 58 pthread_mutex_destroy(&work_mutex); 59 exit(EXIT_SUCCESS); 60 return 0; 61 } 62 void* thread_function(void* arg) 63 { 64 sleep(1); 65 pthread_mutex_lock(&work_mutex); 66 while(strncmp("end",work_area,3)!=0) 67 { 68 printf("You input %d characters\n",(int)strlen(work_area)-1); 69 work_area[0]='\0'; 70 pthread_mutex_unlock(&work_mutex); 71 sleep(1); 72 pthread_mutex_lock(&work_mutex); 73 while(work_area[0]=='\0') 74 { 75 pthread_mutex_unlock(&work_mutex); 76 sleep(1); 77 pthread_mutex_lock(&work_mutex); 78 } 79 } 80 time_to_exit=1; 81 work_area[0]='\0'; 82 pthread_mutex_unlock(&work_mutex); 83 pthread_exit(0); 84 }
新线程首先试图对互斥量加锁。如果它已经被锁住,这个调用将被阻塞直到它被释放为止。一旦获得访问权,我们就检查是否有申请退出程序的请求。如果有,就设置time_to_exit变量,再把工作区的第一个字符设置为\0,然后退出。
如果不想退出,就统计字符个数,然后把work_area数组中的第一个字符设置为null。我们用将第一个字符设置为null的方法通知读取输入的线程,我们已完成了字符统计。然后解锁互斥量并等待主线程继续运行。我们将周期性地尝试给互斥量加锁,如果加锁成功,就检查是否主线程又有字符送来要处理。如果还没有,就解锁互斥量继续等待;如果有,就统计字符个数并再次进入循环。