目录
[1、pthread_mutex_init && pthread_ mutex_destory](#1、pthread_mutex_init && pthread_ mutex_destory)
[2、pthread_mutex_lock && pthread_mutex_unlock](#2、pthread_mutex_lock && pthread_mutex_unlock)
1、概念引入
为了介绍线程的同步与互斥,我们以抢票逻辑引入相关的概念。
示例代码:
#include<iostream>
#include<vector>
int g_ticket = 1000;
void* funtion(void* arg)
{
while(1)
{
if(g_ticket > 0)
{
//模拟抢票逻辑
std::cout << "g_ticket : " << g_ticket << " &g_ticket : " << &g_ticket << std::endl;
g_ticket--;
}
else
{
break;
}
}
return nullptr;
}
const int num = 5;
int main()
{
std::vector<pthread_t> pthread;
for(int i = 0; i < num; i++)
{
pthread_t id;
pthread_create(&id,nullptr,funtion, nullptr);
pthread.emplace_back(id);
}
for(auto& e: pthread)
{
pthread_join(e,nullptr);
}
return 0;
}
我们创建多个线程,然后让不同的线程对同一个变量进行操作。也就是让不同线程访问同一个函数,抢票函数会让票数减减。我们运行一下代码,观察一下结果。
这里我们可以看见,g_ticket的地址是一样的,说明访问的变量一定是同一全局变量。而g_ticket的数值却出现-2,-3。这明显是不符合我们的要求的,所以这里一定是有问题的,而这也叫数据不一致 。
首先解释一下为什么会造成这种现象,首先if中的条件判断是逻辑判断,这是要在cpu内进行的运算的。(我们假设g_ticket的值为1)
每当一个线程在将内存中g_ticket数据放入cpu中的寄存器,由cpu执行判断逻辑运算。在判断成功后,该线程就有可能直接被切走了,没有执行到下面的g_ticket--操作,而内存中的g_ticket还是1。下个线程也会重复上述的操作,这就导致虽然只有一张票,但却还是有多个线程进入该函数中。线程同时执行了ticket--操作,所以就会造成ticket被减到负数的情况。同时,需要注意的是,ticket--也不是原子的,这段代码在cpu中的执行过程分为三步,第一步是将内存中g_ticket读取到cpu中,第二步是将cpu中的g_ticket做减减操作,第三步是将cpu中的值写回内存中。这里的每一步在结束后,线程可能都会被切走,这也会造成g_ticket这个数据不安全,不过这里一般出错概率较低,主要还是上述因素造成的。这种因为原子性问题导致数据不一致情况还是比较常见的。(原子性:只有执行和没执行两种状态,说通俗一点就是翻译成汇编只有一条语句,上面的++操作翻译成汇编就有3条语句) 总结一下,这里g_ticket出现负数情况的原因,其实就是这个共享资源没有被保护,并且访问该共享资源的过程并不是原子性的。
2、互斥锁
如何解决上述的问题呢?这里我们就需要引入锁的概念了。原生线程库不仅提供了线程创建等相关的接口,还提供了互斥锁的相关接口。我们在线程中常用的锁一般称为互斥锁,互斥的概念前面已经有所介绍,这里不再赘述。我们只要对共享资源进行加锁,就能防止出现上述的问题。下面先介绍一下相关的接口。
1、pthread_mutex_init && pthread_ mutex_destory
在使用锁之前,我们首先需要定义一个pthread_mutex_t类型的变量。如果这个变量是局部变量,我们就需要使用pthread_mutex_init进行初始化(这里的第二参数表示锁的属性,这里定义成nullptr即可),同时在使用完后,要对锁进行pthread_mutex_destory释放。如果这个变量是全局变量,则我们只需要在定义变量时,让其等于PTHREAD_MUTEX_INITALIZER进行初始化即可,后面不需要手动进行销毁。
2、pthread_mutex_lock && pthread_mutex_unlock
当我们初始化锁以后,我们就需要使用锁了,pthread_mutex_lock 就表示申请上锁,申请成功,函数返回,继续向后执行;申请失败,一直阻塞直至申请成功;如果函数调用失败,出错返回。而trylock接口在申请失败后会直接返回,这是和lock接口的区别。而unlock就表示解开该锁。
下面演示一下加锁例子,我们以上面抢票逻辑的代码为例子。
ThreadMode.hpp
#ifndef __THREAD_HPP__
#define __THREAD_HPP__
#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>
#include <pthread.h>
namespace ThreadModule
{
template<typename T>
using func_t = std::function<void(T)>;
// typedef std::function<void(const T&)> func_t;
template<typename T>
class Thread
{
public:
void Excute()
{
_func(_data);
}
public:
Thread(func_t<T> func, T data, const std::string &name="none-name")//右值
: _func(func), _data(data), _threadname(name), _stop(true)
{}
static void *threadroutine(void *args) // 类成员函数,形参是有this指针的!!
{
Thread<T> *self = static_cast<Thread<T> *>(args);
self->Excute();
return nullptr;
}
bool Start()
{
int n = pthread_create(&_tid, nullptr, threadroutine, this);
if(!n)
{
_stop = false;
return true;
}
else
{
return false;
}
}
void Detach()
{
if(!_stop)
{
pthread_detach(_tid);
}
}
void Join()
{
if(!_stop)
{
pthread_join(_tid, nullptr);
}
}
std::string name()
{
return _threadname;
}
void Stop()
{
_stop = true;
}
T& Data()
{
return _data;
}
~Thread() {}
private:
pthread_t _tid;
std::string _threadname;
T _data; // 为了让所有的线程访问同一个全局变量
func_t<T> _func;
bool _stop;
};
} // namespace ThreadModule
#endif
#include <iostream>
#include "ThreadMode.hpp"
#include <vector>
int g_ticket = 1000;
using namespace ThreadModule;
template <class T>
class ThreadData
{
public:
ThreadData(int &data, const std::string str) : _data(data), name(str), total(0)
{
}
~ThreadData()
{
}
std::string Getname()
{
return name;
}
void buyticket()
{
_data--;
}
int Geticket()
{
return _data;
}
void Plus()
{
total++;
}
void Total()
{
std::cout << name << " : " << total << std::endl;
}
private:
int &_data;
std::string name;
int total;
};
pthread_mutex_t _lock = PTHREAD_MUTEX_INITIALIZER;//全局锁
void funtion(ThreadData<int> *td)
{
while (1)
{
//加锁
pthread_mutex_lock(&_lock);
if (g_ticket > 0)
{
std::cout << td->Getname() << " get ticket, remain ticket number: " << td->Geticket() << std::endl;
td->buyticket();
pthread_mutex_unlock(&_lock);
td->Plus();
}
else
{
pthread_mutex_unlock(&_lock);
break;
}
}
}
const int num = 5;
int main()
{
std::vector<Thread<ThreadData<int> *>> thread;
for (int i = 0; i < num; i++)
{
// char* threadname = new char[64];
// snprintf(threadname, 64, "Thread-%d", i + 1);
std::string threadname = "thread -" + std::to_string(i + 1);
ThreadData<int> *ptr = new ThreadData<int>(g_ticket, threadname);
thread.emplace_back(Thread<ThreadData<int> *>(funtion, ptr, threadname));
}
for (auto &e : thread)
{
e.Start();
}
for (auto &e : thread)
{
sleep(1);
e.Data()->Total();
e.Join();
delete e.Data();
}
return 0;
}
这里我们着重看funtion执行函数即可,在该函数外部,我定义了一把全局锁。当不同线程执行同一函数,需要访问一块共享资源(临界资源)的代码,我们就称为临界区,其它部分,我们称为非临界区。而我们要保护的,其实就是临界区的资源,这就要求我们在加锁时,尽量只要对临界区的部分进行加锁即可,对其他非临界区的部分,可以不用管。这里当多个线程同时访问同一变量时,就需要去竞争那一把全局锁。谁竞争到了锁,谁就能对临界资源进行访问,其余线程只能在临界区外进行等待。当然,这里不排除有的线程竞争锁的能力很强,让其他线程根本就竞争不到锁的情况,这就会造成其他进程的饥饿问题。在不同系统下,不同线程的竞争能力不同,这和锁的创建时间、os的调度算法等有管。
运行结果
这里就出现了线程4、5的竞争问题,不过这里,我们暂不探究。而票数这个全局变量,在加锁后,就回复正常了。在C++中,对互斥锁的释放和初始化等等操作进行了包装,这里不一一介绍,如果有需要,也可以自行封装。
3、互斥锁原理的简单介绍
互斥锁底层在不同OS下可能有不同的实现方式,这里简单介绍一种。在互斥锁中,实际上表示持有锁的状态就是用一个整数,我们可以用+或-来改变其状态,但我们前面提到,++操作并不是原子性的,所以这个用整数来表示持有锁的状态是有一定问题的。为了解决该问题,系统中有特定的汇编指令,能够原子地交换cpu寄存器和物理内存中的数值。我们用1表示持有锁,0表示不持有锁。
假设我们现在定义了一把锁,我们lock这个整型变量来表示锁地使用情况。如果lock为0,表示锁被使用,不为零,就被没使用。在内存中的数据大部分是被线程共享的,而在cpu上的寄存器中存储的硬件上下文是被线程私有的。
首先,线程内部也有整型变量表示是否持有锁,我们以0表示不持有锁,1表示持有锁。以上图为例,thread-1申请锁时,会首先将线程内部的变量读入寄存器中,然后通过特殊汇编指令与内存中的lock值交换(该过程为原子的),此时就完成了锁的申请。此时时间片耗尽,线程就会切换,同时寄存器中的硬件上下文也会被清空,锁被带走。第二个线程也会将自己表示持有锁状态的变量读入寄存器中,然后重复上述的动作,但是由于lock变量已经为零,所以第二个线程即使交换完后,也是无法持有锁的。解锁,就是把线程上的数据交换会内存中,表现在图上就是线程中的"1"换回内存中lock变量。所以其他线程想要访问临界资源,就只能等待线程把锁释放,访问临界区的过程也就是线程安全的。
以上就是所有的内容,文中如有不对之处,还望各位大佬指正,谢谢!!!