目录
一,同步的概念
线程中的同步与现实中的同步可不一样。线程中的同步概念如下:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
二,线程同步的实现
1,条件变量函数
1,条件变量类型
条件变量的类型是: pthread_cond_t 能用这个类型定义一个条件变量。
pthread_cond_t cond;//定义条件变量
这个类型的构成如下:
typedef union { struct { int __lock; unsigned int __futex; __extension__ unsigned long long int __total_seq; __extension__ unsigned long long int __wakeup_seq; __extension__ unsigned long long int __woken_seq; void *__mutex; unsigned int __nwaiters; unsigned int __broadcast_seq; } __data; char __size[__SIZEOF_PTHREAD_COND_T]; __extension__ long long int __align; } pthread_cond_t;
2,条件变量初始化函数
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
这个函数的作用是初始化一个条件变量:pthread_cond_init(&cond,nullptr);//函数初始化条件变量
同样的,条件变量的初始化也可以使用宏来初始化:
cond = PTHREAD_COND_INITIALIZER;//使用宏来初始化
这个宏的原型如下:
#define PTHREAD_COND_INITIALIZER { { 0, 0, 0, 0, 0, (void *) 0, 0, 0 } }
3,条件变量等待函数
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
这个函数能让线程在条件变量cond的队列里面进行等待,知道条件满足才能唤醒,在这里也可以看到这个函数的第二个参数是一个锁。所以条件变量函数等待的条件就是锁。
pthread_cond_wait(&cond, &lock);//条件等待
4,条件变量唤醒函数
1,唤醒一个等待的线程:
pthread_cond_signal(&cond);//唤醒单个线程
2,唤醒多个线程:
pthread_cond_broadcast(&cond);//唤醒多个线程
5,条件变量销毁函数
int pthread_cond_destroy(pthread_cond_t *cond)
这个函数的作用便是在条件变量不用的情况下销毁掉这个条件变量:
pthread_cond_destroy(&cond);//销毁当前条件变量
三,条件变量等待为什么需要有锁变量
代码示例:
#include<iostream>
#include<pthread.h>
#include<string>
#include<vector>
#include<unistd.h>
using namespace std;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int cnt = 0;//共享资源,临界资源
void* Cnt(void* args)
{
string name = "thread_" + to_string((uint64_t)args);
while(true)
{
pthread_mutex_lock(&lock);//给线程加锁
pthread_cond_wait(&cond, &lock);//等待,为什么等待?因为临界资源没有就绪,等待后需要唤醒
cout << name << ": " << cnt++<<endl;
pthread_mutex_unlock(&lock);//给线程解锁
sleep(1);
}
return nullptr;
}
int main()
{
for (uint64_t i = 1; i <= 4;i++)//创建线程
{
pthread_t td ;
pthread_create(&td, nullptr, Cnt, (void *)i);
}
while(true)//主线程唤醒
{
pthread_cond_broadcast(&cond);
cout << "唤醒开始........" << endl;
sleep(1);
}
return 0;
}
在这里可能会有一些让人疑惑的点,为什么我的线程在执行等待函数时已经是顺序等待的的了还要加锁呢?其实加锁是为了防止一些新来的线程,因为线程一般都会先访问公共资源,只有公共资源不能被访问时新线程才会去等待。所以加锁是必要的。在这里还得补充一点,线程的条件等待是会自动的解锁的,所以不用担心拥有锁的线程在等待时不释放锁导致死锁问题。
四,消费-生产者模型(cp模型)
C代表comsumer,p代表productor。这个模型的特点可以概括为以下几点:
模拟实现生产者消费者模型:
1,建立消费场所Blockqueue
这个场所是一个队列,这个便是交易场所所使用到的特定的数据结构空间。
代码:
#include <iostream>
#include <pthread.h>
#include <queue>
#include<unistd.h>
using namespace std;
template <class T> // 模板类,交易的对象可以任意
class blockqueue // 创建一个消费场所
{
static const int defaultnum = 10; // queue的默认容量
public:
blockqueue(int max_cp = defaultnum)
: max_cp_ (max_cp)
{
pthread_mutex_init(&lock,nullptr);//初始化锁
pthread_cond_init(&c,nullptr);//初始化条件变量
pthread_cond_init(&p, nullptr);
}
void push(T&in)//生产
{
pthread_mutex_lock(&lock);//上锁
if(q.size() == max_cp_)
{
pthread_cond_wait(&p, &lock);//生产满了就要等待
}
q.push(in);//放入到queue中
pthread_cond_signal(&c);//生产完以后就可以唤醒消费
pthread_mutex_unlock(&lock);//解锁
}
T pop()
{
pthread_mutex_lock(&lock);//先锁住,保证线程安全
if(q.size() == 0)//没有商品,就等待
{
pthread_cond_wait(&c, &lock);
}
T front = q.front();
q.pop();
pthread_cond_signal(&p);//消费完商品以后就可以唤醒生产
pthread_mutex_unlock(&lock);
return front;
}
~blockqueue()
{
pthread_mutex_destroy(&lock);//销毁锁
}
private:
queue<T> q; // stl里面的队列,线程不安全
pthread_mutex_t lock; // 保证我的blockqueue是线程安全的,所以加上锁
int max_cp_; // 最大容量
pthread_cond_t c; // 消费者等待的条件变量
pthread_cond_t p; // 生产者等待的条件变量
};
2,执行代码:
#include "Blockqueue.hpp"
void* Comsume(void* args)
{
blockqueue<int> *q = static_cast<blockqueue<int> *>(args);
while(true)
{
int data = q->pop();
cout << "消费了一个商品: " << data << endl;
sleep(1);
}
}
void* Product(void* args)
{
blockqueue<int> *q = static_cast<blockqueue<int> *>(args);
int data = 0;
while(true)
{
q->push(data);
cout << "生产了一个数据: " << data << endl;
data++;
}
}
int main()
{
pthread_t c; // 消费者线程
pthread_t p; // 生产者线程
blockqueue<int> *q = new blockqueue<int>;
pthread_create(&c, nullptr, Comsume, q); // 创建消费者线程,执行消费者方法
pthread_create(&p, nullptr, Product, q); // 创建生产者线程,执行生产方法
pthread_join(c,nullptr);//回收线程
pthread_join(p, nullptr);
return 0;
}
结果:
五,消费-生产者模型(加入数据处理和数据接收)
消费者-生产模型的特点有三个:1,低耦合 2,支持并发 3,支持忙闲不均。
第一个优点比较好理解,因为在这模型里面的两个动作是由两个线程分开干的所以是低耦合的。
第三个优点支持忙闲不均则是因为消费和生产这两个动作是分开的,当我的生产速度太快时因为有交易场所的支持所以消费者可以慢慢的消费。这就是忙闲不均。
但是对于第二个优点是为什么呢?我的消费场所一次只能由一个线程进入去做生产或者消费的动作,那为什么这样会效率高呢?这是因为我们的消费者生产者模型的场景还不全,我们还有生产者获取数据和消费者处理数据的动作没有模拟出来。当这两个动作被加入进来时消费者线程和消费者线程,生产者线程和生产者线程之间便是可以并发执行的。
演示代码:实现计算器
1,首先将任务类定义好
#include<iostream>
#include<string>
using namespace std;
enum
{
Exitcode = 1,
Modexit ,
Unkonw
};
class task//创建一个任务类来执行任务
{
public:
task(int x,int y,char op)
:data1_(x),data2_(y),oper_(op),exit_code_(0),result_(0)
{}
void run()//运行任务:+-*/
{
switch(oper_)
{
case '+':
{
result_ = data1_ + data2_;
}
break;
case '-':
{
result_ = data1_ - data2_;
}
break;
case '*':
{
result_ = data1_ * data2_;
}
break;
case '/':
{
if(data2_ == 0)
{
exit_code_ = Exitcode;
}
else
{
result_ = data1_ / data2_;
}
}
break;
case '%':
{
if(data2_ == 0)
{
exit_code_ = Modexit;
}
else
{
result_ = data1_ % data2_;
}
}
break;
default:
{
exit_code_ = Unkonw;
}
break;
}
}
string Get_result()//将结果显示出来
{
string r;
r+=to_string(data1_);
r += oper_;
r+=to_string(data2_);
r+= '=';
r += to_string(result_);
r += '[';
r += to_string(exit_code_);
r += ']';
return r;
}
string Show_task()//显示任务
{
string r;
r += to_string(data1_);
r += oper_;
r += to_string(data2_);
r += '=';
r+='?';
return r;
}
~task()
{
}
private:
int data1_;//数据1
int data2_;//数据2
char oper_;//运算符
int exit_code_; //退出码
int result_;//运算结果
};
2,模拟输入数据的过程
使用rand()函数模拟创造一些信息给生产者函数接收:
void *Product(void *args)
{
string op = "+-*/%"; // 加减乘除操作
blockqueue<task> *q = static_cast<blockqueue<task> *>(args);
int len = op.size();
while (true)
{
int data1 = rand() % 10000; // 制造数据
int data2 = rand() % 10000;
char oper = op[rand() % len];
task t(data1, data2, oper); // 模拟线程接收数据
q->push(t);
cout << "生产了一个任务: " << t.Show_task() << endl;
}
}
3,模拟线程处理数据
void *Comsume(void *args)
{
blockqueue<task> *q = static_cast<blockqueue<task> *>(args);
while (true)
{
task t = q->pop();
t.run();
cout << "执行了一个任务: " << t.Get_result() << endl;
sleep(1);
}
}
4,消费场所blockqueue
#include <iostream>
#include <pthread.h>
#include <queue>
#include<unistd.h>
using namespace std;
template <class T> // 模板类,交易的对象可以任意
class blockqueue // 创建一个消费场所
{
static const int defaultnum = 10; // queue的默认容量
public:
blockqueue(int max_cp = defaultnum)
: max_cp_ (max_cp)
{
pthread_mutex_init(&lock,nullptr);//初始化锁
pthread_cond_init(&c,nullptr);//初始化条件变量
pthread_cond_init(&p, nullptr);
}
void push(T&in)//生产
{
pthread_mutex_lock(&lock);//上锁
while(q.size() == max_cp_)
{
pthread_cond_wait(&p, &lock);//生产满了就要等待
}
q.push(in);//放入到queue中
pthread_cond_signal(&c); // 生产完以后就可以唤醒消费
pthread_mutex_unlock(&lock);//解锁
}
T pop()
{
pthread_mutex_lock(&lock);//先锁住,保证线程安全
while(q.size() == 0)//没有商品,就等待
{
pthread_cond_wait(&c, &lock);
}
T front = q.front();
q.pop();
pthread_cond_signal(&p); // 消费完商品以后就可以唤醒生产
pthread_mutex_unlock(&lock);
return front;
}
~blockqueue()
{
pthread_mutex_destroy(&lock);//销毁锁
}
private:
queue<T> q; // stl里面的队列,线程不安全
pthread_mutex_t lock; // 保证我的blockqueue是线程安全的,所以加上锁
int max_cp_; // 最大容量
pthread_cond_t c; // 消费者等待的条件变量
pthread_cond_t p; // 生产者等待的条件变量
};
blockqueue这段实现代码其实没什么变化,因为这里实现的是一个模板类。但是值得注意的是这两个变化:
1,在push中
while(q.size() == max_cp_){ pthread_cond_wait(&p, &lock);//生产满了就要等待 }
2,在pop中:
while(q.size() == max_cp_){ pthread_cond_wait(&p, &lock);//生产满了就要等待 }
这两段代码由if条件判断改为了while条件判断,这是因为在线程唤醒时可能会出现一次唤醒多个线程的情况。先被唤醒的线程得到锁以后就去执行代码了。在这个线程归还锁时和他一起被唤醒的线程就可能再次争得这把锁,在queue,size() == max_cp或者queeu.size() == 0可能就会再次修改数据,导致错误。所以要变为while循环判断的方式来避免这种错误。
5,创建多线程执行任务
int main()
{
srand(time(nullptr));
blockqueue<task> *q = new blockqueue<task>;
pthread_t p[3];//三个生产线程
pthread_t c[5];//五个消费线程
// 创建线程
for (int i = 0; i < 3;i++)
{
pthread_create(p+i, nullptr, Product, q);
}
for (int i = 0; i < 5; i++)
{
pthread_create(c+i, nullptr, Comsume, q);
}
//回收线程
for (int i = 0; i < 3; i++)
{
pthread_join(p[i],nullptr);
}
for (int i = 0; i < 5; i++)
{
pthread_join(c[i], nullptr);
}
cout << "main thread quit....." << endl;
return 0;
}