线程同步与互斥(上)

上一篇:线程概念与控制https://blog.csdn.net/Small_entreprene/article/details/146704881?sharetype=blogdetail&sharerId=146704881&sharerefer=PC&sharesource=Small_entreprene&sharefrom=mp_from_link我们学习了线程的控制及其相关概念之后,我们清楚:线程是共享地址空间的,所以线程会共享大部分资源。对于多线程来说,访问的共享资源称为共功资源,而多执行流访问公共资源的时候,可能会造成多种情况的数据不一致问题,因为公共资源并没有加保护,为了解决这样的问题,我们就下来就要学习同步与互斥:

互斥话题

在当前学习进程间通信中的信号量的时候,我们有谈及,现在我们来快速看看什么是互斥,互斥的相关概念:

进程与线程(执行流)互斥机制的基本概念:

  1. 临界资源:指在多线程执行过程中,被多个线程共享的资源。(可以理解为被保护起来的共享资源)

  2. 临界区:指线程内部用于访问临界资源的代码段。

  3. 互斥:确保在任何给定时刻,只有一个执行线程能够进入临界区,从而对临界资源进行访问,这通常用于保护临界资源。

  4. 原子性:指一个操作在执行过程中不会被任何调度机制中断,该操作要么完全执行,要么完全不执行。(后续将讨论如何实现原子性)

看一个现象

我们下面来见见一种现象(除了多执行流往显示器文件上打印这个抢占临界资源的现象外,另外一种数据不一致问题),然后快速的使用锁(pthread锁/互斥锁)来进行解决一下:

样例代码:简单的模拟一下抢票过程(多线程进行抢票)

代码是一个简单的多线程售票系统的示例,其中包含一个共享变量 ticket,表示剩余的票数。代码中创建了四个线程,每个线程都试图通过调用 route 函数来销售车票。然而,由于多个线程同时访问和修改 ticket 变量,这会导致竞态条件(race condition),从而使得程序的行为不可预测。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>int ticket = 100; // 初始化票数为100void *route(void *arg)
{char *id = (char*)arg; // 从线程参数中获取线程IDwhile ( 1 ) // 无限循环,直到票卖完{if ( ticket > 0 ) // 如果还有票{usleep(1000); // 模拟售票操作的延时printf("%s sells ticket:%d\n", id, ticket); // 打印售票信息ticket--; // 票数减一} else {break; // 如果票卖完了,退出循环}}return nullptr; // 线程结束
}int main( void )
{pthread_t t1, t2, t3, t4; // 定义四个线程的变量pthread_create(&t1, NULL, route, (void*)"thread 1"); // 创建线程1pthread_create(&t2, NULL, route, (void*)"thread 2"); // 创建线程2pthread_create(&t3, NULL, route, (void*)"thread 3"); // 创建线程3pthread_create(&t4, NULL, route, (void*)"thread 4"); // 创建线程4pthread_join(t1, NULL); // 等待线程1结束pthread_join(t2, NULL); // 等待线程2结束pthread_join(t3, NULL); // 等待线程3结束pthread_join(t4, NULL); // 等待线程4结束return 0; // 程序结束
}

票数竟然是负数!!! 

解决这个问题

由于 ticket 是一个共享变量,且在 routine 函数中没有适当的同步机制来保护对它的访问,因此当多个线程同时执行 ticket-- 操作时,可能会出现以下问题:

  • 票数不准确:可能会售出超过100张的票,因为多个线程可能同时读取相同的 ticket 值,然后各自减一。

  • 数据竞争:多个线程同时写入 ticket 变量,导致最终的票数不正确。

为了解决这个现象,可以使用互斥锁(mutex)来同步对 ticket 变量的访问。以下是修改后的代码示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>int ticket = 100;                                 // 初始化票数为100
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 初始化互斥锁void *route(void *arg)
{char *id = (char *)arg; // 从线程参数中获取线程IDwhile (1)               // 无限循环,直到票卖完{pthread_mutex_lock(&lock); // 加锁if (ticket > 0)            // 如果还有票{usleep(1000);                               // 模拟售票操作的延时printf("%s sells ticket:%d\n", id, ticket); // 打印售票信息ticket--;                                   // 票数减一}pthread_mutex_unlock(&lock); // 解锁if (ticket <= 0){break; // 如果票卖完了,退出循环}}return NULL; // 线程结束
}int main(void)
{pthread_t t1, t2, t3, t4;                             // 定义四个线程的变量pthread_create(&t1, NULL, route, (void *)"thread 1"); // 创建线程1pthread_create(&t2, NULL, route, (void *)"thread 2"); // 创建线程2pthread_create(&t3, NULL, route, (void *)"thread 3"); // 创建线程3pthread_create(&t4, NULL, route, (void *)"thread 4"); // 创建线程4pthread_join(t1, NULL);                               // 等待线程1结束pthread_join(t2, NULL);                               // 等待线程2结束pthread_join(t3, NULL);                               // 等待线程3结束pthread_join(t4, NULL);                               // 等待线程4结束pthread_mutex_destroy(&lock);                         // 销毁互斥锁return 0;                                             // 程序结束
}

 

理解为什么会数据不一致&&认识加锁的接口

我们先来理解一下这数据为什么会不一致!?

为什么票数会减到负数?

 ticket --和 if ( ticket > 0)判断是导致数据不一致的主要影响。(--不是主要矛盾,但是和它确实有关)

由于 ticket-- 不是原子的(要么减,要么不减),ticket这个变量是在内存当中的,在计算机当中,对一个变量作 -- 其实本身属于算数运算,而在计算机冯诺依曼体系当中,目前我们知道,我们对变量作 -- 操作只能由CPU来进行计算,也就是说在我们所对应的整个计算机当中,只有CPU能够对这个ticket作 -- ,这时候:(简单理解,最主要是三步)

  • 第一步:需要将ticket读入到CPU当中(从内存到CPU,严格来说是导入到CPU的某些寄存器,比如说ebx)(内存本身没有计算能力)。
  • 第二步:CPU要进行对该寄存器里的值作减操作。
  • 第三步:由于 -- 操作是会影响原始的值的,并不是作减法操作,所以需要将减完之后的值写回内存,写回内存要来保证对原始值作更改(100->99)。

其实CPU对ticket的一系列操作,宏观上就是cpu正在执行一个进程或线程的代码,在做进程,线程调度(ticket--),所以从执行流的调度上来讲,CPU会存在当前执行流的上下文数据(防止临时变量因为执行流的切换造成数据的丢失)—————上面作为背景知识,下面来解释说为什么ticket--是原子性的:

CPU内,除了数据类的寄存器,还有pc指针,程序计数器等等,假设pc指针当前指向的是0xff00,代表正在执行ticket--,我们上面的三步就可以翻译成汇编指令:(大概)

0XFF00 mov ebx ticket
0XFF02 减少 ebx 1
0XFF04 mov 写回 ticket所对应的地址 ebx

如图:

在正准备执行第三条语句的时候,该进程发生了切换,线程切换,就需要保存上下文数据,保存了 ebp: 99  ,pc指针: 0XFF04 (正准备执行第三条语句) 在执行第三条语句的时候,线程被切换了,该上下文就被放入到系统的等待队列了:

此时CPU内的所有寄存器的数据已经变相的废弃了,因为可以被覆盖了,CPU就继续选择一个线程B进行调度(上面链入等待队列的我们称为线程A),线程B也有自己的代码和数据,但是连两个线程是执行的相同代码,所以执行地址没有发生改变,将此时CPU寄存器内的值进行覆盖(不需要害怕覆盖造成数据的丢失,因为线程A上下文数据是被保存起来了),线程B照常执行,没有其他线程影响,执行完后,就将100减减之后的99,写回内存了:

线程B就完整的完成了一次ticket--,假设线程B运气很好,一次就将ticket的值减到1,准备将ticket的值减为0时,此时pc指针指向0XFF00,线程B正准备执行0XFF00的时候,线程B被切换了,吸纳线程B就要保存自己的上下文数据:

线程A调回来了,但是CPU首先不是调度线程A,而是对线程A进行恢复上下文:

回复完后执行的是第三步,这就将寄存器中的99写回到内存,这就导致线程B之前的工作全部白做了!!!这就造成了数据不一致问题!我们这个例子为的是解释ticket--操作不是原子的。

我想说的是:一个整数的全局变量,并不是原子的!!! (因为C语言的--操作是会被转化成3条汇编,三条汇编之间的任意一个位置,线程都可能被中断切换,然后换入下一个线程;又因为线程资源是共享的,所以对应的ticket--操作并不是--的)(这也是为什么我们之前说信号量本质就是一个计数器,但是我们不敢用一个整数的全局变量来进行++/--,因为其操作不是原子的)

所以,简单来说:当前,一种对原子性极简式的理解是,当前来看,一条汇编语句,就是原子的!!! 

但是,为什么票数被减到了负数?

其实是该语句:if(ticket > 0) 是主要矛盾!!!对ticket是否大于0进行判断,其实本质是一种计算,这种计算,我们称之为逻辑计算,我们得到的是bool值,而且所有线程都是需要对其进行判断的,假设ticket被安全的减到1,此时线程1将ticket载入到寄存器当中,准备要进行逻辑运算,我们可以将其操作看成两步:

  1. 载入
  2. 判断

但是在执行载入之后,还没有执行下一条汇编语句的时候,线程1被线程2切换走了,线程2将ticket载入了,也是和线程1遭遇一样,被切走了,依次,切到线程4,也是在第一步后被切走,之后,线程1,2,3,4就按照顺序唤醒:

  • 线程1执行--,就将其:1--->0;
  • 线程2执行--,就将其:0--->-1;
  • 线程3执行--,就将其:-1--->-2;
  • 线程4执行--,就将其:-2--->-3;

判断也是访问共享资源!!!其中usleep也是为了堆积线程,然后才能使数据不一致现象更具直观性!

综上:一个全局资源没有加保护,可能在多线程执行的时候,发生并发问题。我们将多线程导致的并发问题,比如说上面的抢票问题,我们称之为线程安全问题!!! 

该routine函数也是被多执行流进入了,因为函数内部又全局资源,所以该函数是不可重入函数。

其实为了让我们该抢票抢到负数,usleep后续辅助之外,重要的是,在多线程中,要制造更多的并发,更多的切换,我们并发的话,是创建了4个线程,那么,我们来好好谈谈这“更多的切换”

我们知道,线程切换其实就是对当前线程的上下文数据,线程上下文切换通常由以下几种情况触发:

  1. 时间片到期:操作系统为每个线程分配一个时间片(Time Quantum),当线程运行的时间达到分配的时间片时,操作系统会强制切换到其他线程。

  2. 线程阻塞:线程在等待某些资源(如 I/O 操作、锁等)时会进入阻塞状态,操作系统会切换到其他就绪的线程。

  3. 线程优先级调整:操作系统根据线程的优先级动态调整线程的调度顺序,高优先级的线程可能会中断低优先级的线程。

  4. 线程主动让出 CPU:线程可以通过调用某些系统调用(如 yield)主动让出 CPU,操作系统会切换到其他线程。

当线程调用 usleep 函数时,它会通过系统调用陷入内核态。内核记录线程的暂停时间,并将其置为等待状态。时间到期后,线程被唤醒并准备返回用户态。此时,内核会恢复线程的上下文信息,检查线程状态和资源使用情况,确保一切正常后,通过特定指令将控制权安全地交还给用户态线程,使其从暂停处继续执行。

所以解决数据不一致问题,我们就需要引入锁得概念:

pthread库为我们用户提供线程得相关接口,线程在并发访问的时候,全局资源那么多,线程就注定要为我们用户提供各种各样得锁,我们所要使用到得锁:pthread_mutex_t:

pthread_mutex_t 是 POSIX 线程库(pthread)中用于实现互斥锁(Mutex)的数据类型,主要用于在多线程程序中保护共享资源,防止多个线程同时访问导致数据竞争和不一致问题。

互斥锁是一种同步机制,用于确保一次只有一个线程可以访问共享资源。当一个线程获取了互斥锁后,其他线程必须等待,直到该线程释放锁。

pthread_mutex_t 是一个不透明的数据类型,其具体实现由线程库提供。通常,它是通过结构体或其他数据结构来实现的,但用户不需要直接操作其内部细节。

互斥锁可以通过静态初始化或动态初始化来创建。

静态初始化
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

这种方式适用于全局或静态变量的互斥锁。一旦是这么定义得,那么该锁不需要被释放!!!该锁会在程序执行结束之后,自动释放

动态初始化
pthread_mutex_t lock;
pthread_mutex_init(&lock, NULL);

这种方式适用于动态分配的互斥锁。pthread_mutex_init 函数的第二个参数是一个指向 pthread_mutexattr_t 的指针,用于设置互斥锁的属性。如果传入 NULL,则使用默认属性。之后不使用了,需要对对应的锁进行释放!!!

其实归根结底,所有得锁都要被所有进程看到的,不管是以参数的形式,假如在main函数中定义,然后传给所有线程,还是说直接定义全局得锁,因为锁要保护我们的代码,所有线程在访问临界资源之前,必须先申请锁,申请锁就需要先看到锁。

怎么申请锁:pthread_mutex_lock(&lock);

加锁(属于申请锁的阻塞版本)
pthread_mutex_lock(&lock);

所有线程竞争锁,如果锁已经被其他线程占用,调用线程将阻塞挂起,直到锁被释放。

不过,多线程竞争申请锁,多线程都得先看到锁,所本身就是临界资源,锁是来保护共享资源的,那么谁来保护锁的安全呢?

所以,pthread_mutex_lock(&lock);这个动作要求是要具有原子性的!!!  

加锁成功,线程就继续向后运行,访问临界区代码,访问临界资源;加锁失败,线程就会阻塞挂起,所以锁提供的能力的本质:执行临界区代码由并行转化成串行。 

注意:加锁:尽量加锁的范围粒度要比较细,尽可能的不要包含太多的非临界区代码。

尝试加锁(属于申请锁的非阻塞版本)
int ret = pthread_mutex_trylock(&lock);
if (ret == 0) {// 锁获取成功
} else if (ret == EBUSY) {// 锁已被占用,获取失败
}

pthread_mutex_trylock 会尝试获取锁,但如果锁已经被占用,它不会阻塞,而是立即返回 EBUSY

解锁
pthread_mutex_unlock(&lock);

释放锁,允许其他线程获取锁。

 当互斥锁不再使用时,可以通过以下函数销毁:

pthread_mutex_destroy(&lock);

销毁互斥锁后,其占用的资源将被释放,但互斥锁不能再被使用。

所以,我们对于来简单的来丰富一下解决方案:(此时还没有进行加锁和解锁)

int ticket = 1000;class ThreadData
{
public:ThreadData(const std::string &n, pthread_mutex_t &lock): name(n),lockp(&lock){}~ThreadData() {}std::string name;pthread_mutex_t *lockp;
};// 加锁:尽量加锁的范围粒度要比较细,尽可能的不要包含太多的非临界区代码
void *route(void *arg)
{ThreadData *td = static_cast<ThreadData *>(arg);while (1){if (ticket > 0){usleep(1000);printf("%s sells ticket:%d\n", td->name.c_str(), ticket);ticket--;}else{break;}}return nullptr;
}int main(void)
{pthread_mutex_t lock;pthread_mutex_init(&lock, nullptr); // 初始化锁pthread_t t1, t2, t3, t4;ThreadData *td1 = new ThreadData("thread 1", lock);pthread_create(&t1, NULL, route, td1);ThreadData *td2 = new ThreadData("thread 2", lock);pthread_create(&t2, NULL, route, td2);ThreadData *td3 = new ThreadData("thread 3", lock);pthread_create(&t3, NULL, route, td3);ThreadData *td4 = new ThreadData("thread 4", lock);pthread_create(&t4, NULL, route, td4);pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);pthread_mutex_destroy(&lock);//锁用完了,就要释放锁!!!:我们是动态初始化的锁(局部的锁)return 0;
}

因为加锁是要尽量细化加锁的范围粒度,尽可能的不要包含太多的非临界区代码,所以,因为在routine函数中,while语句当中的if语句都是临界资源,所以,我们需要在if前进行加锁。不过,不能在while之前进行加锁,不然就会导致一个线程独自将ticket--为0了:

//routine函数中进行加锁(对共享资源进行保护: 共享资源--->临界资源)while (1){pthread_mutex_lock(td->lockp);//进行加锁if (ticket > 0)//.....}

 加锁完成之后,我们需要进行解锁,以实现其他线程获取锁。但是如下的解锁位置是错误的:

// 加锁:尽量加锁的范围粒度要比较细,尽可能的不要包含太多的非临界区代码
void *route(void *arg)
{ThreadData *td = static_cast<ThreadData *>(arg);while (1){// LockGuard guard(*td->lockp); // 加锁完成, RAII风格的互斥锁的实现pthread_mutex_lock(td->lockp);//进行加锁if (ticket > 0){usleep(1000);printf("%s sells ticket:%d\n", td->name.c_str(), ticket);ticket--;}//错误的解锁位置pthread_mutex_unlock(td->lockp);else{break;}}return nullptr;
}

逻辑错误:解锁位置可能导致死锁

在代码中,pthread_mutex_unlock(td->lockp); 被放置在了 if (ticket > 0) 的代码块之外,但与 else 语句同级。这会导致以下问题:

  • 如果 ticket > 0,线程会执行 pthread_mutex_unlock(td->lockp);,这是正常的解锁操作。

  • 但如果 ticket <= 0,线程会进入 else 分支并执行 break,此时 线程会直接退出循环,而没有执行解锁操作

  • 结果:如果线程在 ticket <= 0 时退出循环,它会持有锁但没有释放锁,导致其他线程无法获取锁,从而引发死锁。

代码风格问题:解锁位置不清晰

  • 解锁操作应该与加锁操作对称,即在加锁的代码块结束时进行解锁。在当前代码中,解锁操作被放置在了错误的位置,导致代码逻辑不清晰,容易引发错误。

  • 正确的做法是将解锁操作放在加锁代码块的末尾,确保无论是否进入 ifelse 分支,锁都能被正确释放。(当然也可以在if和else分支里都进行解锁,但是这样的话就会有点代码冗余)

下面是修正后的代码:

void *route(void *arg)
{ThreadData *td = static_cast<ThreadData *>(arg);while (1){pthread_mutex_lock(td->lockp); // 加锁if (ticket > 0){usleep(1000);printf("%s sells ticket:%d\n", td->name.c_str(), ticket);ticket--;}else{break;}pthread_mutex_unlock(td->lockp); // 正确的解锁位置}return nullptr;
}

我们运行这个代码: 

所以,至此我们就完成了对共享资源的保护了!!! (上面我们使用的是局部的锁,也就是动态初始化的锁,我们也可以使用全局的锁,对应的就对全局的锁进行解锁了,而且不需要对其全局的锁进行销毁)


互斥锁的属性可以通过 pthread_mutexattr_t 来设置。例如:

  • 互斥锁类型:可以设置为普通锁(PTHREAD_MUTEX_NORMAL)、递归锁(PTHREAD_MUTEX_RECURSIVE)、错误检查锁(PTHREAD_MUTEX_ERRORCHECK)等。

  • 共享属性:可以设置为进程内共享(PTHREAD_PROCESS_PRIVATE)或进程间共享(PTHREAD_PROCESS_SHARED)。

  • 死锁:如果线程获取锁后没有释放,或者多个线程以不同的顺序获取多个锁,可能会导致死锁。需要谨慎设计锁的使用逻辑。错误的解锁位置可能导致线程在某些情况下没有释放锁,从而引发死锁。
  • 递归锁:如果需要在同一个线程中多次获取同一个锁,可以使用递归锁(PTHREAD_MUTEX_RECURSIVE)。

  • 线程安全:互斥锁是线程安全的,但需要正确使用,否则可能导致数据竞争或死锁。


理解锁

锁被看到,意味着锁也是全局的,但是进程之间想要看到的话就很难,不过线程间想要看到是很容易的,因为线程资源是共享的。但是进程间互斥之前没有讲过,之说了线程间互斥,那么两个进程之间该如何实现互斥呢?

假设有两个进程,创建出来一个共享内存,锁(pthread_mutex_t)的本质就是一个变量,一个空间,我们直接将共享内存的其实地址shm,进行强制类型转化,即 (pthread_mutex_t*)shm; 我们不就可以直接使用pthread_mutex_init(); 进行初始化,使用 pthread_mutex_destroy(); 进行销毁,剩下的就可以通过锁的机制,实现进程间互斥了。

不过我们会避免多进程进行通信(以共享内存的方式,锁只是实现进程间互斥的解决办法),后面学习网络之后,自然知道是为什么了。


加锁之后,在临界区内部,允许线程切换吗?切换了会怎么样?

不管是临界区还是非临界区,在操作系统看来,终归还是代码,所以都是允许随时切换的,那么我们在代码中ticket--,不会还会出现我们上面讨论的问题了吗?

举个例子:

超级自习室:是一个只允许一个人的自习室。

超级自习室旁边的墙上:只有一把钥匙。

我:线程。

每次超级自习室都要好多人惦记,不过,今天我来得比较早,所以我将墙上的钥匙(获取锁成功)拿到了,我就进入到超级自习室里做我在超级自习室里面该做的事情(执行临界区代码),但是超级自习室只容许一个人,所以,我就将其锁住了,其他人并进不来,因为他们没有钥匙,我在里里面呆了一段时间,想去上厕所,因此我出来将其门锁上(线程切换:所以线程是可以切换的),因为其他人并没有我这一把唯一可以进入到超级自习室的锁,所以其他人是进不来的。

经过上面的简单的生活例子,我们知道,其实线程切换并不影响,并不会引发上面没有加锁的数据不一致问题,因为当前线程并没有释放锁,当前线程是持有锁进行切换的,即便被切换走了,其他线程也需要等待该线程回来执行完代码,然后释放锁,其他线程才能够展开对锁的竞争,进入临界区!!!

所以站在外面的人是怎么看待我的自习过程呢?

站在外面人的角度:要么不用这个超级自习室,要么用完这个超级自习室,对外面的人才有意义。 (这就是原子性的特性,我们可以理解为:我的自习,对外面的人,是原子的!!!)(我们上面是将原子性简单理解为一条汇编语句,知道了锁后,我们可以理解说,我们可以将一个临界区进行原子化!!!)


有了上面的过度,我们接下来,来真正理解一下,什么才是锁。

锁的原理

锁的原理是通过一系列机制来确保对共享资源的访问是安全的,避免多个线程或进程同时修改共享资源导致的数据不一致问题。锁的核心目标是互斥(Mutual Exclusion)同步(Synchronization)

锁不一定是互斥锁。锁是一个更广泛的概念,互斥锁(Mutex)只是其中一种常见的类型。我们下面对于互斥目标,主要围绕互斥锁来进行理解。

硬件级实现(只有内核在用):关闭时钟中断

对于一个代码块(不是一条汇编),这代码块可以随时被切换,切换是因为时间片到了,操作系统会一直调度线程,一直在做中断,一直检测线程的时间片,一旦切换,代码就发生交叉了,所以锁的实现的硬件级有一个最简单粗暴的做法:关闭时钟中断。

关闭时钟中断的原理

关闭时钟中断的基本思想是:

  1. 关闭中断:在进入临界区之前,关闭时钟中断,这样当前线程不会被抢占,从而确保临界区代码不会被其他线程中断。

  2. 执行临界区代码:在没有中断的情况下,安全地执行临界区代码。

  3. 打开中断:临界区代码执行完毕后,重新打开中断,恢复正常的线程调度。

这种方法的优点是简单直接,但缺点也非常明显:

  • 风险高:如果临界区代码执行时间过长,或者发生死循环,会导致系统无法响应中断,从而导致系统死机。

  • 仅适用于单核处理器:在多核处理器中,关闭中断无法阻止其他核心上的线程访问共享资源。

现代操作系统中的锁实现

现代操作系统和多核处理器环境中,锁的实现主要依赖于硬件级的原子操作(如 compare-and-swaptest-and-set),而不是关闭中断。这些原子操作由处理器直接支持,确保在多核环境中,对共享变量的操作是原子的。

软件实现(大多使用的锁,并不简单粗暴,使用交换)

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

下面就是pthread_mutex_lock和pthread_mutex_unlock的伪代码:

其实锁就是一种标记位,在内存当中也是需要开辟空间的,我们可以将其锁暂时看成是一个整数:为1,表示该锁是没有任何线程持有的:

接下来,肯定会有很多线程来申请锁,我们就假设为两个线程:线程A和线程B。

我们发现,申请锁的操作都和%al这个寄存器有关(将其置0,在放入当前mutex的值)。进程或者线程切换时,CPU内的寄存器硬件只有一套,但CPU寄存器内的数据可以有多份,各自的一份称为当前执行流的上下文。(切换时,自己打包带走)

换句话说,如果把一个变量的内容,交换到CPU寄存器内部,本质就是把该变量的内容获取到当前执行流的硬件上下文中,再本质一点就是当前CPU寄存器硬件上下文(其实就是各个寄存器的内容)属于进程/线程私有的!!!

所以我们使用swap/exchange将内存中的变量交换到CPU的寄存器中,本质时当前线程/进程在获取锁,因为是交换,不是拷贝!!!所以mutex当中的1,只有一份!!!所以谁申请,谁就持有锁!!!

线程A和线程B申请锁,进来 movb $0, %al ,可能A进来到这就被切走了,但是没有关系,因为该步骤是清理自己的数据的,彼此不会互相影响。

A执行到xchgb %al, mutex 后,可能被切换,线程A被切换走,会带走:%a: 1 ,当线程B要交换时,就是拿mutex中的0换0,因为线程A将1带走了(这就是交换的效果,不是拷贝)。这就是申请锁!!!

unlock就很简单了,只需要将mutex的内容置1(movb $1, mutex),这就可以保证mutex有1可以被其他线程交换拿到了。这就是解锁!!!

我们ticket--不是原子性的就是因为是拷贝,不是交换!!!

上面就是互斥锁,mutex的原理!!!


C++11其实也是为我们用户提供了: std::mutex(头文件:#include<mutex>)就是封装了锁。

那么我们现在来自己封装一个互斥量,面向对象化:

互斥量的封装

Mutex.hpp

#pragma once
#include <iostream>
#include <pthread.h>namespace MutexModule
{// 自定义互斥锁类,封装了 pthread_mutex_t 的操作class Mutex{public:// 构造函数:初始化互斥锁Mutex(){// 使用 pthread_mutex_init 初始化互斥锁// nullptr 表示使用默认的互斥锁属性pthread_mutex_init(&_mutex, nullptr);}// 加锁操作void Lock(){// 调用 pthread_mutex_lock 尝试加锁// 如果锁已被其他线程持有,当前线程将阻塞int n = pthread_mutex_lock(&_mutex);(void)n; // 忽略返回值,实际使用中应检查返回值处理错误}// 解锁操作void Unlock(){// 调用 pthread_mutex_unlock 释放锁// 只有持有锁的线程可以解锁int n = pthread_mutex_unlock(&_mutex);(void)n; // 忽略返回值,实际使用中应检查返回值处理错误}// 析构函数:销毁互斥锁~Mutex(){// 在对象销毁时,释放互斥锁资源pthread_mutex_destroy(&_mutex);}private:pthread_mutex_t _mutex; // 内部使用的 pthread 互斥锁};// RAII 风格的锁管理类,确保互斥锁在作用域结束时自动释放class LockGuard{public:// 构造函数:自动加锁LockGuard(Mutex &mutex) : _mutex(mutex){// 在构造时调用 Mutex 的 Lock 方法加锁_mutex.Lock();}// 析构函数:自动解锁~LockGuard(){// 在作用域结束时,自动调用 Mutex 的 Unlock 方法释放锁_mutex.Unlock();}private:Mutex &_mutex; // 引用一个 Mutex 对象};
}

其中: 

// RAII 风格的锁管理类,确保互斥锁在作用域结束时自动释放class LockGuard{public:// 构造函数:自动加锁LockGuard(Mutex &mutex) : _mutex(mutex){// 在构造时调用 Mutex 的 Lock 方法加锁_mutex.Lock();}// 析构函数:自动解锁~LockGuard(){// 在作用域结束时,自动调用 Mutex 的 Unlock 方法释放锁_mutex.Unlock();}private:Mutex &_mutex; // 引用一个 Mutex 对象};

LockGuard 类通过 RAII(资源获取即初始化)风格管理锁,确保互斥锁在作用域开始时自动加锁,并在作用域结束时自动解锁,从而有效避免因忘记解锁导致的死锁问题,简化代码逻辑,提高线程安全性和程序的可靠性。 (智能指针原理也是类似的)

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 sells ticket:%d\n", td->name.c_str(), ticket);ticket--;}else{break;}usleep(123);}return nullptr;
}int main(void)
{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 2", lock);pthread_create(&t2, NULL, route, td2);ThreadData *td3 = new ThreadData("thread 3", lock);pthread_create(&t3, NULL, route, td3);ThreadData *td4 = new ThreadData("thread 4", 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;
}

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/76707.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[Linux系统编程]进程信号

进程信号 1. 信号入门1.1 信号基本概念1.2 技术应用角度的信号2. 信号的产生2.1 通过终端按键(如键盘)产生信号2.2 通过异常产生信号2.3 调用系统函数向进程发信号2.4 由软件条件产生信号2.5 总结3. 阻塞信号3.1 信号其他相关常见概念3.2 内核中的信号表示3.3 sigset_t3.3.1 …

要素的选择与转出

1.要素选择的三种方式 当要在已有的数据中选择部分要素时&#xff0c;ArcMap提供了三种方式:按属性选择、位置选择及按图形选择。 1)按属性选择 通过设置 SQL查询表达式&#xff0c;用来选择与选择条件匹配的要素。 (1)单击主菜单下【选择】【按属性选择】&#xff0c;打开【按…

Springboot + Vue + WebSocket + Notification实现消息推送功能

实现功能 基于Springboot与Vue架构&#xff0c;首先使用Websocket实现频道订阅&#xff0c;在实现点对点与群发功能后&#xff0c;在前端调用windows自带的消息通知&#xff0c;实现推送功能。 开发环境 Springboot 2.6.7vue 2.6.11socket-client 1.0.0 准备工作 在 Vue.js…

云手机如何防止设备指纹被篡改

云手机如何防止设备指纹被篡改 云手机作为虚拟化设备&#xff0c;其设备指纹的防篡改能力直接关系到账户安全、反欺诈和隐私保护。以下以亚矩阵云手机为例&#xff0c;讲解云手机防止设备指纹被篡改的核心技术及实现方式&#xff1a; 系统层加固&#xff1a;硬件级安全防护 1…

有人DTU使用MQTT协议控制Modbus协议的下位机-含数据库

本文为备忘录&#xff0c;不做太多解释。 DTU型号&#xff1a;G780 服务器&#xff1a;win2018 一。DTU设置 正确设置波特率&#xff0c;进入配置状态&#xff0c;获取当前参数&#xff0c;修改参数&#xff0c;设置并保存所有参数。 1.通道1设置 2.Modbus轮询设置 二&am…

湖北师范大学计信学院研究生课程《工程伦理》9.6章节练习

以下是图片中识别出的文字内容: 1【单选题】当工程师发现所在的企业或公司进行的工程活动会对环境、社会和公众的人身安全产生危害时,应该及时地给予反映或揭发。这属于工程师的( ) A、职业伦理责任 B、社会伦理责任 C、个人伦理责任 D、法律责任 2【单选题】下列哪个不属于工…

Axure RP 9 详细图文安装流程(附安装包)教程包含下载、安装、汉化、授权

文章目录 前言一、Axure RP 9介绍二、Axure RP 9 安装流程1. Axure RP 9 下载2. 启动安装程序3. 安装向导操作4.完成安装 三、Axure RP 9 汉化四、Axure RP 9授权 前言 本基础安装流程教程&#xff0c;将以清晰、详尽且易于遵循的步骤介绍Axure RP 9 详细图文安装流程&#xf…

SpringBoot全局exception处理最佳实践

目录 自定义异常类 抛出异常 全局异常处理器 自定义异常类 通常会继承 Exception 或其子类(如 RuntimeException)来定义业务异常类,用于封装业务相关的错误信息。一般选择继承 RuntimeException,因为它是一个非受检异常,在方法中抛出时不需要显式声明。 // 自定义业…

node ---- 解决错误【Error: error:0308010C:digital envelope routines::unsupported】

1. 报错 在 Node.js 18.18.0 的版本中&#xff0c;遇到以下错误&#xff1a; this[kHandle] new _Hash(algorithm, xofLen);^ Error: error:0308010C:digital envelope routines::unsupported这个错误通常发生在运行项目或构建时&#xff0c;尤其是在使用 Webpack、Vite 或其他…

浙江大学郑小林教授解读智能金融与AI的未来|附PPT下载方法

导 读INTRODUCTION 随着人工智能技术的飞速发展&#xff0c;智能金融已成为金融行业的重要变革力量。浙江大学人工智能研究所的郑小林教授在2025年3月24日的《智能金融&#xff1a;AI驱动的金融变革》讲座中&#xff0c;深入探讨了新一代人工智能在金融领域的应用及未来展望。 …

如何实现浏览器中的报表打印

在浏览器中实现打印一个报表&#xff0c;可以通过以下几种方法来完成。这里介绍一个基本的流程和相关代码示例&#xff1a; 1. 使用 JavaScript 的 window.print() 方法 这是最简单的方法&#xff0c;它会打开打印对话框&#xff0c;让用户选择打印选项。 示例代码&#xff1…

Linux系统调用编程

进程和线程 进程是操作系统资源分配的基本单位&#xff0c;拥有独立的地址空间、内存、文件描述符等资源&#xff0c;进程间相互隔离。每个进程由程序代码、数据段和进程控制块&#xff08;PCB&#xff09;组成&#xff0c;PCB记录了进程状态、资源分配等信息。 线程是…

【力扣hot100题】(054)全排列

挺经典的回溯题的。 class Solution { public:vector<vector<int>> result;void recursion(vector<int>& nums,vector<int>& now){if(nums.size()0){result.push_back(now);return ;}for(int i0;i<nums.size();i){now.push_back(nums[i]);…

【Ragflow】11. 文件解析流程分析/批量解析实现

概述 本文继续对ragflow文档解析部分进行分析&#xff0c;并通过脚本的方式实现对文件的批量上传解析。 文件解析流程 文件解析的请求处理流程大致如下&#xff1a; 1.前端上传文件&#xff0c;通过v1/document/run接口&#xff0c;发起文件解析请求 2.后端api\apps\docum…

2024年零知识证明(ZK)研究进展

Sumcheck 整个领域正在转向更多地依赖于 Sumcheck Protocol Sumcheck是用于验证多项式承诺的协议,常用于零知识证明(ZKP)中,尤其是在可验证计算和扩展性上。它的主要目的是通过对多项式进行分段检查,从而保证某个多项式在给定输入上的正确性,而不需要直接计算出整个多项…

thinkphp每条一级栏目中可自定义添加多条二级栏目,每条二级栏目包含多个字段信息

小程序客户端需要展示团购详情这种结构的内容,后台会新增多条套餐,每条套餐可以新增多条菜品信息,每条菜品信息包含菜品名称,价格,份数等字段信息,类似于购物网的商品多规格属性,数据表中以json类型存储,手写了一个后台添加和编辑的demo 添加页面 编辑页面(json数据…

Vue3引入ElementPlus

1.ElementPlus属于第三方的应用框架&#xff0c;官网地址&#xff1a;设计 | Element Plus &#xff0c;学习可以参考该网站的指南。 2.安装element-plus &#xff0c;指令为&#xff1a;npm install element-plus --save 3.引入elementplus的全局&#xff0c;组件、样式、图标…

react+antd封装一个可回车自定义option的select并且与某些内容相互禁用

需求背景 一个select框 现在要求可多选 并且原有一个any的选项 其他选项为输入后回车自己增加 若选择了any 则其他选项不可选择反之选择其他选项any不可选择 并且回车新增时也不可直接加入到选中数组只加入到option内 并且不可重复添加新内容 实现过程 <Form.Item …

Oracle数据库数据编程SQL<8 文本编辑器Notepad++和UltraEdit(UE)对比>

首先&#xff0c;用户界面方面。Notepad是开源的&#xff0c;界面看起来比较简洁&#xff0c;可能更适合喜欢轻量级工具的用户。而UltraEdit作为商业软件&#xff0c;界面可能更现代化&#xff0c;功能布局更复杂一些。不过&#xff0c;UltraEdit支持更多的主题和自定义选项&am…

【学Rust写CAD】30 Alpha256结构体补充方法(alpha256.rs)

源码 impl Alpha256 {#[inline]pub fn alpha_mul(&self, x: u32) -> u32 {let mask 0xFF00FF;let src_rb ((x & mask) * self.0) >> 8;let src_ag ((x >> 8) & mask) * self.0;(src_rb & mask) | (src_ag & !mask)} }代码分析 功能 输…