【Linux】线程同步与互斥
一、线程间互斥
1 .进程线程间的互斥相关概念
- 临界资源:多线程执行流共享的资源就叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
- 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
临界资源和临界区
- 多个线程之间能够看到的同一份资源被称作临界资源,而专门用来访问临界资源的代码段被称为临界区。保护临界资源本质就是想办法把访问临界资源的代码保护起来。
- 因为多线程的大部分资源都是共享的,因此线程之间进行通信不需再去创建第三方资源。
互斥和原子性
- 如果多个线程同时对临界资源进行操作,就可能导致数据不一致的问题,这些问题可以通过互斥解决。互斥能够保证在任何时刻都只能有一个线程(执行流)进入临界区对临界资源进行访问。
- 原子性指的是不会被任何调度机制打断的操作,对于原子性来说,只有 完成 / 未完成 两种状态,要么不做,要么就做完,没有中间状态。要么执行要么就不执行。
为什么需要互斥,看下面一段代码:
模拟抢票软件系统:
int tickets = 1000; void* get_ticket(void* args)
{std::string name = static_cast<const char*>(args);while (true){if (tickets > 0){usleep(1000); // 模拟抢票花费的时间std::cout<<name<<" get a tickets :"<<tickets<<std::endl;--tickets;}else{break;}} return nullptr;
}
int NUM=4;
int main()
{pthread_t threads[NUM];for(int i=0;i<NUM;i++){char* name=new char[128];snprintf(name,128,"thread - %d",i+1);pthread_create(threads+i,nullptr,get_ticket,(void*)name);}for(auto tid:threads){pthread_join(tid,nullptr);}return 0;
}
执行结果:
可以看见,总共10000张票,最后却抢到了负号票。
问题:为什么票抢到了负数?
因为线程是OS调度的基本单位。
- if 语句在判断条件为真 (即 tickets > 0)时,此时代码可以并发的切换到其他线程执行该任务。
- usleep 用于模拟抢票的过程,在这个过程中,可能有很多个线程会进入该代码段。
- --tickets和 if (tickets > 0)本身不是一个原子的操作。
假设此时 tickets 的值为 1,线程 A 刚把 g_tickets 从内存拷贝到寄存器中,正准备判断时,线程 A 时间片到了。必须得切换到其他线程,此时线程 A 就只好带着自己得寄存器中存着的 tickets 离开,此时线程 A 的寄存器中存着的 tickets 的值是 1。
切换到线程 B 之后,线程 B 也要把 g_tickets 从内存拷贝到寄存器中,线程 B 通过 if 判断出 tickets 的值 > 0,然后线程 B 就会将 tickets 在内存中的的值减 1,此时 tickets 的值已经变成 0 了。
等到线程 A 切换回来后,线程 A 的寄存器中 g_tickets 的值还是 1,线程 A 以为 tickets 的值依旧是 1 ,能够通过 if 判断,但不知道线程 B 已经将 tickets 减到 0 了,线程 A 再对 tickets 在内存中的值减 1 时就会将 g_tickets 减到 - 1 去。
然而对 g_tickets 执行判断和自减操作都需要再次将 g_tickets 从内存读取到寄存器中。
在多线程访问时,这种将票数干到负数的情况被称为数据不一致。
问题:让 g_tickets 进行自减为什么不是原子的操作?
OS在对变量进行 -- 操作时,从汇编层面上看,其实有 3 条指令:
- load :将共享变量ticket从内存加载到寄存器中
- update : 更新寄存器里面的值,执行-1操作
- store :将新值,从寄存器写回共享变量ticket的内存地址
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临 界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
2 .互斥量
为了搞定这个问题,就需要一把锁,在有线程访问临界区时,用这把锁将临界区锁起来,访问完之后再解锁,这把锁被称为互斥量 mutex

3 . 互斥量的接口
- 方法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
- 方法2,动态分配:
#include <pthread.h>int pthread_mutex_init( /* 初始化成功时返回 0,失败时返回错误码 */pthread_mutex_t *restrict mutex, /* 需要初始化的互斥量 (锁) */const pthread_mutexattr_t *restrict attr); /* 互斥量 (锁) 的属性,一般设置为 空 即可 */
#include <pthread.h>int pthread_mutex_destroy( /* 销毁成功时返回 0,失败时返回错误码 */pthread_mutex_t *mutex); /* 要销毁的互斥量 */
- 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
#include <pthread.h>int pthread_mutex_lock( /* 上锁成功时返回 0,失败时返回错误码 */pthread_mutex_t *mutex); /* 需要上锁的互斥量 */
注意:
- 加锁的范围,颗粒的一定要小。尽可能的给少的代码块加锁,如果给一大段的代码加锁,线程之间就变成串行执行了,多线程就没意义了。
- 一般来说,都是给临界区加锁,只需要保护会访问到临界资源的那部分代码即可。
- 谁加的锁,就让谁解锁,最好不要出现线程 A 加锁却让线程 B 解锁的情况。
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
- 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
互斥量的解锁
#include <pthread.h>int pthread_mutex_unlock( /* 解锁成功时返回 0,失败时返回错误码 */pthread_mutex_t *mutex); /* 需要解锁的互斥量 (锁) */
改进上面抢票系统:我们加一把全局互斥锁进去
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;//全局互斥量
int tickets = 1000; void* get_ticket(void* args)
{std::string name = static_cast<const char*>(args);while (true){pthread_mutex_lock(&mutex);//锁上if (tickets > 0){usleep(1000); // 模拟抢票花费的时间std::cout<<name<<" get a tickets :"<<tickets<<std::endl;--tickets;pthread_mutex_unlock(&mutex);//解锁}else{pthread_mutex_unlock(&mutex);//解锁break;}} return nullptr;
}int NUM=4;
int main()
{pthread_t threads[NUM];for(int i=0;i<NUM;i++){char* name=new char[128];snprintf(name,128,"thread - %d",i+1);pthread_create(threads+i,nullptr,get_ticket,(void*)name);}for(auto tid:threads){pthread_join(tid,nullptr);}return 0;
} 执行结果:
4 . 互斥量的实现原理
临界区中的线程也能被线程切换走
即使是在临界区中的线程,也是能够进行线程切换的。但即使该线程被切换走了,其他线程也无法进入临界区。因为该线程被切走时,它是锁着走的,锁没有没解开就意味着其他线程是无法申请到锁,从而进入临界区的。
其他线程想要进入临界区,只能等待正在访问临界区的线程将锁解开,然后其他线程去竞争这把锁,争到锁的线程才能访问临界区。
锁也是需要被保护的共享资源
多线程之间需要竞争锁才能访问临界区,这说明了锁本身也是一种临界资源。
既然锁也是临界资源,那么就需要被保护起来,实际上,锁只要保证申请锁的过程是原子的就能保护好自己。
如何保证申请锁的过程是原子的
为了实现互斥锁的操作,大多数的体系结构都提供了 swap 或 exchange 指令,其作用是将寄存器和内存单元的数据互换。
由于从汇编层面上看,只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时,另一个处理器的交换指令只能等待总线周期。
查看一下 lock 和 unlock 的伪代码:
线程 lock 争锁的本质
内存中设 mutex 的初始值为 1,al 是线程独立拥有的一组寄存器中的的一个,每个线程都有这样的一组寄存器(寄存器硬件只有一组,这里指的是数据),当线程申请锁时,需要执行以下步骤:
- 使用 movb 指令将 al 寄存器中的值清零,多个线程可同时执行该动作。
- 使用 xchgb 指令将 al 寄存器和 mutex 中的值互换。
- 判断 al 寄存器中的值是否 > 0,若 > 0 则申请锁成功,此时就能进入临界区访问临界资源了。申请锁失败的线程会被挂起等待,直到锁被释放后再次去竞争申请锁。
因此,线程之间争锁的本质就是在争夺 mutex 中的那个 1,看哪个线程自己的寄存器能抢到这个 1,就说明哪个线程争到了这把锁。
线程只要想争锁,就必须从第 1 步开始。即使前面的线程中途被切走,这个线程也是拿着这个 1 走的,mutex 中的值还是 0,其余线程没法争锁。结论:把数据从内存移动到CPU寄存器中,本质是把数据从共享,变成线程私有。
线程 unlock 解锁的本质
- 使用 movb 指令将内存中的 mutex 的值重置回 1,让下一个申请锁的线程在执行 xchgb 交换指令后能够得到这个 1。
- 唤醒所有因为申请锁失败而被挂起等待 mutex 的线程,让它们继续去争锁。
二、可重入VS线程安全
1 .概念
- 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作, 并且没有锁保护的情况下,会出现该问题。
- 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们 称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重 入函数,否则,是不可重入函数。
2 .常见的线程不安全的情况
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
3 . 常见的线程安全的情况
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
- 类或者接口对于线程来说都是原子操作
- 多个线程之间的切换不会导致该接口的执行结果存在二义性
4 . 常见不可重入的情况
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
- 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
- 可重入函数体内使用了静态的数据结构
5 .常见可重入的情况
- 不使用全局变量或静态变量
- 不使用用malloc或者new开辟出的空间
- 不调用不可重入函数
- 不返回静态或全局数据,所有数据都有函数的调用者提供
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
6 .可重入与线程安全联系
- 函数是可重入的,那就是线程安全的
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
7 .可重入与线程安全区别
- 可重入函数是线程安全函数的一种
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生
- 死锁,因此是不可重入的。
三、常见锁概念
1 .死锁
- 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
当两个线程都需要同时持有两把锁才能开始干活,但是此时各自持有一把锁,都在期望得到对方的锁,且都不会释放自己持有的锁,那么这两个线程就陷入了永久等待对方的持有的资源使得任务都无法推进的一种死锁状态。
当然死锁并不是多线程才能产生,当单线程已经获得锁并且没有释放的时候再次去申请该锁,也会形成等待锁的释放而阻塞的状态。进而形成死锁。
2 .死锁四个必要条件
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
3 . 避免死锁
- 破坏死锁的四个必要条件
- 加锁顺序一致
- 避免锁未释放的场景
- 资源一次性分配
四、线程同步
1 .同步概念与竞态条件
- 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
- 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解
问题:什么是饥饿问题?如何解决?
在某些操作系统中,线程之间可能不是等概率的竞争到锁,而可能是某些线程本身的争锁能力特别厉害,导致任务全让这个线程干完了,其他线程竞争不到锁就会产生饥饿问题。
但如果线程在申请锁之后什么也不做,就一直在申请锁和释放锁,纯纯的浪费资源。
解决:
线程同步能够有效解决饥饿问题,而条件变量则是实现线程同步的一种机制。同步规定,上次持有锁的线程短期内不能再持有锁,就能很好的解决饥饿问题。同步就需要用到条件变量。
- 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
- 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
故事说明
现在小明在一张桌子上放一个苹果,而旁边有一群蒙着眼睛的人,因为他们的眼睛被蒙着,他们如果想拿到这个苹果,就会时不时来桌子前摸一摸看看桌子是否有苹果,并且谁来桌子前摸苹果是无序的,这时的场面就很混乱,小明一看不行,于是小明就桌子上放了个铃铛,并且组织需要苹果的人排好队,有苹果小明就会摇响铃铛,排在第一个的人就拿走苹果,然后如果还想要苹果就到队尾排队等待。此时混乱的场面就显得井然有序了。在本故事中,小明就是操作系统,苹果就是临界资源,一群蒙着眼睛都人就是多线程,铃铛就是条件变量,排队就是实现同步,摇响铃铛就是唤醒线程。
为什么需要使用条件变量
使用条件变量主要是因为它们提供了在多线程编程中一种有效的同步机制。当多个线程需要等待某个特定条件成立才能继续执行时,条件变量就显得尤为重要。通过条件变量,线程可以安全地进入等待状态,直到被其他线程显式地唤醒或满足等待的条件。这有助于避免线程的无谓轮询或忙等待,提高了系统的响应能力和效率。
注意:在使用条件变量时,必须确保与互斥锁一起使用,以避免竞态条件的发生。
条件变量就是一种数据类型
struct cond
{int flag; // 1. 判断条件变量是否就绪tcb_queue; // 2. 维护一个线程队列,线程就是在这个队列里排队
};
2 .条件变量函数
pthread_cond_t
pthread_cond_t是 POSIX 线程库(Pthreads)中用于表示条件变量的数据类型。-
可以使用
pthread_cond_t类型来定义一个条件变量。 -
所有的条件变量函数的返回值都是调用函数成功时返回 0,失败时返回错误码。
初始化条件变量
- 同初始化互斥量一样,初始化条件变量也有静态初始化和动态初始化两种方式。
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
- 全局的条件变量可以使用 静态 / 动态 的方式初始化。
- 局部的条件变量必须使用 动态 的方式初始化。
#include <pthread.h>int pthread_cond_init(pthread_cond_t *restrict cond, /* 需要初始化的条件变量 */const pthread_condattr_t *restrict attr); /* 条件变量的属性,一般都设置为空 */
销毁条件变量
#include <pthread.h>int pthread_cond_destroy(pthread_cond_t *cond); // 销毁指定的 cond 条件变量
#include <pthread.h>int pthread_cond_wait( pthread_cond_t *restrict cond, /* 条件变量,指定线程需要去 cond 条件变量处等待 */pthread_mutex_t *restrict mutex); /* 互斥锁,需要释放当前线程所持有的互斥锁 */
- 哪个线程调用的该函数,就让哪个线程去指定的条件变量处等待,还要将这个线程持有的锁释放,让其他线程能够争夺这把锁。
- 线程在哪调用的这个函数,被唤醒之后就要从这个地方继续向下执行后续代码。
- 当线程被唤醒之后,线程是在临界区被唤醒的,线程要重新参与对 mutex 锁的竞争,线程被唤醒 + 重新持有锁两者加起来线程才真正被唤醒。
pthread_cond_wait 的行为如下:
- 解锁互斥锁:调用 pthread_cond_wait 的线程首先会释放(解锁)它当前持有的互斥锁。这一步是必要的,因为条件变量通常与互斥锁一起使用,以确保对共享数据的访问是同步的。解锁互斥锁允许其他线程获取该锁,从而可以安全地修改共享数据。
- 加入等待队列:在解锁互斥锁之后,调用 pthread_cond_wait 的线程会将自己添加到与该条件变量相关联的等待队列中。此时,线程进入阻塞状态,等待被唤醒。
- 阻塞并等待信号:线程在等待队列中保持阻塞状态,直到它收到一个针对该条件变量的信号(通过 pthread_cond_signal 或 pthread_cond_broadcast 发出)。需要注意的是,仅仅因为线程在等待队列中并不意味着它会立即收到信号;它必须等待直到有其他线程显式地发出信号。
- 重新获取互斥锁:当线程收到信号并准备从 pthread_cond_wait 返回时,它首先会尝试重新获取之前释放的互斥锁。如果此时锁被其他线程持有,那么该线程会阻塞在互斥锁的等待队列中,直到获得锁为止。这一步确保了线程在继续执行之前能够重新获得对共享数据的独占访问权。
- 检查条件:一旦线程成功获取到互斥锁,它会再次检查导致它调用 pthread_cond_wait 的条件是否现在满足。虽然通常认为在收到信号时条件已经满足,但这是一个编程错误的常见来源。正确的做法是在每次从 pthread_cond_wait 返回后都重新检查条件,因为可能有多个线程在等待相同的条件,或者条件可能在信号发出和线程被唤醒之间发生变化。
- 返回并继续执行:如果条件满足,线程会从 pthread_cond_wait 返回,并继续执行后续的代码。如果条件仍然不满足,线程可以选择再次调用 pthread_cond_wait 进入等待状态,或者执行其他操作。
唤醒在条件变量处等待的线程
- 唤醒条件变量的方式有 2 种,分别是唤醒全部线程以及唤醒首个线程。
#include <pthread.h>int pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒在 cond 条件变量队列处等待的 所有 线程
int pthread_cond_signal(pthread_cond_t *cond); // 唤醒在 cond 条件变量队列处等待的 首个 线程
- 虽然该函数说是唤醒了线程,但是其实只是一种伪唤醒,只有当线程被唤醒 + 重新持有锁才是真唤醒。
- 只有被真唤醒的线程才会继续去执行后续代码。
问题:在调用pthread_cond_wait前如果已经提前收到唤醒通知会怎么样?
如果在调用pthread_cond_wait之前线程已经收到了条件变量的唤醒通知(通过pthread_cond_signal或pthread_cond_broadcast),那么该通知实际上会被“记住”,直到线程真正进入pthread_cond_wait并准备返回。这是因为条件变量的实现通常包含一个等待队列,用于存储那些正在等待条件变量的线程。当调用pthread_cond_signal或pthread_cond_broadcast时,会唤醒等待队列中的一个或多个线程,但如果没有线程实际在pthread_cond_wait中等待,那么这个通知就会被保留,直到有线程调用pthread_cond_wait。
问题:为什么 pthread_cond_wait 需要互斥量 ?
- 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
- 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。

- 按照上面的说法,我们设计出如下的代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了,如下代码:
// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
- 由于解锁和等待不是原子操作。调用解锁之后, pthread_cond_wait 之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么 pthread_cond_wait 将错过这个信号,可能会导致线程永远阻塞在这个 pthread_cond_wait 。所以解锁和等待必须是一个原子操作。
- int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t * mutex); 进入该函数后,会去看条件量等于0不?等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样。
3 .条件变量使用规范
- 等待条件代码
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
- 给条件发送信号代码
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);相关文章:
【Linux】线程同步与互斥
一、线程间互斥 1 .进程线程间的互斥相关概念 临界资源:多线程执行流共享的资源就叫做临界资源 临界区:每个线程内部,访问临界资源的代码,就叫做临界区 互斥:任何时刻,互斥保证有且只有一个执行流进入临界…...
003、网关路由问题
1. nginx配置404跳转回默认路由 https://blog.csdn.net/masteryee/article/details/83689954 https://blog.csdn.net/IbcVue/article/details/133230460 https://www.jb51.net/server/317970ynk.htm https://blog.csdn.net/u014438244/article/details/120531287 https://blog…...
Eclipse 快捷键:提高开发效率的利器
Eclipse 快捷键:提高开发效率的利器 Eclipse 是一款广泛使用的集成开发环境(IDE),它为Java、C、PHP等编程语言提供了强大的开发支持。对于开发者来说,熟练掌握Eclipse的快捷键不仅能提高编码效率,还能减少…...
Agent智能体
Agent(智能体)是一个能够感知环境并采取行动的自主实体,通常被设计用于在特定的环境中执行任务。智能体可以通过学习、推理等方式来决策,目标是最大化某种效用或实现某个预定的目标。它们广泛应用于自动化系统、游戏AI、机器人、自…...
用Promise实现前端并发请求
/** * 构造假请求 */ async function request(url) {return new Promise((resolve) > {setTimeout(() > {resolve(url);},// Math.random() * 500 800,1000,);}); }请求一次,查看耗时,预计应该是1s: async function requestOnce() {c…...
通过队列实现栈
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。 实现 MyStack 类: void push(int x) 将元素 x 压入栈顶。int pop() 移除并返回栈顶元素。int to…...
Mac下可以平替paste的软件pastemate,在windows上也能用,还可以实现数据多端同步
Mac平台上非常经典的剪贴板管理工具:「Paste」。作为一款功能完善且易用的工具,「Paste」在实际使用中体现出了许多令人欣赏的特点。但是它是一个收费软件,一年至少要24美元. 现有一平替软件pastemate,功能更加丰富,使用更加方便。 下载地址…...
106. 从中序与后序遍历序列构造二叉树
文章目录 106. 从中序与后序遍历序列构造二叉树思路 105. 从前序与中序遍历序列构造二叉树思路 思考 106. 从中序与后序遍历序列构造二叉树 106. 从中序与后序遍历序列构造二叉树 给定两个整数数组 inorder 和postorder,其中 inorder 是二叉树的中序遍历ÿ…...
监控和日志管理:深入了解Nagios、Zabbix和Prometheus
在现代IT运维中,监控和日志管理是确保系统稳定性和性能的关键环节。本文将介绍三种流行的监控工具:Nagios、Zabbix和Prometheus,帮助您了解它们的特点、使用场景以及如何进行基本配置。 一、Nagios Nagios 是一个强大的开源监控系统&#x…...
Win10下载Python:一步步指南
Win10下载Python:一步步指南 在Win10操作系统中下载并安装Python可能是一项挑战性的任务,但是在本文中,我们将向您提供三个不同的方法,以便轻松地完成这项任务。 方法一:使用Microsoft Store Microsoft Store是一个…...
Race Karts Pack 全管线 卡丁车赛车模型素材
是8辆高细节、可定制的赛车,内部有纹理。经过优化,可在手机游戏中使用。Unity车辆系统已实施-准备驾驶。 此套装包含8种不同的车辆,每种车辆有8-10种颜色变化,总共有75种车辆变化! 技术细节: -每辆卡丁车模型使用4种材料(车身、玻璃、车轮和BrakeFlare) 纹理大小: -车…...
C#——switch案例讲解
案例:根据输入的内容判断执行哪一条输出语句 string number txtUserName.Text; switch(number) { case"101":MessageBox.Show("您进入了101房间");break; case"102":MessageBox.Show("您进入了102房间");break; case&quo…...
技术美术一百问(02)
问题 前向渲染和延迟渲染的流程 前向渲染和延迟渲染的区别 G-Buffer是什么 前向渲染和延迟渲染各自擅长的方向总结 GPU pipeline是怎么样的 Tessellation的三个阶段 什么是图形渲染API? 常见的图形渲染API有哪些? 答案 1.前向渲染和延迟渲染的流程 【例图…...
12 函数的应用
函数的应用 一、Shell递归函数 函数优点: 函数在程序设计中是一个非常重要的概念,它可以将程序划分成一个个功能相对独立的代码块,使代码的模块化更好,结构更加清晰,并可以有效地减少程序的代码量。 递归…...
鸿蒙开发(NEXT/API 12)【硬件(接入手写套件)】手写功能开发
接入手写套件后,可以在应用中创建手写功能界面。界面包括手写画布和笔刷工具栏两部分,手写画布部分支持手写笔和手指的书写效果绘制,笔刷工具栏部分提供多种笔刷和编辑工具,并支持对手写功能进行设置。接入手写套件后将自动开启一…...
基于python+flask+mysql的音频信息隐藏系统
博主介绍: 大家好,本人精通Java、Python、C#、C、C编程语言,同时也熟练掌握微信小程序、Php和Android等技术,能够为大家提供全方位的技术支持和交流。 我有丰富的成品Java、Python、C#毕设项目经验,能够为学生提供各类…...
18724 二叉树的遍历运算
### 思路 1. **递归构建树**: - 先序遍历的第一个节点是根节点。 - 在中序遍历中找到根节点的位置,左边部分是左子树,右边部分是右子树。 - 递归构建左子树和右子树。 2. **递归生成后序遍历**: - 递归生成左子树的…...
代理模式简介:静态代理VS与动态代理
代理模式:静态代理VS动态代理 1、定义2、分类2.1 静态代理2.2 动态代理 3、使用场景4、总结 💖The Begin💖点点关注,收藏不迷路💖 1、定义 代理模式是一种设计模式,通过代理对象控制对目标对象的访问。简而…...
使用 Dockerfile 和启动脚本注册 XXL-Job 执行器的正确 IP 地址
解决方案:使用 Dockerfile 和启动脚本注册 XXL-Job 执行器的正确 IP 地址 在使用容器化方式注册 XXL-Job 执行器时,由于容器的 IP 地址是动态分配的,可能会导致调度中心无法访问执行器。为了解决这个问题,可以使用 Dockerfile 和…...
Python连接Kafka收发数据等操作
目录 一、Kafka 二、发送端(生产者) 三、接收端(消费者) 四、其他操作 一、Kafka Apache Kafka 是一个开源流处理平台,由 LinkedIn 开发,并于 2011 年成为 Apache 软件基金会的一部分。Kafka 广泛用于构…...
毕设程序java师生交流系统的设计与实现 基于Java的师生互动教学平台设计与实现 基于SpringBoot的在线教育沟通系统开发
毕设程序java师生交流系统的设计与实现343xt8ar(配套有源码 程序 mysql数据库 论文) 本套源码可以在文本联xi,先看具体系统功能演示视频领取,可分享源码参考。随着信息技术的飞速发展,传统的教育模式正在经历一场深刻的变革。互联…...
突破语言壁垒:XUnity.AutoTranslator的创新解决方案
突破语言壁垒:XUnity.AutoTranslator的创新解决方案 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator 当你打开一款期待已久的国外游戏,却发现满屏外文让剧情理解寸步难行࿱…...
Windows右键菜单终极管理指南:ContextMenuManager完全掌控你的系统交互体验
Windows右键菜单终极管理指南:ContextMenuManager完全掌控你的系统交互体验 【免费下载链接】ContextMenuManager 🖱️ 纯粹的Windows右键菜单管理程序 项目地址: https://gitcode.com/gh_mirrors/co/ContextMenuManager Windows右键菜单管理一直…...
AEB紧急制动系统与carsim及simulink联仿技术:卓越效果与性能的完美结合
紧急制动系统AEB,carsim与simulink联仿,效果极好 ,踩下刹车的那一刻,方向盘突然传来剧烈震动。盯着屏幕里那辆虚拟的前车尾灯,我手心全是汗——这已经是今天第三次测试紧急制动了。Carsim里那台SUV正以60km/h的速度冲向…...
颈腰椎病引发 “耳后疼痛”:耳根刺痛,可能是颈椎在 “捣乱”
很多人出现耳后持续性刺痛或按压痛,会误以为是中耳炎、腮腺炎,实则部分耳后疼痛与颈椎病变相关。颈椎病变压迫枕大神经(从颈椎延伸至耳后),会导致神经分布区域疼痛;同时颈椎肌肉痉挛、僵硬,牵拉…...
Windows Defender完全卸载终极指南:彻底移除系统安全组件的完整解决方案
Windows Defender完全卸载终极指南:彻底移除系统安全组件的完整解决方案 【免费下载链接】windows-defender-remover A tool which is uses to remove Windows Defender in Windows 8.x, Windows 10 (every version) and Windows 11. 项目地址: https://gitcode.c…...
LFM2.5-1.2B-Thinking-GGUF效果展示:同一Prompt下Thinking中间态与终版回答对比图
LFM2.5-1.2B-Thinking-GGUF效果展示:同一Prompt下Thinking中间态与终版回答对比图 1. 模型简介 LFM2.5-1.2B-Thinking-GGUF是Liquid AI推出的轻量级文本生成模型,特别适合在资源有限的环境中快速部署和使用。该模型采用GGUF格式存储,通过ll…...
STM32F103引脚功能全解析:从供电到通信接口的实战配置指南
STM32F103引脚功能全解析:从供电到通信接口的实战配置指南 在嵌入式系统开发中,STM32F103系列微控制器因其出色的性能和丰富的外设资源,成为众多开发者的首选。这款基于ARM Cortex-M3内核的MCU,不仅具备72MHz的主频,还…...
Qwen-Turbo-BF16惊艳案例:霓虹雨街中不同材质(金属/玻璃/布料)反射率差异还原
Qwen-Turbo-BF16惊艳案例:霓虹雨街中不同材质(金属/玻璃/布料)反射率差异还原 你有没有想过,为什么一张好的夜景图片,尤其是那种霓虹闪烁的雨夜街景,看起来那么真实、那么有“感觉”? 关键往往…...
解锁新可能:ArkData 在智能穿戴设备中的应用
解锁新可能:ArkData 在智能穿戴设备中的应用随着人们对健康生活的重视,智能穿戴设备愈发普及。这些设备能够实时收集心率、步数、睡眠等健康数据,为人们的健康管理提供重要参考。在这一背景下,如何高效管理和利用这些健康数据成为…...


