Linux:多线程中的互斥与同步
多线程
- 线程互斥
- 互斥锁
- 互斥锁实现的原理
- 封装原生线程库
- 封装互斥锁
- 死锁
- 避免死锁的四种方法
- 线程同步
- 条件变量
线程互斥
在多线程中,如果存在有一个全局变量,那么这个全局变量会被所有执行流所共享。但是,资源共享就会存在一种问题:并发访问
什么是并发访问呢?
好比学校的教室,教室属于共享的资源。如果没有安排课的话,那么任何人都可以使用这间教室。假设某一天刚好就是本班老师要补课占用这间教室,没有提前跟教务处报备。恰好此时此刻又有其他班想要用此间教室,在大家都没有商量的情况下,都在这间教室上课,这不得乱套了。并发就相当于两个班同时使用这间教室。
下面来介绍几个概念:
- 互斥:多个进程或线程在访问共享资源时不会发生冲突
- 临界资源:一次仅允许一个进程或线程访问的资源=
- 临界区:进程中访问临界资源的那段代码
- 原子性:一个事务或操作的所有步骤作为一个整体执行,这些步骤要么全部执行,要么全部不执行
临界资源 于 临界区 是属于匹配的关系
回到上面提到的,共享资源的存在,没有做特殊处理的话,会引发并发访问,使得多线程造成数据不一致的问题。
下面来举个例子:实现一个线程池,让这些线程去抢票(票数 1000 是共享资源)
#include <pthread.h>
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <string>int tickets = 1000;void *threadRoutine(void *arg)
{string name = static_cast<char*>(arg);//类型转换+构造while(true){if(tickets > 0){//模仿抢票时间usleep(2000);//usleep:微秒(秒、毫秒、微秒、纳秒)cout << name << " get a ticket: " << tickets-- << endl; }else{break;}}
}int main()
{//创建4个子线程pthread_t t[4];int n = sizeof(t) / sizeof(t[0]);for(int i = 0; i < n; i++){char* tname = new char[64];snprintf(tname, 64, "thread -> %d", i + 1);pthread_create(t + i, nullptr, threadRoutine, tname);}//等待线程for(int i = 0; i < n; i++){pthread_join(t[i], nullptr);}return 0;
}
运行查看效果:

当然中间的抢票过程不重要,重要的是结果。从上面的结果来看,当票数为0的时候还有线程继续执行抢票的操作,导致票变成负数。这个合理吗?很不合理。这就是并发带来的坏处。
为了避免并发访问造成的数据不一致,需要对共享资源做保护处理,被保护的资源也被称为 临界资源。任何一个线程都有属于自己的代码会去访问临界资源,这些代码也可以称为 临界区;同样的,在线程中没有访问到临界资源的代码被称为 非临界区。
可以说 临界资源 是衡量 共享资源 的;而 临界区 是衡量 线程代码的!
如何对共享资源进行保护呢?可以通过加锁的方式保护共享资源
互斥锁
下面来介绍几个关于互斥锁的API接口,使用互斥锁需要包含头文件:#include <pthread.h>
一般的,要定义一个互斥锁需要用到数据类型:pthread_mutex_t
pthread_mutex_t mutex; //定义一个互斥锁对象
- 初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
参数介绍:
mutex:指向初始化的互斥锁对象的指针
attr:设置互斥锁的属性
返回值:初始化成功返回0,失败错误码被设置
- 销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数介绍:
mutex:指向想要销毁的互斥锁
返回值:销毁成功返回0,失败错误码被设置
关于初始化互斥锁 和 销毁互斥锁 这里需要注意一点:
- 如果定义了一个全局的 或者 静态的互斥锁对象,可以通过使用宏:
PTHREAD_MUTEX_INITIALIZER来初始化这个互斥锁对象,并且这个互斥对象是不需要手动去销毁的
前提必须是 全局的 或是 静态的 互斥锁对象
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- 上锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
- 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
通过上面的互斥锁的提供的API,将上面的抢票代码稍作修改,将上面的互斥锁同线程名封装成 TDate 类,让多个线程用到同一个锁:
#include <pthread.h>
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <string>using namespace std;class TDate
{
public://构造TDate(const char* tname, pthread_mutex_t *mutex):_tname(tname),_mutex(mutex){}~TDate(){}public:string _tname;pthread_mutex_t *_mutex;
};int tickets = 1000;void *threadRoutine(void *arg)
{TDate* td = static_cast<TDate*>(arg);//类型转换+构造while(true){//将票数(共享资源),上锁保护pthread_mutex_lock(td->_mutex);if(tickets > 0){//模仿抢票时间usleep(2000);//usleep:微秒(秒、毫秒、微秒、纳秒)cout << td->_tname << " get a ticket: " << tickets-- << endl; //抢票成功,执行解锁操作pthread_mutex_unlock(td->_mutex);}else{pthread_mutex_unlock(td->_mutex); //先解锁再跳出循环break;}}
}int main()
{//定义互斥锁对象pthread_mutex_t mutex;//初始化互斥锁pthread_mutex_init(&mutex, nullptr);//创建4个子线程pthread_t t[4];int n = sizeof(t) / sizeof(t[0]);for(int i = 0; i < n; i++){char tname[64] = { 0 };snprintf(tname, sizeof(char)*64, "thread -> %d", i + 1);TDate* td = new TDate(tname, &mutex);pthread_create(t + i, nullptr, threadRoutine, td);}//等待线程for(int i = 0; i < n; i++){pthread_join(t[i], nullptr);}//销毁互斥锁pthread_mutex_destroy(&mutex);return 0;
}
再来看看运行结果:

上锁其实就是:当一个线程访问一块临界区时,将其他要访问这块临界区的线程阻塞起来;解锁就是:将临界资源重新开放,让所有的线程可以重新访问到临界资源。通过上锁、解锁的操作,就可以很好的解决并发的问题。
对于互斥锁,需要注意以下细节:
- 凡是访问同一个临界资源的线程,都要进行加锁保护,而且必须加同一把锁,不能有例外
- 加锁本质是给 临界区 加锁,加锁的粒度尽量要细一些(加锁的代码不宜太多)
- 由于线程都必须看到同一把锁,锁本身就是公共资源,对此加锁和解锁操作就是原子性的
- 在临界区线程是有可能被切换的,但是 切换线程不影响临界区的资源被修改
这是因为锁只有一把,由于锁提前被切走的线程先申请了,对于其他线程是不能再次申请锁资源。至此,其他线程无法对临界区的资源做任何修改
互斥锁实现的原理
在计算机体系结构中存在两个指令:swap 和 exechange,这两个指令作用是把寄存器和内存单元的数据做交换

加锁的过程是原子性的。如何去理解这句话呢?
下面来看一段伪代码:

上面的伪代码中,lock 相当于 pthread_mutex_lock();unlock 就相当于 pthread_mutex_unloc()。为了方便描述,我们将定义一个互斥锁对象:pthread_mutex_t mutex,这里的 mutex 我们可以看成就定义了一个大于0的整数。
在多线程中,上面加锁和解锁的操作,是由线程来调用的,谁先来谁先调用。
由于互斥锁是共享资源,定义了一把锁,就会在内存中开辟对应的空间,这里的mutex内容假设为1:

由于寄存器硬件只有一套,寄存器内部的数据是每个线程都有的(就是线程数据内容有多份,但是不能说寄存器有多个)
寄存器个数 != 寄存器内容(线程的上下文)
好比图书馆内,有很多的公共座位。当你去自习学习,带上自己的学习工具。学习工具是属于个人的,座位是公共的。当每次学习时,别人是不能占有你的学习工具。当你学习完要离开,你只能带走你的学习资料,但是板凳座椅还是在那可以被其他人使用。这里的学习工具就是寄存器内容,座椅板凳就是寄存器。
说了这么多铺垫,回过来看看前面的伪代码:

假设存在两个线程:threadA、threadB。线程A比较快,先申请了锁资源,调用了 lock API。
先执行第一条伪代码:movb $0, %al,线程A向寄存器 al 中写入数据 0

接下来走到第二条伪代码:xchgb %al,mutex (xchgb就是开始提到的 exchange 指令),将内存中的 mutex 值 1,交换到 CPU 中的 al 寄存器中。

由于 mutex 是共享资源,线程A 将 mutex 互斥锁对象的共享资源交换到寄存器中,其实就是将共享资源数据交换到自己的私有上下文中。可以说 :交换的本质就是加锁,由于 exchange 指令是一条汇编,所以说 加锁是原子性的
下面执行第三步:判断寄存器 al 内部的值

如果 al 内部的值大于0,那么直接结束 return 返回。
但是,有没有一种可能,就是此时正要执行第三步的时候,线程A时间片到了,直接切换到 线程B 运行。前面提到过,就是当一个线程切换到另一个线程的时候,寄存器内容是要被切换的线程一起带走的,这就导致共享资源的 mutex 内部原来的 1 变成 0 。
接下来,线程B来了哈,执行加锁的操作。
第一步先将 al 寄存器 值设置为0;第二步将寄存器与内存中的 mutex 内容进行交换,这下交换是什么,交换的是 0 啊。

mutex 内部的值是 0 ,原因是线程A被切走,线程A 没有回来。此刻,线程B 进行执行后续的代码,寄存器 al 内部的值为0,阻塞挂起 。不管后续来多少的线程,都会跟线程B一样,被阻塞着。直到线程A被调度回来,线程A会将原有的上下文带回,并且进行执行被调度走后的代码。
加锁明白了,解锁就更加简单了:

在诸多线程中,只有线程A没有被阻塞。线程A执行完对应的临界区代码后,需要解锁操作。将内存中的 mutex 值交换回1。此时,其他线程再继续执行的时候内存中的 mutex 值就不是0了,继续可以向后执行并发操作。
加锁和解锁的代码是线程在调用,说白了就是谁用锁谁调用。加锁与解锁其实就是一种,让没有调用互斥锁的线程通不通过的策略
封装原生线程库
模拟封装一个简化版的C++线程库:
实现 Thread 类,类中的成员函数调用原生线程库的API。具体实现的内容如下:
#include <iostream>
#include <pthread.h>
#include <string>
#include <cstdlib>class Thread
{
public:// 定义枚举类型typedef enum{NEW = 0,RUNNING,EXITED} ThreadStatus;typedef void* (*func_t)(void*); // 函数指针public://构造Thread(int num, func_t func, void* args): _tid(0),_status(NEW),_func(func),_args(args){char name[128];snprintf(name, sizeof(name), "thread->%d", num);_name = name;}// 获取线程状态int status(){return _status;}// 获取线程名称const std::string &threadname(){return _name;}// 获取线程tid:线程运行才返回,否则返回0const std::string threadid(){if (_status == RUNNING)return stringID();elsereturn "0";}// 线程id转换为16进制,以字符串输出const std::string stringID(){char id[64];snprintf(id, sizeof(id), "0x%x", _tid);return id;}//利用静态成员函数,防止默认参数(this)生成static void* runHelper(void* args){Thread* ts = static_cast<Thread*>(args);//类型转换(*ts)(); //函数对象return nullptr;}void operator()() //仿函数{if(_func != nullptr) _func(_args);}//线程执行void run(){// 创建线程// int n = pthread_create(&_tid, nullptr, runHelper, nullptr);int n = pthread_create(&_tid, nullptr, runHelper, this);//传入this,Thread本身if(n != 0) exit(1); // 更改线程状态_status = RUNNING;}// 等待线程void join(){int n = pthread_join(_tid, nullptr);if (n != 0) {std::cerr << "main thread join thread " << _name << " error" << std::endl;return;}_status = EXITED;}~Thread() {}
private:pthread_t _tid; // 线程idstd::string _name; // 线程名func_t _func; // 线程未来要执行的回调函数void* _args; //回调函数的参数ThreadStatus _status; // 线程所处的状态
};
封装互斥锁
实现两个类:Mutex类 和 lockGuard类
Mutex 类中的成员函数主要用于调用原生线程库中的互斥锁API;而lockGuard 主要实现构造与析构,其类内部包含 Mutex 对象的成员。
#include <iostream>
#include <pthread.h>
#include <string>class Mutex
{
public:Mutex(pthread_mutex_t *mutex): _mutex(mutex){}void lock(){// 上锁pthread_mutex_lock(_mutex);}void unlock(){// 解锁pthread_mutex_unlock(_mutex);}~Mutex() {}private:pthread_mutex_t *_mutex;
};class lockGuard
{
public://利用RAII思想lockGuard(pthread_mutex_t *mutex): _mutex(mutex){_mutex.lock();}~lockGuard(){_mutex.unlock();}private:Mutex _mutex;
};
lockGuard 类对象利用RAII思想,只需要实例化出lockGuard 对象后,使共享资源保护起来形成临界资源。不需要手动去上锁解锁操作
示例:
#include "lockGuard.hpp"
#include "Thread.hpp"int tickets = 1000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //定义全局的互斥锁void *threadRoutine(void *arg)
{string message = static_cast<const char*>(arg);while(true){lockGuard lockguard(&mutex); //利用RAII思想if(tickets > 0){usleep(2000);//usleep:微秒cout << message << " get a ticket: " << tickets-- << endl;}else{break;}}
}int main()
{Thread t1(1, threadRoutine, (void*)"hello world");Thread t2(2, threadRoutine, (void*)"hello world");Thread t3(3, threadRoutine, (void*)"hello world");Thread t4(4, threadRoutine, (void*)"hello world");t1.run();t2.run();t3.run();t4.run();t1.join();t2.join();t3.join();t4.join();return 0;
}
运行结果如下:

死锁
- 死锁:一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态
举个简单的例子:
有一天,有两个小朋友(小明 和 小强)都去小卖部买棒棒。这两个小朋友身上都有 5 毛钱。来到小卖部后,老板说:一个棒棒糖要 1 元。此时,小明说:小强要不你把你的 5 毛给我,我就可以买这个棒棒糖吃啦!小强一听肯定不行,说到:那你为什么不能给我你的5毛钱,我买来吃呢?谁也不让谁的状态,也就是死锁状态。举例的小明和小强就是两个线程、5 毛钱就是锁、棒棒糖就是临界资源、老板就是操作系统。
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
不剥夺的条件就是上面举例的,小明和小强不能为买棒棒糖的事情去抢对方的钱。
引入多线程就是为了高效的工作,但是多线程就会产生并发问题;为了解决并发问题,就引入了互斥锁,但是互斥锁的存在就会导致死锁的产生;对此,引入了一个解决问题的办法,往往就会滋生另一个问题的产生。
那么如何避免死锁呢?
避免死锁的四种方法
避免死锁的核心思想就是:破坏产生死锁的4个条件
- 不加锁:不会产生互斥条件
- 主动释放锁:不会产生请求与保护条件(不争锁资源)
- 按照顺序申请锁
- 控制线程统一释放锁(剥夺锁资源)
以代码的方式来解释一下第四点:
#include <pthread.h>
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <string>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;void *threadRoutine(void *arg)
{cout << "I am a new thread " << endl;pthread_mutex_lock(&mutex);cout << "I got a mutex!" << endl;// 再次申请锁会发生阻塞pthread_mutex_lock(&mutex);cout << "I alive again" << endl;return nullptr;
}int main()
{pthread_t t1;int n = pthread_create(&t1, nullptr, threadRoutine, nullptr);if (n != 0)exit(1);sleep(3);// 主线程cout << "main thread run begin" << endl;pthread_mutex_unlock(&mutex); //主线程释放锁cout << "main thread unlock..." << endl;sleep(3);return 0;
}
来看看运行结果:

不同线程申请锁资源是可以被其他线程释放的。为了避免死锁,可以将锁资源进行剥夺处理,也就是 控制线程统一释放锁。
线程同步
一个线程访问临界区过程一般是:申请锁,访问资源,再是释放锁。
如果在一个多线程程序中,一个线程一直就是执行这样的操作:优先申请了锁,在访问完临界区代码后,释放锁。在释放锁后,即刻又申请锁(中间没有间隔时间),访问临界资源,释放锁。循环反复,就会造成一个结果每次都只有这个线程在访问这个临界区,其他线程一直处于阻塞状态。这样的现象被称为:饥饿
互斥规则也是为了避免并发问题的产生,保证了数据的正确性。但是,互斥的规则也有不合理性,犹如上面提到的。
对此,需要对原有的规则进行修改:
- 同步:在保证数据的安全性前提下,要让线程能够按照某种特定的顺序访问临界资源
线程同步是为了解决多线程中的饥饿问题,让多线程进行协同工作
条件变量
- 条件变量:在多线程中用于实现线程间同步的一种机制
条件变量主要用于实现 等待 到 唤醒 的逻辑;条件变量本身不是锁,它通常是配合互斥锁一起使用。
下面来介绍一些关于环境变量的接口:使用条件变量需要包含头文件 #include <pthread.h>
- 条件变量的初始化
条件变量的初始化有两种方式,跟互斥锁差不多
第一种:通过宏的方式初始化全局的条件变量,
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
第二种:通过条件变量的 pthread_cond_init API 来初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
- 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
- 让线程处于等待状态
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
- 唤醒线程
唤醒线程的方式有两种:
将处于阻塞队列的线程一个一个唤醒
int pthread_cond_signal(pthread_cond_t *cond);
直接唤醒全部的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
下面来举个示例代码:
#include <iostream>
#include <pthread.h>
#include <unistd.h>using namespace std;#define NUM 5//初始化互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;void* active(void* args)
{string name = static_cast<const char*>(args);while(true){//上锁pthread_mutex_lock(&mutex);cout << name << "活动" << endl;//解锁pthread_mutex_unlock(&mutex);}
}int main()
{//创建线程池pthread_t tids[NUM];for(int i = 0; i < NUM; i++){char* name = new char[64];snprintf(name, sizeof(name), "thread->%d", i + 1);pthread_create(tids + i, nullptr, active, name);}//等待线程池for(int i = 0; i < NUM; i++){pthread_join(tids[i], nullptr);}return 0;
}
创建一个线程池,其中包含5个线程,每个线程都会去回调 active 函数。其中,对active的打印代码进行上锁与解锁操作,在这里直接设置了死循环。设置死循环是为了模拟其中一个线程频繁对锁资源的申请,使其他线程处于饥饿的状态。
来看看执行效果:

下面来修改代码,利用条件变量 API,当线程申请锁后,让其处于等待状态。在主线程中,再将一个个线程唤醒:
#include <iostream>
#include <pthread.h>
#include <unistd.h>using namespace std;#define NUM 5//初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//初始化互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;void* active(void* args)
{string name = static_cast<const char*>(args);while(true){//上锁pthread_mutex_lock(&mutex);//利用条件变量使线程处于阻塞状态pthread_cond_wait(&cond, &mutex);cout << name << "活动" << endl;//解锁pthread_mutex_unlock(&mutex);}
}int main()
{//创建线程池pthread_t tids[NUM];for(int i = 0; i < NUM; i++){char* name = new char[32];snprintf(name, 32, "thread->%d", i + 1);pthread_create(tids + i, nullptr, active, name);}sleep(2);//主进程while(true){cout << "main thread wakeup thread..." << endl;pthread_cond_signal(&cond); //唤醒单个线程sleep(1);}//等待线程池for(int i = 0; i < NUM; i++){pthread_join(tids[i], nullptr);}return 0;
}
编译代码,再来看看运行效果:

可以看到,线程调度是不确定的,如果没有使用条件变量,除了其中一个线程之外,其他线程会处于饥饿状态;使用了条件变量的接口后,解决了饥饿问题,线程调度的也变得有序起来。
条件变量等待作用就是:允许多线程在cond中的队列式等待;
条件变量的唤醒线程功能就是:将等待的线程从队列中一个一个的出队(先进先出),从而形成有序的情形。
通过利用条件变量的方式就能达到 线程的同步 的效果。
相关文章:
Linux:多线程中的互斥与同步
多线程 线程互斥互斥锁互斥锁实现的原理封装原生线程库封装互斥锁 死锁避免死锁的四种方法 线程同步条件变量 线程互斥 在多线程中,如果存在有一个全局变量,那么这个全局变量会被所有执行流所共享。但是,资源共享就会存在一种问题࿱…...
数据仓库之主题域
数据仓库的主题域(Subject Area)是按照特定业务领域或主题对数据进行分类和组织的方式。每个主题域集中反映一个特定的业务方面,使得数据分析和查询更加清晰和高效。主题域通常与企业的关键业务过程相关,能够帮助用户在数据仓库中…...
【简易版tinySTL】 vector容器
文章目录 基本概念功能思路代码实现vector.htest.cpp 代码详解变量构造函数析构函数拷贝构造operatorpush_backoperator[]insertprintElements 本实现版本 和 C STL标准库实现版本的区别: 基本概念 vector数据结构和数组非常相似,也称为单端数组vector与…...
BRAVE:扩展视觉编码能力,推动视觉-语言模型发展
视觉-语言模型(VLMs)在理解和生成涉及视觉与文本的任务上取得了显著进展,它们在理解和生成结合视觉与文本信息的任务中扮演着重要角色。然而,这些模型的性能往往受限于其视觉编码器的能力。例如,现有的一些模型可能对某…...
使用 Verdaccio 建立私有npm库
网上有很多方法,但很多没标注nginx的版本所以踩了一些坑,下方这个文档是完善后的,对linux不是很熟练,所以不懂linux不会搭建的跟着做就可以了 搭建方法 首先需要一台云服务器 以139.196.226.123为例登录云服务器 下载node cd /usr/local/lib下载node 解压 下载 wget https://…...
个人职业规划(含前端职业+技术线路)
1. 了解自己的兴趣与长处 喜欢擅长的事 职业方向 2. 设定长期目标(5年) 目标内容 建立自己的品牌建立自己的社交网络 适量参加社交活动,认识更多志同道合的小伙伴寻求导师指导 建立自己的作品集 注意事项 每年元旦进行审视和调整永葆积极…...
LeetCode | 344.反转字符串
设置头尾两个指针,依靠中间变量temp交换头尾指针所指元素,头指针后移,尾指针前移,直到头尾指针重合或者头指针在尾指针后面一个元素 class Solution(object):def reverseString(self, s):""":type s: List[str]:r…...
一步一步用numpy实现神经网络各种层
1. 首先准备一下数据 if __name__ "__main__":data np.array([[2, 1, 0],[2, 2, 0],[5, 4, 1],[4, 5, 1],[2, 3, 0],[3, 2, 0],[6, 5, 1],[4, 1, 0],[6, 3, 1],[7, 4, 1]])x data[:, :-1]y data[:, -1]for epoch in range(1000):...2. 实现SoftmaxCrossEntropy层…...
vue学习(二)
9.vue中的数据代理 通过vm对象来代理data对象中的属性操作(读写),目的是为了更加方便操作data中的数据 基本原理:通过Object.defineProperty()把data对象所有属性添加到vm上,为每一个添加到vm上的属性,都增…...
Maven 介绍
Maven open in new window 官方文档是这样介绍的 Maven 的: Apache Maven is a software project management and comprehension tool. Based on the concept of a project object model (POM), Maven can manage a projects build, reporting and documentation fr…...
QT截图程序三-截取自定义多边形
上一篇文章QT截图程序,可多屏幕截图二,增加调整截图区域功能-CSDN博客描述了如何截取,具备调整边缘功能后已经方便使用了,但是与系统自带的程序相比,似乎没有什么特别,只能截取矩形区域。 如果可以按照自己…...
Unity的三种Update方法
1、FixedUpdate 物理作用——处理物理引擎相关的计算和刚体的移动 (1) 调用时机:在固定的时间间隔内,而不是每一帧被调用 (2) 作用:用于处理物理引擎的计算,例如刚体的移动和碰撞检测 (3) 特点:能更准确地处理物理…...
[Python学习篇] Python字典
字典是一种可变的、无序的键值对(key-value)集合。字典在许多编程(Java中的HashMap)任务中非常有用,因为它们允许快速查找、添加和删除元素。字典使用花括号 {} 表示。字典是可变类型。 语法: 变量 {key1…...
react项目中如何书写css
一:问题: 在 vue 项目中,我们书写css的方式很简单,就是在 .vue文件中写style标签,然后加上scope属性,就可以隔离当前组件的样式,但是在react中,是没有这个东西的,如果直…...
PostgreSQL源码分析——绑定变量
这里分析一下函数中应用绑定变量的问题,但实际应用场景中,不推荐这么使用。 prepare divplan2(int,int) as select div($1,$2); execute divplan2(4,2);语法解析 分别分析prepare语句以及execute语句。 gram.y中定义 /******************************…...
Zynq学习笔记--了解中断配置方式
目录 1. 简介 2. 工程与代码解析 2.1 Vivado 工程 2.2 Vitis 裸机代码 2.3 关键代码解析 3. 总结 1. 简介 Zynq 中的中断可以分为以下几种类型: 软件中断(Software Generated Interrupt, SGI):由软件触发,通常…...
吴恩达机器学习 第二课 week2 多分类问题
目录 01 学习目标 02 实现工具 03 概念与原理 04 应用示例 05 总结 01 学习目标 (1)理解二分类与多分类的原理区别 (2)掌握简单多分类问题的神经网络实现方法 (3)理解多分类问题算法中的激活函数与损失…...
112、路径总和
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。 叶子节点 是指没有子节点…...
Vue 封装组件之Input框
封装Input组件:MyInput.vue <template><div class"base-input-wraper"><el-inputv-bind"$attrs"v-on"$listeners"class"e-input":style"inputStyle":value"value":size"size"input&quo…...
一段代码让你了解Java中的抽象
我们先来看一道题! 计算几何对象的面积之和)编写一个方法,该方法用于计算数组中所有几何对象的面积之和。该方法的签名是: public static double sumArea(GeometricObject[] a) 编写一个测试程序,该程序创建一个包含四…...
网络六边形受到攻击
大家读完觉得有帮助记得关注和点赞!!! 抽象 现代智能交通系统 (ITS) 的一个关键要求是能够以安全、可靠和匿名的方式从互联车辆和移动设备收集地理参考数据。Nexagon 协议建立在 IETF 定位器/ID 分离协议 (…...
Vue记事本应用实现教程
文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展:显示创建时间8. 功能扩展:记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...
springboot 百货中心供应链管理系统小程序
一、前言 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,百货中心供应链管理系统被用户普遍使用,为方…...
RocketMQ延迟消息机制
两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数,对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后…...
【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 初始化服务器)
服务端执行命令请求的过程 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 初始化服务器1. 初始化服务器状态结构初始化RedisServer变量 2. 加载相关系统配置和用户配置参数定制化配置参数案…...
江苏艾立泰跨国资源接力:废料变黄金的绿色供应链革命
在华东塑料包装行业面临限塑令深度调整的背景下,江苏艾立泰以一场跨国资源接力的创新实践,重新定义了绿色供应链的边界。 跨国回收网络:废料变黄金的全球棋局 艾立泰在欧洲、东南亚建立再生塑料回收点,将海外废弃包装箱通过标准…...
在WSL2的Ubuntu镜像中安装Docker
Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包: for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...
MySQL用户和授权
开放MySQL白名单 可以通过iptables-save命令确认对应客户端ip是否可以访问MySQL服务: test: # iptables-save | grep 3306 -A mp_srv_whitelist -s 172.16.14.102/32 -p tcp -m tcp --dport 3306 -j ACCEPT -A mp_srv_whitelist -s 172.16.4.16/32 -p tcp -m tcp -…...
优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...
2023赣州旅游投资集团
单选题 1.“不登高山,不知天之高也;不临深溪,不知地之厚也。”这句话说明_____。 A、人的意识具有创造性 B、人的认识是独立于实践之外的 C、实践在认识过程中具有决定作用 D、人的一切知识都是从直接经验中获得的 参考答案: C 本题解…...
