互斥
在之前的学习中,了解到了临界资源、临界区、等等。
临界资源:多线程(执行流)共享的资源。
临界区:每一个线程(执行流)内部访问临界资源的代码。
**互斥:简单来说就是在任何时刻保证有且只有一个执行流进入临界区,访问临界资源;**对临界资源起保护作用
原子性:不会被任何调度机制打断的操作;要么完成,要么未完成
互斥量mutex
在了解互斥量之前,先来看以下代码:
cpp
#include <iostream>
#include <unistd.h>
int count = 1000;
void *ticket(void *args)
{
// 预定演唱会门票
std::string name = static_cast<char *>(args);
while (count > 0)
{
std::cout << name << " 售出门票 : " << count << std::endl;
count--;
usleep(100);
}
return (void *)100;
}
int main()
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, nullptr, ticket, (void *)"pthread-1 ");
pthread_create(&t2, nullptr, ticket, (void *)"pthread-2 ");
pthread_create(&t3, nullptr, ticket, (void *)"pthread-3 ");
pthread_create(&t4, nullptr, ticket, (void *)"pthread-4 ");
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
pthread_join(t4, nullptr);
return 0;
}
上述代码,模拟演唱会门票的预定,由4个线程(执行流)同时进行售票;
看一下运行结果:

可以发现,一共1000
张门票,4
个线程(执行流)同时进行售票(count--
);
在售票的过程中,竟然出现了-1
、-2
;
为什么会出现这种情况呢?
很简单,多个执行流访问
count
,当一个线程执行到count--
时,另一个线程已经进入whlile
循环,这样count--
后已经<0
了;此外,如果线程执行
while
内代码,还未执行到count--
,线程就被切换了;此时其他线程执行count
还是满足条件的。那也就是说,线程在访问
count
时,会被别的线程打扰,从而导致数据不一致问题。
原子性
此外,对于上述代码还存在一个问题,那就是count--
不是原子的。
到这里,可能会感觉很懵,count--
不是原子的?
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax #
600b34 <count>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) #
600b34 <count>
通过反汇编,可以看一下count--
部分的汇编代码,可以看到是分三步进行的:
load
:将共享变量count
从内存加载到寄存器中update
:更新寄存器中的值,执行-1
操作store
:将新值从寄存器中写回共享变量count
的内存地址
互斥锁mutex
对于上述的多执行流访问临界资源,导致数据不一致问题;
所以必须有互斥行为:
代码进入临界区执行时,不允许其他线程进入临界区;
简单来说就是:在任意时刻只允许一个线程(执行流)访问临界资源
要做到在任意时刻只有一个执行流访问资源,那就需要一把锁(互斥锁)

对临界区加锁,在执行临界区代码之前,先申请锁。
- 如果申请成功,说明当前没有线程访问临界资源,可以继续执行;
- 申请失败则说明当前有线程正在访问临界资源(执行临界区代码),就要等待。
当执行完临界区代码,就要释放锁,让其他线程可以进行临界区访问临界资源。
Linux上提供的这把锁叫互斥量。
1. 创建(初始化)信号量
创建并初始化信号量有两种方式:动态分配/静态分配;
简单来说就是定义全局变量/局部变量
静态分配:
c
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态分配:
如果将互斥量定义局部变量,就需要我们手动调用相关接口去初始化和销毁该信号量。的点点滴滴的点点滴滴
初始化互斥量接口函数:
pthread_mutex_init
c
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数:
参数一:指要初始化的互斥量。
参数二:可以设置互斥量的相关属性,
nullptr
表示默认。返回值:
初始化成功返回
0
,失败返回对应的错误码(非0)
2. 销毁互斥量
创建出来的互斥量需要进行销毁:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数和返回值就简单多了:传递要销毁的互斥量;
销毁成功返回
0
,失败则返回对应错误码(非0
)
在调用pthread_mutex_destroy
时要注意:
- 使用
PTHREAD_MUTEX_INITIALIZER
初始化的互斥量不能销毁 - 不能销毁一个已经加锁的互斥量
- 对于要销毁的互斥量,要保证后面不会再被使用
3. 加锁和解锁
c
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
调用pthread_mutex_lock
,如果互斥量未被锁定(未锁)该函数将互斥量锁定,然后返回;
如果互斥量处于锁定状态,(或者存在其他线程同时申请互斥量,但是没有竞争到互斥量),那pthread_lock
就会陷入阻塞(执行流被挂起)等待互斥量解锁再继续运行。
简单来说就时,调用
pthread_mutex_lock
申请互斥量,申请成功就返回继续运行;申请失败就阻塞等待。
调用pthread_mutex_unlock
,对互斥量解锁。
所以,有了互斥量;我们就可以随上面模拟售票的代码进行加锁;
保证在任意时刻,最多只有一个线程进入临界区访问临界资源:
cpp
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int count = 1000;
void *ticket(void *args)
{
// 预定演唱会门票
std::string name = static_cast<char *>(args);
while (count > 0)
{
pthread_mutex_lock(&mutex);
if (count > 0)
{
std::cout << name << " 售出门票 : " << count << std::endl;
count--;
}
pthread_mutex_unlock(&mutex);
usleep(100);
}
return (void *)100;
}
这里使用PTHREAD_MUTEX_INITIALIZER
,我们也可以创建局部互斥量,然后调用pthread_mutex_init
和phtread_mutex_destroy
初始化和销毁互斥量。
创建局部互斥量,这里要让线程访问到同一个互斥量,可以使用全局指针、也可以通过pthread_create
创建线程时的参数传递给线程。
互斥量实现原理
我们知道,对于count++
它并不是原子的;那互斥量呢?申请互斥量和释放互斥量是原子的吗?是的
为了实现互斥量,绝大部分体系结构都提供了swap
或者esxchange
指令,该指令可以把寄存器和内存单元的数据交换;因为只有一条指令,就保证了原子性。
那互斥量是如何实现的呢?

简单来说就是:在某一个寄存器中,存储值1
;当线程调用pthread_mutex_lock
时,就会交换当前线程互斥量的值和寄存器中的值。
当线程调用pthread_mutex_lock
拿到寄存器中的值1
,就表示该线程申请锁成功;其他线程再去申请时,寄存器存储的值为0
就申请锁失败,就会被挂起等待。
当调用pthread_mutex_unlock
时,就会对寄存器写入1
;表示释放该互斥量。
互斥量封装
对于互斥量,C++
中也存在对应的互斥量类;
这里简单对互斥量进行封装:
cpp
class mutex
{
mutex()
{
pthread_mutex_init(&_mutex, nullptr);
}
~mutex()
{
pthread_mutex_destroy(&_mutex);
}
void Lock()
{
pthread_mutex_lock(&_mutex);
}
void Unlock()
{
pthread_mutex_unlock(&_mutex);
}
private:
pthread_mutex_t _mutex;
};
这样在使用时,就可以面向对象式的调用Lock
和Unlock
来进行申请锁和释放锁了
此外,我们还可以随mutex
再次封装实现自动申请和释放锁。
cpp
class lockgroup
{
public:
lockgroup()
{
_mutex.Lock();
}
~lockgroup()
{
_mutex.Unlock();
}
private:
mutex _mutex;
};
这样在申请和释放锁时,就不需要显式调用Lock
和Unlock
了。
到这里本篇文章内容就结束了,感谢支持