1.条件变量
条件变量本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个会合的场所。
1.1 条件变量函数:
pthread_cond_init 函数
pthread_cond_destroy 函数
pthread_cond_wait 函数
pthread_cond_timedwait 函数
pthread_cond_signal 函数
pthread_cond_broadcast 函数
以上 6 个函数的返回值都是:成功返回 0, 失败直接返回错误号。
pthread_cond_t 类型 用于定义条件变量
pthread_cond_t cond;
1.2 pthread_cond_init 函数
初始化一个条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
参 2:attr 表条件变量属性,通常为默认值,传 NULL 即可
也可以使用静态初始化的方法,初始化条件变量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
1.3 pthread_cond_destroy 函数
销毁一个条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
1.4 pthread_cond_wait 函数
阻塞等待一个条件变量
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
函数作用:
- 阻塞等待条件变量 cond(参 1)满足
- 释放已掌握的互斥锁(解锁互斥量)相当于 pthread_mutex_unlock(&mutex) ,1.2.两步为一个原子操作。
- 当被唤醒,pthread_cond_wait 函数返回时,解除阻塞并重新申请获取互斥锁 pthread_mutex_lock(&mutex);
1.5 pthread_cond_timedwait 函数
限时等待一个条件变量
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
参 3: 参看 man sem_timedwait 函数,查看 struct timespec 结构体。
struct timespec {
time_t tv_sec; /* seconds */ 秒
long tv_nsec; /* nanosecondes*/ 纳秒
}
形参 abstime:绝对时间。
如:time(NULL)返回的就是绝对时间。而 alarm(1)是相对时间,相对当前时间定时 1 秒钟。
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; 定义 timespec 结构体变量 t
t.tv_sec = cur+1; 定时 1 秒
pthread_cond_timedwait (&cond, &mutex, &t); 传参 参 APUE.11.6 线程同步条件变量小节
在讲解 setitimer 函数时我们还提到另外一种时间类型:
struct timeval {
time_t tv_sec; /* seconds */ 秒
suseconds_t tv_usec; /* microseconds */ 微秒
};
1.6 pthread_cond_signal 函数
唤醒至少一个阻塞在条件变量上的线程
int pthread_cond_signal(pthread_cond_t *cond);
1.7 pthread_cond_broadcast 函数
唤醒全部阻塞在条件变量上的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
2.生产者消费者模型
线程同步典型的案例即为生产者消费者模型,而借助条件变量来实现这一模型,是比较常见的一种方法。
假定有两个线程,一个模拟生产者行为,一个模拟消费者行为。
两个线程同时操作一个共享资源(一般称之为汇聚),生产向其中添加产品,消费者从中消费掉产品。
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
struct msg {
struct msg *next;
int num;
};
struct msg *head;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *consumer(void *p)
{
struct msg *mp;
for (;;) {
pthread_mutex_lock(&lock);
while (head == NULL) { //头指针为空,说明没有节点 可以为 if 吗
pthread_cond_wait(&has_product, &lock);
}
mp = head;
head = mp->next; //模拟消费掉一个产品
pthread_mutex_unlock(&lock);
printf("-Consume ---%d\n", mp->num);
free(mp);
sleep(rand() % 5);
}
}
void *producer(void *p)
{
struct msg *mp;
while (1) {
mp = malloc(sizeof(struct msg));
mp->num = rand() % 1000 + 1; //模拟生产一个产品
printf("-Produce ---%d\n", mp->num);
pthread_mutex_lock(&lock);
mp->next = head;
head = mp;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&has_product); //将等待在该条件变量上的一个线程唤醒
sleep(rand() % 5);
}
}
int main(int argc, char *argv[])
{
pthread_t pid, cid;
srand(time(NULL));
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
zhaoxr@zhaoxr-ThinkPad-E450:~/pthread$ ./pthread_cond
-Produce ---425
-Consume ---425
-Produce ---486
-Consume ---486
-Produce ---699
-Consume ---699
-Produce ---736
-Consume ---736
-Produce ---452
-Consume ---452
-Produce ---529
-Consume ---529
-Produce ---664
-Consume ---664
-Produce ---340
-Consume ---340
^C
3.条件变量的优点:
相较于 mutex 而言,条件变量可以减少竞争。
如直接使用 mutex,除了生产者、消费者之间要竞争互斥量以外,
消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。
有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。
4.信号量
进化版的互斥锁(1 --> N)
由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,
使用互斥锁是没有办法实现的,只能将整个数据对象锁住。
这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。
线程从并行执行,变成了串行执行。与直接使用单进程无异。
信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。
5.信号量函数:
sem_init 函数
sem_destroy 函数
sem_wait 函数
sem_trywait 函数
sem_timedwait 函数
sem_post 函数
以上 6 个函数的返回值都是:成功返回 0, 失败返回-1,同时设置 errno。(注意,它们没有 pthread 前缀)
sem_t 类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使用文件描述符)。
sem_t sem; 规定信号量 sem 不能 < 0。头文件 <semaphore.h>
5.1 信号量基本操作:
sem_wait: 1. 信号量大于 0,则信号量-- (类比 pthread_mutex_lock)
2. 信号量等于 0,造成线程阻塞
sem_post: 将信号量++,同时唤醒阻塞在信号量上的线程 (类比 pthread_mutex_unlock)
但,由于 sem_t 的实现对用户隐藏,所以所谓的++、--操作只能通过函数来实现,而不能直接++、--符号。
信号量的初值,决定了占用信号量的线程的个数。
5.2 sem_init 函数
初始化一个信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
参 1:sem 信号量
参 2:pshared 取 0 用于线程间;取非 0(一般为 1)用于进程间
参 3:value 指定信号量初值
5.3 sem_destroy 函数
销毁一个信号量
int sem_destroy(sem_t *sem);
5.4 sem_wait 函数
给信号量加锁 –
int sem_wait(sem_t *sem);
5.5 sem_post 函数
给信号量解锁 ++
int sem_post(sem_t *sem);
5.6 sem_trywait 函数
尝试对信号量加锁 – (与 sem_wait 的区别类比 lock 和 trylock)
int sem_trywait(sem_t *sem);
5.7 sem_timedwait 函数
限时尝试对信号量加锁 –
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
参 2:abs_timeout 采用的是绝对时间。
定时 1 秒:
time_t cur = time(NULL); 获取当前时间。
struct timespec t; 定义 timespec 结构体变量 t
t.tv_sec = cur+1; 定时 1 秒
t.tv_nsec = t.tv_sec +100;
sem_timedwait(&sem, &t); 传参
6.信号量举例
#include<stdio.h>
#include <semaphore.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
sem_t sem;
void *func(void* arg){
int i=(int)arg;
while(1){
sem_wait(&sem);
printf("我是%d,我正在吃饭,当前座位剩余:%d\n",i,sem);
sleep(2);
sem_post(&sem);
sleep(4);
}
return NULL;
}
int main()
{
pthread_t tid[10];
sem_init(&sem,0,5);
int i;
for(i=0;i<10;i++){
pthread_create(&tid[i],NULL,func,(void*)i);
}
for(i=0;i<10;i++){
pthread_join(tid[i],NULL);
}
sem_destroy(&sem);
return 0;
}
zhaoxr@zhaoxr-ThinkPad-E450:~/pthread$ ./sem
我是0,我正在吃饭,当前座位剩余:4
我是1,我正在吃饭,当前座位剩余:3
我是2,我正在吃饭,当前座位剩余:2
我是3,我正在吃饭,当前座位剩余:1
我是4,我正在吃饭,当前座位剩余:0
我是5,我正在吃饭,当前座位剩余:1
我是8,我正在吃饭,当前座位剩余:2
我是6,我正在吃饭,当前座位剩余:0
我是7,我正在吃饭,当前座位剩余:1
我是9,我正在吃饭,当前座位剩余:0
我是0,我正在吃饭,当前座位剩余:4
我是4,我正在吃饭,当前座位剩余:2
我是1,我正在吃饭,当前座位剩余:3
我是3,我正在吃饭,当前座位剩余:1
我是2,我正在吃饭,当前座位剩余:0
^C
7.生产者消费者信号量模型
使用信号量完成线程间同步,模拟生产者,消费者问题。
【sem_product_consumer.c】
规定: 如果□中有数据,生产者不能生产,只能阻塞。
如果□中没有数据,消费者不能消费,只能等待数据。
定义两个信号量:S 满 = 0, S 空 = 1
(S 满代表满格的信号量,S 空表示空格的信号量,程序起始,格子一定为空)
所以有:
T 生产者主函数 {
sem_wait(S 空); //空格减1
生产....
sem_post(S 满); //大饼加1
}
T 消费者主函数 {
sem_wait(S 满);//大饼减1
消费....
sem_post(S 空);//空格加1
}
假设: 线程到达的顺序是:T 生、T 生、T 消。
那么: T 生 1 到达,将 S 空-1,生产,将 S 满+1
T 生 2 到达,S 空已经为 0, 阻塞
T 消 到达,将 S 满-1,消费,将 S 空+1
三个线程到达的顺序是:T 生 1、T 生 2、T 消。而执行的顺序是 T 生 1、T 消、T 生 2
这里,S 空 表示空格子的总数,代表可占用信号量的线程总数-->1。其实这样的话,信号量就等同于互斥锁。
但,如果 S 空=2、3、4……就不一样了,该信号量同时可以由多个线程占用,不再是互斥的形式。
因此我们说信号量是互斥锁的加强版。
8.哲学家吃饭问题
让所有哲学家听到吃饭口令之后,都拿自己右手边的筷子,但是选定某一个哲学家A必须拿自己左手边的筷子;
这样子,A哲学家右边的哲学家B肯定可以第一个吃到饭,吃完饭放下筷子,下面一个人就可以吃饭了;
以此类推。。。
9.共享内存
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>
int a=10;
void* func(void* arg)
{
sleep(1);
a=100;
printf("我要修改a\n");
sleep(2);
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,func,NULL);
while(1){
printf("%d\n",a);
sleep(1);
}
pthread_join(tid,NULL);
return 0;
}
zhaoxr@zhaoxr-ThinkPad-E450:~/pthread$ ./pthread_com
10
我要修改a
100
100
100
100
100
100
100
^C