多线程与线程互斥
我们初步学习完线程之后,就要来试着写一写多线程了。在写之前,我们需要继续来学习一个线程接口——叫做线程分离。
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join
操作,否则无法释放资源,从而造成系统泄漏。如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
功能:用于分离一个线程,需要头文件<pthread.h>
。
函数原型:int pthread_detach(pthread_t thread);
参数:thread
是被线程分离的ID。
返回值:分离成功返回0,分离失败返回错误码。
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:pthread_detach(pthread_self());
注意:一个线程被分离后,不允许再被线程等待,这样会发生报错。
void *thread_run(void *arg)
{pthread_detach(pthread_self());printf("%s\n", (char *)arg);return NULL;
}int main()
{pthread_t tid;if (pthread_create(&tid, NULL, thread_run, (void *)"thread run...") != 0){printf("create thread error\n");return 1;}int ret = 0;sleep(1); // 很重要,要让线程先分离,再等待.目的是确保新创建的线程有足够的时间执行if (pthread_join(tid, NULL) == 0){printf("pthread wait success\n");ret = 0;}else{printf("pthread wait failed\n");ret = 1;}cout << ret << endl;return 0;
}
由于线程被分离,所以pthread_join
实际上应该是失败的,除非在等待之前线程已经结束。当把pthread_detach(pthread_self());
注释掉后,就会显示等待成功。
线程互斥
线程一旦被创建,线程几乎都会共享同一份资源。那么这样会不会出现一个问题,就是一个线程正在做事情,还没有做完,这时其他线程就有可能也会开始干这件事,导致程序出错。
我们来模拟一下抢票过程,演示并讲解 什么是线程互斥。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <thread>
#include <vector>
#include <cstdio>
using namespace std;// 线程互斥
#define NUM 4 // 4个线程
class threadData
{
public:threadData(int number){threadname = "thread-" + to_string(number);}
public:string threadname;
};int tickets = 1000; // 用多线程,模拟一轮抢票,票数设置为1000张
void *getTickets(void *args)
{threadData *td = static_cast<threadData *>(args);const char *name = td->threadname.c_str();while (1){if (tickets > 0){usleep(1000);printf("who = %s, get a ticket:%d\n", name, tickets);tickets--;}elsebreak;}printf("%s quit...\n", name);return nullptr;
}int main()
{vector<pthread_t> tids;vector<threadData *> thread_datas;for (int i = 1; i <= NUM; i++){pthread_t tid;threadData *td = new threadData(i);thread_datas.push_back(td);pthread_create(&tid, nullptr, getTickets, thread_datas[i - 1]);tids.push_back(tid);}for (auto thread : tids){pthread_join(thread, nullptr);}for (auto td : thread_datas){delete td;}return 0;
}
我们模拟的是一个抢票系统,创建4个线程加1个主线程来抢1000张票,当票数小于等于0时就退出抢票。
我们首先要模拟一个顾客,可以用线程来模拟,封装成一个类。
class threadData
{
public:threadData(int number){threadname = "thread-" + to_string(number);}
public:string threadname;
};
其中的变量threadname
代表的是哪个线程,即顾客的名字。
接着我们让线程去执行抢票程序。
void *getTickets(void *args)
{threadData *td = static_cast<threadData *>(args);const char *name = td->threadname.c_str();while (1){if (tickets > 0){usleep(1000);printf("who = %s, get a ticket:%d\n", name, tickets);tickets--;}elsebreak;}printf("%s quit...\n", name);return nullptr;
}
抢票程序会一直执行下去,直到票数小于等于0,tickets
代表的是票的编号,哪个线程抢到了哪张票。
主函数中,
int main()
{vector<pthread_t> tids;vector<threadData *> thread_datas;for (int i = 1; i <= NUM; i++){pthread_t tid;threadData *td = new threadData(i);thread_datas.push_back(td);pthread_create(&tid, nullptr, getTickets, thread_datas[i - 1]);tids.push_back(tid);}for (auto thread : tids){pthread_join(thread, nullptr);}for (auto td : thread_datas){delete td;}return 0;
}
先声明了一个存储线程 ID 的vector
容器,用于保存创建的多个线程的标识符,接着声明了一个存储指向threadData
类型指针的vector
容器,用于保存每个线程所需要的数据结构的指针。然后创建4个线程,每个线程都去执行抢票函数,并且把每个线程的tid都存储在tids
数组当中。第二个for
循环等待5个线程,最后一个for
循环是释放每个动态分配的threadData
对象的内存。
我们来看一下结果:
奇怪的是,我们只有1000张票,为什么会抢到1003张票呢?最后的0,-1,-2是怎么回事。
接下来,我们就来深入解析一下。根据代码结果,我们会知道一个问题就是共享数据会造成数据不一致问题,这肯定和多线程并发访问是有关的,对一个全局变量进行多线程并发进行–/++操作是不安全的。我们也知道要对数据做计算,必须是在CPU内部进行的,因此进行tickets--
操作是分3步的。
而且每一步都会对应一条汇编指令,比如以下这样:
MOV eax, [0x1000] ; 读取 tickets 的值到CPU的寄存器中
DEC eax ; 减 1
MOV [0x1000], eax ; 将值写回 tickets并保存到内存当中
在解释之前,我们要先知道一个知识点:任何一个线程在任意一个时间点都有可能会被切换。先假设线程1开始执行抢票函数。首先开始执行tickets--
的第一步操作:读取数据,然后突然线程1的时间片结束了,要结束调度当前进程,但并不是把自己的PCB拿走就完了,它要把自己的上下文数据保存起来。为什么会有上下文数据呢?因为线程在执行调度执行的时候,每个线程都是CPU调度的基本单位,所以当它被调度的时候,它一定会有自己的硬件上下文。
线程在执行的时候,将共享数据加载到CPU寄存器的本质:把数据的内容,变成了自己的上下文,即以拷贝的方式,给自己单独拿了一份。
线程1被切换走之后,线程2开始执行抢票过程,不过线程2的运气很好,把3个过程全部执行完了。tickets
变成了999。然后线程2一直执行下去,直到tickets
变成了10。
变成10之后,线程2的第一步操作:读取数据完成,接着正准备执行第二步操作:--
的时候,它的时间片结束了,于是线程2被切换的时候把10保存到了自己的上下文中。
这时又该到了线程1执行的时候,不过线程1开始执行的时候,并不是执行上一次未完成的第二步操作,而是先恢复自己的上下文,即把1000保存到CPU的寄存器当中,完成之后,执行剩下的步骤。
OK,这样我们就知道了多线程抢票的基本过程了,现在我们来了解一下为什么会出现票数为负数的情况。我们假设经过多线程抢票之后,票数只剩下了一张,当进行抢票的时候,首先要判断tickets
是否大于0,判断语句也是一种运算,而判断的本质逻辑是先读取内存数据到CPU的寄存器当中,然后再进行判断;打印票数的时候,也需要读取数据。
所以tickets=1
的时候,多个线程可以同时执行这个判断语句吗?这当然是不行的,我们只有一个CPU,也只有一个寄存器,也代表CPU每次只能执行一次运算,不能同时判断,但是注意我们写了usleep
语句,线程是要被切换走的,寄存器中的内容是属于这个线程上下文的。
线程1被切换走的时候,剩下的线程就进入循环开始竞争执行if语句的判断,但都会先被usleep
休眠,当剩余线程休眠完之后,线程1开始被唤醒,先恢复自己的上下文,此时线程1从内存读取到票数是1,然后执行tickets--
的三步,步骤执行完成之后写回内存当中的tickets
变成了0。
紧接着线程2被唤醒,跟线程1的步骤一样,先恢复上下文,它不知道内存当中的tickets
变成了0,往下执行–操作,此时读取到的tickets
是0,但是已经在if语句的内部当中了,只能往下执行代码,所以写回内存中的票数就变成了-1。线程2和线程3也是一样,执行–操作之后,从内存读取到的数据是-1,写回内存当中就变成了-2。
就是因为我们把判断和更新分开,而在中间发生了大量的线程切换,最终可能出现tickets
本来就是1,但是确让大量的线程同时对一个变量进行操作,从而导致出现了票数为负的情况。
锁
那么这种情况该如何解决呢?简单来说就是对共享数据的任何访问,必须保证任何时候只有一个执行流进行访问,于是就诞生了“锁”的概念。
在讲解这个概念之前,我们要先知道几个概念:
临界资源
:在某段时间内只允许一个进程使用的资源称为临界资源。临界区
:每个进程中访问临界资源的那段程序称为临界区。原子性
:表示一个操作对外表现只有两种状态:还没开始
和已经结束
。不可能出现第三种状态,正在执行中
。
几个进程共享同一临界资源,它们必须以相互排斥的方式使用临界资源,即当一个进程正在使用临界资源且尚未使用完毕时,其它进程必须延迟对该资源的进一步操作,在当前进程使用完毕之前,不能从中插入使用这个临界资源,否则将会造成信息混乱和操作出错。
用刚才的抢票过程来帮助我们理解原子性这个概念。我们说过tickets--
是对应三条汇编指令的,并不是在一瞬间就把tickets--
这个操作完成的,所以这就会导致一个线程在执行3条指令的过程中,就有可能会有其他的线程将其打断。假如说一个线程在抢完票之后,tickets--
操作立即执行完成,下一个线程在访问这个变量的时候,一定是上一个线程把三条指令全部执行完毕,而不是在tickets--
的过程中,就可以避免这个多线程并行访问问题的发生。所以这就要求tickets
是原子的,即要么没有线程执行tickets--
操作,要么已经执行完毕,根本不可能是在tickets--
的过程中。
那么该如何保证tickets
是原子的呢?这就该要学习线程互斥了,什么是线程互斥呢?
线程互斥通过对共享资源的访问进行限制,确保在同一时刻只有一个线程能够访问该资源。这样可以避免多个线程同时对共享资源进行修改而产生不可预测的结果。
线程互斥可以有效地保护共享资源,确保多线程程序的正确性和稳定性。但同时也可能会带来一定的性能开销,因为线程在获取和释放互斥锁时需要进行一些系统调用和同步操作。
线程互斥是靠“锁”实现的,Linux上提供的这把锁叫互斥锁,锁的规则如下:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
互斥锁
互斥锁是第三方线程库pthread
库提供的,需要头文件<pthread.h>
,我们先讲解锁的创建和销毁。互斥锁的类型是pthread_mutex_t
,可以分为全局互斥锁和局部互斥锁,这两个锁的创建方式也不同,我们一会会把2种创建方式都展示一下。
全局互斥锁
函数原型:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
这样就创建了一个全局变量的互斥锁,这种方式也叫静态分配,全局的互斥锁必须使用宏来初始化,即用PTHREAD_MUTEX_INITIALIZER
来初始化。定义在全局的互斥锁,可以不需要手动去释放锁,不过你想手动去释放的话,也是可以的,不过不建议这样做。
局部互斥锁
pthread_mutex_init
函数原型:int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
功能:是用来初始化锁的。
参数:mutex
,类型是pthread_mutex_t *
的指针,指向一个互斥锁变量,第二个参数是锁的属性,一般不需要考虑这个,设置为nullptr
即可。
返回值:成功返回0,失败返回错误码。
pthread_mutex_destory
函数原型:int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:销毁互斥锁。
参数:跟初始化的时候一样,需要销毁的时候,传入变量即可。
返回值:成功返回0,失败返回错误码。
销毁互斥锁的时候需要注意三点:
- 使用
PTHREAD_ MUTEX_ INITIALIZER
初始化的互斥量不需要销毁。 - 不要销毁一个已经加锁的互斥量。
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。
互斥锁被创建好之后,需要对锁进程操作,主要有2个操作:申请锁和释放锁。
函数原型如下:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_lock:pthread_mutex_lock
锁定给定的互斥锁。如果互斥锁当前处于解锁状态,则它将被锁定并归调用线程所有,并且pthread_mutex_lock
立即返回。如果互斥锁已被另一个线程锁定,则pthread_mutex_lock
会暂停调用线程,进行阻塞等待,直到互斥锁已解锁。
pthread_mutex_unlock:解锁互斥锁,表明已经访问完毕临界区了,其他线程可以来访问了。
现在我们来演示一下局部创建一个互斥锁,还是刚才的抢票过程。
首先我们要知道,必须要让每个线程使用同一把锁才不会出现之前的问题,即就是每个线程进入临界区访问临界资源的时候都需要访问同一把锁。加锁的原则是尽量的要保证临界区代码,越少越好。加锁和解锁之间的区域叫做临界区,所以pthread_mutex_lock
接口要放在抢票函数的while循环当中。
我们可以把加锁接口放在while循环外面吗?从代码上来说是可以的,因为加锁的位置是程序员决定的,想加在哪都可以,但是逻辑上来说,是不对的,因为加在外部的话,第一个线程就会一直进行抢票,直到为0,这样就会导致其他线程没有票可以抢。既然加锁的位置需要考虑,那么解锁的位置也需要考虑,如果按照图片中解锁位置放置的话,就会导致线程进入临界区之后,要是没有票就会直接break掉,这样下一个线程就无法进入到临界区当中了。所以正确的位置如下所示。
class threadData
{
public:threadData(int number, pthread_mutex_t *mutex){threadname = "thread-" + to_string(number);lock = mutex;}
public:string threadname;pthread_mutex_t *lock;
};int tickets = 1000; // 用多线程,模拟一轮抢票,票数设置为1000张void *getTickets(void *args)
{threadData *td = static_cast<threadData *>(args);const char *name = td->threadname.c_str();while (1){pthread_mutex_lock(td->lock); // 申请锁成功。才能往后执行。不成功就阻塞等待if (tickets > 0){usleep(1000);printf("who = %s, get a ticket:%d\n", name, tickets);tickets--;pthread_mutex_unlock(td->lock);}else{pthread_mutex_unlock(td->lock);break;}}printf("%s quit...\n", name);return nullptr;
}
int main()
{pthread_mutex_t lock;pthread_mutex_init(&lock, nullptr); // 初始化锁vector<pthread_t> tids;vector<threadData *> thread_datas;for (int i = 1; i <= NUM; i++){pthread_t tid;threadData *td = new threadData(i, &lock);thread_datas.push_back(td);pthread_create(&tid, nullptr, getTickets, thread_datas[i - 1]);tids.push_back(tid);}for (auto thread : tids){pthread_join(thread, nullptr);}for (auto td : thread_datas){delete td;}pthread_mutex_destroy(&lock); // 销毁锁return 0;
}
代码结果:
从结果看出,没有出现抢到负数的情况了,但是我们发现只有一个线程在一直抢票。这种情况正常吗?其实是挺正常的,因为线程对于锁的竞争能力可能会不同。这个现象我们用一个例子进行讲解。
比如说你们学校里有一个VIP自习室,这个自习室一次只能进去一个人上自习,当有人在里面自习的时候,外面的人就只能等待,而且这个自习室外面放置了一把钥匙,当上自习的时候,就用这个钥匙把门打开,进去在把门锁上,自习完毕的时候,就把钥匙放回原位,让下一个人自习。
你每天都是早早的起床,然后第一个到达自习室并且上自习,有一天你学了2个小时之后,觉得肚子饿了,然后收,拾东西就去吃早饭,当你走到门口之后,看到门外站了许多人都在等待自习室的使用权,你觉得要是自己走了之后,在回来还想上自习,那么就需要等待前面的人用完之后才能轮到你。于是没有走,而是又去学了十几分钟,觉得饿了又要准备出去,然后你就开门——放钥匙——拿钥匙——关门——……。因为这个钥匙离你最近,其他人需要往前走几步才能拿到钥匙,所以你就重复上面的步骤,导致其他人只能等待,因为长时间得不到锁资源导致产生了饥饿问题,我们把这种环境叫做“纯互斥”环境。
这个案例就能说明线程对于锁的竞争能力会有不同,第一个线程会抢完票之后,又会接着下一次的抢票。那么这种情况该如何解决呢?这个时候自习室有一个观察员,它制定了几条规则:一、外面来的,必须排队;二、出来的人,不能立即重新申请锁,必须排到队列的尾部。根据这几条规则,就让所有的线程(人)获取锁(钥匙),按照一定的顺序性获取资源,把这种现象叫做同步。
每个线程必须申请同一把锁,才能进入到临界区,所以锁也是共享资源。线程在申请锁的时候,锁也是保护共享资源的,那么谁来保证锁的安全呢?其实这点完全不用担心,申请锁和释放锁本身就被设计成为了原子性操作。在临界区中,线程是可以被切换的,在线程被切出去的时候,是持有锁被切走的,线程不在的期间,照样没有人能进入临界区访问临界资源。这就相当于你在自习室的时候,想去上厕所,但是你并不想把钥匙放在原位,于是你就带在身上,这样其他人就无法进入到这个自习室了。
这次我们把锁设置成全局变量。
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
class threadData
{
public:threadData(int number){threadname = "thread-" + to_string(number);}
public:string threadname;
};
void *getTickets(void *args)
{threadData *td = static_cast<threadData *>(args);const char *name = td->threadname.c_str();while (1){pthread_mutex_lock(&lock);if (tickets > 0){usleep(1000);printf("who = %s, get a ticket:%d\n", name, tickets);tickets--;pthread_mutex_unlock(&lock);}else{pthread_mutex_unlock(&lock);break;}usleep(13); // 我们抢到了票,我们会立马抢下一张吗?其实多线程还要执行得到票之后的后续动作。使用usleep模拟}printf("%s quit...\n", name);return nullptr;
}
注意看,我在抢票函数中的末尾加入了usleep(13)
,目的是为了更好的模拟出真实情况,因为抢完票之后还会有其他的地址,并不会立即抢下一张票。(该代码结果与锁是局部还是全局的无关,与最后的usleep
有关)
互斥锁原理
那么互斥锁是如何做到可以把临界区变成原子性的呢?为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
先来看一下加锁的汇编(伪代码)
lock:movb $0, %alxchgb %al, mutexif(al寄存器的内容 > 0){return 0;}else挂起等待;go to lock;
我们通过图片来讲解一下原理:
看到有2个线程,一个CPU和一个内存,CPU内部有一个寄存器,用来存储变量的值,而内存中锁的值,我们假设是1。
现在假设线程1来执行pthread_mutex_lock
指令,首先执行movb $0, %al
,意思是把寄存器中的值设置为0。
然后执行xchgb %al, mutex
,意思是把mutex
的值与寄存器中的值进行交换。这样寄存器中的值就变成了1,mutex
的值就变成了0.
接着执行剩下的代码,
if(al寄存器的内容 > 0){return 0;
}else挂起等待;
所以说就是判断寄存器中的值是0还是大于0的情况,如果大于0就证明争夺到了锁,lock
函数就返回0,否则就进行挂起等待。这样一个线程就得到了一把锁。这是基本情形,下面我们来看一下特殊的情况。
当线程1在执行完第一条汇编指令的时候,寄存器中的值是1,然后线程2来调度了。
别忘了线程在任何时候都有可能被切换。线程1在被切换的时候,保存了自己的上下文,包括寄存器中的值,然后线程2来调度,执行了2条汇编指令,把内存中的mutex
的值与寄存器中的值进行交换,于是线程2也申请到了锁。
此时线程1再次被调度,线程2被切换走了,保存自己的上下文,线程1重新开始执行汇编。
线程1恢复上下文之后,线程1的&al
为0,然后与mutex
的值进行交换,但是mutex
的值也是0,这表明锁已经被另一个线程拿走了,于是执行if语句的内容,线程1就只能挂起等待了。
所以并不是谁先调用lock
函数,谁就能抢到锁,而是谁先执行xchgb %al, mutex
语句,把非0值放到自己的%al
中,谁才能抢到锁。
因此交换的本质就是把内存中的数据,交换到CPU的寄存器当中,即就是把数据交换到线程的硬件上下文中。把一个共享的锁,让一个线程以一条汇编的方式,交换到自己的上下文中。
再来看解锁汇编:
unlock:moveb $1, mutex唤醒等待mutex的线程;return 0;
其实就是把寄存器中的1交还给mutex
,接着唤醒所有等待mutex
的线程,让它们重新开始竞争锁,最后返回0。
死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。若无外力作用,这组进程或线程将永远不能执行。
下面,我将举一个例子,并且用画图的方式来帮助大家理解死锁的原理。
如图所示,有四辆车同时到达了一个十字路口,如果这四辆车中有一辆右拐让路,则可以避免堵塞的发生,但是如果四辆车各不相让,都继续执行,就会发生图示的堵塞情况——堵塞。
上述举例死锁发生的原因可知,每辆车都占据着一个车道,因为所需要的第二个车道被另一辆车占据,所以导致四辆车都不能前进。如果把车道看成是车辆行驶必须拥有的资源,由于每一辆车都拥有一个资源(车道),又试图占据另一个已被其他车辆占有的资源(车道),而系统中的资源有限(只有四个车道),因此四辆车都不能前进,导致死锁。所以资源不足是产生死锁的原因之一。
现在我把这个示例转换成计算机的视角来演示一遍。现在有2个线程和2把互斥锁。
现在有个要求,一个线程要想访问临界资源,必须同时拥有2把锁,然后线程1去申请mutex-1
,线程2去申请mutex-2
。
接着线程1去申请mutex-2
,但是这把锁已经被线程2申请了,于是线程1陷入阻塞;同理,线程2申请mutex-2
,这把锁已经被线程1申请了,线程2陷入阻塞。
现在线程1等待线程2解锁mutex-2
,线程2等待线程1解锁mutex-1
,双发互相等待,然后就永远的陷入等待状态,这就是死锁。
发生死锁的4个必要条件如下:
- 互斥条件。指线程对所分配的资源进行排他性使用,即在一段时间内某资源只能由一个线程占有。如果此时有其他线程要求使用该资源,要求使用资源者只能阻塞,直到占用该资源的线程用完该资源为止。
- 请求和保持。线程已经占有了至少一个资源,但又提出了新的资源请求,而该资源已经被其他线程占有,此时线程阻塞,但继续占有已经获得的资源。
- 不可剥夺条件。线程已经获得了资源,在它使用完毕前不能被剥夺,只能使用完毕后自己释放。
- 环路条件。存在一个线程与资源的环形链,在该链中,每个线程都正在等待一个被占用的资源。
如果产生了死锁,那么该如何解除呢?
解除死锁的方法
- 撤销所有死锁进程。这是操作系统中最常用的方法,也是最容易实现的方法。
- 把每个死锁的进程恢复到前面定义的某个检查点,并重新运行这些进程。要实现这个方法需要系统有重新运行和重新启动机制。该方法的风险是有可能再次发生原来发生过的死锁,但是操作系统的不确定性(随机性)使得不会总是发生同样的事情。
- 有选择地撤销死锁进程,直到不存在死锁。选择撤销进程的顺序基于最小代价原则。每次撤销一个进程后,要调用死锁检测算法检测是否仍然存在死锁。
- 剥夺资源,直到不存在死锁。和第三点一样,也需要基于最小代价原则选择要剥夺的资源。同样也需要在每次剥夺一个资源后调用死锁检测算法,检测系统是否仍然存在死锁。
最小代价原则
- 到目前为止消耗的处理机时间最少。
- 到目前为止产生的输出最少。
- 预计剩下的执行时间最长。
- 到目前为止分配的资源总量最少。
- 进程的优先级最低。
- 撒销某进程对其他进程的影响最小
相关文章:
多线程与线程互斥
我们初步学习完线程之后,就要来试着写一写多线程了。在写之前,我们需要继续来学习一个线程接口——叫做线程分离。 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法…...
BMS工具箱用来执行贝叶斯模型平均(BMA)计算模块
贝叶斯模型平均(Bayesian Model Averaging,BMA)是一种用于处理模型不确定性的统计方法,通过结合多个模型的预测结果来提高预测的准确性和鲁棒性。在 MATLAB 中,可以使用专门的工具箱(如 BMS 工具箱…...
Java死锁排查:线上救火实战指南
想象一下,你正在值班,突然监控告警红成一片,用户反馈雪花般飘来:“系统卡死了!用不了了!” —— 这很可能就是Java应用遭遇了“死锁”这个大魔王。这时候,你就是救火队长,首要任务不…...
第十九次博客打卡
今天学习的内容是Java中的常见循环。 在 Java 中,常见的循环结构主要有以下几种:for 循环、while 循环、do-while 循环以及增强型 for 循环(也称为 for-each 循环)。 1. for 循环 for 循环是一种非常灵活的循环结构,…...
智能体制作学习笔记1——智能体
01 智能体_哔哩哔哩_bilibili 大语言模型可以理解成一个很厉害的人。 但是要完成一些特定的工作,除了大语言模型,还需要一些工具和业务流程,这样才能自动化帮我们完成特定的工作,这个就叫做智能体。 突然发现放视频的时候出现了…...
Python常见问题
文章目录 1.python有哪些数据类型2.python中的元组和列表的区别是什么?3.python中的break、continue、pass代表什么意思?4.如何在python中生成一个随机数?5.Python有哪些常见的内置函数?6.请用自己最擅长的编程语言,将…...
小程序 存存上下滑动的页面
推荐阅读文档: Vue3组合式API之getCurrentInstance详解 - 且行且思 - 博客园Vue2中,可以通过this来获取当前组件实例; Vue3中,在setup中无法通过this获取组件实例,console.log(this)打印出来的值是undefined。 在Vue3…...
更换git位置并在pycharm中重新配置
更新 PyCharm 中的 Git 路径 更新 PyCharm 终端的 Shell 路径 检查环境变量 确保系统环境变量中的 Path 包含了新的 Git 安装路径 ,如果使用unins0000自动卸载就不会有旧路径。...
AI世界的崩塌:当人类思考枯竭引发数据生态链断裂
AI世界的崩塌:当人类思考枯竭引发数据生态链断裂 ——论过度依赖AI创作对技术进化的反噬 一、数据生态的恶性循环:AI的“自噬危机” 当前AI模型的训练依赖于人类创造的原始数据——书籍、论文、艺术作品、社交媒体动态等。据统计,2025年全球…...
OkHttp连接池
🧰 调整连接池的核心参数 ✅ 最大空闲连接数(maxIdleConnections): 含义:连接池中最多保留的空闲连接数量。默认值:5建议值:10~50(视并发量而定) ✅ 连接保持时间&…...
哈希表的实现01
文章目录 哈希表的实现01哈希概念直接定址法哈希冲突负载因子将关键字转换为整数 哈希函数除法散列法:乘法散列法(了解)全域散列法(了解) 处理哈希冲突(开放定址法)线性探测:二次探测…...
学习日志06 java
还有四天要去比赛了,能赢吗?逼自己一把。。。!!加油! 1 对比一下java重写还是不重写tostring的区别 1. 不重写 toString() 的情况 java class Point {private int x;private int y;public Point(int x, int y) {th…...
spring中的@MapperScan注解详解
一、核心功能与作用 MapperScan是Spring与MyBatis框架集成时用于批量扫描Mapper接口的核心注解,其主要功能包括: 自动注册Mapper接口 通过指定包路径,Spring会自动扫描该路径下的所有Mapper接口,并将其注册为Spring Bean&#x…...
PYTHON训练营DAY25
BUG与报错 一、try else try:# 可能会引发异常的代码 except ExceptionType: # 最好指定具体的异常类型,例如 ZeroDivisionError, FileNotFoundError# 当 try 块中发生 ExceptionType 类型的异常时执行的代码 except: # 不推荐:捕获所有类型的异常&…...
视频图像压缩领域中 DCT 的 DC 系数和 AC 系数详解
引言 在数字图像与视频压缩领域,离散余弦变换(Discrete Cosine Transform, DCT)凭借其卓越的能量集中特性,成为JPEG、MPEG等国际标准的核心技术。DCT通过将空域信号映射到频域,分离出DC系数(直流分量&…...
YOLO v1:目标检测领域的革命性突破
引言 在计算机视觉领域,目标检测一直是一个核心任务,它不仅要识别图像中的物体类别,还要确定物体的精确位置。传统目标检测方法如R-CNN系列虽然准确率高,但计算复杂度高、速度慢。2016年,Joseph Redmon等人提出的YOLO…...
AI智能体 | 使用Coze一键制作“假如书籍会说话”视频,18个作品狂吸17.6万粉,读书博主新标杆!(附保姆级教程)
目录 一、整体工作流设计 二、制作工作流 2.1 开始节点 2.2 大模型_生成对话文案 2.3 代码_字幕切割 2.4 画板_对话背景 2.5 循环_对话语音01 2.5.1 选择器_2 2.5.2 语音合成02 2.5.3 语音合成03 2.5.4 变量聚合_1 2.5.5 视频合成01 2.6 循环_3 2.6.1 选择器_3 …...
HVV蓝队实战面试题
HVV蓝队实战,防守筹备之“部署蜜罐捕获横向扫描行为”。 蜜罐通过模拟内网脆弱服务(如SMB、SSH、数据库端口),诱捕攻击者突破边界后的横向探测行为。 通过监测高频端口扫描、非常规协议请求及非授权IP段遍历,结合多源…...
正则表达式(二)-高级应用_谨慎使用
没事建议别瞎用正则表达式,能让后端处理好的数据,尽量后端处理好,减少前端对数据的处理,保证数据原始的完整性,减少前端耗能。(其实就是懒╮(╯▽╰)╭) 1. 分组捕获 分组捕获用于提取匹配的子字符串,使用 () 定义分组。 示例:提取日期中的年、月、日 (\d{4})-(\d{2…...
在K8S集群中部署EFK日志收集
目录 引言环境准备安装自定义资源部署ElasticsearchMaster 节点与 Data 节点的区别生产优化建议安装好以后测试ES是否正常部署Fluentd测试filebeat是否正常推送日志部署Kibana获取账号密码,账号是:elastic集群测试 引言 系统版本为 Centos7.9内核版本为…...
解决常见数据库问题:保障数据安全与稳定的全方位指南
本文结合行业最佳实践与前沿技术,系统性总结数据库运维中的核心问题与解决方案,助力开发者构建高可靠、高性能的数据服务) 一、性能优化:从SQL到架构的全面调优 性能问题是数据库运维中最常见的挑战,直接影响用户体验…...
武汉科技大学人工智能与演化计算实验室许志伟课题组参加2025中国膜计算论坛
武汉科技大学人工智能与演化计算实验室许志伟课题组参加2025中国膜计算论坛 2025年5月9日至11日,第五届中国膜计算论坛(CWMC 2025)在成都信息工程大学隆重召开。会议由 国际膜计算学会(IMCS) 主办,汇聚了来…...
Femap许可硬件绑定
在电磁仿真领域,Femap软件因其卓越的性能和广泛的应用场景而备受用户青睐。为了确保软件的安全与稳定运行,Femap提供了许可硬件绑定的功能。本文将详细介绍Femap许可硬件绑定的概念和优势,帮助您了解并充分利用这一功能,确保软件在…...
构建优雅对象的艺术:Java 建造者模式的架构解析与工程实践
一、建造者模式的本质与核心价值 在面向对象的软件设计中,创建复杂对象一直是一个需要精心处理的问题。当一个对象的构建需要多个步骤,并且这些步骤具有不同的组合方式时,传统的构造函数方式会显得力不从心。建造者模式(Builder …...
vim启动的时候,执行gg
在 Vim 编辑器中,gg 命令是一个非常有用的命令,它可以将光标快速移动到当前窗口的顶部(即第一行)。如果你想在 Vim 启动时自动执行 gg 命令,有几种方法可以实现这一点: 方法 1:使用 Vim 的启动…...
【SSL部署与优化】HTTP/2与HTTPS的协同效应
HTTP/2与HTTPS的协同效应:为何HTTP/2强制要求TLS 1.2? HTTP/2是HTTP协议的现代升级版,旨在通过多路复用、头部压缩等技术提升性能。然而,HTTP/2的设计与部署与HTTPS(TLS加密)紧密相关,甚至强制…...
JavaScript篇:揭秘函数式与命令式编程的思维碰撞
大家好,我是江城开朗的豌豆,一名拥有6年以上前端开发经验的工程师。我精通HTML、CSS、JavaScript等基础前端技术,并深入掌握Vue、React、Uniapp、Flutter等主流框架,能够高效解决各类前端开发问题。在我的技术栈中,除了…...
ubuntu24.04上安装NVIDIA driver+CUDA+cuDNN+Anaconda+Pytorch
一、NVIDIA driver 使用Ubuntu系统的:软件和更新——>附加驱动,安装NVIDIA驱动。 二、CUDA 安装命令:sudo apt install nvidia-cuda-toolkit 三、cuDNN cuDNN 9.10.0 Downloads | NVIDIA Developer 四、Anaconda Download Anaconda Di…...
vue3基础学习(上) [简单标签] (vscode)
目录 1. Vue简介 2. 创建Vue应用 2.1 下载JS文件 2.2 引用JS文件 2.3 调用Vue方法编辑 2.4 运行一下试试: 2.5 代码如下 3.模块化开发模式 3.1 Live Server插件 3.2 运行 4. 常用的标签 4.1 reactive 4.1.1 运行结果 4.1.2 代码: 4.2 ref 4.2.1 运行结果 4.2.2…...
.Net HttpClient 使用代理功能
HttpClient 使用代理功能 实际开发中,HttpClient 通过代理访问目标服务器是常见的需求。 本文将全面介绍如何在 .NET 中配置 HttpClient 使用代理(Proxy)功能,包括基础使用方式、代码示例、以及与依赖注入结合的最佳实践。 注意…...
深入理解Java适配器模式:从接口兼容到设计哲学
引言:接口不兼容的困局 在软件开发中,我们经常遇到这样的场景: 旧系统有一个「RS232串口设备」(仅支持sendByRS232(String data)方法),新系统需要通过「USB接口」(要求sendByUSB(String data)…...
非异步信号安全函数
这个程序演示了如何使用sigaction来捕获和处理信号(特别是SIGINT,即CtrlC)。以下是关键点和潜在问题的分析: 程序功能 信号捕获:注册自定义处理函数handler来捕获信号2(SIGINT,通常由CtrlC触发…...
PHP黑白胶卷底片图转彩图功能 V2025.05.15
关于底片转彩图 传统照片底片是摄影过程中生成的反色图像,为了欣赏照片,需要通过冲印过程将底片转化为正像。而随着数字技术的发展,我们现在可以使用数字工具不仅将底片转为正像,还可以添加色彩,重现照片原本的色彩效…...
【C++ / STL】封装红黑树实现map和set
文章目录 一. 源码及框架分析1.决定搜索类型的传参思考:为什么要传第一个参数 2.KeyOfValue的作用 二. 模拟实现map和set1. 实现出复用红黑树框架,并支持insert2. 支持iterator的实现iterator实现思路分析【iterator操作实现详解】 3.支持map的[ ]操作4.map和set代码…...
记录: Windows下远程Liunx 系统xrdp 用到的一些小问题(免费踩坑 记录)
采用liunx Ubuntu22.04版本以下,需要安装 xrdp 或者VNC 具体过程就是下载 在linux命令行里 首先更新软件包:sudo apt update 安装xrdp服务:sudo apt install xrdp 启动XRDP:sudo systemctl start xrdp(如果在启动的…...
WordPress 文章和页面:它们的区别是什么?
很多刚接触WordPress的用户,在创建网站内容时往往会遇到这样一个问题:“我应该用‘文章’还是‘页面’?”虽然两者都能发布内容,但它们之间到底有什么区别呢?这篇文章将从易于理解的角度,帮助大家厘清WordP…...
【工具变量】各省市场化指数-杨兴权版共三个方法(1997-2023年)
市场化指数是衡量中国各地区市场化改革进程的重要指标。本次数据基于杨兴全、马连福和夏立军三位学者的研究成果,系统整理并更新了我国1997-2023年间31个省、自治区、直辖市的市场化指数,便于研究者进行横向和纵向比较分析。 一、数据介绍 数据名称&…...
Android App View——团结引擎车机版实现安卓应用原生嵌入 3D 开发场景
团结引擎 1.5.0 版本已于 4 月 14 日正式发布,从 1.5.0 版本开始,团结引擎车机版带来了一个激动人心的新能力 —— Android App View。现在,开发者可以将任意第三方安卓应用以 2D 组件或 3D 组件的形式,原生嵌入到 Tuanjie 开发的…...
完整的 CentOS 6.10 虚拟机安装启动脚本
好的!下面是一个 完整的 CentOS 6.10 虚拟机安装启动脚本,专为你在 macOS(M 系芯片) QEMU(x86_64 软件模拟) 环境设计,确保你能顺利启动并安装一个接近 Red Hat 6.4 的开发环境。 ⸻ ✅ 前提准…...
如何远程执行脚本不留痕迹
通常我们在做远程维护的时候,会有这么一个需求,就是我想在远程主机执行一个脚本,但是这个脚本我又不想保留在远程主机上,那么有人就说了,那就复制过去再登录远程执行不就行了吗?嗯嗯,但是这还不…...
观测云:从云时代走向AI时代
过去十年,云计算让企业的数据处理能力实现了指数级增长,而观测云作为全栈监控观测平台,见证并参与了这一进程。通过强大的数据采集、处理与展示能力,观测云帮助数百家企业实现了对 IT 基础设施、应用服务、业务链路的全面掌控。 …...
解密企业级大模型智能体Agentic AI 关键技术:MCP、A2A、Reasoning LLMs- consistency is the key
解密企业级大模型智能体Agentic AI 关键技术:MCP、A2A、Reasoning LLMs- consistency is the key DeepSeek v3的时候,它模型已经足够强大到能带来consistency稳定性。所以当这个DeepSeek R1 Zero或者DeepSeek R1使用GRPO进行训练的时候,它能够…...
鸿蒙OSUniApp 实现图片上传与压缩功能#三方框架 #Uniapp
UniApp 实现图片上传与压缩功能 前言 在移动应用开发中,图片上传是一个非常常见的需求。无论是用户头像、朋友圈图片还是商品图片,都需要上传到服务器。但移动设备拍摄的图片往往尺寸较大,直接上传会导致流量消耗过大、上传时间过长&#x…...
SymPy | 如何提取指定项的系数
SymPy 是 Python 中一个强大的符号计算库,广泛应用于数学、物理和工程领域的符号运算。在代数表达式的处理中,提取特定项的系数是一项常见且重要的操作。本文将详细介绍 SymPy 中提取指定项系数的多种方法,并通过丰富的示例帮助读者掌握这些技…...
MUSE Pi Pro 更换kernel内核及module模块
视频讲解: MUSE Pi Pro 更换kernel内核及module模块 脚本仓库: https://github.com/LitchiCheng/MUSE-Pi-Pro-Learning 结合上期编译内核,编译成功后的输出如下: 输入 uname -a 可以看到如下信息,未修改的内核时间在 …...
java每日精进 5.14【参数校验】
参数校验 1.1概述 本文使用 Hibernate Validator 框架对 RESTful API 接口的参数进行校验,确保数据入库的正确性。 例如,在用户注册时,校验手机号格式、密码强度等。如果校验失败,抛出 ConstraintViolationException 或相关异…...
CPS联盟+小程序聚合平台分销返利系统开发|小红书番茄网盘CPA拉新推广全解析
导语: 在私域流量与社交电商爆发的时代,CPS联盟分销返利系统与小红书CPA拉新推广成为企业增长的核心引擎。本文深度解析如何通过小程序聚合平台开发、多层级返利机制搭建及精准CPA推广策略,快速占领市场,实现用户裂变与收益倍增。…...
基于EFISH-SCB-RK3576/SAIL-RK3576的光伏逆变器控制器技术方案
(国产化替代J1900的能源物联网解决方案) 一、硬件架构设计 电力转换与控制模块 高精度功率控制: Cortex-M0硬实时核驱动多相PWM(频率>200kHz),动态调节DC-AC转换误差<0.5%FPGA实现MPPT算法…...
HarmonyOS NEXT 适配高德地图FlutterSDK实现地图展示,添加覆盖物和移动Camera
HarmonyOS NEXT 适配高德地图 Flutter SDK 实现地图展示,添加覆盖物和移动 Camera 在现代移动应用开发中,地图功能是许多应用的核心组成部分之一。HarmonyOS NEXT 提供了强大的跨平台开发能力,而高德地图 Flutter SDK 则为开发者提供了丰富的…...
计算机网络:手机和基站之间是通过什么传递信息的?怎么保证的防衰减,抗干扰和私密安全的?
手机与基站之间的通信依赖无线电磁波信号,其传输介质、频率选择、抗衰减/抗干扰技术及隐私保护机制共同构成了现代移动通信的核心。以下从技术原理到实际应用逐一解析: 一、信号本质与传输介质 1. 信号类型 电磁波:手机与基站通过射频(RF)电磁波传递信息,采用数字调制技…...