个人的UNP阅读笔记。
关键字:IPC的分类(持续时间,维护方式,以及特性)
条件变量(条件锁)
IPC(进程间通信)根据持续时间的划分
注意,这个持续类型和IPC由谁维护无关。管道和FIFO随进程持续,但是他们由内核维护,确切的说,由虚拟文件系统(VFS)维护一个inode。(这个inode是特别的,不同于普通文件的inode)。并且FIFO仅仅是vfs中的inode创建,与磁盘没有半毛钱关系。
下图是
IPC维护方式
解释:左上的文件系统就相当于把消息写到txt中,让txt来传递(这个方式真是特别熟悉啊哈哈)。中间的意思是由内核维护。内核维护经常能保持比较高的速度,这是因为内核中进行IO操作比较快。相比用户态少了一层拷贝。但是若是正常计算还是没有共享内存快,毕竟进入内核要有上下文切换。
条件锁(条件变量)
用互斥锁进行同步,大家都很熟悉,但是这样由一个问题,当消费者持锁消费时,生产者线程往往需要执行while循环,占用CPU,形成浪费。所以linux里提供了条件变量。这样可以让 没有执行的条件的线程睡上一会,等条件变量通知他在执行, 效率相对较高。
附赠代码(其实就是UNP的源码拉)
#include "unpipc.h"
#define MAXNITEMS 1000000
#define MAXNTHREADS 100
int nitems;
int buff[MAXNITEMS];
struct {
pthread_mutex_t mutex;
int nput;
int nval;
}put={
PTHREAD_MUTEX_INITIALIZER
};
struct {
pthread_mutex_t mutex;
pthread_cond_t cond;
int nready;
}nready={
PTHREAD_MUTEX_INITIALIZER,PTHREAD_COND_INITIALIZER// 这种方式初始化 这个结构体,这些都是unix的宏。
};
void * produce(void*),*consume(void*);
int main(int argc,char **argv){
int i,nthreads,count[MAXNTHREADS];
pthread_t tid_produce[MAXNTHREADS],tid_consume;// 初始化 n个生产者线程,1一个消费者线程。(进程同理啊,反正unix里的线程是LWP)
if(argc!=3)
err_quit("usage:prodcons3<#items><#threads>");
nitems=min(atoi(argv[1]),MAXNITEMS);
nthreads=min(atoi(argv[2]),MAXNTHREADS);// 设置线程数和资源数。 argc是 函数编译语句 带的参数个数。gcc 后面带几个参数。argv是参数。
Set_concurrency(nthreads+1);// 设置并发数,这样其实很好,因为若线程很多,效率巨慢,参见C10K问题。
for(i=0;i<nthreads;i++){
count[i]=0;//tongji 0
pthread_create(&tid_produce[i],NULL,produce,&count[i]);
}
pthread_create(&tid_consume,NULL,consume,NULL);
for(i=0;i<nthreads;i++){
pthread_join(tid_produce[i],NULL);//让主线程等 父线程,不然主线程马上就会结束。
printf("count[%d]= %d\n",i,count[i]);
}
pthread_join(tid_consume,NULL);
exit(0);
}
void *produce(void *arg){
for(;;){
pthread_mutex_lock(&put.mutex);// 上put锁。往数组里放东西。
if(put.nput>=nitems){
pthread_mutex_unlock(&put.mutex);
return (NULL);
}
buff[put.nput]=put.nval;
put.nput++;
put.nval++;
pthread_mutex_unlock(&put.mutex);
pthread_mutex_lock(&nready.mutex);
if(nready.nready==0)
pthread_cond_signal(&nready.cond);
nready.nready++;
pthread_mutex_unlock(&nready.mutex);
/*
int dosignal;
pthread_mutex_lock(&nready.mutex);
dosignal=(nready.nready==0);
nready.nready++;
pthread_mutex_unlock(&nready.mutex);
if(dosignal)pthread_cond_signal(&nready.cond);
*/
*((int*)arg)+=1;
}
return NULL;
}
void *consume(void *arg){
int i;
for(i=0;i<nitems;i++){
// printf("%d nothing\n",i);
pthread_mutex_lock(&nready.mutex);
while(nready.nready==0)// !!!
pthread_cond_wait(&nready.cond,&nready.mutex);// 用while循环是为了 避免虚假唤醒(spurious wakeup),在多核机器上,
一次pthread_mutex_signal可能会导致好几个cond_wait的线程执行。用while更保险
nready.nready--;
pthread_mutex_unlock(&nready.mutex);
if(buff[i]!=i)
printf("buff[%d]=%d\n",i,buff[i]);
}
return (NULL);
}
为什么pthread_cond_wait要传入一个锁?
首先,条件锁内部是不需要传入一个互斥锁的。因为条件锁虽然也是临界资源,但是内部是由futex实现的(futex大家可以理解为,所有的锁都是内核维护一个结构,但是如果频繁的进入内核这真是太惨了,所以在用户态维护一些状态,当查询锁时先看这个用户态信息,减少进入内核态的次数。如果锁被用了就wait呗,这样比频繁的进入内核态查询要快)。所以条件锁本身不需要互斥锁。这个锁是为了 维护 唤醒队列中的线程。
这个锁为了这样一种情况。
cond_wait有将当前线程放入唤醒队列的操作,当放入唤醒队列之前,发送的signal并不会被收到。(可能会导致该线程饿死哦)
所以当调用cond_wait时,先将线程加入 唤醒队列,然后解锁,等被唤醒时,在加锁。 而这个锁 和 发送cond_signal的锁时一样的,保证了 消费者线程进入 唤醒队列之前 不会发送signal(因为发signal那个线程根本拿不到锁!)