线程同步与互斥(上)
上一篇:线程概念与控制https://blog.csdn.net/Small_entreprene/article/details/146704881?sharetype=blogdetail&sharerId=146704881&sharerefer=PC&sharesource=Small_entreprene&sharefrom=mp_from_link我们学习了线程的控制及其相关概念之后,我们清楚:线程是共享地址空间的,所以线程会共享大部分资源。对于多线程来说,访问的共享资源称为共功资源,而多执行流访问公共资源的时候,可能会造成多种情况的数据不一致问题,因为公共资源并没有加保护,为了解决这样的问题,我们就下来就要学习同步与互斥:
互斥话题
在当前学习进程间通信中的信号量的时候,我们有谈及,现在我们来快速看看什么是互斥,互斥的相关概念:
进程与线程(执行流)互斥机制的基本概念:
-
临界资源:指在多线程执行过程中,被多个线程共享的资源。(可以理解为被保护起来的共享资源)
-
临界区:指线程内部用于访问临界资源的代码段。
-
互斥:确保在任何给定时刻,只有一个执行线程能够进入临界区,从而对临界资源进行访问,这通常用于保护临界资源。
-
原子性:指一个操作在执行过程中不会被任何调度机制中断,该操作要么完全执行,要么完全不执行。(后续将讨论如何实现原子性)
看一个现象
我们下面来见见一种现象(除了多执行流往显示器文件上打印这个抢占临界资源的现象外,另外一种数据不一致问题),然后快速的使用锁(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切换走了,线程2将ticket载入了,也是和线程1遭遇一样,被切走了,依次,切到线程4,也是在第一步后被切走,之后,线程1,2,3,4就按照顺序唤醒:
- 线程1执行--,就将其:1--->0;
- 线程2执行--,就将其:0--->-1;
- 线程3执行--,就将其:-1--->-2;
- 线程4执行--,就将其:-2--->-3;
判断也是访问共享资源!!!其中usleep也是为了堆积线程,然后才能使数据不一致现象更具直观性!
综上:一个全局资源没有加保护,可能在多线程执行的时候,发生并发问题。我们将多线程导致的并发问题,比如说上面的抢票问题,我们称之为线程安全问题!!!
该routine函数也是被多执行流进入了,因为函数内部又全局资源,所以该函数是不可重入函数。
其实为了让我们该抢票抢到负数,usleep后续辅助之外,重要的是,在多线程中,要制造更多的并发,更多的切换,我们并发的话,是创建了4个线程,那么,我们来好好谈谈这“更多的切换”
我们知道,线程切换其实就是对当前线程的上下文数据,线程上下文切换通常由以下几种情况触发:
-
时间片到期:操作系统为每个线程分配一个时间片(Time Quantum),当线程运行的时间达到分配的时间片时,操作系统会强制切换到其他线程。
-
线程阻塞:线程在等待某些资源(如 I/O 操作、锁等)时会进入阻塞状态,操作系统会切换到其他就绪的线程。
-
线程优先级调整:操作系统根据线程的优先级动态调整线程的调度顺序,高优先级的线程可能会中断低优先级的线程。
-
线程主动让出 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
时退出循环,它会持有锁但没有释放锁,导致其他线程无法获取锁,从而引发死锁。
代码风格问题:解锁位置不清晰
-
解锁操作应该与加锁操作对称,即在加锁的代码块结束时进行解锁。在当前代码中,解锁操作被放置在了错误的位置,导致代码逻辑不清晰,容易引发错误。
-
正确的做法是将解锁操作放在加锁代码块的末尾,确保无论是否进入
if
或else
分支,锁都能被正确释放。(当然也可以在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)只是其中一种常见的类型。我们下面对于互斥目标,主要围绕互斥锁来进行理解。
硬件级实现(只有内核在用):关闭时钟中断
对于一个代码块(不是一条汇编),这代码块可以随时被切换,切换是因为时间片到了,操作系统会一直调度线程,一直在做中断,一直检测线程的时间片,一旦切换,代码就发生交叉了,所以锁的实现的硬件级有一个最简单粗暴的做法:关闭时钟中断。
关闭时钟中断的原理
关闭时钟中断的基本思想是:
-
关闭中断:在进入临界区之前,关闭时钟中断,这样当前线程不会被抢占,从而确保临界区代码不会被其他线程中断。
-
执行临界区代码:在没有中断的情况下,安全地执行临界区代码。
-
打开中断:临界区代码执行完毕后,重新打开中断,恢复正常的线程调度。
这种方法的优点是简单直接,但缺点也非常明显:
-
风险高:如果临界区代码执行时间过长,或者发生死循环,会导致系统无法响应中断,从而导致系统死机。
-
仅适用于单核处理器:在多核处理器中,关闭中断无法阻止其他核心上的线程访问共享资源。
现代操作系统中的锁实现
现代操作系统和多核处理器环境中,锁的实现主要依赖于硬件级的原子操作(如 compare-and-swap
和 test-and-set
),而不是关闭中断。这些原子操作由处理器直接支持,确保在多核环境中,对共享变量的操作是原子的。
软件实现(大多使用的锁,并不简单粗暴,使用交换)
为了实现互斥锁操作,大多数体系结构都提供了 swap
或 exchange
指令。该指令的作用是把寄存器和内存单元的数据相交换。由于只有一条指令,保证了原子性。即使是多处理器平台,访问内存的总线周期也有先后。一个处理器上的交换指令执行时,另一个处理器的交换指令只能等待总线周期。
下面就是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;
}
相关文章:
线程同步与互斥(上)
上一篇:线程概念与控制https://blog.csdn.net/Small_entreprene/article/details/146704881?sharetypeblogdetail&sharerId146704881&sharereferPC&sharesourceSmall_entreprene&sharefrommp_from_link我们学习了线程的控制及其相关概念之后&#…...
ngx_test_full_name
定义在 src\core\ngx_file.c static ngx_int_t ngx_test_full_name(ngx_str_t *name) { #if (NGX_WIN32)u_char c0, c1;c0 name->data[0];if (name->len < 2) {if (c0 /) {return 2;}return NGX_DECLINED;}c1 name->data[1];if (c1 :) {c0 | 0x20;if ((c0 &…...
R 列表:深入解析及其在数据分析中的应用
R 列表:深入解析及其在数据分析中的应用 引言 在R语言中,列表(List)是一种非常重要的数据结构,它允许将不同类型的数据项组合在一起。列表在数据分析、统计建模以及数据可视化中扮演着关键角色。本文将深入探讨R列表…...
智能体中的知识库、数据库与大模型详解
前言 在 LLM(大语言模型)驱动的智能体架构中,知识库(Knowledge Base)、数据库(Database)和大模型(LLM)是关键组成部分,它们共同决定了智能体的理解能力、决策…...
AMD Versal™ AI Core Series VCK190 Evaluation Kit
AMD Versal™ AI Core Series VCK190 Evaluation Kit AMD VCK190 是首款 Versal™ AI Core 系列评估套件,可帮助设计人员使用 AI 和 DSP 引擎开发解决方案,与当前的服务器级 CPU 相比,该引擎能够提供超过 100 倍的计算性能。Versal AI Core …...
ARM-外部中断,ADC模数转换器
根据您提供的图片,我们可以看到一个S3C2440微控制器的中断处理流程图。这个流程图展示了从中断请求源到CPU的整个中断处理过程。以下是流程图中各个部分与您提供的寄存器之间的关系: 请求源(带sub寄存器): 这些是具体的…...
【从零实现Json-Rpc框架】- 项目实现 - 客户端注册主题整合 及 rpc流程示意
📢博客主页:https://blog.csdn.net/2301_779549673 📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson 📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正! &…...
AWS 云运维管理指南
一、总体目标 高可用性:通过跨可用区 (AZ) 和跨区域 (Region) 的架构设计,确保系统运行可靠。性能优化:优化AWS资源使用,提升应用性能。安全合规:利用AWS内置安全服务,满足行业合规要求(如GDPR、ISO 27001、等保2.0)。成本管控:通过成本优化工具,减少浪费,实现FinOp…...
vector的实现:
我们之前讲了vector的接口,我们今天来看一下vector的底层的实现: 在gitee上面我们的这个已经实现好了,我们看gitee就可以:vector的实现/vector的实现/vector的实现.h 拾亿天歌/拾亿天歌 - 码云 - 开源中国 我们在这强调比较难的…...
flutter 专题 九十六 Flutter开发之常用Widgets
上一篇,我们介绍了基础Widgets,接下来,我们看一下Flutter开发中一些比较常见的Widget。 Flutter Widget采用现代响应式框架构建,这是从 React 中获得的灵感,中心思想是用widget构建你的UI。 Widget描述了他们的视图在…...
Linux环境下内存错误问题排查与修复
最近这几天服务器总是掉线,要查一下服务器的问题。可以首先查看一下计算机硬件,这是一台某鱼上拼凑的服务器: sudo lshw -shortH/W path Device Class Description system NF5270M3 (To be filled by O…...
flutter 专题 六十八 Flutter 多图片上传
使用Flutter进行应用开发时,经常会遇到选图、拍照等需求。如果要求不高,Flutter图库选择可以使用官方提供的image_picker,如果需要多选,那么可以使用multi_image_picker插件库。multi_image_picker库支持图库管理,多选…...
与总社团联合会合作啦
2025.4.2日,我社团向总社团联合会与暮光社团发起合作研究“浔川代码编辑器v2.0”。至3日,我社团收到回复: 总社团联合会: 总社团联合会已收到浔川社团官方联合会的申请,经考虑,我们同意与浔川社团官方联合…...
技巧:使用 ssh 设置隧道代理访问 github
问题 由于不可知的原因,在国内服务器不能访问 Github。但是有clone代码需求,这里介绍一种可行的方法。 解决办法 使用 ssh 设置代理,让代理服务器请求 github 解决。 第一步 ssh -fND 1080 用户名代理服务器IP这里的意思是监听 1080 端口…...
安装 TabbyAPI+Exllamav2 和 vLLM 的详细步骤
在 5090 显卡上成功安装 TabbyAPIExllamav2 和 vLLM 并非易事,经过一番摸索,我总结了以下详细步骤,希望能帮助大家少走弯路。 重要提示: 用户提供的 PyTorch 安装使用了 cu128,这并非标准 CUDA 版本。请根据你的系统实…...
Linux 进程信号
目录 信号 生活角度的信号 技术应用角度的信号 signal函数 信号概念 用kill -l命令可以察看系统定义的信号列表 信号处理常见方式概览 信号的产生 通过键盘组合键发送信号 通过系统函数向进程发信号 由软件条件产生信号 由硬件异常产生信号 信号的保存 阻塞信号 …...
【学习篇】fastapi接口定义学习
fastapi学习链接:用户指南 1. 路径参数 访问fastapi接口的默认http路径为http://127.0.0.1:8000,/items为定义的接口函数read_item的路径,/{item_id}这个用大括号括起来的参数就是路径参数,接口函数可以通过引用这个路径参数名称…...
第十二步:react
React 1、安装 1、脚手架 npm i -g create-react-app:安装react官方脚手架create-react-app 项目名称:初始化项目 2、包简介 react:react框架的核心react-dom:react视图渲染核心react-native:构建和渲染App的核心…...
MySQL简介
MySQL 是由瑞典 MySQL AB 公司开发的一款开源关系型数据库管理系统(RDBMS),现归属 Oracle 公司。以下是其核心特点及简介: 1. 基础特性 - 开源免费:遵循 GPL 协议,个人及中小型企业可免费使用,…...
AIGC时代Kubernetes企业级云原生运维实战:智能重构与深度实践指南
文章目录 一、AIGC技术栈与Kubernetes的深度融合1. 智能配置生成:从YAML到自然语言2. 动态资源优化:AI驱动的弹性伸缩 二、智能运维体系架构深度解析四维能力矩阵增强实现:关键组件升级代码示例: 三、企业级实战策略深度实践策略1…...
市场波动与交易策略优化
市场波动与交易策略优化 在交易市场中,价格波动是常态。如何有效应对市场的波动,制定合理的交易策略,成为许多交易者关注的重点。本文将探讨市场波动的影响因素,并介绍应对不同市场波动环境的策略。 一、市场波动的影响因素 市场供…...
Prolog语言的移动UI设计
Prolog语言的移动UI设计 随着移动设备的普及,用户界面的设计已成为软件开发的重要组成部分。移动UI设计不仅要注重美观,更要关注用户体验和功能的实现。使用Prolog语言进行移动UI设计,虽然相对少见,但其逻辑编程的特性为复杂的交…...
linux 命令 awk
awk 是 Linux/Unix 系统中一个强大的文本处理工具,尤其擅长处理结构化文本数据(如日志、表格数据)。它不仅是命令行工具,还是一种脚本语言,支持变量、条件、循环等编程特性 1. 基本语法 awk [选项] 模式 {动作} 文件名…...
在 PyQt 加载 UI 三种方法
目录 方法一:使用 uic 模块动态加载 (不推荐用于大型项目) 方法二:将 UI 文件编译为 Python 模块后导入 方法3:使用uic模块直接在代码中加载UI文件 注意事项 总结: 在PyQt中,加载UI文件通常…...
前端快速入门学习2-HTML
一、概述 HTML全称是Hypertext Markup Language(超文本标记语言) HTML通过一系列的 标签(也称为元素) 来定义文本、图像、链接等等。HTML标签是由尖括号包围的关键字。 标签通常成对出现,包括开始标签和结束标签(也称为双标签),内容位于这两个标签之间…...
Cortex-M系列MCU的位带操作
Cortex-M系列位带操作详解 位带(Bit-Banding)是Cortex-M3/M4等处理器提供的一种硬件特性,允许通过别名地址对内存或外设寄存器中的单个位进行原子读-改-写操作,无需禁用中断或使用互斥锁。以下是位带操作的完整指南: …...
【嵌入式-stm32电位器控制LED亮灭以及编码器控制LED亮灭】
嵌入式-stm32电位器控制LED亮暗 任务代码Key.cKey.hmain.c 实验现象 任务 本文主要介绍利用stm32f103C8T6实现电位器控制PWM的占空比大小来改变LED亮暗程度,按键实现使用定时器非阻塞式,其中一个按键切换3个LED的控制状态,另一个按键是重置当…...
抖音热点视频识别与分片处理机制解析
抖音作为日活数亿的短视频平台,其热点视频识别和分片处理机制是支撑高并发访问的核心技术。以下是抖音热点视频识别与分片的实现方案: 热点视频识别机制 1. 实时行为监控系统 用户行为聚合:监控点赞、评论、分享、完播率等指标的异常增长曲线内容特征分析:通过AI识别视频…...
添加购物车功能
业务需求: 用户提交三个字段,服务端根据提交的字段判断是菜品还是套餐,根据菜品或者套餐添加购物车表中。 代码实现 RestController Slf4j RequestMapping("/user/shoppingCart") public class ShoppingCartController {Autowired…...
蓝桥杯备赛 Day16 单调数据结构
单调栈和单调队列能够动态的维护,还需用1-2两个数组在循环时从单调栈和单调队列中记录答案 单调栈 要点 1.时刻保持内部元素具有单调性质的栈(先进后出),核心是:入栈时逐个删除所有"更差的点",一般可分为单调递减栈、单调递增栈、单调不减栈、单调不增…...
AI Agent开发大全第十九课-神经网络入门 (Tensorflow)
(前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站)。 一、从买房困惑到神经元:神经网络的灵感来源 1.1 房地产经纪人的定价难题 想象一个周末的房产中介门店,经纪人小李正面对10份不同房源的报...
Mac VM 卸载 win10 安装win7系统
卸载 找到相应直接删除(移动到废纸篓) 可参考:mac如何卸载虚拟机win 下载 win7下载地址...
torch.nn中的非线性激活使用
1、神经网络中的非线性激活 在神经网络中,**非线性激活函数(Non-linear Activation Functions)**是引入非线性变换的关键组件,使神经网络能够学习并建模复杂的非线性关系。如果没有激活函数,无论神经网络有多少层&…...
【安全】Web渗透测试(全流程)_渗透测试学习流程图
1 信息收集 1.1 域名、IP、端口 域名信息查询:信息可用于后续渗透 IP信息查询:确认域名对应IP,确认IP是否真实,确认通信是否正常 端口信息查询:NMap扫描,确认开放端口 发现:一共开放两个端口&…...
要素的选择与转出
1.要素选择的三种方式 当要在已有的数据中选择部分要素时,ArcMap提供了三种方式:按属性选择、位置选择及按图形选择。 1)按属性选择 通过设置 SQL查询表达式,用来选择与选择条件匹配的要素。 (1)单击主菜单下【选择】【按属性选择】,打开【按…...
C 语言命令行参数:让程序交互更灵活
一、引言 在 C 语言编程领域,命令行参数是一种极为实用的机制,它允许我们在执行程序时,从外部向程序传递数据。这一特性极大地增强了程序的灵活性和可控性,避免了在代码中对数据进行硬编码。比如在开发系统工具、脚本程序时&…...
部署nerdctl工具
nerdctl 是一个专为Containerd设计的容器管理命令行工具,旨在提供类似 Docker 的用户体验,同时支持 Containerd 的高级特性(如命名空间、compose等)。 1、下载安装 wget https://github.com/containerd/nerdctl/releases/downlo…...
SOA 架构
定义与概念:SOA 将应用程序的不同功能单元(称为服务)进行封装,并通过定义良好的接口和协议来实现这些服务之间的通信和交互。这些服务可以在不同的平台和编程语言中实现,彼此之间相互独立,能够以松散耦合的…...
K8s私有仓库拉取镜像报错解决:x509 certificate signed by unknown authority
前言 在Kubernetes环境中使用自签名证书的私有Harbor镜像仓库时,常会遇到证书验证失败的问题。本文将详细讲解如何解决这个常见的证书问题。 环境信息: Kubernetes版本:1.28.2容器运行时:containerd 1.6.20私有仓库:…...
在线考试系统带万字文档java项目java课程设计java毕业设计springboot项目
文章目录 在线考试系统一、项目演示二、项目介绍三、万字项目文档四、部分功能截图五、部分代码展示六、底部获取项目源码带万字文档(9.9¥带走) 在线考试系统 一、项目演示 在线考试系统 二、项目介绍 1、管理员角色: 考试管理&…...
Axure RP 9 详细图文安装流程(附安装包)教程包含下载、安装、汉化、授权
文章目录 前言一、Axure RP 9介绍二、Axure RP 9 安装流程1. Axure RP 9 下载2. 启动安装程序3. 安装向导操作4.完成安装 三、Axure RP 9 汉化四、Axure RP 9授权 前言 本基础安装流程教程,将以清晰、详尽且易于遵循的步骤介绍Axure RP 9 详细图文安装流程…...
动态规划练习题④
583. 两个字符串的删除操作 给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。 每步 可以删除任意一个字符串中的一个字符。 示例 1: 输入: word1 "sea", word2 "eat" 输出: 2 解释: 第一步将 &quo…...
多输入多输出 | Matlab实现BO-GRU贝叶斯优化门控循环单元多输入多输出预测
多输入多输出 | Matlab实现BO-GRU贝叶斯优化门控循环单元多输入多输出预测 目录 多输入多输出 | Matlab实现BO-GRU贝叶斯优化门控循环单元多输入多输出预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 Matlab实现BO-GRU贝叶斯优化门控循环单元多输入多输出预测&#…...
爬虫工程师的社会现状
现在网上你只要搜索教程就是韦世东;k哥爬虫教你爬虫方面的逆向知识;然后看着这些逆向js百例;搞得我很尴尬我做了这么多年的爬虫工程师;现在算什么;这些逆向的东西我并没有很深层次的了解;但是工作的内容也依旧解决了;并没有到爬虫工程师非要会那么多逆向才能算的上是合格的爬虫…...
Flink 1.20 Kafka Connector:新旧 API 深度解析与迁移指南
Flink Kafka Connector 新旧 API 深度解析与迁移指南 一、Flink Kafka Connector 演进背景 Apache Flink 作为实时计算领域的标杆框架,其 Kafka 连接器的迭代始终围绕性能优化、语义增强和API 统一展开。Flink 1.20 版本将彻底弃用基于 FlinkKafkaConsumer/FlinkK…...
Vue2 父子组件数据传递与调用:从 ref 到 $emit
提示:https://github.com/jeecgboot/jeecgboot-vue2 文章目录 案例父组件向子组件传递数据的方式父组件调用子组件方法的方式子组件向父组件传递数据的方式流程示意图 案例 提示:以下是本篇文章正文内容,下面案例可供参考 以下是 整合后的关…...
【matplotlib参数调整】
1. 基本绘图函数常用参数 折线图 import matplotlib.pyplot as plt import numpy as npx np.linspace(0, 10, 100) y np.sin(x)plt.plot(x, y, colorred, linestyle--, linewidth2,markero, markersize5, labelsin(x), alpha0.8) plt.title(折线图示例) plt.xlabel(X 轴) p…...
如何使用 IntelliJ IDEA 开发命令行程序(或 Swing 程序)并手动管理依赖(不使用 pom.xml)
以下是详细步骤: 1. 创建项目 1.1 打开 IntelliJ IDEA。 1.2 在启动界面,点击 Create New Project(创建新项目)。 1.3 选择 Java,然后点击 Next。 1.4 确保 Project SDK 选择了正确的 JDK 版本&#x…...
Linux红帽:RHCSA认证知识讲解(十 一)配置NTP 时间同步、用户密码策略与使用 autofs 实现 NFS 自动挂载
Linux红帽:RHCSA认证知识讲解(十 一)配置NTP 时间同步、用户密码策略与 NFS 自动挂载 前言一、配置 NTP 时间同步1.1 NTP 简介1.2 安装和配置 NTP 客户端 二、配置新建用户密码过期时间2.1 查看用户密码过期时间2.2 修改密码过期时间 三、使用…...
ffmpeg音视频处理流程
文章目录 FFmpeg 音视频处理流程详细讲解总结音视频处理流程相关的 FFmpeg 工具和命令 FFmpeg 的音视频处理流程涵盖了从输入文件读取数据、编码和解码操作、数据处理、以及最终输出数据的完整过程。为了更好地理解这一流程,我们可以从以下几个关键步骤来分析&#…...