《TCP/IP网络编程》学习笔记 | Chapter 18:多线程服务器端的实现
《TCP/IP网络编程》学习笔记 | Chapter 18:多线程服务器端的实现
- 《TCP/IP网络编程》学习笔记 | Chapter 18:多线程服务器端的实现
- 线程的概念
- 引入线程的背景
- 线程与进程的区别
- 线程创建与运行
- pthread_create
- pthread_join
- 可在临界区内调用的函数
- 工作(Worker)线程模型
- 线程存在的问题和临界区
- 多个线程访问同一变量的问题
- 临界区位置
- 线程同步
- 同步的两面性
- 互斥量
- 信号量
- 销毁线程的 3 种方法
- 多线程并发服务器端的实现
- 习题
- (1)单 CPU 系统中如何同时执行多个进程?请解释该过程中发生的上下文切换。
- (2)为何线程的上下文切换速度相对更快?线程间数据交换为何不需要类似 IPC 的特别技术?
- (3)请从执行流角度说明进程和线程的区别。
- (6)请说明完全销毁 Linux 线程的 2 种办法。
《TCP/IP网络编程》学习笔记 | Chapter 18:多线程服务器端的实现
线程的概念
引入线程的背景
多进程模型的缺点:
- 创建(复制)进程的工作本身会给操作系统带来相当沉重的负担
- 进程间通信的实现难度高,为了完成进程间数据交换,需要特殊的 IPC 技术
- 每秒少则数十次、多则数千次的“上下文切换”(Context Switching)是创建进程时最大的开销
只有一个 CPU 的系统是将时间分成多个微小的块后分配给了多个进程。为了分时使用 CPU ,需要「上下文切换」的过程。
运行程序前需要将相应进程信息读入内存,如果运行进程 A 后紧接着需要运行进程 B ,就应该将进程 A 相关信息移出内存,并读入进程 B 相关信息。这就是上下文切换。但是此时进程 A 的数据将被移动到硬盘,所以上下文切换要很长时间,即使通过优化加快速度,也会存在一定的局限。
为了保持多进程的优点,同时在一定程度上克服其缺点,人们引入了线程(Thread)的概念。这是为了将进程的各种劣势降至最低程度(不是直接消除)而设立的一种「轻量级进程」。线程比进程具有如下优点:
- 线程的创建和上下文切换比进程的创建和上下文切换更快
- 线程间交换数据无需特殊技术
线程与进程的区别
每个进程的内存空间由三个部分构成:
- 数据区:用于保存全局变量
- 堆区域:用于向malloc等函数的动态分配提供空间
- 栈区域:函数运行时会使用
每个进程都有独立的这种空间,多个进程的内存结构如图所示:
为了得到多条代码流而复制整个内存区域的负担太重了,所以有了线程的出现。不应完全分离内存结构,而只需要分离栈区域。通过这种方式可以获得如下优势:
- 上下文切换时不需要切换数据区和堆
- 可以利用数据区和堆交换数据
实际上这就是线程。线程为了保持多条代码执行流而隔开了栈区域,因此具有如下图所示的内存结构:
多个线程共享数据区和堆。为了保持这种结构,线程将在进程内创建并运行。也就是说,进程和线程可以定义为如下形式:
- 进程:在操作系统构成单独执行流的单位
- 线程:在进程构成单独执行流的单位
如果说进程在操作系统内部生成多个执行流,那么线程就在同一进程内部创建多条执行流。因此,操作系统、进程、线程之间的关系可以表示为下图:
线程创建与运行
pthread_create
线程具有单独的执行流,因此需要单独定义线程的 main 函数,还需要请求操作系统在单独的执行流中执行该函数,完成该功能的函数如下:
#include <pthread.h>int pthread_create(pthread_t *restrict thread,const pthread_attr_t *restrict attr,void *(*start_routine)(void *),void *restrict arg);
参数:
- thread:保存新创建线程 ID 的变量地址值。线程与进程相同,也需要用于区分不同线程的 ID
- attr:用于传递线程属性的参数,传递 NULL 时,创建默认属性的线程
- start_routine:相当于线程 main 函数的、在单独执行流中执行的函数地址值(函数指针)
- arg:通过第三个参数传递的调用函数时包含传递参数信息的变量地址值
成功时返回 0,失败时返回 -1。
要想理解好上述函数的参数,需要熟练掌握restrict关键字和函数指针相关语法。但如果只关注使用方法,那么该函数的使用比想象中要简单。下面通过简单示例了解该函数的功能。
#include <stdio.h>
// #include <stdlib.h>
#include <pthread.h>
#include <unistd.h>void *thread_main(void *arg);int main(int argc, char *argv[])
{pthread_t t_id;int thread_param = 5;// 请求创建一个线程,从 thread_main 调用开始,在单独的执行流中运行。同时传递参数if (pthread_create(&t_id, NULL, thread_main, (void *)&thread_param) != 0){puts("pthread_create() error");return -1;}sleep(10); // 延迟进程终止时间puts("end of main");// system("pause");return 0;
}
void *thread_main(void *arg) // 传入的参数是 pthread_create 的第四个
{int i;int cnt = *((int *)arg);for (int i = 0; i < cnt; i++){sleep(1);puts("running thread");}return NULL;
}
线程相关代码在编译时需要添加-lpthread选项声明需要连接线程库,只有这样才可以调用头文件pthread.h中声明的函数。
运行结果:
上述程序的执行如下图所示。其中,虚线代表执行流称,向下的箭头指的是执行流,横向箭头是函数调用。
可以看出,程序在主进程没有结束时,生成的线程每隔一秒输出一次“running thread”,共输出5次,最后输出 main 函数中的“end of main”。
但是如果主进程没有等待十秒,而是直接结束,将主函数中的 sleep(10) 改成 sleep(2)。
这样不会输出 5 次“running thread”,main 函数返回后整个进程将被销毁,无论线程有没有运行完毕,如下图所示。
pthread_join
通过调用 sleep 函数控制线程的执行相当于预测程序的执行流程,但实际上这是不可能完成的事情。而且稍有不慎,很可能干扰程序的正常执行流。例如,怎么可能在上述示例中准确预测 thread_main 函数的运行时间,并让 main 函数恰好等待这么长时间呢?因此,我们不用 sleep 函数,而是通常利用下面的函数控制线程的执行流。
#include <pthread.h>int pthread_join(pthread_t thread, void **status);
参数:
- thread:该参数值 ID 的线程终止后才会从该函数返回
- status:保存线程的 main 函数返回值的指针的变量地址值
成功时返回 0,失败时返回 -1。
调用该函数的进程(或线程)将进入等待状态,直到第一个参数为ID的线程终止为止。另外,还能得到线程的 main 函数返回值。
下面是该函数的用法示例代码:
#include <stdio.h>
// #include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>void *thread_main(void *arg);int main(int argc, char *argv[])
{pthread_t t_id;int thread_param = 5;void *thr_ret;// 请求创建一个线程,从 thread_main 调用开始,在单独的执行流中运行,同时传递参数if (pthread_create(&t_id, NULL, thread_main, (void *)&thread_param) != 0){puts("pthread_create() error");return -1;}// main函数将等待 ID 保存在 t_id 变量中的线程终止if (pthread_join(t_id, &thr_ret) != 0){puts("pthread_join() error");return -1;}printf("Thread return message : %s \n", (char *)thr_ret);free(thr_ret);// system("pause");return 0;
}void *thread_main(void *arg) // 传入的参数是 pthread_create 的第四个
{int i;int cnt = *((int *)arg);char *msg = (char *)malloc(sizeof(char) * 50);strcpy(msg, "Hello, I'm thread~ \n");for (int i = 0; i < cnt; i++){sleep(1);puts("running thread");}return (void *)msg; // 返回值是 thread_main 函数中内部动态分配的内存空间地址值
}
运行结果:
可以看出,线程输出了5次字符串,并且把返回值给了主进程。
下面是该函数的执行流程图:
可在临界区内调用的函数
在同步的程序设计中,临界区指的是一个访问共享资源(如共享设备、共享存储器)的程序片段,而这些共享资源有无法同时被多个线程访问的特性。
稍后将讨论哪些代码可能成为临界区,多个线程同时执行临界区代码时会产生哪些问题等内容。现阶段只需理解临界区的概念即可。根据临界区是否引起问题,函数可分为以下2类:
- 线程安全函数
- 非线程安全函数
线程安全函数被多个线程同时调用也不会发生问题。反之,非线程安全函数被同时调用时会引发问题。但这并非有关于临界区的讨论,线程安全的函数中同样可能存在临界区。只是在线程安全的函数中,同时被多个线程调用时可通过一些措施避免问题。
幸运的是,大多数标准函数都是线程安全函数。操作系统在定义非线程安全函数的同时,提供了具有相同功能的线程安全的函数。
比如,第 8 章的 gethostbyname 函数不是线程安全的:
struct hostent *gethostbyname(const char *hostname);
同时提供线程安全的同一功能的函数:
struct hostent *gethostbyname_r(const char *name,struct hostent *result,char *buffer,int intbuflen,int *h_errnop);
线程安全函数结尾通常是 _r(与 Windows 不同)。但是使用线程安全函数会给程序员带来额外的负担,可以通过以下方法自动将 gethostbyname 函数调用改为 gethostbyname_r 函数调用:声明头文件前定义 _REENTRANT 宏。
无需特意更改源代码,可以在编译的时候指定编译参数定义宏:
gcc -D_REENTRANT mythread.c -o mthread -lpthread
工作(Worker)线程模型
下面的示例是计算从 1 到 10 的和,但并不是通过 main 函数进行运算,而是创建两个线程,其中一个线程计算 1 到 5 的和,另一个线程计算 6 到 10 的和,main 函数只负责输出运算结果。这种方式的线程模型称为「工作线程」。
#include <stdio.h>
// #include <stdlib.h>
#include <pthread.h>void *thread_summation(void *arg);
int sum = 0;int main(int argc, char *argv[])
{pthread_t id_t1, id_t2;int range1[] = {1, 5};int range2[] = {6, 10};pthread_create(&id_t1, NULL, thread_summation, (void *)range1);pthread_create(&id_t2, NULL, thread_summation, (void *)range2);pthread_join(id_t1, NULL);pthread_join(id_t2, NULL);printf("result: %d \n", sum);// system("pause");return 0;
}void *thread_summation(void *arg)
{int start = ((int *)arg)[0];int end = ((int *)arg)[1];while (start <= end){sum += start;start++;}return NULL;
}
该程序的执行流程图:
运行结果:
可以看出计算结果正确,两个线程都用了全局变量 sum,证明了 2 个线程共享保存全局变量的数据区。
但是本例子本身存在临界区相关问题,可以从下面的代码看出,下面的代码和上面的代码相似,只是增加了发生临界区错误的可能性,即使在高配置系统环境下也容易产生的错误。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>#define NUM_THREAD 100void *thread_inc(void *arg);
void *thread_des(void *arg);
long long num = 0;int main(int argc, char *argv[])
{pthread_t thread_id[NUM_THREAD];int i;printf("sizeof long long: %d \n", sizeof(long long));for (i = 0; i < NUM_THREAD; i++){if (i % 2)pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);elsepthread_create(&(thread_id[i]), NULL, thread_des, NULL);}for (i = 0; i < NUM_THREAD; i++)pthread_join(thread_id[i], NULL);printf("result: %lld \n", num);system("pause");return 0;
}void *thread_inc(void *arg)
{int i;for (i = 0; i < 50000000; i++)num += 1;return NULL;
}
void *thread_des(void *arg)
{int i;for (i = 0; i < 50000000; i++)num -= 1;return NULL;
}
运行结果:
理论上来说,上面代码的最后结果应该是 0。但实际上,每次运行的结果都不同,且不是 0。
线程存在的问题和临界区
多个线程访问同一变量的问题
上述代码的问题:2 个线程正在同时访问全局变量 num。
在详细解释问题之前,先了解一个概念———寄存器,它是存放值的地方。当线程从进程中获取值进行运算的时候,线程实际上会将这个值放入自己的寄存器在计算完成后才会重新更新进程内部的数据区。这种机制看似没啥问题,但是如果一但一个线程在将寄存器中的值更新回数据区之前它被操作系统中断掉,转而运行第二个线程,那么此时第二个线程实际上使用的是一个过期的值。
任何内存空间,只要被同时访问,都有可能发生问题。 因此,线程访问变量 num 时应该阻止其他线程访问,直到线程 1 运算完成。这就是同步。
临界区位置
临界区的定义:函数内同时运行多个线程时引发问题的多条语句构成的代码块。
全局变量 num 不能视为临界区,因为它不是引起问题的语句,只是一个内存区域的声明。
临界区通常位于由线程运行的函数内部。
下面是刚才代码的两个 main 函数:
void *thread_inc(void *arg)
{int i;for (i = 0; i < 50000000; i++)num += 1; // 临界区return NULL;
}
void *thread_des(void *arg)
{int i;for (i = 0; i < 50000000; i++)num -= 1; // 临界区return NULL;
}
由上述代码可知,临界区并非 num 本身,而是访问 num 的两条语句,这两条语句可能由多个线程同时运行,也是引起这个问题的直接原因。产生问题的原因可以分为以下三种情况:
- 2 个线程同时执行 thread_inc 函数
- 2 个线程同时执行 thread_des 函数
- 2 个线程分别执行 thread_inc 和 thread_des 函数
比如发生以下情况:线程 1 执行 thread_inc 的 num+=1 语句的同时,线程 2 执行 thread_des 函数的 num-=1 语句。
也就是说,两条不同的语句由不同的线程执行时,也有可能构成临界区。前提是这 2 条语句访问同一内存空间。
线程同步
前面讨论了线程中存在的问题,下面就是解决方法——线程同步。
同步的两面性
线程同步用于解决线程访问顺序引发的问题。需要同步的情况可以从如下两方面考虑:
- 同时访问同一内存空间时发生的情况
- 需要指定访问同一内存空间的线程顺序的情况
之前已解释过前一种情况,因此重点讨论第二种情况。这是「控制线程执行的顺序」的相关内容。假设有 A、B 两个线程,线程 A 负责向指定的内存空间内写入数据,线程 B 负责取走该数据。所以这是有顺序的,不按照顺序就可能发生问题。所以这种也需要进行同步。
互斥量
互斥量是 Mutual Exclusion 的简写,表示不允许多个线程同时访问。互斥量主要用于解决线程同步访问的问题。
互斥量就像是一把锁,把临界区锁起来,不允许多个线程同时访问。下面是互斥量的创建及销毁函数:
#include <pthread.h>int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *attr);int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:
- mutex:创建互斥量时传递保存互斥量的变量地址值,销毁时传递需要销毁的互斥量地址
- attr:传递即将创建的互斥量属性,没有特别需要指定的属性时传递 NULL
成功时返回 0,失败时返回其他值。
从上述函数声明中可以看出,为了创建相当于锁系统的互斥量,需要声明如下 pthread_mutex_t 型变量:
pthread_mutex_t mutex;
该变量的地址值传递给 pthread_mutex_init 函数,用来保存操作系统创建的互斥量(锁系统)。调用 pthread_mutex_destroy 函数时同样需要该信息。如果不需要配置特殊的互斥量属性,则向第二个参数传递 NULL 时,可以利用 PTHREAD_MUTEX_INITIALIZER 宏进行如下声明:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
但推荐尽可能的使用 pthread_mutex_init 函数进行初始化,因为通过宏进行初始化时很难发现发生的错误。
#include <pthread.h>int pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex);
成功时返回 0 ,失败时返回其他值。
进入临界区前调用的函数就是 pthread_mutex_lock 。调用该函数时,发现有其他线程已经进入临界区,pthread_mutex_lock 函数不会返回,直到里面的线程调用 pthread_mutex_unlock 函数退出临界区位置。也就是说,其他线程让出临界区之前,当前线程一直处于阻塞状态。
接下来整理一下代码的编写方式。创建好互斥量的前提下,可以通过如下结构保护临界区:
pthread_mutex_lock(&mutex);
// 临界区开始
// ...
// 临界区结束
pthread_mutex_unlock(&mutex);
简言之,就是利用 lock 和 unlock 函数围住临界区的两端。此时互斥量相当于一把锁,阻止多个线程同时访问,还有一点要注意,线程退出临界区时,如果忘了调用 pthread_mutex_unlock 函数,那么其他为了进入临界区而调用 pthread_mutex_lock 的函数无法摆脱阻塞状态。这种情况称为「死锁」,需要格外注意。
下面利用互斥量解决之前示例中遇到的问题:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>#define NUM_THREAD 100void *thread_inc(void *arg);
void *thread_des(void *arg);long long num = 0;
pthread_mutex_t mutex; // 保存互斥量读取值的变量int main(int argc, char *argv[])
{pthread_t thread_id[NUM_THREAD];int i;pthread_mutex_init(&mutex, NULL); // 创建互斥量for (i = 0; i < NUM_THREAD; i++){if (i % 2)pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);elsepthread_create(&(thread_id[i]), NULL, thread_des, NULL);}for (i = 0; i < NUM_THREAD; i++)pthread_join(thread_id[i], NULL);printf("result: %lld \n", num);pthread_mutex_destroy(&mutex); // 销毁互斥量system("pause");return 0;
}void *thread_inc(void *arg)
{int i;pthread_mutex_lock(&mutex); // 上锁for (i = 0; i < 50000000; i++)num += 1; // 临界区pthread_mutex_unlock(&mutex); // 解锁return NULL;
}
void *thread_des(void *arg)
{int i;for (i = 0; i < 50000000; i++){pthread_mutex_lock(&mutex); // 上锁num -= 1; // 临界区pthread_mutex_unlock(&mutex); // 解锁}return NULL;
}
运行结果:
thread_inc 函数中临界区的划分范围比较大,这是考虑到如下优点所做的决定:最大限度减少互斥量lock、unlock函数的调用次数。
信号量
销毁线程的 3 种方法
多线程并发服务器端的实现
习题
(1)单 CPU 系统中如何同时执行多个进程?请解释该过程中发生的上下文切换。
(2)为何线程的上下文切换速度相对更快?线程间数据交换为何不需要类似 IPC 的特别技术?
(3)请从执行流角度说明进程和线程的区别。
(6)请说明完全销毁 Linux 线程的 2 种办法。
相关文章:
《TCP/IP网络编程》学习笔记 | Chapter 18:多线程服务器端的实现
《TCP/IP网络编程》学习笔记 | Chapter 18:多线程服务器端的实现 《TCP/IP网络编程》学习笔记 | Chapter 18:多线程服务器端的实现线程的概念引入线程的背景线程与进程的区别 线程创建与运行pthread_createpthread_join可在临界区内调用的函数工作&#…...
Unity AI 技术浅析(三):智能代理(Agents)
Unity AI的智能代理(Agents)技术是实现游戏和虚拟现实应用中非玩家角色(NPC)、敌人、盟友等智能行为的核心。通过智能代理,开发者可以为虚拟角色赋予感知、决策和行动的能力,使其能够与环境和其他角色进行复杂的交互。 一、智能代理的基本原理 智能代理是能够在特定环境…...
加油站~~
求最少加油次数就是一个贪心问题,这里也不需要证明,根据我们的常识也知道就是走到油不够的时候就加油就好了,这里的no solution这有在两个加油站之间的距离大于了加满油后的行驶距离才会存在,其他情况都是可以计算的。代码有很多细…...
【商城实战(24)】商城性能大揭秘:压力测试与性能监控实战
【商城实战】专栏重磅来袭!这是一份专为开发者与电商从业者打造的超详细指南。从项目基础搭建,运用 uniapp、Element Plus、SpringBoot 搭建商城框架,到用户、商品、订单等核心模块开发,再到性能优化、安全加固、多端适配…...
Linly-Talker:开源数字人框架的技术解析与影响
一、引言:AI 数字人的发展趋势 近年来,数字人(Digital Human) 技术迅速发展,从最早的 2D 虚拟主播,到如今能够进行实时交互的 3D 智能助手,AI 在多模态交互领域的应用愈发广泛。各大互联网公司…...
【Nexus】Maven 私服搭建以及上传自己的Jar包
Nexus 安装 docker run -d -uroot --name nexus3 --restartalways -p 8081:8081 -v /data/nexus-data/blobs:/nexus-data/blobs -v /etc/localtime:/etc/localtime sonatype/nexus3这里也提供一下docker-composer的方法 .env 文件 VERSIONlatest CONTAINER_NAMECONTAINER_N…...
wlwrap 与 rlwrap 的区别对比:图形显示协议的演变
在 Linux 系统中,许多工具和程序依赖于命令行界面(CLI)来进行交互,尤其是对于那些没有图形用户界面的应用程序。在这种情况下,命令行编辑、历史记录和自动补全等功能是提升工作效率和用户体验的关键。rlwrap 和 wlwrap…...
矩阵交换行(信息学奥赛一本通-1119)
【题目描述】 给定一个55的矩阵(数学上,一个rc的矩阵是一个由r行c列元素排列成的矩形阵列),将第n行和第m行交换,输出交换后的结果。 【输入】 输入共6行,前5行为矩阵的每一行元素,元素与元素之间以一个空格分开。 第6行包含两个整…...
CP210x 驱动秘籍:打通 Windows 11 端口的任督二脉
前言 江湖上,众多电脑侠客在安装 Windows 11 时,遭遇“端口失踪案”。原来,这是缺少 CP210x 驱动的“内功心法”。今日,贫道便传授一套独门秘籍,助你打通端口“任督二脉”,让数据流畅如江湖中轻功高手。 …...
IDEA集成git,项目的克隆,远程仓库中文件的添加删除
目录 一、克隆项目 二、使用IDEA完成文件的上传和删除 1.配置git 2.上传 3.删除(通过git bash) 一、克隆项目 点击克隆,复制url ,如下 打开你想要克隆到哪里,右击,选择 open Git Bash here 这一步之后…...
分治构造格雷码
题目描述 格雷码是一种二进制编码方式,其特性是任意两个相邻的码只有一位二进制位不同。给定一个整数 n,表示格雷码的位数,请生成所有 n 位格雷码,并按照格雷码的标准顺序输出。 输入格式 输出样例输入包含一个整数 n,…...
区跨链知识和概念
1、以太坊 Geth 源码解析 Geth(Go Ethereum)是以太坊官方提供的 Go 语言实现的客户端,广泛用于以太坊全节点运行、挖矿、DApp 开发等。理解 Geth 的源码有助于掌握以太坊区块链底层逻辑,如区块同步、EVM 执行、P2P 交互等。 2、…...
C# net deepseek RAG AI开发 全流程 介绍
deepseek本地部署教程及net开发对接 步骤详解:安装教程及net开发对接全流程介绍 DeepSeekRAG 中的 RAG,全称是 Retrieval-Augmented Generation(检索增强生成),是一种结合外部知识库检索与大模型生成能力的技术架构。其…...
REST 请求返回 Invalid Credentials
REST 请求返回 “Invalid Credentials”(无效凭据),通常表示身份验证失败。可能的原因和解决方案如下: 可能的原因 & 解决方案 用户名或密码错误 确保使用正确的用户名和密码。如果 API 需要 Base64 编码的 Authorization 头…...
方案推介:206页WORD版ERP系统软件投标书整体解决方案
(推介资料包含于绑定资源内) 该文档是一份 ERP 系统软件投标书,围绕ERP 系统展开,全面阐述了其为机械加工企业提供的整体解决方案、应用价值、行业成功案例及标准功能,旨在助力企业实现信息化管理升级 。 ERP 软件系统…...
WebSocket生命周期和vue中使用
ing。。。晚点更新 进入页面,生命周期挂载后,window监听ws连接online 正常情况,心跳包检测避免断开 非正常情况,ws.onclose断开, 判断1000状态吗,触发重连函数。 定时器,重连,判断…...
RabbitMQ消息持久化与Lazy模式对比分析
RabbitMQ消息持久化与Lazy模式对比分析 在RabbitMQ中,消息持久化与Lazy模式是两种不同的机制,分别针对消息可靠性、存储优化等不同维度设计。以下从六个层面进行深度对比: 一、核心目标与作用对象差异 维度消息持久化(delivery_…...
Springboot中的异常处理
ControllerAdvice虽然只是在处理Controller注解的类,在Service层抛出的异常如果没有在Service层被处理的话,会向上抛出到到Controller层,再被异常处理器捕获 1. 全局异常处理 ControllerAdvice:全局处理器,处理有Con…...
深入解析Go语言Channel:源码剖析与并发读写机制
文章目录 Channel的内部结构Channel的创建过程有缓冲Channel的并发读写机制同时读写的可能性发送操作的实现接收操作的实现 并发读写的核心机制解析互斥锁保护环形缓冲区等待队列直接传递优化Goroutine调度 实例分析:有缓冲Channel的并发读写性能优化与最佳实践缓冲…...
C++中虚析构函数的作用是什么?为什么基类需要虚析构函数?
C中虚析构函数的作用是什么?为什么基类需要虚析构函数? 在C中,虚析构函数(virtual destructor)的作用是确保在通过基类指针或引用删除派生类对象时,能够正确调用派生类的析构函数,从而避免资源…...
ClickHouse合并任务与查询延迟专项测试
ClickHouse合并任务与查询延迟专项测试 1. 测试目的 验证周期性高延迟(~900ms)是否由后台合并任务(Merge)引起。 2. 测试环境 组件配置ClickHouse版本24.8.3.13服务器硬件8核CPU / 32GB内存 / NVMe SSD测试表log_test 3. 测试…...
3.14学习总结
今天完成了几道关于二叉树的算法题 关于二叉树的最小最大深度和数据流中的第k大元素,用到优先队列,学习了有关java的基础知识,学习了双指针法。...
OpenHarmony自定义子系统、部件与模块
如图所示,OpenHarmony系统源码中,大体上按照不同种类的功能分成多个子系统,然后一个子系统内部进一步在同类功能上的差异性划分成一个或多个部件,也就是说一个部件表示一个具体功能的源码集合。最后一个部件的源码再划分成一个或多…...
PPT 相关资料介绍
文章目录 一、iSlide 工具二、免费 PPT 模板下载三、Kimi 一键生成 PPT 一、iSlide 工具 iSlide 官网 二、免费 PPT 模板下载 7个完全免费的PPT模板下载网站 优品PPT 第一PPT 三、Kimi 一键生成 PPT ☆...
C# 发送邮件 报错:此请求已被阻止,因为当用在 GET 请求中时,会将敏感信息透漏给第三方网站。
C# 发送邮件 报错:此请求已被阻止,因为当用在 GET 请求中时,会将敏感信息透漏给第三方网站。 报错信息分析 当你遇到如下报错时: 此请求已被阻止,因为当用在 GET 请求中时,会将敏感信息透漏给第三方网站。…...
大数据-spark3.5安装部署之standalone模式
真实工作中还是要将应用提交到集群中去执行,Standalone模式就是使用Spark自身节点运行的集群模式,体现了经典的master-slave模式。集群共三台机器,具体如下 u22server4spark: master worker u22server4spark2: worke…...
接口自动化入门 —— Jmeter实现在接口工具中关联接口处理方案
1. JMeter 接口关联处理的核心概念 接口关联是指在多个接口请求之间共享数据,例如将一个接口的返回值作为另一个接口的输入参数。常见的场景包括: 使用登录接口返回的 Token 作为后续接口的认证信息。 将一个接口返回的 ID 作为另一个接口的请求参数。…...
WebForms HTML:深入理解与高效运用
WebForms HTML:深入理解与高效运用 引言 随着互联网技术的飞速发展,WebForms HTML作为Web开发中的一种重要技术,已经成为了许多开发者日常工作中不可或缺的一部分。本文将深入探讨WebForms HTML的原理、应用场景以及高效运用技巧࿰…...
VSCode 搭建C++编程环境 2025新版图文安装教程(100%搭建成功,VSCode安装+C++环境搭建+运行测试+背景图设置)
名人说:博观而约取,厚积而薄发。——苏轼《稼说送张琥》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 目录 一、VScode下载及安装二、安装 MinGW-w64 工具链三、Windows环境变量配置四、检查 M…...
【Linux 内核 | 操作系统 | 内核编译】内核编译中与锁调试相关的设置有哪一些?内核 Debug 选项中 LockDep 和其他锁调试选项详解
问题描述: 我在看内核锁调试的信息时,看到了一些内核编译参数相关的设置,开启这些信息可以帮助我们在测试环境中调试锁的竞争情况,可以详细的打印出来一些线程持有锁,一些线程争抢锁的信息。 以下是我的配置&#…...
LinuX---Shell---变量
系统预定义变量 常用系统变量 PATH、HOME、PWD、SHELL、USER等 获取变量的值 语法:$变量名 $和变量名之间不能有空格。 案例实操 查看系统变量的值 fengubuntu:~$ echo $PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/u…...
深入理解Spring MVC:构建灵活的Web应用
大家好!今天我们来聊聊Spring框架中的一个重要模块——Spring MVC。Spring MVC是一个基于MVC(Model-View-Controller)架构的Web框架,它提供了强大的功能来处理HTTP请求、生成动态内容以及管理Web应用程序的流程。无论是构建RESTfu…...
详解SQL数据查询功能
数据查询 一、 单表查询1. 选择表中的若干列2. 选择表中的若干元组3. ORDER BY 子句4. 聚合函数5. GROUP BY 子句6. LIMIT 子句综合示例: 二、 多表查询1. 等值连接查询 (Equi-Join)2. 非等值连接查询 (Non-Equi Join)3. 自然连接查询 (Natural Join)4. 复合条件连接…...
ArcGIS Pro 车牌分区数据处理与地图制作全攻略
在大数据时代,地理信息系统(GIS)技术在各个领域都有着广泛的应用,而 ArcGIS Pro 作为一款功能强大的 GIS 软件,为数据处理和地图制作提供了丰富的工具和便捷的操作流程。 车牌数据作为一种重要的地理空间数据…...
Git 本地常见快捷操作
Git 本地常见快捷操作 📌 1. 基本操作 操作命令初始化 Git 仓库git init查看 Git 状态git status添加所有文件到暂存区git add .添加指定文件git add <file>提交更改git commit -m "提交信息"修改最后一次提交信息git commit --amend -m "新…...
大视频背景暗黑风格的wordpress企业主题免费下载
整体风格是黑色的,首页首屏大视频背景,动态效果非常好。向下滚动时,滚动的特效也不错。 原文 https://www.bixugao.com/wp/26.html...
Apache Tomcat漏洞,对其进行升级
我们付出一些成本,时间的或者其他,最终总能收获一些什么。 升级背景: 近日,新华三盾山实验室监测到 Apache 官方修复了一个远程代码执行漏洞 (CVE-2025-24813) ,其CVSS3 漏洞评分为 7.5 。 影响范围 9.0.0.M1 ≤…...
fs的proxy_media模式失效
概述 freeswitch是一款简单好用的VOIP开源软交换平台。 在fs的使用过程中,某些场景只需要对rtp媒体做透传,又不需要任何处理。 在fs1.6的版本中,我们可以使用proxy_media来代理媒体的转发,媒体的协商由AB路端对端处理ÿ…...
Spring Boot与Apache Ignite集成:构建高性能分布式缓存和计算平台
1. 前言 1.1 什么是Apache Ignite Apache Ignite是一个高性能的分布式内存计算平台,支持内存缓存、分布式计算、流处理和机器学习等功能。它提供了低延迟的数据访问和强大的计算能力,适用于需要高性能和可扩展性的应用。 1.2 为什么选择Apache Ignite 高性能:Ignite利用内…...
深度学习优化-Gradient Checkpointing
数学原理参考: 梯度检查点技术(Gradient Checkpointing)详细介绍:中英双语-CSDN博客 视频讲解参考: 用梯度检查点来节省显存 gradient checkpointing_哔哩哔哩_bilibili Gradient Checkpointing(梯度检查…...
Linux内核实时机制19 - RT调度器3 - 实时任务出入队
Linux内核实时机制19 - RT调度器3 - 实时任务出入队 1、enqueue_task_rt和dequeue_task_rt都会调用dequeue_rt_stack接口, 当请求的rt_se对应的是任务组时,会从顶部到请求的rt_se将调度实体出列。 2、任务添加到rt运行队列时, 如果存在多个…...
CRM企业客户关系管理系统产品原型方案
客户关系管理系统(CRM)是企业产品应用中的典范,旨在通过信息技术和互联网技术提升企业核心竞争力,优化企业与顾客在销售、营销和服务方面的互动。本作品提供了一套通用型的CRM系统原型模板,涵盖数据管理、审批流程、统…...
HashMap ,HashTable , ConcurrentHashMap 面试
双列集合 HashMap 线程不安全的 HashMap 允许键和值为 null。不过要留意,HashMap 并非线程安全的,在多线程环境下使用可能会出现问题。 数组链表红黑树 jdk1.8 双列集合 存储keyvalue 底层数组的形式存在,初始值 为16 也可以在new HashMap…...
PyTorch 系列教程:探索自然语言处理应用
本文旨在介绍如何使用PyTorch进行自然语言处理(NLP)的基础知识,包括必要的库、概念以及实际代码示例。通过阅读本文,您将能够开始您的NLP之旅。 1. 理解PyTorch PyTorch是一个开源的机器学习库,基于Torch库࿰…...
【操作系统安全】任务2:用户与用户组
目录 一、用户与用户组介绍 1.1 用户 1.2 用户组 1.3 用户与用户组的关系 二、用户与用户组管理 2.1 用户管理 2.1.1 创建用户 2.1.2 设置用户密码 2.1.3 删除用户 2.2 用户组管理 2.2.1 创建用户组 2.2.2 删除用户组 2.2.3 将用户添加到用户组 三、影子账户创建…...
DeepSeek技术解析:MoE架构实现与代码实战
以下是一篇结合DeepSeek技术解析与代码示例的技术文章,重点展示其核心算法实现与落地应用: DeepSeek技术解析:MoE架构实现与代码实战 作为中国AI领域的创新代表,DeepSeek在混合专家模型(Mixture of Experts, MoE&…...
LLM对齐方法作用:主要解决大型语言模型(LLMs)输出与人类价值观、需求和安全规范不一致的问题
LLM对齐方法作用:主要解决大型语言模型(LLMs)输出与人类价值观、需求和安全规范不一致的问题 对齐方法(Alignment Methods) 主要解决大型语言模型(LLMs)输出与人类价值观、需求和安全规范不一致的问题。其核心目标是让模型生成的内容更符合人类预期,同时确保伦理合规性…...
【SpringMVC】常用注解:@RequestBody
1.作用 用于获取请求实体内容,直接使用得到的是keyvalue&keyvalue的数据。获取请求实体内容不适用get请求。 2.属性 required 描述是否有请求体,默认值为true。当取值为true时,get 请求方式会报错。如果取值为false,get请…...
brpc中的doublyBufferedData解析
double buffer解析(附brpc改进版) 双buffer是一个工程中常见的解决读写问题的结构。指的是读只读buff,写操作发生在写buff上。当写buff写完之后switch两个buff,然后写进程擦除原来的读数据,更新为最新的数据。 我们有…...
基于Android的记事本APP设计与实现:从需求分析到功能实现(超级简单记事本,附源码+文档报告)
基于Android的记事本APP设计与实现:从需求分析到功能实现 (以前大学课堂作业,抄在这里当个回忆吧) 引言 随着社会的不断进步,信息化建设不断发展,电子文字输入在生活、学习、工作中占有越来越重要的作用…...