1.互斥量mutex
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量属于单个线程,其它线程无法获得这种变量。
而有些时候,很多变量要在线程间共享,也就是共享变量,可以通过数据的共享,完成线程之间的交互。
但是多个并发的操作共享变量,会带来一些问题,像数据不一致。
// 操作共享变量会有问题的售票系统代码
#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;
}
}
}
int main( void )
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, "thread 1");
pthread_create(&t2, NULL, route, "thread 2");
pthread_create(&t3, NULL, route, "thread 3");
pthread_create(&t4, NULL, route, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
}
⼀次执⾏结果:
thread 4 sells ticket:100
...
thread 4 sells ticket:1
thread 2 sells ticket:0
thread 1 sells ticket:-1
thread 3 sells ticket:-2
由结果可以看出已经出现负数了
出现的缘由
if语句判断条件为真后,代码可以并发的切换到其它线程
usleep这个模拟漫长业务的过程,可能会有很多线程进入该代码段
--ticket操作本身不是一个原子操作
取出 ticket-- 部分的汇编代码objdump -d a.out > test.objdump152 40064b : 8b 05 e3 04 20 00 mov 0x2004e3 (%rip),%eax #600b 34 <ticket>153 400651 : 83 e8 01 sub $ 0x1 ,%eax154 400654 : 89 05 da 04 20 00 mov %eax, 0x2004da (%rip) #600b 34 <ticket>
--操作并不是原子操作,而是对应三条汇编指令:
load:将共享变量ticket从内存加载到寄存器中
update:更新寄存器里面的值,执行-1操作
store:将新值从寄存器写回共享变量ticket的内存地址
也就是说执行指令时就可能切换线程了,别的线程进来后又可能把所有指令执行完,然后前一个线程又回来执行最后一个指令,就会导致数据不一致。
要处理上面的出现的情况,需要有:
代码必须有互斥行为:代码进入临界区执行时,不允许其它线程进入临界区
如果多个线程同时要执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
本质就是需要一把锁,Linux提供的这把锁叫互斥量
2.互斥锁函数介绍
pthread_mutex_t
是 POSIX 线程库(pthread)中定义的一种数据类型,用于表示互斥锁(mutex)
pthread_mutex_init函数
初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
pthread_mutex_destroy函数
销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_lock函数
锁定互斥锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
pthread_mutex_unlock函数
解锁互斥锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
3.互斥量实现原理
由前面的例子,前置++和后置++都不是原子性的,有可能会有数据一致性的问题
为了实现互斥锁的操作,大多数体系结构都提供了swap或者exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性, 即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行另一个处理器的交换指令只能等总线周期。
申请锁的过程
这个锁可以看出int变量1。
假设线程1来申请锁,先执行move指令,把al寄存器的内容置为0,但此时线程1被切换了,线程1要把上下文带走。接着线程2来了,move操作,xchgb操作都执行了,把mutex的1和al中的o交换完毕了,判断条件也执行了,申请锁成功返回了。这时,mutex里面的内容从1变成0了。然后线程1调度回来继续执行了,先恢复上下文,将0恢复到al中,虽然寄存器只有一套,但是每一个线程都有自己的内容,然后交换mutex的值,此时mutex的值是0,然后判断是否大于0,不是就申请失败了,线程被挂起。
xchgb交换的本质,就是把内存的数据,交换到CPU寄存器中,mutex这把锁是所有线程共享的,用指令把这个持有锁的状态交换到自己的上下文中,当前线程就持有锁了。1状态就是持有锁的状态。xchgb是指令就是原子的,执行就一定会成功,要不然就不执行。
解锁操作
这里之所以不再次交换1和0,是因为只有持有锁的线程才可以交换,如果这个线程异常了,一直不释放就会出现问题,所以通过movb指令就可以在其它线程把锁解开,避免出现异常情况。
初始化:
movb $0, %al
:将寄存器al
初始化为0。这里al
用于存储mutex
的值。尝试获取锁:
xchgb %al, mutex
:使用xchg
指令尝试将al
的值(0)与mutex
变量的值进行交换。如果mutex
的初始值为0(表示锁是可用的),则交换后al
将包含mutex
的原始值(0),mutex
将变为1(表示锁已被占用)。(注意不是拷贝而是替换)检查锁的状态:
if(al寄存器的内容 > 0)
:检查al
寄存器的内容。如果al
的值大于0,说明mutex
已经被其他线程占用,当前线程应该放弃获取锁。
return 0;
:如果锁已被占用,函数返回0,表示获取锁失败。等待获取锁:
else
:如果al
的值为0,说明锁是可用的,但需要进一步处理以确保线程安全。
挂起等待;
:如果锁已经被其他线程占用,当前线程需要挂起等待,直到锁被释放。
goto lock;
:线程在等待一段时间后,重新尝试获取锁。
多线程并发和切换问题
在多线程编程中,通过创建更多的线程可以提高程序的并发性,同时执行多个任务的能力,这种并发性的增加会导致更多的线程切换,因为操作系统需要多个线程之间分配CPU时间。
切换的时间点
时间片:一个线程的时间片用完时,即使没有完成执行,也会被切换出去,执行另一个线程。
阻塞IO:一个线程执行I/O操作并进入阻塞状态时,操作系统会切换其它线程来提高CPU利用率
sleep等:线程进入睡眠状态,在睡眠期间,操作系统会切换到其它线程
操作系统切换到一个新的线程时,会从内核态到用户态,操作系统在进行调度和切换时运行在内核态,切换到新线程又会回到用户态,在切换回用户态时,操作系统会进行一系列的检查,确保选中的线程处于安全的状态,可以执行。
互斥锁特点
原子性:把一个互斥量锁定为一个原子操作,则操作系统保证了如果一个线程锁定了一个互斥量,没有其它线程在同一时间可以成功锁定这个互斥量
唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其它线程可以锁定这个互斥量
非繁忙等待:如果一个线程已经锁定了一个互斥量,第二个线程试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何CPU资源),直到第一个线程解除对这个互斥量的锁定为止,,第二个线程被唤醒继续执行,同时解锁这个互斥量。
饥饿问题
线程对于锁是有竞争力的,有这样的情况,线程刚把锁给释放掉,其它线程还来不及被唤醒,而刚释放掉的线程又离锁近,就可以释放完锁后又把锁申请到了,就导致其它线程长时间得不到锁,从而造成饥饿。可以在释放完锁后加个休眠,来保证锁被其它线程申请。
互斥锁也是共享资源
每一个线程进入临界区访问临界资源时,第一件事是去申请一把锁,那就是说锁本身也是临界资源或者共享资源,锁时保护临界资源的,那么锁由什么保护?是因为申请和释放锁本身是原子的,要么拿到完整锁,要么拿不到,不会出现,申请一半被切换情况。
临界区中线程切换
临界区中的线程也是可以被切换的,临界区也是代码,线程在执行到任何地方都有可能被切换。在临界区切换锁会怎么样,锁不会怎么样,因为线程被切换出去时是持有锁走的,只要没执行unlock,就没有线程能进入临界区访问临界资源。其它线程关注的是有没有释放锁,只要有锁访问临界区就是原子的,一定要干完才行,过程是不可干预的。
4.互斥量的封装
代码中定义了三个类,ThreadData是用来描述锁和线程名字的,Mutex是描述锁的,包含了创建互斥锁和解开互斥锁,会有销毁互斥锁,LockGuard类使互斥锁可以像全局变量定义的互斥锁一样,程序结束时自动释放。
Mutex.hpp文件
#pragma once
#include<iostream>
#include<pthread.h>
namespace MutexModule
{
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_mutex,nullptr);
}
void Lock()
{
int n=pthread_mutex_lock(&_mutex);
(void)n;
}
void Unlock()
{
int n=pthread_mutex_unlock(&_mutex);
(void)n;
}
~Mutex()
{
pthread_mutex_destroy(&_mutex);
}
private:
pthread_mutex_t _mutex;
};
class LockGuard
{
public:
LockGuard(Mutex& mutex):_mutex(mutex)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.Unlock();
}
private:
Mutex& _mutex;
};
}
testmutex.cpp文件
#include <iostream>
#include <mutex>
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include "Mutex.hpp"
using namespace MutexModule;
int ticket=1000;
class ThreadData
{
public:
ThreadData(const std::string& n,Mutex& lock)
:name(n),
lockp(&lock)
{
}
~ThreadData()
{
}
std::string name;
Mutex* lockp;
};
void* route(void* arg)
{
ThreadData* td=static_cast<ThreadData*>(arg);
while(1)
{
LockGuard guard(*td->lockp);//加锁完成,RAII风格的互斥锁实现
if(ticket>0)
{
usleep(1000);
printf("%s sell ticket:%d\n",td->name.c_str(),ticket);
ticket--;
}
else
{
break;
}
usleep(123);
}
return nullptr;
}
int main()
{
Mutex lock;
pthread_t t1,t2,t3,t4;
ThreadData* td1=new ThreadData("thread 1",lock);
pthread_create(&t1,NULL,route,td1);
ThreadData* td2=new ThreadData("thread 1",lock);
pthread_create(&t2,NULL,route,td2);
ThreadData* td3=new ThreadData("thread 1",lock);
pthread_create(&t3,NULL,route,td3);
ThreadData* td4=new ThreadData("thread 1",lock);
pthread_create(&t4,NULL,route,td4);
pthread_join(t1,NULL);
pthread_join(t2,NULL);
pthread_join(t3,NULL);
pthread_join(t4,NULL);
return 0;
}