在任意时刻只允许一个执行流访问某段代码就可以叫作互斥。在本篇将介绍互斥锁(mutex)
让我们以抢票模型来开始互斥锁的学习,对于抢票这件事有两个原则,一是大家都会尽量抢更多的票,二是一旦票没了就不能再抢了。所以接下来以这段代码为例,看看这样的抢票方式会不会引起问题
int tickets = 1000;
void* route(void* args)
{
int id = *(int*)args;
delete((int*)args);
while (true)
{
if (tickets > 0)
{
usleep(1000);
cout << id << " 抢到 " << tickets << endl;
tickets--;
}
else { break; }
}
}
int main()
{
pthread_t tid[5];
for (int i = 0; i < 5; i++)
{
int *id = new int(i);
pthread_create(tid + i, nullptr, route, id);
}
for (int i = 0; i < 5; i++)
{
pthread_join(tid[i], nullptr);
}
return 0;
}
我创建了五个进程,分别让他们进行抢票,其中每次对tickets进行–就代表一次抢票的完成,当票数小于等于0时抢票应该结束,那么运行结果是什么呢?
我们发现问题在于当票数小于0时仍有线程在进行抢票操作,这显然是不符合逻辑的,那么原因是什么呢?
我们知道运算操作是由CPU进行的,也就是说我们每次对tickets进行–操作都要把tickets的数据从内存到CPU,再由CPU进行运算,然后再把数据从CPU拷回内存。由此我们也可以知道- -操作并不具备原子性。因此不能让我们获得预期结果的主要原因就是- -操作实际上是由三个操作组成的:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
// 参数: mutex:要初始化的互斥量 attr:nullptr
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
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_ lock 时,可能会遇到以下情况:
让我们看看用mutex改进上面抢票例子的代码
class Ticket
{
private:
int tickets;
pthread_mutex_t mtx;
public:
Ticket()
: tickets(1000)
{
// 初始化
pthread_mutex_init(&mtx, nullptr);
}
bool GetTicket()
{
// 加锁
pthread_mutex_lock(&mtx);
// 锁定临界区,每次只允许一个线程访问
bool res = false;
if (tickets > 0)
{
usleep(1000);
res = true;
cout << pthread_self() << " 抢到票 : " << tickets << endl;
tickets--;
}
else{cout << "票抢完了" << endl;}
// 解锁
pthread_mutex_unlock(&mtx);
return res;
}
~Ticket()
{
// 销毁
pthread_mutex_destroy(&mtx);
}
};
void* route(void* args)
{
Ticket *t = (Ticket*)args;
while (true) { if (!t->GetTicket()) { break; } }
}
int main()
{
Ticket *t = new Ticket();
pthread_t tid[5];
for (int i = 0; i < 5; i++)
{
int *id = new int(i);
pthread_create(tid + i, nullptr, route, (void*)t);
}
for (int i = 0; i < 5; i++)
{
pthread_join(tid[i], nullptr);
}
return 0;
}
因为mutex本身也是一个临界资源,所以我们必须得先保证mutex本身的安全,而保证他安全的方式就是使mutex具有原子性,也就是分别用一条汇编代码来实现lock和unlock。
mutex的原理大致可以这么理解:mutex的初始值为1,当一个线程进入临界区并加锁后,mutex的变为0,解锁后变回1。伪码如下
lock()
{
if (mutex == 1)
{
mutex--;
return 0;
}
else
{
return -1;
}
}
但我们知道–其实不是原子的,因此mutex实际上绝不可能以这种方式实现。为了实现互斥锁操作,大多数体系结构提供了swap和exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理平台,访问内存的周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。我们再来看一段伪码,
// 假设有A,B两线程同时访问这段代码
// CPU在执行线程A的代码的时候,CPU内寄存器的数据是不是线程A私有的?
// 是的,这部分数据在线程A被切换后就会保存到线程A的上下文中
lock:
move $0, %al // 把0赋给寄存器al,设置各自的上下文数据
xchgb %al, mutex // 交换寄存器和内存中的数据
if (al寄存器的内容 > 0) {
return 0;
} else
挂起内容;
goto lock;
unlock:
movb $1, mutex
唤醒等待mutex的线程;
return 0;
}
mutex的本质其实是通过一条汇编,将锁数据交换到自己的上下文中!
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待的状态。
死锁四个必要条件: