互斥
概念:任何时刻,都保证一次只有一个执行流进入临界区,访问临界资源。
要保证一次只有一个执行流进入临界区,而且不能被其他线程干扰,要做到就需要有一个锁。在Linux下,把这把锁叫做互斥量
在进入临界区之前该线程申请这个锁,然后该进程进入临界区访问临界资源,在访问完后再解锁,别的线程再进行重新申请锁。
互斥量的接口
互斥量的函数的头文件都是#include <pthread.h>
,gcc/g++编译的时候都要加上-lpthread
初始化互斥量
静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
mutex: 要初始化的互斥量
attr: 设为nullptr即可.
返回值:成功返回0,失败返回错误码
销毁互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);
mutex:要销毁的互斥量
返回值:成功返回0,失败返回错误码
互斥量加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
mutex:要加锁的互斥量
返回值:成功返回0,失败返回错误码
互斥量解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
mutex:要解锁的互斥量
返回值:成功返回0,失败返回错误码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
int ticket = 100;
pthread_mutex_t mutex;
void *route(void *arg)
{
char *id = (char*)arg;
while ( 1 )
{
pthread_mutex_lock(&mutex);在临界区之前互斥量加锁
if ( ticket > 0 )
{
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
pthread_mutex_unlock(&mutex);出了临界区互斥量解锁
}
else
{
pthread_mutex_unlock(&mutex);最后一个票时,因为是加锁的,不执行if下面的语句无法解锁,要在else后面也要解一次锁
break;
}
}
}
int main( void )
{
pthread_t t1, t2, t3, t4;
pthread_mutex_init(&mutex, NULL);
pthread_create(&t1, NULL, route, "thread 1");
pthread_create(&t2, NULL, route, "thread 2");
pthread_create(&t3, NULL, route, "thread 3");
pthread_create(&t4, NULL, route, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&mutex);
}
效果
死锁:死锁是多个执行流占着自己不释放的资源,而去申请被其他执行流不会释放的资源而处于一种永久等待的状态
死锁的四个必要条件
1.互斥条件:一个资源一次只能被一个执行流使用
2.请求与保持条件:一个执行流申请其他资源申请不到时,不会释放自己原本的资源
3.不剥夺条件:一个执行流在未结束之前,不能剥夺该执行流所掌握的资源
4.循环等待条件:多个执行流形成首尾相接循环等待资源的关系
避免死锁
1.破坏死锁的四个必要条件的一个或多个
2.资源一次性分配
3.加锁顺序一致
4.银行家算法
5.死锁检测
条件变量
与互斥量不同,条件变量用于自动阻塞一个线程,直到某个条件的满足,会唤醒这个线程。一般条件变量和锁一起配合使用。
条件变量有两个动作:一是一个线程不满足条件而挂起,并释放自带的锁;二是挂起线程受到某种信号被唤醒重新申请锁
条件变量的接口
条件变量的函数的头文件都是#include <pthread.h>
,gcc/g++编译的时候都要加上-lpthread
初始化条件变量
静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
动态初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
cond: 要初始化的条件变量
attr: 设为nullptr即可.
返回值:成功返回0,失败返回错误码
销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);int pthread_mutex_destroy(pthread_mutex_t *mutex);
cond:要销毁的条件变量
返回值:成功返回0,失败返回错误码
等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
cond:要等待的条件变量
mutex:互斥量
如果一个线程带着锁挂起(被阻塞),会自动释放这个锁,避免死锁。这也是该函数内需要传入锁的原因
唤醒等待
int pthread_cond_signal(pthread_cond_t *cond);一次只能唤醒一个在等待队列中的线程
int pthread_cond_broadcast(pthread_cond_t *cond);一次唤醒在等待队列的所有线程
cond:需要唤醒的条件变量
如果一个线程被唤醒,会重新申请锁
基于阻塞队列的单生产者单消费者模型
阻塞队列:设定一个队列是有容量的,当队列已满时,无法往队列中插入数据;队列已空时,无法往队列中读取数据。
生产者消费者模型:生产者的任务是生产数据,消费者的任务是消费数据。
代码:
blockqueue.h
#pragma once
#include <queue>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
class BlockQueue
{
private:
bool IsFull()
{
return q.size()==capacity;
}
bool IsEmpty()
{
return q.size()==0;
}
void ConsumerWait()
{
pthread_cond_wait(&c,&lock);
}
void ProductorWait()
{
pthread_cond_wait(&p,&lock);
}
void BlockQueueLock()
{
pthread_mutex_lock(&lock);
}
void BlockQueueUnlock()
{
pthread_mutex_unlock(&lock);
}
public:
BlockQueue(int _capacity = 3):capacity(_capacity)
{
pthread_mutex_init(&lock,nullptr);
pthread_cond_init(&c,nullptr);
pthread_cond_init(&p,nullptr);
}
void PushData(int data)
{
BlockQueueLock();
while(IsFull())循环判定,防止signal出错而没有唤醒消费者,进行下面的错误插入
{
pthread_cond_signal(&c);当队列已满,给消费者发信号将消费者线程唤醒,进行消费
ProductorWait();
}
q.push(data);
BlockQueueUnlock();
}
void PopData(int &data)
{
BlockQueueLock();
while(IsEmpty())循环判定,防止signal出错而没有唤醒生产者,进行下面的错误读取
{
pthread_cond_signal(&p);当队列已空,给生产者发信号将生产者线程唤醒,进行生产
ConsumerWait();
}
data = q.front();
q.pop();
BlockQueueUnlock();
}
~BlockQueue()
{
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&c);
pthread_cond_destroy(&p);
}
private:
std::queue<int>q;
int capacity;阻塞队列的容量
pthread_mutex_t lock;互斥量
pthread_cond_t c;生产者的条件变量
pthread_cond_t p;消费者的条件变量
};
blockqueue.cpp
#include "blockqueue.h"
#include <iostream>
using namespace std;
void* productor(void*arg)
{
BlockQueue*bq = (BlockQueue*)arg;
srand((unsigned int)time(nullptr));
while(1)
{
int data = rand()%100+1;//产生随机数
bq->PushData(data);
cout<<"product a number:"<<data<<endl;
sleep(1);
}
}
void* consumer(void*arg)
{
BlockQueue*bq = (BlockQueue*)arg;
int data;
while(1)
{
bq->PopData(data);
cout<<"consume a number:"<<data<<endl;
sleep(1);
}
}
int main()
{
BlockQueue*bq = new BlockQueue;
pthread_t c,p;
pthread_create(&p,nullptr,productor,(void*)bq);
pthread_create(&c,nullptr,consumer,(void*)bq);
pthread_join(p,nullptr);
pthread_join(c,nullptr);
delete bq;
return 0;
}
运行效果
阻塞队列的容量为3.生产三个随机数,给消费者发信号,告知消费者可以消费了,然后生产者进行阻塞,消费者消费三个数据给生产者发信号,告知生产者可以生产了,然后消费者进行阻塞。造成循环反复的生产消费。
POSIX信号量
POSIX信号量用于线程间同步,达到无冲突访问共享资源
同步:在保证数据安全的前提上,让线程按照某种特定的顺序访问临界资源,从而不产生饥饿问题。
信号量是一个计数器,本质是描述临界资源数目的计数器。
只要信号量申请了,那么一定在临界区有一份资源是属于申请信号量的程序的.
信号量的常见操作P(- - 预定某种资源,将信号量数目减1),V(++,释放某种资源,将信号量数目加1),而且PV操作都是原子的,即要么没有申请或释放信号量,要么申请了信号量数目–,释放了信号量数目++。
问:全局变量是否能充当信号量?
答:不能,因为对全局变量的++,–操作并非原子的,可能会有多个线程同时访问全局变量造成变量内容的不确定性。
信号量的接口
信号量的函数的头文件都是#include <semaphore.h>
,gcc/g++编译的时候都要加上-lpthread
信号量初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem:要初始化的信号量
pshared:进程间设为非0,线程间设为0
value:信号量的初始值
返回值:成功返回0,失败返回-1
信号量销毁
int sem_destroy(sem_t *sem);
sem:要销毁的信号量
返回值:成功返回0,失败返回-1
信号量的等待(预定,P操作)
int sem_wait(sem_t *sem);
sem:要等待的信号量,会将信号量的值减1,表明有程序预定了临界区的某份资源
返回值:返回值:成功返回0,失败返回-1
信号量的增加(释放,V操作)
int sem_post(sem_t *sem);
sem:要增加的信号量,会将信号量的值加1,表明已有程序释放了临街区的资源
返回值:返回值:成功返回0,失败返回-1
基于环形队列的单生产者单消费者模型
环形队列:用数组模拟实现,用模运算实现环形特性
ringqueue.h
#pragma once
#include <iostream>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
#include <vector>
#include <pthread.h>
#include <semaphore.h>
class RingQueue
{
private:
void P(sem_t&t)
{
sem_wait(&t);
}
void V(sem_t&t)
{
sem_post(&t);
}
public:
RingQueue(int _cap = 20):cap(_cap),v(_cap)
{
sem_init(&c,0,0);
sem_init(&p,0,cap);
}
void PushData(int& data)
{
static int pos_p = 0;设为静态,防止每次插入数据都是从0开始
P(p);生产一个数据,就将生产者的信号量减1
v[pos_p] = data;
V(c);将消费者的信号量加1,表明可以进行消费
pos_p++;
pos_p%=cap;
}
void PopData(int& data)
{
static int pos_c = 0;同样设为静态,防止每次读取数据都是从0开始
P(c);消费一个数据,就将消费者的信号量减1
data = v[pos_c];
V(p);将生产者的信号量加1,表明可以进行生产
pos_c++;
pos_c%=cap;
}
~RingQueue()
{
sem_destroy(&c);
sem_destroy(&p);
}
private:
std::vector<int>v;
int cap;环形队列的容量
sem_t c;生产者的信号量
sem_t p;消费者的信号量
};
ringqueue.cpp
#include "ringqueue.h"
void* productor(void*arg)
{
RingQueue*rq = (RingQueue*)arg;
srand((unsigned long)time(nullptr));
while(true)
{
int data = rand()%100+1;
rq->PushData(data);
std::cout<<"productor number is:"<<data<<std::endl;
sleep(1);
}
}
void* consumer(void*arg)
{
RingQueue*rq = (RingQueue*)arg;
while(true)
{
int data = 0;
rq->PopData(data);
std::cout<<"consumer number is:"<<data<<std::endl;
sleep(1);
}
}
int main()
{
RingQueue*rq = new RingQueue(5);
pthread_t c,p;
pthread_create(&p,nullptr,productor,rq);
pthread_create(&c,nullptr,consumer,rq);
pthread_join(p,nullptr);
pthread_join(c,nullptr);
delete rq;
return 0;
}
运行效果:
生产一个数据,消费一个数据,循环往复。