【Linux】多线程:线程互斥、互斥锁、线程安全
目录
一、多线程访问公共资源时所产生的问题
二、互斥相关背景概念
互斥量mutex(锁)的引入
三、互斥量
1、初始化互斥量(mutex)
2、互斥量加锁
3、互斥量解锁
4、 销毁互斥量
四、互斥量的使用
1、使用静态互斥量
2、使用动态互斥量
五、互斥锁的原理
一、多线程访问公共资源时所产生的问题
我们知道一个进程中的多个线程共享进程的地址空间,因此进行线程间的通信是极为容易的,这也就意味着进程中的多个线程可以随意访问进程中的“公共资源”。但多线程的并发访问会不会对这些公共资源造成不可预测的结果呢?
在此之前,为了方便使用,我们先对pthread库中的线程进行一下封装。
#include <pthread.h>
#include <string>
#include <functional>
using FuncType = std::function<void(const std::string&)>;//包装器
//线程类的封装
class Thread
{
private:pthread_t _tid;//线程IDstd::string _thread_name;//线程名FuncType _func;//线程的执行函数bool _is_running;//线程的状态//...
private:void Excute(){_is_running = true;_func(_thread_name);_is_running = false;}//类中的函数参数包含this指针,使用static修饰static void* ThreadRoute(void* arg){Thread* self = static_cast<Thread*>(arg);self->Excute();return (void*)0;}
public:Thread(std::string thread_name, FuncType func):_thread_name(thread_name), _func(func){_is_running = false;}//线程启动bool Start(){int ret = pthread_create(&_tid, NULL, ThreadRoute, (void*)this);if (ret != 0){return false;}std::cout << _thread_name << " has Started" << std::endl;return true;}//线程取消bool Stop(){if(_is_running){int ret = pthread_cancel(_tid);if (ret != 0){return false;} std::cout << _thread_name << " has Stoped" << std::endl;_is_running = false;}return true;}//回收线程bool Join(){if(!_is_running){int ret = pthread_join(_tid, NULL);//不关心线程返回值,设置为NULLif (ret != 0){return false;} }std::cout << _thread_name << " has Joined" << std::endl;return true;}
};
我们写一个程序来验证一下:我们在全局区创建一个变量表示票数,让多个线程同时去“抢票”,直到票数为0时停止抢票。
#include <pthread.h>
#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <vector>
#include "Thread.hpp"
#define BUFF_SIZE 128
int ticket_num = 1000;
// 线程函数
void grab_tickets(const std::string& name)
{while(true){if(ticket_num > 0){usleep(1000);//模拟每次的业务处理时长ticket_num--;std::cout << name << " has get a ticket! " << "Remaining Quantity is : " << ticket_num << std::endl;}else{break;}}
}int main()
{// 存储每个线程的tidstd::vector<Thread> threads;// 假设创建5个线程,此时加上主线程,该进程中共有6个线程for (int i = 0; i < 5; i++){std::string name = "Thread - " + std::to_string(i + 1);threads.emplace_back(name, grab_tickets);}for (auto& t : threads){if(!t.Start()){perror("Start false!!!");exit(-1);}}sleep(1);//1秒后未退出就强制退出for (auto& t : threads){if(!t.Stop()){perror("Stop false!!!");exit(-1);}}for (auto& t : threads){if(!t.Join()){perror("Join false!!!");exit(-1);}}return 0;
}
我们运行程序,看一下结果:

可以观察到,我们在线程的执行函数中使用了条件判断,当票的数量小于等于0时就停止抢票。但是我们发现程序运行后的票数竟然变为了负数。这是为什么呢?由此可以看出,抢票的操作并不是“原子的”。
• if 语句判断条件为真以后, 代码可以并发的切换到其他线程。
• usleep 这个模拟漫长业务的过程, 在这个漫长的业务过程中, 可能有很多个。线程会进入该代码段。
• --ticket 操作本身就不是一个原子操作。 而是对应三条汇编指令:
1、 将共享变量 ticket 从内存加载到寄存器中。
2、更新寄存器里面的值, 执行 -1 操作。
3、 将新值从寄存器写回共享变量 ticket 的内存地址。
要解决以上问题, 需要做到三点:
• 代码必须要有互斥行为: 当代码进入临界区执行时, 不允许其他线程进入该临界区。
• 如果多个线程同时要求执行临界区的代码, 并且临界区没有线程在执行, 那么只能允许一个线程进入该临界区。
• 如果线程不在临界区中执行, 那么该线程不能阻止其他线程进入临界区。
要做到这三点, 本质上就是需要一把锁。 Linux 上提供的这把锁叫互斥量。
二、互斥相关背景概念
1、临界资源:多线程执行流共享的资源就叫做临界资源。
2、临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
3、互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
4、原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。
互斥量mutex(锁)的引入
1、大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个 线程,其他线程无法获得这种变量。
2、但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量(如全局区变量),可以通过数据的共享,完成线程之间的交互。
3、多个线程并发的操作共享变量,会带来一些问题,如上述例子中的“抢票”。
因此,在多线程编程中需要保证对共享资源操作的原子性。而对共享资源操作的保护本质上就是保护操作共享资源的代码片段,也就是临界区。因此,我们只需要保证对临界区操作的原子性就能够保证多线程正确地操作临界资源。
上述操作的实现我们可以通过互斥量,也就是为临界区的起始位置和结束位置加一把“锁”来控制。当多个线程并发进入临界区前,都会去竞争互斥锁的归属。只有拿到互斥锁的那个线程才会去执行临界区的代码,而其他线程阻塞在申请锁的函数当中。在退出临界区时,该线程会释放它所拥有的互斥锁。如果该线程需要再次执行该任务,此时该线程会与其他线程一起重新参与锁的竞争。

三、互斥量
1、初始化互斥量(mutex)
方法 1:静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- 解释:这是静态初始化互斥量的方式。
PTHREAD_MUTEX_INITIALIZER是一个宏,用于初始化互斥量mutex,该宏定义在 POSIX 线程库中。静态初始化适用于那些在程序编译时已知的全局或静态互斥量。- 优点:不需要额外的初始化函数调用,不需要手动释放互斥量,生命周期随程序。
- 限制:仅适用于静态或全局互斥量的初始化,不能用于动态创建的互斥量。
方法 2:动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
- 参数:
mutex:指向要初始化的互斥量的指针。这个互斥量必须在使用之前被初始化。attr:用于指定互斥量的属性。可以为NULL,表示使用默认的属性;也可以指定一个pthread_mutexattr_t结构体来设置互斥量的自定义属性。- 返回值:如果初始化成功,返回
0;否则,返回一个错误代码。- 优点:适用于动态分配的互斥量,也可以通过
attr参数设置互斥量的特定属性,例如递归锁、进程间锁等。- 使用示例:
在使用动态分配方法时,需要在不再需要互斥量时调用pthread_mutex_t mutex; pthread_mutex_init(&mutex, NULL);pthread_mutex_destroy来销毁它,以释放相关资源。
2、互斥量加锁
函数原型
int pthread_mutex_lock(pthread_mutex_t *mutex);参数
mutex:指向要加锁的互斥量的指针。返回值
- 如果加锁成功,返回
0。- 如果加锁失败,返回一个错误代码。例如,如果调用
pthread_mutex_lock的线程已经持有该锁,且互斥量的属性设置为递归锁,则可能返回EDEADLK(死锁错误)。
调用 pthread_ lock 时, 可能会遇到以下情况:
• 互斥量处于未锁状态, 该函数会将互斥量锁定, 同时返回成功
• 发起函数调用时, 其他线程已经锁定互斥量, 或者存在其他线程同时申请互斥量,但没有竞争到互斥量, 那么 pthread_ lock 调用会陷入阻塞(执行流被挂起), 等待互斥量解锁。
3、互斥量解锁
函数原型
int pthread_mutex_unlock(pthread_mutex_t *mutex);参数
mutex:指向要解锁的互斥量的指针。返回值
- 如果解锁成功,返回
0。- 如果解锁失败,返回一个错误代码。例如,如果调用
pthread_mutex_unlock的线程没有持有该锁,可能会返回EPERM(操作不允许)错误。
4、 销毁互斥量
【注意】:
• 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁。
• 不要销毁一个已经加锁的互斥量。• 已经销毁的互斥量, 要确保后面不会有线程再尝试加锁。
函数原型
int pthread_mutex_destroy(pthread_mutex_t *mutex);参数
mutex:指向要销毁的互斥量的指针。该互斥量必须已经被初始化,并且在调用pthread_mutex_destroy之前,所有对该互斥量的操作(如pthread_mutex_lock和pthread_mutex_unlock)都必须已经完成。返回值
- 如果成功,返回
0。- 如果销毁失败,返回一个错误代码。例如,如果有其他线程正在使用该互斥量,可能会返回
EBUSY。
四、互斥量的使用
1、使用静态互斥量
我们可以在全局区创建一个互斥量,使用宏进行初始化。在使用互斥锁之前,我们需要让不同的线程看到同一把锁,进而去并发竞争这把锁,而所有线程都恰好能够看到全局区的变量。
#include <pthread.h>
#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <vector>
#include "Thread.hpp"
#define BUFF_SIZE 128
int ticket_num = 1000;//全局的互斥量,使用宏进行初始化
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;// 线程函数
void grab_tickets(const std::string& name)
{while(true){pthread_mutex_lock(&g_mutex);//加锁//----------------临界区起始位置------------------------if(ticket_num > 0){usleep(1000);//模拟每次的业务处理时长ticket_num--;std::cout << name << " has get a ticket! " << "Remaining Quantity is : " << ticket_num << std::endl;//----------------临界区结束位置1------------------------pthread_mutex_unlock(&g_mutex);//解锁}else{//----------------临界区结束位置2------------------------pthread_mutex_unlock(&g_mutex);//解锁break;}}
}int main()
{// 存储每个线程的tidstd::vector<Thread> threads;// 假设创建5个线程,此时加上主线程,该进程中共有6个线程for (int i = 0; i < 5; i++){std::string name = "Thread - " + std::to_string(i + 1);threads.emplace_back(name, grab_tickets);}for (auto& t : threads){if(!t.Start()){perror("Start false!!!");exit(-1);}}sleep(10);for (auto& t : threads){if(!t.Join()){perror("Join false!!!");exit(-1);}}return 0;
}
2、使用动态互斥量
实际上,我们也可以将互斥锁进行进一步的封装。在上述代码中,有两个退出路径,分别在if和else中。我们可以在进入临界区前创建一个局部对象,在创建锁对象时在构造函数中实现加锁。当退出临界区后,因为该对象只是一个局部变量,会自动进行销毁。所以我们在析构函数中进行解锁即可。具体实现如下:
#pragma once
#include <pthread.h>class LockGuard
{
private:pthread_mutex_t* _mutex;
public:LockGuard(pthread_mutex_t* mutex):_mutex(mutex){pthread_mutex_lock(_mutex);//加锁}~LockGuard(){pthread_mutex_unlock(_mutex);//解锁}
};
同样的,当在局部使用pthread_mutex_init函数创建局部互斥锁的时候,我们也需要让所有线程看到同一把锁。因此,所有的线程都需要拿到这把局部锁的地址,进而去竞争和使用。那如何做到呢?我们可以对封装好的线程对象稍作修改——为线程类添加一个私有变量:pthread_mutex_t* mutex用来存储互斥锁的地址。如此,当线程需要使用互斥锁时,直接从线程对象中获取到局部锁的地址即可。
Thread.hpp:
#include <pthread.h>
#include <string>
#include <functional>
class Thread;//声明类
using FuncType = std::function<void(Thread* thread)>;//包装器
//线程类的封装
class Thread
{
private:pthread_t _tid;//线程IDstd::string _thread_name;//线程名FuncType _func;//线程的执行函数bool _is_running;//线程的状态pthread_mutex_t* _mutex;//局部互斥锁的地址
private:void Excute(){_is_running = true;_func(this);_is_running = false;}//类中的函数参数包含this指针,使用static修饰static void* ThreadRoute(void* arg){Thread* self = static_cast<Thread*>(arg);self->Excute();return (void*)0;}
public:Thread(std::string thread_name, FuncType func, pthread_mutex_t* mutex):_thread_name(thread_name), _func(func), _mutex(mutex){_is_running = false;}//线程启动bool Start(){int ret = pthread_create(&_tid, NULL, ThreadRoute, (void*)this);if (ret != 0){return false;}std::cout << _thread_name << " has Started" << std::endl;return true;}//线程取消bool Stop(){if(_is_running){int ret = pthread_cancel(_tid);if (ret != 0){return false;} std::cout << _thread_name << " has Stoped" << std::endl;_is_running = false;}return true;}//回收线程bool Join(){if(!_is_running){int ret = pthread_join(_tid, NULL);//不关心线程返回值,设置为NULLif (ret != 0){return false;} }std::cout << _thread_name << " has Joined" << std::endl;return true;}//获取互斥锁的地址pthread_mutex_t* get_mutex_address(){return _mutex;}std::string get_name(){return _thread_name;}
};
#include <pthread.h>
#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <vector>
#include "Thread.hpp"
#include "LockGuard.hpp"int ticket_num = 1000;//全局的互斥量,使用宏进行初始化
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;// 线程函数
void grab_tickets(Thread* thread)
{while(true){LockGuard lock_guard(thread->get_mutex_address());//加锁//----------------临界区起始位置------------------------if(ticket_num > 0){usleep(1000);//模拟每次的业务处理时长ticket_num--;std::cout << thread->get_name() << " has get a ticket! " << "Remaining Quantity is : " << ticket_num << std::endl;//----------------临界区结束位置1------------------------}else{//----------------临界区结束位置2------------------------//退出时自动解锁break;}}
}int main()
{// 存储每个线程的tidstd::vector<Thread> threads;//创建锁pthread_mutex_t local_mutex;int ret = pthread_mutex_init(&local_mutex, NULL);if(ret != 0){perror("Mutex init false!!!");exit(-1);}// 假设创建5个线程,此时加上主线程,该进程中共有6个线程for (int i = 0; i < 5; i++){std::string name = "Thread - " + std::to_string(i + 1);threads.emplace_back(name, grab_tickets, &local_mutex);}for (auto& t : threads){if(!t.Start()){perror("Start false!!!");exit(-1);}}sleep(10);//10秒后未退出就强制退出for (auto& t : threads){if(!t.Stop()){perror("Stop false!!!");exit(-1);}}for (auto& t : threads){if(!t.Join()){perror("Join false!!!");exit(-1);}}//销毁锁pthread_mutex_destroy(&local_mutex);return 0;
}
如此便解决了多线程并发所带来的对公共资源操作的问题。但新的问题又出现了:运行上述程序的同学会发现,上述程序的执行结果出现了一个线程长时间占用锁的情况,这显然是不符合“抢票”这一动作的规则的。我们想要的是多个线程依次抢票,而不是“一家独大”。要解决这种情况,需要引入条件变量的使用,在下节内容中会详细讲解。
五、互斥锁的原理
为了实现互斥锁操作,大多数体系结构都提供了 swap 或 exchange 指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把 lock 和 unlock 的伪代码改一下:

在cpu中,寄存器只有一套。每个线程在执行时都有自己的线程上下文(thread context),包括寄存器的状态、程序计数器(PC)、堆栈指针等。上下文切换发生在线程切换时,需要保存当前线程的上下文,并恢复下一个线程的上下文。
上下文切换:
- 当操作系统在多线程环境中切换线程时,会将当前线程的寄存器状态(即上下文)保存到内存中,然后加载下一个线程的寄存器状态。这样,每个线程在切换时都能从它上次运行的状态继续执行。
- 实际上,处理器寄存器的物理数量是有限的,通常不会为每个线程提供独立的寄存器。相反,操作系统和处理器通过保存和恢复寄存器状态来模拟每个线程拥有自己的寄存器集。
当一个线程竞争到互斥锁时,首先会将自己al寄存器的内容清零,接着会将互斥量与自己的al寄存器的内容进行交换。由于cpu硬件电路在实现交换指令时是原子的,所以此时不会被其他线程介入,这也就保证了对互斥量操作的原子性,也就是说加锁这一操作本身就是原子的。
当该线程竞争到互斥量之后,使用原子交换指令(如 xchg 指令)将互斥量的值与 al 寄存器中的值交换。交换操作完成后,al 寄存器中会存储互斥量原来的值。通过检查这个值,线程可以判断是否成功获得互斥量。如果原值为 0,则说明互斥量之前是空闲的,线程成功获得了锁;如果原值非 0,则说明其他线程已经持有锁,当前线程需要挂起等待。
而在竞争到互斥量之后,线程仍可能会被cpu调度切换,但此时内存中唯一的互斥量已经被该线程带走,成为了线程自身的数据。此时,即使其他线程被调度,也无法执行临界区的代码,只能挂起等待抢走互斥量的线程执行完临界区代码后将互斥量交换回内存中。此后该线程再与其他线程一起重新竞争这把锁。
相关文章:
【Linux】多线程:线程互斥、互斥锁、线程安全
目录 一、多线程访问公共资源时所产生的问题 二、互斥相关背景概念 互斥量mutex(锁)的引入 三、互斥量 1、初始化互斥量(mutex) 2、互斥量加锁 3、互斥量解锁 4、 销毁互斥量 四、互斥量的使用 1、使用静态互斥量 2、…...
进程之间的通信方式
前言 每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。 Linux提供了以下进程通信方式: 一、管道 所谓的管道,就是内核里面的一串缓存。…...
动手学深度学习(pytorch)学习记录26-卷积神经网路(LeNet)[学习记录]
目录 LeNet模型训练 LeNet 总体来看,LeNet(LeNet-5)由两个部分组成: 卷积编码器:由两个卷积层组成; 全连接层密集块:由三个全连接层组成。 每个卷积块中的基本单元是一个卷积层、一个sigmoid激活函数和平均…...
log4j 和 java.lang.OutOfMemoryError PermGen space
还是OneCoder在项目中沙箱的问题,用classloader隔离做的沙箱,反复运行用户的任务,出现永生区内存溢出: java.lang.OutOfMemoryError: PermGen space 这个问题在tomcat重复热部署的时候其实比较常见。其道理也和我们沙箱的道理基本…...
2024.9.9营养小题【2】
营养: 1、什么数是丑数? 2、数学数学,丑数的数学意义,哎,数学思维我是忘干净了。 3、可以把while循环换成for循环。由此又想到了一点,三个循环结构各有使用场景。 for(;n%factors[i]0;n/factors[i]){}...
uniapp的barcode组件去掉自动放大功能
autoZoom“false” <barcode id1 class"barcode" autoZoom"false" autostart"false" ref"barcode" background"rgb(0,0,0)" frameColor"#1C86EE"scanbarColor"#1C86EE" :filters"fil" ma…...
H5接入Steam 获取用户数据案例
官方文档地址 1.注册 Steam API Key: 你需要一个 Steam Web API Key,可以在 Steam API Key 页面 获取。https://steamcommunity.com/dev/apikey 2.使用 OpenID 登录: 实现 Steam OpenID 登录,以便用户通过 Steam 账户登录你的…...
《A Few Useful Things to Know about Machine Learning》论文导读
版权声明 本文原创作者:谷哥的小弟作者博客地址:http://blog.csdn.net/lfdfhl机器学习作为人工智能领域的重要分支,近年来得到了广泛的关注和应用。Pedro Domingos的经典论文《A Few Useful Things to Know about Machine Learning》为我们提供了对机器学习深入且全面的理解…...
隔壁老樊2024全国巡回演唱会重磅来袭,首站广州正式官宣!
汹涌人潮将城市填满,斑驳心绪漂浮在时间之隙,当生活的喜悲逐渐演化成歌,天空将自己负载的缄默倾泻,那些或酸涩、或热烈的点滴滑落心海,那层悬挂在「我」与世界分野的无形壁垒,渐也被曙光渗透消融。 提炼生…...
【C++】list(下)
个人主页~ list(上)~ list 四、模拟实现1、list.h(1)关于整个list的搭建①节点②迭代器③接口 (2)自定义类型实例化 2、test.cpp(1)test1(2)test2 五、额外小…...
千云物流 -低代码平台MySQL备份数据
windows备份 全量备份 创建备份目录 需要在安装数据库的服务器上创建备份目录,所有如果要做备份至少需要两倍的硬盘空间, mkdir D:\mysql_backup\full_backup准备备份脚本 创建一个windows批处理文件(例如 full_backup.bat),用来执行全量…...
MySQL:进阶巩固-视图
目录 一、视图的概述二、视图的基本使用2.1 创建视图2.2 查询视图2.3 修改视图2.4 删除视图 一、视图的概述 视图是一种虚拟存在的表,视图中的数据并不在数据库中实际的存在,行列数据来自于视图中查询的表,并且是在使用视图时动态生成的。 通…...
分布式事务Seata原理及其项目使用
0.Seata官方文档 1.Seata概念及原理 Seata是什么 Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。 Seata主要由三个重要组…...
JS_分支结构
if结构 这里的if结构几乎和JAVA中的一样,需要注意的是 if()中的非空字符串会被认为是trueif()中的非零数字会被认为是trueif()中的非空对象会被认为是true <script> if(false){// 非空字符串 if判断为true console.log(true) }else{ console.log(false) } if(){// 长度…...
决策树(Decison Tree)—有监督学习方法、概率模型、生成模型、非线性模型、非参数化模型、批量学习
定义 ID3算法 输入:训练数据集(T= { ( x 1 , y 1 ) , ( x 2 , y 2 ) , ⋯ , ( x N , y N ) } \left\{(x_1,y_1),(x_2,y_2),\cdots,(x_N,y_N)\right\} {(x1,y1),(x2,y2),⋯,(xN,yN)}),特征集A阀值 ε \varepsilon ε 输出:决策树T (1)若D中所有实例属于同一…...
java 自定义注解校验实体类属性
直接上代码 1.是否启用参数校验注解 Target({ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) Documented public interface EnableArgumentsCheck {/*** 是否启用*/boolean enable() default true;} 2.参数校验自定义注解 /*** 参数校验自定义注解* 属性定义&#…...
光伏并网发电系统中电能质量监测与优化技术探讨
0引言 随着清洁能源技术的持续进步与广泛应用,光伏并网发电系统亦逐步崭露头角。作为一种关键的电力供应方式,其受到了广泛的关注。然而,由于天气等外部条件的影响,光伏发电系统面临若干挑战。电能质量问题,诸如电压波…...
网页解析的那些事
Vue 方面 模板语法理解 熟悉 Vue 的模板语法,包括插值表达式(如{{ message }})、指令(如v-if、v-for、v-bind等)。理解这些语法元素如何将数据与 DOM 元素进行绑定和交互。例如,v-for指令用于循环渲染列表数…...
从文字到世界:2024外语阅读大赛报名开启,赛氪网全程护航
中国外文局CATTI项目管理中心与中国外文界联合宣布,将举办2024年外语阅读大赛,旨在激发外语学习兴趣,选拔并培养优秀的语言应用人才,同时向世界展示和传播具有中国特色的优秀文化。此次大赛旨在激发外语学习兴趣,选拔优…...
微信小程序知识点(二)
1.下拉刷新事件 如果页面需要下拉刷新功能,则在页面对应的json配置文件中,将enablePullDownRefresh配置设置为true,如下 {"usingComponents": {},"enablePullDownRefresh": true } 2.上拉触底事件 在很多时候&#x…...
在四层代理中还原真实客户端ngx_stream_realip_module
一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡(如 HAProxy、AWS NLB、阿里 SLB)发起上游连接时,将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后,ngx_stream_realip_module 从中提取原始信息…...
Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!
一、引言 在数据驱动的背景下,知识图谱凭借其高效的信息组织能力,正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合,探讨知识图谱开发的实现细节,帮助读者掌握该技术栈在实际项目中的落地方法。 …...
浅谈不同二分算法的查找情况
二分算法原理比较简单,但是实际的算法模板却有很多,这一切都源于二分查找问题中的复杂情况和二分算法的边界处理,以下是博主对一些二分算法查找的情况分析。 需要说明的是,以下二分算法都是基于有序序列为升序有序的情况…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
Element Plus 表单(el-form)中关于正整数输入的校验规则
目录 1 单个正整数输入1.1 模板1.2 校验规则 2 两个正整数输入(联动)2.1 模板2.2 校验规则2.3 CSS 1 单个正整数输入 1.1 模板 <el-formref"formRef":model"formData":rules"formRules"label-width"150px"…...
USB Over IP专用硬件的5个特点
USB over IP技术通过将USB协议数据封装在标准TCP/IP网络数据包中,从根本上改变了USB连接。这允许客户端通过局域网或广域网远程访问和控制物理连接到服务器的USB设备(如专用硬件设备),从而消除了直接物理连接的需要。USB over IP的…...
rnn判断string中第一次出现a的下标
# coding:utf8 import torch import torch.nn as nn import numpy as np import random import json""" 基于pytorch的网络编写 实现一个RNN网络完成多分类任务 判断字符 a 第一次出现在字符串中的位置 """class TorchModel(nn.Module):def __in…...
html css js网页制作成品——HTML+CSS榴莲商城网页设计(4页)附源码
目录 一、👨🎓网站题目 二、✍️网站描述 三、📚网站介绍 四、🌐网站效果 五、🪓 代码实现 🧱HTML 六、🥇 如何让学习不再盲目 七、🎁更多干货 一、👨…...
比较数据迁移后MySQL数据库和OceanBase数据仓库中的表
设计一个MySQL数据库和OceanBase数据仓库的表数据比较的详细程序流程,两张表是相同的结构,都有整型主键id字段,需要每次从数据库分批取得2000条数据,用于比较,比较操作的同时可以再取2000条数据,等上一次比较完成之后,开始比较,直到比较完所有的数据。比较操作需要比较…...
【p2p、分布式,区块链笔记 MESH】Bluetooth蓝牙通信 BLE Mesh协议的拓扑结构 定向转发机制
目录 节点的功能承载层(GATT/Adv)局限性: 拓扑关系定向转发机制定向转发意义 CG 节点的功能 节点的功能由节点支持的特性和功能决定。所有节点都能够发送和接收网格消息。节点还可以选择支持一个或多个附加功能,如 Configuration …...
