
因为线程是共享地址空间的,就会共享大部分资源,这种共享资源就是公共资源,当多执行流访问公共资源的时候,就会出现各种情况的数据不一致问题。为了解决这种问题,我们就需要学习线程的同步与互斥,本篇将介绍线程的互斥。
1.相关概念
- 临界资源:多线程执⾏流被保护的共享资源就叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有⼀个执⾏流进⼊临界区,访问临界资源,通常对临界资源起保护作⽤
- 原⼦性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
1.1 数据不一致
⼤部分情况,线程使⽤的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量
归属单个线程,其他线程⽆法获得这种变量。 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
下面是一个模拟抢票的简单代码。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
void *route(void *arg)
{
char *id = (char *)arg;
while (1)
{
if (ticket > 0) // 判断票的数量
{
usleep(1000); // 模拟抢票的时候花费的时间
printf("%s sells ticket:%d\n", id, ticket); // 假设这里就是抢到票了
ticket--; // 更新票的数量
}
else
{
break;
}
}
return nullptr;
}
int main()
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, (void *)"thread 1");
pthread_create(&t2, NULL, route, (void *)"thread 2");
pthread_create(&t3, NULL, route, (void *)"thread 3");
pthread_create(&t4, NULL, route, (void *)"thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}
运行后会发现,这个票数居然还减到了负数。

主要是因为usleep让所有线程在判断tickets>0时,全部进到判断里,但是usleep却不让线程往后执行,大大提升了线程被同时进到临界区的机会,tickets就会被减到负数。(不一致原因详情:课42)
全局资源没有加保护就可能会有并发问题,这也是线程安全问题。
1.2 见一见锁
解决上面出现的问题我们可以给临界区代码加锁。

mutex互斥锁,也叫互斥量,它的类型就叫pthread_mutex_t,使用锁需要头文件pthread.h。
使用的时候先对锁初始化,直接用PTHREAD_MUTEX_INITIALIZER这个宏初始化就行。
cpp
int ticket = 100;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 定义一把锁并初始化
void *route(void *arg)
{
char *id = (char *)arg;
while (1)
{
pthread_mutex_lock(&lock); // 加锁
if (ticket > 0) // 判断票的数量
{
usleep(1000); // 模拟抢票的时候花费的时间
printf("%s sells ticket:%d\n", id, ticket); // 假设这里就是抢到票了
ticket--; // 更新票的数量
pthread_mutex_unlock(&lock); // 解锁
}
else
{
pthread_mutex_unlock(&lock); // 防止走到else时锁没解
break;
}
}
return nullptr;
}

可以看到加锁之后就没有出现数据被减到负数了,而且还能感受到这个代码的运行速度变慢了。
2.认识mutex
- 全局的锁:这种方式定义的锁不用被释放,程序运行结束会自动释放。
- 局部的锁:就要用到相关的函数,初始化锁的函数第二个参数就是锁的一些属性,不用管。局部的锁要调用destroy释放。
- 不管是全局的还是局部的锁,线程在访问公共资源之前都要申请锁,lock加锁,unlock解锁。线程申请锁成功,继续向后运行,申请失败会阻塞挂起申请执行流。trylock是非阻塞版本,不考虑。
所有线程都要竞争申请锁,所以首先所有线程都要看到锁,所以锁本身就是临界资源;锁是用来保护临界区资源的,但是谁来保护锁?所以要求锁的申请和解除必须是原子的。
锁提供的能力本质就是:执行临界区代码的执行流由并行转为串行。
2.1 接口使用
前面我们已经使用过全局的锁了,现在就用一下局部锁。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include <iostream>
int ticket = 100;
struct Data
{
Data(const std::string &name, pthread_mutex_t *plock)
: _name(name),
_plock(plock)
{}
std::string _name;
pthread_mutex_t *_plock;
};
void *route(void *arg)
{
Data* d = static_cast<Data *>(arg);
while (1)
{
pthread_mutex_lock(d->_plock); // 加锁
if (ticket > 0)
{
usleep(1000);
printf("%s sells ticket:%d\n", d->_name.c_str(), ticket);
ticket--;
pthread_mutex_unlock(d->_plock); // 解锁
}
else
{
pthread_mutex_unlock(d->_plock); // 防止走到else时锁没解
break;
}
}
return nullptr;
}
int main()
{
pthread_mutex_t lock; // 局部锁
pthread_mutex_init(&lock, nullptr); // 对锁初始化
Data d1("thread 1", &lock);
Data d2("thread 2", &lock);
Data d3("thread 3", &lock);
Data d4("thread 4", &lock);
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, &d1);
pthread_create(&t2, NULL, route, &d2);
pthread_create(&t3, NULL, route, &d3);
pthread_create(&t4, NULL, route, &d4);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&lock); // 销毁锁
return 0;
}
操作还是比较简单的。
对临界区资源进行保护,本质就是用锁对临界区代码进行保护。

- 加锁之后,在临界区内部,依旧允许线程切换,因为当前线程并没有释放锁,依旧持有锁,带着锁被切换的,其他的线程必须等我回来执行完代码,将锁释放后,他们才可以展开对锁的竞争从而进入临界区。
- 这把锁要么没被使用,要么已经被使用完了,这两种状态才对其他线程有意义,这就体现了原子性。
- 在线程访问临界区资源时不会被其他线程打扰,也是一种变相的原子性的表现。
2.2 mutex的原理
硬件实现:关闭时钟中断(了解即可)。
软件实现:为了实现互斥锁操作,⼤多数体系结构都提供了swap或exchange汇编指令,该指令的作⽤是把寄存器和内存单元的数据相交换,由于只有⼀条指令,保证了原⼦性,下面有段伪代码。

申请锁
进程/线程切换:CPU内部的寄存器硬件只有一套,但是CPU寄存器的数据可以有多份,每份就是当前执行流的上下文数据。
把一个变量的内容换到CPU内部,其实就是把变量的内容获取到当前执行流的硬件上下文中,CPU寄存器的硬件上下文属于进程/线程私有的。
我们用swap或exchange将内存中的变量交换到寄存器中,其实就是当前进程/线程在获取锁,是交换,而不是拷贝,所以锁只有一份,谁申请谁持有。

当后面来的执行流想申请锁,首先会把寄存器清0,然后在交换的这一步时,就只会用0换0,因为这个1已经被之前的线程申请走了,此时申请锁失败,线程就会阻塞挂起。

解锁
解锁的时候,只需要往内存里的mutex写1。

2.3 C++里的mutex
cpp
#include <mutex> //需要包含的头文件
std::mutex cpp_mutex; //定义锁
cpp_mutex.lock(); //加锁
cpp_mutex.unlock(); //解锁

3.封装mutex
cpp
//Mutex.hpp文件
#include <iostream>
#include <pthread.h>
#include <cstring>
#include <cstdio>
namespace MyMutex
{
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(_plock, nullptr); // 锁初始化
}
void Lock() // 加锁
{
int n = pthread_mutex_lock(_plock);
if (n != 0)
std::cerr << "pthread_mutex_lock fail: " << strerror(n) << std::endl;
}
void UnLock() // 解锁
{
int n = pthread_mutex_unlock(_plock);
if (n != 0)
std::cerr << "pthread_mutex_unlock fail: " << strerror(n) << std::endl;
}
~Mutex()
{
pthread_mutex_destroy(_plock); // 锁释放
}
private:
pthread_mutex_t *_plock;
};
}
cpp
//测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <string>
#include <iostream>
#include "Mutex.hpp"
using namespace MyMutex;
int ticket = 100;
struct Data
{
Data(const std::string &name, Mutex *plock)
: _name(name),
_plock(plock)
{
}
std::string _name;
Mutex *_plock;
};
void *route(void *arg)
{
Data *d = static_cast<Data *>(arg);
while (1)
{
d->_plock->Lock(); // 加锁
if (ticket > 0)
{
usleep(1000);
printf("%s sells ticket:%d\n", d->_name.c_str(), ticket);
ticket--;
d->_plock->UnLock(); // 解锁
}
else
{
d->_plock->UnLock(); // 解锁
break;
}
}
return nullptr;
}
int main()
{
Mutex lock; //用自己实现的锁
Data d1("thread 1", &lock);
Data d2("thread 2", &lock);
Data d3("thread 3", &lock);
Data d4("thread 4", &lock);
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, &d1);
pthread_create(&t2, NULL, route, &d2);
pthread_create(&t3, NULL, route, &d3);
pthread_create(&t4, NULL, route, &d4);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}
我们还可以进一步封装这个锁,让他可以自动的加锁解锁。需要在实现一个LockGuard类。
cpp
#include <iostream>
#include <pthread.h>
#include <cstring>
#include <cstdio>
namespace MyMutex
{
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(_plock, nullptr); // 锁初始化
}
void Lock() // 加锁
{
int n = pthread_mutex_lock(_plock);
if (n != 0)
std::cerr << "pthread_mutex_lock fail: " << strerror(n) << std::endl;
}
void UnLock() // 解锁
{
int n = pthread_mutex_unlock(_plock);
if (n != 0)
std::cerr << "pthread_mutex_unlock fail: " << strerror(n) << std::endl;
}
~Mutex()
{
pthread_mutex_destroy(_plock); // 锁释放
}
private:
pthread_mutex_t *_plock;
};
class LockGuard
{
public:
LockGuard(Mutex *mutex)
: _mutex(mutex)
{
_mutex->Lock(); // 构造时加锁
}
~LockGuard()
{
_mutex->UnLock(); // 析构时解锁
}
private:
Mutex *_mutex;
};
}
cpp
void *route(void *arg)
{
Data *d = static_cast<Data *>(arg);
while (1)
{
{
LockGuard lock_guard(d->_plock);
if (ticket > 0)
{
usleep(1000);
printf("%s sells ticket:%d\n", d->_name.c_str(), ticket);
ticket--;
}
else
{
break;
}
}
}
return nullptr;
}
这个就叫做RAII风格的互斥锁实现。
本篇分享就到这里,我们下篇见~
