一直对信号量和互斥锁只有一个模糊的认识,今天特别学习了,总结一下
一、从作用上来讲
互斥锁是用在多线程多任务互斥的
信号量用于线程的同步
二、从原理上讲
线程互斥锁pthread_mutex_t的实现原理:
在Linux下,信号量和线程互斥锁的实现都是通过futex系统调用。
Futex 由一块能够被多个进程共享的内存空间(一个对齐后的整型变量)组成;这个整型变量的值能够通过汇编语言调用CPU提供的原子操作指令来增加或减少,并且一个进程可以等待直到那个值变成正数。Futex的操作几乎全部在应用程序空间完成;只有当操作结果不一致从而需要仲裁时,才需要进入操作系统内核空间执行。这种机制允许使用 futex的锁定原语有非常高的执行效率:由于绝大多数的操作并不需要在多个进程之间进行仲裁,所以绝大多数操作都可以在应用程序空间执行,而不需要使用(相对高代价的)内核系统调用。
源码实现
pthread_mutex_lock:
atomic_dec(pthread_mutex_t.value);
if(pthread_mutex_t.value!=0)
futex(WAIT)
else
success
pthread_mutex_unlock:
atomic_inc(pthread_mutex_t.value);
if(pthread_mutex_t.value!=1)
futex(WAKEUP)
else
success
信号量sem_t的实现原理(直接从glibc/nptl/DESIGN-sem.txt中摘的):
显示代码打印
sem_wait(sem_t*sem)
{
for (;;) {
if(atomic_decrement_if_positive(sem->count))
break;
futex_wait(&sem->count, 0)
}
}
sem_post(sem_t*sem)
{
n = atomic_increment(sem->count);
// Pass the new value of sem->count
futex_wake(&sem->count, n + 1);
}
从源码看出
1.pthread_mutex_lock 操作一定会将互斥锁减一,因此互斥锁可以为负
sem_wait 操作会判断sem if positive,也就是是否大于0,不大于0则futex_wait(&sem->count, 0) 等待,因此信号量一定>=0
2.互斥锁只有在1->0的过程是不会锁住的,其他情况均futex(WAIT),也就是在临界资源处“锁住”了
信号量只有在大于0的时候是不会锁住,其他情况都会被锁
3.pthread_mutex_unlock会将互斥锁加1,sem_post也一样,会将信号量加1
因此在错误的使用信号量,多次调用mutex_unlock或sem_post都会导致锁或信号量值多加1,就起不到锁的效果了
三、使用
1.互斥锁的使用
pthread_mutex_t mutex; //定义锁
pthread_mutex_init(&mutex, NULL); //默认属性初始化锁
pthread_mutex_lock(&mutex); //申请锁
pthread_mutex_unlock(&mutex); //释放锁
设置锁的属性
函数pthread_mutexattr_setpshared和函数pthread_mutexattr_settype用来设置互斥锁属性。
前一个函数设置属性pshared,它有两个取值,PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。前者用来不同进程中的线程同步,后者用于同步本进程的不同线程。在上面的例子中,我们使用的是默认属性PTHREAD_PROCESS_ PRIVATE。后者用来设置互斥锁类型,可选的类型有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它们分别定义了不同的上所、解锁机制,一般情况下,选用最后一个默认属性。用法如下:
pthread_mutexattr_tmutexAttr;
pthread_mutexattr_init(&mutexAttr);
pthread_mutexattr_setpshared(&mutexAttr,PTHREAD_PROCESS_PRIVATE);
pthread_mutexattr_settype(&mutexAttr,PTHREAD_MUTEX_DEFAULT);
pthread_mutex_init(&mutex,&mutexAttr);
pthread_mutex_lock阻塞线程直到pthread_mutex_unlock被调用。
还有另外两个函数可以用:intpthread_mutex_trylock (pthread_mutex_t *__mutex),该函数立即返回,根据返回状态判断加锁是否成功。intpthread_mutex_timedlock (pthread_mutex_t *mutex, struct timespec*__restrict),该函数超时返回。
互斥锁主要用来互斥访问临界区。用于线程的互斥。
2.信号量的使用
信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。当公共资源增加时,调用函数sem_post()增加信号量。
只有当信号量值大于0时,才能使用公共资源,使用后,函数sem_wait()减少信号量。函数sem_trywait()和函数
pthread_mutex_trylock()起同样的作用,它是函数sem_wait()的非阻塞版本。
下面我们逐个介绍和信号量有关的一些函数,它们都在头文件/usr/include/semaphore.h中定义。
信号量的数据类型为结构sem_t,它本质上是一个长整型的数。函数sem_init()用来初始化一个信号量。它的原型为:
extern int sem_init __P ((sem_t *__sem, int __pshared,unsigned int __value));
sem为指向信号量结构的一个指针;pshared不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享;value给出了信号量的初始值。
函数sem_post( sem_t *sem )用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不在阻塞,选择机制同样是由线程的调度策略决定的。
函数sem_wait( sem_t *sem )被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。函数sem_trywait ( sem_t *sem )是函数sem_wait()的非阻塞版本,它直接将信号量sem的值减一。
函数sem_destroy(sem_t *sem)用来释放信号量sem。
3.补充, 条件变量
简单的说,条件变量能使线程wait,另一个线程在满足条件的时候发signal,然后所有处于wait的线程按照wait的先后得到唤醒(或者一起唤醒)
条件变量用来阻塞线程等待某个事件的发生,并且当等待的事件发生时,阻塞线程会被通知。
互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线承间的同步。
条件变量的结构为pthread_cond_t
最简单的使用例子:
pthread_cond_tcond;
pthread_cond_init(&cond,NULL);
pthread_cond_wait(&cond,&mutex);
...
pthread_cond_signal(&cond);
调用pthread_cond_wait的线程会按调用顺序进入等待队列,当有一个信号产生时,先进入阻塞队列的线程先得到唤醒。条件变量在某些方面看起来差不多。
正如上面所说的,条件变量弥补了互斥锁的不足。
接着上面列举的生产者、消费者例子中,我们这里增加消费者(并假设缓冲区可以放任意多条信息),比如有3个消费者线程,如果都使用互斥锁,那么三个线程都要不断的去查看缓冲区是否有消息,有就取走。这无疑是资源的极大浪费。如果我们用条件变量,三个消费者线程都可以放心的“睡觉”,缓冲区有消息,消费者会收到通知,那时起来收消息就好了。
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<sys/time.h>
staticchar buff[50];
pthread_mutex_tmutex;
pthread_mutex_tcond_mutex;
pthread_cond_tcond;
void consumeItem(char*buff)
{
printf("consumer item\n");
}
void produceItem(char*buff)
{
printf("produce item\n");
}
void *consumer(void*param)
{
int t = *(int*)param;
while (1)
{
pthread_cond_wait(&cond,&cond_mutex);
pthread_mutex_lock(&mutex);
printf("%d: ", t);
consumeItem(buff);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void *producer(void*param)
{
while (1)
{
pthread_mutex_lock(&mutex);
produceItem(buff);
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond);
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid_c, tid_p, tid_c2, tid_c3;
void *retval;
pthread_mutex_init(&mutex, NULL);
pthread_mutex_init(&cond_mutex, NULL);
pthread_cond_t cond;
pthread_cond_init(&cond, NULL);
int p[3] = {1,2, 3}; //用来区分是哪个线程
pthread_create(&tid_p, NULL, producer,NULL);
pthread_create(&tid_c, NULL, consumer,&p[0]);
pthread_create(&tid_c2, NULL, consumer,&p[1]);
pthread_create(&tid_c3, NULL, consumer,&p[2]);
pthread_join(tid_p, &retval);
pthread_join(tid_c, &retval);
pthread_join(tid_c2, &retval);
pthread_join(tid_c3, &retval);
return 0;
}
要注意的是,条件变量只是起阻塞和唤醒线程的作用,具体的判断条件还需用户给出,例如缓冲区到底有多少条信息等。
与条件变量有关的其它函数有:
/* 销毁条件变量cond */
int pthread_cond_destroy (pthread_cond_t *cond);
/* 唤醒所有等待条件变量cond的线程 */
int pthread_cond_broadcast (pthread_cond_t *cond);
/* 等待条件变量直到超时 */
int pthread_cond_timedwait (pthread_cond_t *cond,
pthread_mutex_t *mutex,
const struct timespec *abstime);
/* 初始化条件变量属性 attr*/
int pthread_condattr_init (pthread_condattr_t *attr);
/* 销毁条件变量属性 */
int pthread_condattr_destroy (pthread_condattr_t *attr)
/* 读取条件变量属性attr的进程共享标志 */
int pthread_condattr_getpshared (const pthread_condattr_t *
attr,
int *pshared);
/* 更新条件变量属性attr的进程共享标志 */
int pthread_condattr_setpshared (pthread_condattr_t *attr,
int pshared);
一般来说,条件变量被用来进行线程间的同步。