[ Linux ] 互斥量实现原理,线程安全

时间:2022-12-20 14:12:42

上篇文章我们对抢票系统做了加锁处理,对互斥量tickets进行加锁。而本篇博文来谈谈互斥量实现的原理以及相关问题。

1.上篇遗留问题

我们在临界资源对应的临界区中加锁了,就不是多行代码了吗?如果还是多行代码,难道不会被切换走吗?

在讨论这个问题之前我们先复习一下昨天的代码:

#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <pthread.h>

using namespace std;

int tickets = 1000;

//全局初始化
//pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_t mutex;//锁的声明

void *startRoutine(void *args)
{
const char* name = static_cast<const char *>(args);

while(true)
{
pthread_mutex_lock(&mutex);//如果申请不到就会阻塞线程
if(tickets > 0)
{
usleep(1000);
cout<<name<<" 抢到了一张票,票的编号是...."<< tickets << endl;
tickets--;
pthread_mutex_unlock(&mutex);
}
else
{
cout<<name<<" 没有抢到票,没有票了............"<< tickets << endl;
pthread_mutex_unlock(&mutex);
break;
}
}
return nullptr;
}

int main()
{
pthread_mutex_init(&mutex,nullptr);
pthread_t tid1,tid2,tid3,tid4;

pthread_create(&tid1,nullptr,startRoutine,(void*)"thread 1");
pthread_create(&tid2,nullptr,startRoutine,(void*)"thread 2");
pthread_create(&tid3,nullptr,startRoutine,(void*)"thread 3");
pthread_create(&tid4,nullptr,startRoutine,(void*)"thread 4");



pthread_join(tid1,nullptr);
pthread_join(tid2,nullptr);
pthread_join(tid3,nullptr);
pthread_join(tid4,nullptr);

pthread_mutex_destroy(&mutex);



return 0;
}

我们发现这是我们的加锁后抢票的代码,我们也可以在主线程内定义锁,然后通过结构体传入新线程内,具体的玩法如下代码所示:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <cstdlib>
#include <pthread.h>

using namespace std;

int tickets = 10000;

//全局初始化
//pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// pthread_mutex_t mutex;//锁的声明
#define NAMESIZE 64
typedef struct threadData
{
char name[NAMESIZE];
pthread_mutex_t *mutexp;//声明锁
}threadData;

void *startRoutine(void *args)
{
//const char* name = static_cast<const char *>(args);
threadData *td = static_cast<threadData*>(args);
while(true)
{
pthread_mutex_lock(td->mutexp);//如果申请不到就会阻塞线程
if(tickets > 0)
{
usleep(1000);
cout<<td->name<<" 抢到了一张票,票的编号是...."<< tickets << endl;
tickets--;
pthread_mutex_unlock(td->mutexp);
usleep(200);
}
else
{
cout<<td->name<<" 没有抢到票,没有票了............"<< tickets << endl;
pthread_mutex_unlock(td->mutexp);
break;
}
}
return nullptr;
}

int main()
{
//pthread_mutex_init(&mutex,nullptr);
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_t tid1,tid2,tid3,tid4;

threadData *td1 = new threadData();
strcpy(td1->name,"thread 1");
td1->mutexp = &mutex;
pthread_create(&tid1,nullptr,startRoutine,td1);

threadData *td2 = new threadData();
strcpy(td2->name,"thread 2");
td2->mutexp = &mutex;
pthread_create(&tid2,nullptr,startRoutine,td2);

threadData *td3 = new threadData();
strcpy(td3->name,"thread 3");
td3->mutexp = &mutex;
pthread_create(&tid3,nullptr,startRoutine,td3);

threadData *td4 = new threadData();
strcpy(td4->name,"thread 4");
td4->mutexp = &mutex;
pthread_create(&tid4,nullptr,startRoutine,td4);



pthread_join(tid1,nullptr);
pthread_join(tid2,nullptr);
pthread_join(tid3,nullptr);
pthread_join(tid4,nullptr);

//pthread_mutex_destroy(&mutex);



return 0;
}

那么我们在临界资源对应的临界区中加锁了,就不是多行代码了吗?如果还是多行代码,难道不会被切换走吗?

[ Linux ] 互斥量实现原理,线程安全

我们在临界区内可以被切走吗?

答案是当然可以。因为线程执行的加锁解锁也是代码,线程在任意代码出都可以被切换,但是线程加锁是原子的,要么拿到锁要么拿不到锁。但是由于该线程占用着锁,即使在临界区内被切换走,其他线程也不可能访问临界区,因为他们根本拿不到锁,锁已经被刚才的线程占用了。因此该线程会被阻塞在申请锁的阶段。因此一个线程持有锁,该线程根本不担心任何的切换问题。对于其他线程而言,该线程访问临界区 只有没有进入和使用完毕两种状态对其他线程有意义。

2.如何实现线程加锁和解锁的原子性

经过上面的理解,我们已经可以意识到单纯的++i和i++都不是原子的,有可能会有数据一致性问题。

为了实现互斥锁操作,大多数体系结构都提供了swap或者exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理平台,访问内存的总线周期也有先有后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

[ Linux ] 互斥量实现原理,线程安全

凡是在寄存器中的数据,全部都是线程的内部上下文!!多个线程看起来同时在访问寄存器,但是互不印象。

[ Linux ] 互斥量实现原理,线程安全

本质:将数据从内存读入寄存器,本质是将数据从共享变成线程私有!由于是交换而不是拷贝,因此这个变量只有一份,就犹如一个令牌,谁拿到这个令牌就相当于谁持有锁!而被该线程私有!

3.可重入和线程安全

3.1概念

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

3.2常见的线程不安全的情况

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

3.3常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

3.4常见不可重入的情况

  • 调用了malloc/free函数,因为malloc函数使用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构

3.5常见可重入的情况

  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

3.6可重入与线程安全联系

  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

3.7可重入与线程安全区别

  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的


(本篇完)