【Linux】线程的互斥
因为线程是共享地址空间的,就会共享大部分资源,这种共享资源就是公共资源,当多执行流访问公共资源的时候,就会出现各种情况的数据不一致问题。为了解决这种问题,我们就需要学习线程的同步与互斥,本篇将介绍线程的互斥。
1.相关概念
- 临界资源:多线程执⾏流被保护的共享资源就叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有⼀个执⾏流进⼊临界区,访问临界资源,通常对临界资源起保护作⽤
- 原⼦性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
1.1 数据不一致
下面是一个模拟抢票的简单代码。
#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这个宏初始化就行。
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 接口使用
前面我们已经使用过全局的锁了,现在就用一下局部锁。
#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
#include <mutex> //需要包含的头文件std::mutex cpp_mutex; //定义锁cpp_mutex.lock(); //加锁
cpp_mutex.unlock(); //解锁
3.封装mutex
//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;};
}
//测试
#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类。
#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;};
}
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风格的互斥锁实现。
本篇分享就到这里,我们下篇见~