个人主页:Lei宝啊
愿所有美好如期而遇
目录
问题与解释
首先我们给出一个操作共享变量有问题的售票系统代码:
#define PTHREAD_NUM 5
int tickets = 10000;
class ThreadData
{
public:
ThreadData(int& tickets, string& name)
:_tickets(tickets)
,_name(name)
{}
int total = 0;
int& _tickets;
string _name;
};
void func(ThreadData* td)
{
while(true)
{
if(td->_tickets > 0)
{
usleep(1000);
cout << td->_name << ": " << tickets << endl;
td->_tickets--;
td->total++;
}
else
{
break;
}
}
}
假设有多个线程同时去抢票,就会出现将票抢到负数的可能,我们将代码补全去跑,是会跑出票为负数的,这里我们不多做演示,给出这种现象的解释:
在票数为1之前出现的错误我们先不谈,我们就从票数已经就剩一张,多个线程还在抢来解释。
CPU进行逻辑运算时,将内存中tickets变量的值拷贝一份进CPU寄存器中,与上面的0进行比较。设我们有三个线程,第一个线程在if比较后,时间片到了,线程被CPU切换,线程二开始跑,也进行逻辑运算,当他比较之后,时间片也到了,线程又被CPU切换,这样三个线程用同一张票的值都进入了抢票逻辑。
当其中一个线程对tickets--后,将tickets的值写回内存(其实因为tickets--操作不是原子的,这里仍然有可能出现一种小概率情况,我们后面说),这样其他线程在读取内存tickets值时,再去--,就会减到负数,然后写回内存,其他线程打印这个值时,从内存读上来就是负数了。
tickets--这个操作,将他编译成汇编代码,他是有三句的:将tickets的值读进寄存器,在寄存器内做--,将值写回内存。如果说,一个线程,将值读入寄存器,然后就被切换了,此时他会保存他的硬件上下文,将读入寄存器的值带走,下一个线程再读取tickets,假设他们没有被切换,而是一直--,当最初的线程再执行时,将他的硬件上下文覆盖到寄存器上,他的tickets值就和内存中tickets的值不同,如果他后续--做完再写回内存,那么因为数据不一致,整个抢票就乱了。
这里我们解释一下原子:就是经过编译后,只有一条汇编指令,就是原子的。
那么我们如何解决这个问题?对全局的共享资源做保护!也就是对tickets做保护!怎么对这个临界资源做保护?我们可以使用一把锁,也就是互斥量mutex。
互斥量接口
两种初始化mutex方法(初始化锁)
第一种:
我们先创建一个pthread_mutex_t的mutex变量,然后将他的地址传给第一个参数,第二个参数用来设置互斥锁的属性,我们设置为nullptr默认设置即可。
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, nullptr);
第二种:
使用宏在全局进行初始化。
加锁与解锁
第一个pthread_mutex_lock就是申请锁,unlock就是解锁,两个函数中间的代码就是需要被保护的临界资源。如果申请锁成功,函数返回,并允许继续向后执行;申请锁不成功,那么线程就阻塞或被挂起,不允许向后执行,直到锁被释放,然后再去申请锁;函数调用失败,出错返回。
trylock尝试去申请锁,申请失败,则错误返回,线程不会阻塞。
class ThreadData
{
public:
ThreadData(int& tickets, string& name, pthread_mutex_t& mutex)
:_tickets(tickets)
,_name(name)
,_mutex(mutex)
{}
int total = 0;
int& _tickets; //临界资源
string _name;
pthread_mutex_t& _mutex; //锁
};
void func(ThreadData* td)
{
while(true)
{
pthread_mutex_lock(&td->_mutex);
if(td->_tickets > 0)
{
cout << td->_name << ": " << tickets << endl;
td->_tickets--;
pthread_mutex_unlock(&td->_mutex);
usleep(1000);
td->total++;
}
else
{
pthread_mutex_unlock(&td->_mutex);
break;
}
}
}
如果我们想要再优雅一点,可以做个封装:
class LockGuard
{
public:
LockGuard(pthread_mutex_t* mutex)
:_mutex(mutex)
{
pthread_mutex_lock(_mutex);
}
~LockGuard()
{
pthread_mutex_unlock(_mutex);
}
private:
pthread_mutex_t* _mutex;
};
这样就是一个具有RAII风格的互斥锁了。
void func(ThreadData* td)
{
while(true)
{
LockGuard lg(&td->_mutex);
if(td->_tickets > 0)
{
cout << td->_name << ": " << tickets << endl;
td->_tickets--;
td->total++;
}
else
{
break;
}
}
}
在最后不再使用锁时,如果使用第一种初始化锁的方式,需要调用下面的函数对锁进行释放。
如果是全局使用宏进行初始化的锁,则不需要。
整体代码:
#include <pthread.h>
#include <iostream>
#include <vector>
#include <string>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <unistd.h>
using namespace std;
#define PTHREAD_NUM 5
int tickets = 10000;
class ThreadData
{
public:
ThreadData(int& tickets, string& name, pthread_mutex_t& mutex)
:_tickets(tickets)
,_name(name)
,_mutex(mutex)
{}
int total = 0;
int& _tickets; //临界资源
string _name;
pthread_mutex_t& _mutex; //锁
};
template<class T>
class Thread
{
public:
Thread(function<void(T*)> func, T* data)
:_func(func)
,_data(data)
{}
static void* routine(void* args)
{
Thread* thread = reinterpret_cast<Thread*>(args);
thread->_func(thread->_data);
return nullptr;
}
void CreateRun()
{
int n = pthread_create(&_tid, nullptr, routine, this);
if(n != 0) cout << "create: " << strerror(n) << endl;
}
void Join()
{
pthread_join(_tid, nullptr);
}
void detach()
{
pthread_detach(_tid);
}
private:
//希望能够得到临界资源
T* _data;
pthread_t _tid;
function<void(T*)> _func;
};
// void func(ThreadData* td)
// {
// while(true)
// {
// pthread_mutex_lock(&td->_mutex);
// if(td->_tickets > 0)
// {
// cout << td->_name << ": " << tickets << endl;
// td->_tickets--;
// pthread_mutex_unlock(&td->_mutex);
// td->total++;
// }
// else
// {
// pthread_mutex_unlock(&td->_mutex);
// break;
// }
// }
// }
class LockGuard
{
public:
LockGuard(pthread_mutex_t* mutex)
:_mutex(mutex)
{
pthread_mutex_lock(_mutex);
}
~LockGuard()
{
pthread_mutex_unlock(_mutex);
}
private:
pthread_mutex_t* _mutex;
};
void func(ThreadData* td)
{
while(true)
{
LockGuard lg(&td->_mutex);
if(td->_tickets > 0)
{
cout << td->_name << ": " << tickets << endl;
td->_tickets--;
td->total++;
}
else
{
break;
}
}
}
int main()
{
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, nullptr);
vector<Thread<ThreadData>> vttd;
vector<ThreadData*> del;
for(int i=0; i<PTHREAD_NUM; i++)
{
string name = "pthread--"+to_string(i);
ThreadData* thread = new ThreadData(tickets, name, mutex);
vttd.emplace_back(func, thread);
del.emplace_back(thread);
}
for(auto &e: vttd) e.CreateRun();
for(auto &e: vttd) e.Join();
for(auto &e: del) cout << e->_name << ": " <<e->total << endl;
for(auto &e: del) delete e;
return 0;
}
小总结:一开始的问题在于多个执行流并发执行临界区代码,而互斥其实就是保证了多个线程能够串行化的执行临界区代码,从而解决了问题。
同时,我们希望对临界资源的加锁粒度越细越好,也就是尽量使得只给关于临界资源的代码加锁,不相干的代码我们不希望他也加锁。
互斥的底层实现
互斥量为什么能保护临界资源?
首先,我们可以肯定的是,互斥量的一定是原子的,如果不是原子的,那么他也一定会出现多执行流并行导致出错的问题,因为他也是临界资源,锁也是被多线程所共享的。
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期
当一个线程开始执行pthread_mutex_lock时:
movb $0, %al 将%al寄存器中的值初始化为0
xchgb %al, mutex 将内存中mutex的值,即1与%al寄存器的值做交换
如果al寄存器的值大于0,那么这个线程成功申请锁,就返回了,如果没有申请成功,那么就挂起等待,直到锁被释放,线程被唤醒时,再去申请锁。
当一个线程开始执行pthread_mutex_unlock时:
movb $1, mutex 将内存中mutex的值置1
那么在这个申请锁的过程中线程可以被切换吗?
答案是可以的,但是假设在线程执行xchgb后线程被切换,那么这个%al寄存器中的1就成为这个线程的硬件上下文,被这个线程带走,即使后来线程再去申请锁,由于这个1只有一个,所以注定了在锁释放之前,他申请不到锁,也就注定了他不能执行临界区代码!
我们给出一个结论:内存中的共享资源,所有的线程都可以进行访问,但是一旦转移到寄存器中,那么就属于一个线程私有了!!!(因为寄存器只有一套,而寄存器数据,也就是线程的硬件上下文却有多套,每一次线程切换,切走的线程都会带走自己的硬件上下文,要执行的线程会将自己的数据覆盖到寄存器上!!!)
这样也就保证了执行临界区代码的整个过程是原子的,因为只有一个线程可以执行,这也就保证了线程安全!!!