读写锁
与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享
当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞。但是考虑一种情况,当前持有互斥锁的线程只是要读访问共享资源,而同时有其他几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法获取访问共享资源了,但实际上多个线程同时读访问共享资源并不会导致问题。
在对数据的读写操作中,更多是读操作,写操作相对较少。例如:数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。
读写锁的特点:
1.如果有其他线程读数据,则允许其他线程执行读操作,但不允许写操作
2.如果有其他线程写数据,则其他线程都不允许读、写操作
3.写是独占的,写的优先级高(防止写着饿死)
说了这么多,发现就是操作系统课本中的读者写者问题....哈哈哈
相关函数
读写锁类型:pthread_rwlock_t
pthread_rwlock_init函数
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
作用:初始化一把读写锁
参数:
attr表示读写锁属性,通常使用默认属性,传NULL即可
pthread_rwlock_destroy函数
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
作用:销毁一把读写锁
pthread_rwlock_rdlock函数
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
作用:以读方式请求读写锁(请求读锁)
pthread_rwlock_wrlock函数
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
作用:以写方式请求读写锁(请求写锁)
pthread_rwlock_unlock函数
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
作用:解锁
pthread_rwlock_tryrdlock函数
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
作用:非阻塞请求读锁
pthread_rwlock_trywrlock函数
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
作用:非阻塞请求写锁
上个案例(同时有多个线程对同一共享数据读、写操作)
#include <stdio.h> #include <pthread.h> #include <unistd.h> pthread_rwlock_t rwlock; int counter = 0; //写线程 void *th_write(void *arg) { int t; int i = (int)arg; while(1) { pthread_rwlock_wrlock(&rwlock); //上写锁 sleep(2); printf("writer:%d %lu counter:%d\n",i,pthread_self(),++counter); pthread_rwlock_unlock(&rwlock); //解锁 usleep(10000); } } //读线程 void *th_read(void *arg) { int i = (int)arg; while(1) { pthread_rwlock_rdlock(&rwlock); //上读锁 printf("read:%d %lu counster:%d\n",i,pthread_self(),counter); pthread_rwlock_unlock(&rwlock); sleep(3); } } int main(void) { int i; pthread_t tid[8]; pthread_rwlock_init(&rwlock,NULL); for(i=0;i<3;++i) { pthread_create(&tid[i],NULL,th_write,(void *)i); } for(i=0;i<5;++i) { pthread_create(&tid[i],NULL,th_read,(void *)i); } //回收子线程 for(i=0;i<8;++i) { pthread_join(tid[i],NULL); } //销毁读写锁 pthread_rwlock_destroy(&rwlock); return 0; }
条件变量
条件变量本身不是锁,但是可以引起线程阻塞。通常与互斥锁配合使用。给多线程提供一个合适的场所。(线程同步问题,互斥锁和读写锁解决线程互斥)
相关函数
条件变量的类型:pthread_cond_t
pthread_cond_init函数
int pthread_cond_int(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
参数:attr表示条件变量的属性,通常传NULL
可以使用静态初始化方法,初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER
pthread_cond_destroy函数
int pthread_cond_destroy(pthread_cond_t *cond);
作用:销毁一个条件变量
pthread_cond_wait函数
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
作用:1.阻塞等待一个条件变量cond满足[不满足:阻塞等待满足 满足:不阻塞]
2.释放已掌握的互斥量(解锁互斥量)相当于 pthread_mutex_unlock(&mutex);
[1,2为原子操作,不可分割]
3.当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁
pthread_mutex_lock(&mutex);
pthread_cond_timedwait函数
int pthread_cond_timewat(pthread_cond _t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
作用:限时等待一个条件变量
参数:
abstime是一个绝对时间,time(NULL)返回的就是一个绝对时间,
alarm(1)是相对时间,相对当前时间1s。
使用方法:
错误用法:
struct timespec t = {1,0};
pthread_cond_timedwait(&cond,&mutex,&t); //只能定时到 1970.1.1 00:00:01
正确用法:
time_t cur = time(NULL); //获取当前时间
struct timespec t;
t.tv_sec = cur+1; //定义一秒
pthread_cond_timedwait(&cond,&mutex,&t);
pthread_cond_signal函数
int pthread_cond_signal(pthread_cond_t *cond);
作用:唤醒一个或多个等待的线程
pthread_cond_broadcast函数
int pthread_cond_broadcast(pthread_cond_t *cond);
作用:通过广播唤醒所有的线程
案例见消费者生产者模型
生产者消费者模型
生产者模型中的对象:生产者、消费者、容器
一端专门生产商品,一端专门消费商品。那么如此以来就会有一些问题需要注意,当容器满了,生产者阻塞等待消费者消费商品使容器可以存放物品。当容器为空,则需要阻塞等待生产者生产物品并存放容器内。
进入容器的线程每一时刻保持只有一个(形成互斥关系,生产者与消费者保持同步关系)
为什么要讲生产者和消费者模型,因为在实际的项目开发中,这个模式用到的地方很多
生产者消费者模型实现(条件变量版)
线程同步典型的案例即为生产者消费者模型,而借助条件变量来实现这一模型,使比较方便的。假定有两个线程,一个模拟生产者的行为,一个模拟消费者的行为。两个线程同时操作一个共享资源(一般称之为汇聚),生产者向其中添加商品,消费者从中消费产品。
因为程序中的插入删除操作比频繁(我就用链表来实现容器了)
#include <stdio.h> #include <unistd.h> #include <pthread.h> struct msg { struct msg *next; int num; }; struct msg *head; //静态方法初始化 pthread_cond_t cond = PTHREAD_COND_INITIALIZER; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //消费者 void* consumer(void *p) { struct msg *mp; while(1) { //取产品 pthread_mutex_lock(&mutex); while(head==NULL) //不能用 if 防止虚假唤醒 { pthread_cond_wait(&cond,&mutex); } mp = head; head = mp->next; pthread_mutex_unlock(&mutex); //消费产品 printf("消费:%d\n",mp->num); free(mp); sleep(rand()%5); } return NULL; } //生产者 void *product(void *arg) { struct msg *mp; while(1) { //生产产品 mp = malloc(sizeof(struct msg)); mp->num = rand()%200; printf("生产:%d\n",mp->num); //防止产品 pthread_mutex_lock(&mutex); mp->next = head; head = mp; pthread_mutex_unlock(&mutex); pthread_cond_signal(&cond); sleep(rand()%5); } return NULL; } int main(void) { pthread_t pid,cid; srand(time(NULL)); pthread_create(&pid,NULL,product,NULL); pthread_create(&cid,NULL,consumer,NULL); pthread_join(pid,NULL); pthread_join(cid,NULL); return 0; }
条件变量的优点:
相对于mutex而言,条件变量可以减少竞争。
如果直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间在的竞争是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争,提高程序效率。
信号量
由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一个对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并发执行,变成了串行执行。与直接使用单进程无异。
信号量是相对折中的处理方式,既能保证同步,数据不混乱,又能提高线程并发。
(进程间也可以使用哦)
信号量像是PV操作
相关函数
信号量的类型:sem_t
信号量的初始值,决定了占用信号量的线程个数
sem_init函数
int sem_init(sem_t *sem,int pshared,unsigned int value);
参数:
sem_t 信号量
pshared 0为线程,非0(一般为1)用于进程
value 指定信号量的初值
sem_destroy函数
int sem_destroy(sem_t *sem);
作用:销毁一个信号量
sem_wait函数
int sem_wait(sem_t *sem);
作用:先判断sem==0阻塞,value -1
对信号量的值减1,如果为0,阻塞
sem_trywait函数
int sem_trywait(sem_t *sem);
作用:尝试对信号量减1 (非阻塞)
sem_timedwait函数
sem_timedwait(sem_t *sem,const struct timespec *abs_timeout);
作用:限时尝试对信号量加锁 --
参数:abs_timeout是个绝对时间
定时1秒:
time_t cur = time(NULL); //获取当前时间
struct timespec t; //定义 timespec 结构体变量 t
t.tv_sec = sur + 1;
//t.tv_nsec = t.tv_sec + 100;
sem_timedwait(&sem,&t);
sem_post函数
int sem_post(sem_t *sem);
作用:对信号量加1
sem_getvalue函数
sem_getvalue(sem_t *sem,int *sval);
作用:获取当前信号量的值
案例见生产者消费者模型
生产者消费者模型实现(信号量版)
#include <stdio.h> #include <unistd.h> #include <pthread.h> #include <semaphore.h> #define NUM 5 int queue[NUM]; //缓冲区 sem_t produc_number,blank_number; void *product(void *arg) { int i = 0; while(1) { sem_wait(&blank_number); //缓冲区是否已满 queue[i] = rand()%100 + 1; //生产产品 printf("放缓冲区:%d\n",queue[i]); sem_post(&produc_number); //产品数 ++ i = (i+1)%NUM; sleep(rand()%3); } return NULL; } void *consumer(void *arg) { int i = 0; while(1) { sem_wait(&produc_number); //产品数量-- printf("取走缓冲区:%d\n",queue[i]); queue[i] = 0; sem_post(&blank_number); //格子数目++ i = (i+1)%NUM; //循环队列,下一个位置 sleep(rand()%3); } return NULL; } int main(void) { pthread_t pid,cid; sem_init(&blank_number,0,NUM); //初始时缓冲区空格子为5(或者说初始时生产者为5) sem_init(&produc_number,0,0); //初始时产品数为0(或者说消费者为0) pthread_create(&pid,NULL,product,NULL); pthread_create(&cid,NULL,consumer,NULL); pthread_join(pid,NULL); pthread_join(cid,NULL); //线程销毁 sem_destroy(&blank_number); sem_destroy(&produc_number); return 0; }
文件锁
借助fcntl函数来实现锁机制,操作文件的进程没有获取锁时,可以打开,但无法执行read、write操作,fcntl函数:获取、设置文件访问控制权限
int fcntl(int fd,int cmd,.../*arg*/);
参2:
F_SETLK(struct flock*) 设置文件锁(trylock)
F_SETLKW(struct flock*) 设置文件锁(lock) W-->wait
F_GETLK(struct flock*); 获取文件锁
参3:
struct flock{
...
short l_type; 锁的类型:F_RDLCK F_WRLCK F_UNLCK(解锁)
short l_whence; 偏移位置:SEEK_SET SEEK_CUR SEEK_END
short l_start; 起始位置:1000
short l_len; 长度:0表示整个问价加锁
short l_pid; 持有该锁的进程ID:F_GETLK
...
}
#include <stdio.h> #include <fcntl.h> #include <unistd.h> void sys_err(char *str) { perror(str); exit(-1); } int main(int arg,char *argv[]) { int fd; struct flock f_lock; if(arg < 2) { printf("./a.txt filename\n"); exit(1); } if(fd == open(argv[1],O_RDWR) < 0) { sys_err("open"); } // f_lock.l_type = F_WRLCK; //设置写锁 f_lock.l_type = F_RDLCK; //设置读锁 f_lock.l_whence = SEEK_SET; //文件头部 f_lock.l_start = 0; f_lock.l_len = 0; fcntl(fd,F_SETLKW,&f_lock); //上锁 printf("get flock\n"); sleep(10); f_lock.l_type = F_UNLCK; fcntl(fd,F_SETLK,&f_lock); //解锁 printf("un flock\n"); close(fd); return 0; }
依旧遵循“读共享,写独占”。但进程不加锁直接操作文件,依旧可以访问成功,但数据势必出现混乱。
多线程间共享文件描述符,而给文件加锁,是通过修改文件描述符所指向的文件结构体中的成员变量实现的,所以多线程无法使用文件锁。