橙色
生产者和消费者模型
生产者和消费是操作系统中一种重要的模型,它描述的是一种等待和通知的机制。
具体可观看该文章:生产者和消费者模型
互斥锁
互斥锁是用一种简单的方法控制线程对共享资源的操作。在某种意义上可以将互斥锁看成一个全局变量,即可简单理解为某一时刻只能被一个线程所操作。
互斥锁有两种状态:上锁和解锁。
- 在某一时刻只能有一个线程掌握着互斥。
- 掌握着互斥锁的线程可以对共享资源进行操作
- 若其他线程想要上锁一个已经被上锁的互斥锁,该线程就会被挂起,等到已上锁的线程释放掉互斥锁为止。
- 互斥锁保证了每个线程按顺序对共享资源进行操作。
pthread_mutex_init (pthread_mutex_t *_mutex,const pthread_mutex_t *mutexatter)
初始化互斥锁函数
第一个参数mutex是只想要初始化的互斥锁的指针
第二个参数mutex是指向属性对象的指针,定义的是初始化互斥锁的属性,一般为NULL,即默认属性。
此外,也可以用宏PTHREAD_MUTEX_INTIALIZER 初始化静态分配的互斥锁。
pthread_mutex_destroy (pthread_mutex_t *mutex)
锁毁互斥锁函数
参数为指向互斥锁的指针
当这两个函数成功完成时返回0.否则返回错误编号指明错误。
int pthread_mutex_lock(pthread_mutex_t *mutex);
以阻塞的方式申请互斥锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
以非阻塞的方式申请互斥锁
pthread_mutex_unlock(pthread_mutex_t *mutex)
释放互斥锁
释放操作只能由占有该互斥锁的线程完成。
条件变量
函数解析
条件变量不是锁,条件变量可以使线程在满足某个条件以后,阻塞或者解除阻塞
/*
条件变量的类型 pthread_cond_t
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
- 等待,调用了该函数,线程会阻塞。
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
- 等待多长时间,调用了这个函数,线程会阻塞,直到指定的时间结束。
int pthread_cond_signal(pthread_cond_t *cond);
- 唤醒一个或者多个等待的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
- 唤醒所有的等待的线程
*/
代码举例
第58行为什么要引入条件变量呢?因为单纯依靠互斥锁的话,如果一直没有节点被生产出来且容器中节点为0,那么消费者子线程就会一直进行死循环,这无疑是极其浪费资源的。所以比较好的做法就是当没有节点的时候,消费者去通知生产者来生产节点。
第32行用pthread_cond_signal还是pthread_cond_signal都是可以的
第58行pthread_cond_wait(&cond, &mutex),当消费者子线程阻塞在这里时,第23行pthread_mutex_lock(&mutex),生产者子线程还能拿到锁吗?不能的话岂不是陷入死锁了?其实不是这样的,当消费者子线程阻塞在这里时,wait会对mutex互斥锁进行一个解锁的操作,使得该锁可以被拿。但要注意的是当wait解除阻塞的时候就又会把mutex锁加上,所以在下面需要有一个解锁的操作
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
// 创建一个互斥量
pthread_mutex_t mutex;
// 创建条件变量
pthread_cond_t cond;
struct Node{
int num;
struct Node *next;
};
// 头结点
struct Node * head = NULL;
void * producer(void * arg) {
// 不断的创建新的节点,添加到链表中
while(1) {
pthread_mutex_lock(&mutex);
struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
//这里用的是头插法,把新生成的节点插到了链表的最前面
newNode->next = head;
head = newNode;
newNode->num = rand() % 1000;//成一个介于0到999之间的随机整数
printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
// 只要生产了一个,就通知消费者消费
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
usleep(100);
}
return NULL;
}
void * customer(void * arg) {
while(1) {
pthread_mutex_lock(&mutex);
// 保存头结点的指针
struct Node * tmp = head;
// 判断是否有数据
if(head != NULL) {
// 有数据
head = head->next;
printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
free(tmp);
pthread_mutex_unlock(&mutex);
usleep(100);
} else {
// 没有数据,需要等待
// 当这个函数调用阻塞的时候,会对互斥锁进行解锁,当不阻塞的,继续向下执行,会重新加锁。
pthread_cond_wait(&cond, &mutex);
pthread_mutex_unlock(&mutex);
}
}
return NULL;
}
int main() {
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
// 创建5个生产者线程,和5个消费者线程
pthread_t ptids[5], ctids[5];
for(int i = 0; i < 5; i++) {
pthread_create(&ptids[i], NULL, producer, NULL);
pthread_create(&ctids[i], NULL, customer, NULL);
}
for(int i = 0; i < 5; i++) {
pthread_detach(ptids[i]);
pthread_detach(ctids[i]);
}
//这里的while(1)死循环主要是防止上面的子线程刚一创建好,下面就把互斥锁和条件变量给销毁了
while(1) {
sleep(10);
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
//这里调用pthread_exit()函数,使得主线程结束而子线程不会结束,且后面的return 0也不会执行
pthread_exit(NULL);
return 0;
}
运行结果:
1、消费者和生产者这种模型下,生产者和消费者是使用同一把锁的。因为只要是多个线程操作相同的一份数据,就需要保证代码同步。
2、问:如果容器中的数据足够多,消费者不执行pthread_cond_wait(&cond,&mutex)
那么生产者中的pthread_cond_signal(&cond)信号通知给谁,或者信号哪了?
答:signal是唤醒一个或者多个睡眠的线程,如果数据足够多,线程没有休眠,即使收到了信号也不会做任何的处理。《Liunx/UNIX系统编程手册》第531页有句话,条件变量并不保存状态信息,只是传递应用程序状态信息的一种通讯机制。发送信号时若无任何线程在等待该条件变量,这个也就会不了了之。线程如在此后等待该条件变量,只有当再次收到此变量的下一信号时,方可解除阻塞状态。
3、问:当消费者执行到pthread_cond_wait对mutex进行了解锁,那么这个时候其他消费者线程会不会抢先对mutex进行加锁,从而导致生产者无法正常生产呢?
答: 其他消费者也在pthread_cond_wait等待
信号量
函数解析
/*
信号量的类型 sem_t
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 初始化信号量
- 参数:
- sem : 信号量变量的地址
- pshared : 0 用在线程间 ,非0 用在进程间
- value : 信号量中的值
int sem_destroy(sem_t *sem);
- 释放资源
int sem_wait(sem_t *sem);
- 对信号量加锁,调用一次对信号量的值-1,如果值为0,就阻塞
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
int sem_post(sem_t *sem);
- 对信号量解锁,调用一次对信号量的值+1
int sem_getvalue(sem_t *sem, int *sval);
sem_t psem;
sem_t csem;
init(psem, 0, 8);
init(csem, 0, 0);
producer() {
sem_wait(&psem);
sem_post(&csem)
}
customer() {
sem_wait(&csem); //当csem信号量不为0时,减一;为0时,就堵塞在这里
sem_post(&psem)
}
*/
代码举例
这里是引用
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>
// 创建一个互斥量
pthread_mutex_t mutex;
// 创建两个信号量
sem_t psem;
sem_t csem;
struct Node{
int num;
struct Node *next;
};
// 头结点
struct Node * head = NULL;
void * producer(void * arg) {
// 不断的创建新的节点,添加到链表中
while(1) {
sem_wait(&psem);
pthread_mutex_lock(&mutex);
struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
newNode->next = head;
head = newNode;
newNode->num = rand() % 1000;
printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
pthread_mutex_unlock(&mutex);
sem_post(&csem);
}
return NULL;
}
void * customer(void * arg) {
while(1) {
sem_wait(&csem);
pthread_mutex_lock(&mutex);
// 保存头结点的指针
struct Node * tmp = head;
head = head->next;
printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
free(tmp);
pthread_mutex_unlock(&mutex);
sem_post(&psem);
}
return NULL;
}
int main() {
pthread_mutex_init(&mutex, NULL);
sem_init(&psem, 0, 8);
sem_init(&csem, 0, 0);
// 创建5个生产者线程,和5个消费者线程
pthread_t ptids[5], ctids[5];
for(int i = 0; i < 5; i++) {
pthread_create(&ptids[i], NULL, producer, NULL);
pthread_create(&ctids[i], NULL, customer, NULL);
}
for(int i = 0; i < 5; i++) {
pthread_detach(ptids[i]);
pthread_detach(ctids[i]);
}
while(1) {
sleep(10);
}
pthread_mutex_destroy(&mutex);
pthread_exit(NULL);
return 0;
}
因为使用的是头插法,所以最后加入链表的最先被打印出来。
信号量的大小决定了容器的容量