信号量称为进化版的互斥锁。由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。例如,有5台打印机被多个线程共同使用,如果使用互斥锁,则每次同时只能有一个打印机被一个线程使用,但是如果采用信号量,那么同时最多可有5个线程共同使用五台打印机,使得共享资源得到充分利用,程序并行性提高,效率更高。其实信号量初值为1时的情况就是互斥锁。信号量是互斥锁的加强版。
主要应用函数:
sem_init函数 sem_destroy函数 sem_wait函数
sem_trywait函数 sem_timedwait函数 sem_post函数
以上6 个函数的返回值都是:成功返回0,失败返回-1,同时设置errno。(注意,它们没有pthread前缀) 因为,信号量既可以用于线程间同步,也可以用于进程间同步,因此失败返回值为-1,同时置errno。
sem_t类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使用文件描述符)。
sem_t sem; 规定信号量sem不能 < 0。 头文件
(1)信号量基本操作
sem_wait: 信号量大于0,则信号量--(类比pthread_mutex_lock);信号量等于0,造成线程阻塞
sem_post:将信号量++,同时唤醒阻塞在信号量上的线程(类比pthread_mutex_unlock)。
由于sem_t的实现对用户隐藏,所以所谓的++、--操作只能通过函数来实现,而不能直接++、--符号。信号量的初值,决定了占用信号量的线程的个数。
(2)sem_init函数
int sem_init(sem_t *sem, int pshared, unsigned int value);
作用:初始化一个信号量。参1:sem信号量;参2:pshared取0用于线程间;取非0(一般为1)用于进程间;参3:value指定信号量初值。
(3)sem_destroy函数
int sem_destroy(sem_t *sem);
作用:销毁一个信号量
(4)sem_wait函数
int sem_wait(sem_t *sem);
作用:给信号量加锁
(5)sem_post函数
int sem_post(sem_t *sem);
作用:给信号量解锁 ++
(6)sem_trywait函数
int sem_trywait(sem_t *sem);
作用:尝试对信号量加锁--(与sem_wait的区别类比lock和trylock)
(7)sem_timedwait函数
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
作用:限时尝试对信号量加锁。参2:abs_timeout采用的是绝对时间(同前面)。
总结:互斥量、信号量既可以用于进程间同步,也可以用于线程间同步(一般来说,进程间同步信号量用的多一点);条件变量需要结合互斥量使用;读写锁用于线程间同步,而文件锁用于进程间同步。
(8)生产者消费者信号量模型
使用信号量完成线程间同步,模拟生产者,消费者问题。
分析:如果仓库中装满了产品,生产者不能生产,只能阻塞;仓库中没有产品,消费者不能消费,只能等待数据。
//两个消费者,一个生产者的情况
#include
#include
#include
#include
#include
#include
#define NUM 10 //仓库空间容量为10
int prdc[NUM] = {0}; //0值代表该位置为闲置(空)
sem_t sem_blank; //定义一个信号量表示闲置的空间
sem_t sem_prdc; //定义一个信号量表示产品的数量
int j=0; //两个消费者都需要消耗产品,因此定义全局变量j,两消费者互斥访问
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //互斥锁 用于互斥访问j
void *productor( void *arg ) //生产者
{
srand( time( NULL ) );
int i = 0;
while(1) {
sem_wait( &sem_blank );
prdc[i] = (rand( )%400+1);
printf("++++++++The production is %d.\n", prdc[i]);
sem_post( &sem_prdc ); //对全局变量的操作必须位于wait与post之间
i=( (i+1)%NUM );
sleep( rand( )%3 );
}
return NULL;
}
void *consumer( void *arg )
{
int s = (int)arg;
srand( time( NULL ) );
while(1) {
sem_wait( &sem_prdc );
pthread_mutex_lock( &mutex ); //互斥访问j
printf("I am the %dth consumer, and I consumed the product of %d.\n", s, prdc[j]);
prdc[j] = 0;
j =( (j+1)%NUM );
pthread_mutex_unlock( &mutex );
sem_post( &sem_blank );
sleep( rand( )%3 );
}
return NULL;
}
int main( void )
{
pthread_t pid, cid1, cid2;
int ret;
sem_init( &sem_blank, 0, NUM );
sem_init( &sem_prdc, 0, 0 );
ret = pthread_create( &pid, NULL, productor, NULL );
if( ret != 0 )
{
fprintf( stderr, "pthread_create error1: %s.\n", strerror(ret) );
exit(1);
}
ret = pthread_create( &cid1, NULL, consumer, (void *)1 );
if( ret != 0 )
{
fprintf( stderr, "pthread_create error2: %s.\n", strerror(ret) );
exit(1);
}
ret = pthread_create( &cid2, NULL, consumer, (void *)2 );
if( ret != 0 )
{
fprintf( stderr, "pthread_create error3: %s.\n", strerror(ret) );
exit(1);
}
pthread_join( pid, NULL );
pthread_join( cid1, NULL );
pthread_join( cid2, NULL );
sem_destroy( &sem_blank );
sem_destroy( &sem_prdc );
pthread_mutex_destroy( &mutex);
return 0;
}
[root@localhost 02_pthread_sync_test]# ./semaphore
++++++++The production is 331.
I am the 2th consumer, and I consumed the product of 331.
++++++++The production is 371.
I am the 1th consumer, and I consumed the product of 371.
++++++++The production is 213.
I am the 1th consumer, and I consumed the product of 213.
++++++++The production is 260.
I am the 2th consumer, and I consumed the product of 260.
++++++++The production is 296.
I am the 1th consumer, and I consumed the product of 296.
++++++++The production is 59.
++++++++The production is 118.
I am the 2th consumer, and I consumed the product of 59.
I am the 1th consumer, and I consumed the product of 118.
++++++++The production is 228.
I am the 1th consumer, and I consumed the product of 228.
++++++++The production is 27.
++++++++The production is 360.
++++++++The production is 190.
I am the 2th consumer, and I consumed the product of 27.
I am the 2th consumer, and I consumed the product of 360.
I am the 1th consumer, and I consumed the product of 190.
++++++++The production is 177.
++++++++The production is 211.
I am the 1th consumer, and I consumed the product of 177.
I am the 2th consumer, and I consumed the product of 211.
分析:
练习:结合生产者消费者信号量模型,揣摩sem_timedwait函数作用。编程实现,一个线程读用户输入, 另一个线程打印“hello world”。如果用户无输入,则每隔5秒向屏幕打印一个“hello world”;如果用户有输入,立刻打印“hello world”到屏幕。
#include
#include
#include
#include
#define N 1024
sem_t s;
void *tfn(void *arg)
{
char buf[N];
while (1) {
read(STDIN_FILENO, buf, N); //阻塞读
printf( " %s", buf);
sem_post(&s);
}
return NULL;
}
int main(void)
{
pthread_t tid;
struct timespec t = {0, 0};
sem_init(&s, 0, 0);
pthread_create(&tid, NULL, tfn, NULL);
t.tv_sec = time(NULL) + 1; //防止时间失效,使sem_timedwait参数无效
t.tv_nsec = 0;
while (1) {
sem_timedwait(&s, &t);
printf(" hello world\n");
t.tv_sec = time(NULL) + 5;
t.tv_nsec = 0;
}
pthread_join(tid, NULL);
sem_destroy(&s);
return 0;
}
分析:
理解read函数的作用。read系统调用从文件中读数据时,默认方式为阻塞读,即如果没有读取数据(没有遇到换行符(enter键或者结束符)),则会一直阻塞等待数据的到来。对于空文件,有结束符,因此read读取时直接返回结果,读取的字节数为0,结束符不会打印出来;对于从标准输入(键盘)读取数据时,只有按到了enter(换行符),则read才会读取到数据,而换行符会被打印出来。read函数阻塞的话,则信号量的值一直为0,则主控线程中每隔5s打印一次hello world;(此时sem_timedwait函数执行失败,返回-1,置errno)。一旦read读取了数据,则信号量加1,主控线程马上执行sem_timedwait(&s, &t);成功,返回0 ,且向屏幕打印一次hello world。