先来看看一些基本概念:
互斥量mutex:
比如一个大家熟知的栗子:售票。我们用一个全局整形变量记录票的个数,多个线程并发的去抢票,我们不难写出下面这样的代码:
int g_ticket=10000;
void* Run(void* args)
{
string name=static_cast<const char*>(args);
while(true)
{
if(g_ticket<=0)
{
break;
}
else
{
cout<<"I am "<<name<<",is running tickets"<<g_ticket<<endl;
g_ticket--;
}
usleep(2000);
}
return nullptr;
}
int main()
{
pthread_t ptids[5];
for(int i=0;i<5;++i)
{
char* name=new char[26];
snprintf(name,26,"pthread%d",i+1);
pthread_create(ptids+i,nullptr,Run,name);
}
for(int i=0;i<5;++i)
{
pthread_join(ptids[i],nullptr);
}
return 0;
}
当我们运行时:
我们发现,有多个线程抢到了同一张票,并且打印混乱。有些情况下票还有可能变成了负数,而这就是线程不安全所带来的问题,解决办法我们在下面会给出详细解释。
我们来分析下上面的代码为什么会出现那样的结果?
我们可以取出渐渐ticket取出ticket–部分的汇编代码:
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>
- -
操作并不是原子操作,而是对应三条汇编指令:
要解决以上问题,需要做到三点:
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量
。
初始互斥量有两种方式:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL
这两种方式选择哪一种都是OK的。
销毁互斥量需要注意:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调用pthread_mutex_lock 时,可能会遇到以下情况:
所以我们可以改进下上面的抢票:
int g_tictet=10000;
pthread_mutex_t mtu=PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;
void* Run(void* args)
{
string name=static_cast<const char*>(args);
while(true)
{
pthread_mutex_lock(&mtu);
if(g_tictet<=0)
{
pthread_mutex_unlock(&mtu);
break;
}
else
{
cout<<"I am "<<name<<",is running tickets"<<g_tictet<<endl;
g_tictet--;
}
pthread_mutex_unlock(&mtu);
usleep(2000);
}
return nullptr;
}
int main()
{
pthread_t ptids[5];
for(int i=0;i<5;++i)
{
char* name=new char[26];
snprintf(name,26,"pthread%d",i+1);
pthread_create(ptids+i,nullptr,Run,name);
}
for(int i=0;i<5;++i)
{
pthread_join(ptids[i],nullptr);
}
return 0;
}
当我们再次运行时:
我们发现不会出现多个线程抢占同一张票并且打印混乱的情况了。
代码中值得注意的事情有:加锁的策略是:选用的粒度一般是越细越好。
搞了这么多,那么互斥量的实现原理究竟是啥捏?
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange
指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
我们可以自己实现一份lock和unlock的伪代码:
lock:
movb $0,%al
xchgb %al,mutex
if(al寄存器的内容>0)
return 0;//表示申请锁成功
else
挂起等待;
goto lock;
unlock:
movb $1,%al
唤醒等待mutex的线程;
return 0;//表示释放锁成功
通过上面的伪代码我们可以知道当初始值mutex的值为1时,假设线程1先进行申请锁,会先将寄存器中的值改为0,然后用寄存器中的0交换mutex中的1,此时1被线程1给拿到了,假设此时线程1的时间片到了,要切换线程2执行,在切换之前先保存了线程1的上下文数据,然后切换;此时线程2从头执行将寄存器中的数值改为0,然后交换,但是唯一的1已经被线程1给拿走了,所以线程而只有挂起等待;当重新切换回线程1的时候,线程1会重新恢复上下文数据,也就是寄存器的内容会被恢复到切换前,所以判断寄存器的内容>0,申请成功。此时我们发现就算是有多个线程并发的抢占锁资源时,也只有一个线程能够申请成功,其他线程在挂起等待,因为这里面的1
只有一个,并且是以交换形式进行的,可以理解这里面的1本质就是一把锁。
释放资源就更好理解了,将寄存器的值修改为1,然后唤醒等待锁的线程即可。从释放锁的那段伪代码中我们也能够看到:当多个线程申请同一把锁时,一个线程申请了锁后,虽然其他线程不能够申请了,但是却可以释放该锁。
不安全情况:
安全情况:
不可重入:
可重入:
联系:
区别:
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
死锁避免算法有银行家算法和死锁检测算法,大家有兴趣可以自行下去研究。
我们上面写的代码中,我们能否自己实现一个简易版本的创建线程(类似于C++11提供的线程库那样)的类呢?以及加锁和解锁能够使用RAII的思想来帮助我们完成呢?当然是可以的,我们可以自己实现一个更加优雅的代码:
mutexGuard.hpp:
#pragma once
#include
#include
using namespace std;
class mutexGurad
{
public:
mutexGurad(pthread_mutex_t* mutex)
:_mutex(mutex)
{
pthread_mutex_lock(_mutex);
}
~mutexGurad()
{
pthread_mutex_unlock(_mutex);
}
private:
pthread_mutex_t* _mutex;
};
thread.hpp:
#pragma once
#include
#include
using namespace std;
class threadProcess
{
public:
enum stu
{
NEW,
RUNNING,
EXIT
};
template <class T>
threadProcess(int num, T exe, void *args)
: _tid(0),
_status(NEW),
_exe(exe),
_args(args)
{
char name[26];
snprintf(name, 26, "thread%d", num);
_name = name;
}
static void *runHelper(void *args)
{
threadProcess *ts = (threadProcess *)args;
(*ts)();
return nullptr;
}
void operator()() // 仿函数
{
if (_exe != nullptr)
_exe(_args);
}
void Run()
{
int n = pthread_create(&_tid, nullptr, runHelper, this);
if (n != 0)
exit(-1);
_status = RUNNING;
}
void Join()
{
int n = pthread_join(_tid, nullptr);
if (n != 0)
exit(-1);
_status = EXIT;
}
private:
string _name;
pthread_t _tid;
stu _status;
function<void *(void *)> _exe;
void *_args;
};
测试程序:
int g_tictet = 10000;
pthread_mutex_t mtu = PTHREAD_MUTEX_INITIALIZER;
void *Run(void *args)
{
string name = static_cast<const char *>(args);
while (true)
{
{
mutexGurad mutGuard(&mtu);
if (g_tictet <= 0)
{
break;
}
else
{
cout << "I am " << name << ",is running tickets" << g_tictet << endl;
g_tictet--;
}
}
usleep(1000);
}
return nullptr;
}
int main()
{
threadProcess thpro1(1, Run, (void *)"thread1");
threadProcess thpro2(2, Run, (void *)"thread2");
threadProcess thpro3(3, Run, (void *)"thread3");
thpro1.Run();
thpro2.Run();
thpro3.Run();
thpro1.Join();
thpro2.Join();
thpro3.Join();
return 0;
}
当我们运行时:
我们依旧能够得到正确的结果,并且代码写起来也好看多了。除此之外,我们还可以拿到线程的其他特性,这里我就不在测试了。