Linux系统编程系列之线程池
Linux系统编程系列(16篇管饱,吃货都投降了!)
1、Linux系统编程系列之进程基础
2、Linux系统编程系列之进程间通信(IPC)-信号
3、Linux系统编程系列之进程间通信(IPC)-管道
4、Linux系统编程系列之进程间通信-IPC对象
5、Linux系统编程系列之进程间通信-消息队列
6、Linux系统编程系列之进程间通信-共享内存
7、Linux系统编程系列之进程间通信-信号量组
8、Linux系统编程系列之守护进程
9、Linux系统编程系列之线程
10、Linux系统编程系列之线程属性
11、Linux系统编程系列之互斥锁和读写锁
12、Linux系统编程系列之线程的信号处理
13、Linux系统编程系列之POSIX信号量
14、Linux系统编程系列之条件变量
15、Linux系统编程系列之死锁
16、 Linux系统编程系列之线程池
一、什么是线程池
线程池就是将许多线程,放置在一个池子中(实际就是一个结构体),只要有任务,就将任务投入池中,这些线程们通过某些机制,及时处理这些任务,一旦处理完后又重新回到池子中重新接收新任务。为了便于管理,线程池还应当提供诸如初始化线程池,增删线程数量,检测未完成任务的数据,检测正在执行任务的线程的数量、销毁线程池等等基本操作。
二、特性
1、更好的满足并发性需求
2、更加节省系统资源,降低负载
3、避免每次需要执行任务时都创建新的线程
4、可以限制并发线程数量,帮助控制应用程序的性能和稳定性
核心思想:通过精巧的设计使得池子中的线程数量可以动态地发生变化,让线程既可以应对并发性需求,又不会浪费系统资源
三、使用场景
用来应对某种场景,要在程序中创建大量线程,并且这些线程的数量和生命周期均不确定,可能方生方死,也可能常驻内存,请看下面举例。
1、处理大量密集型任务
线程池可以处理多个任务,从而减少了创建和销毁线程的开销,提高了系统性能
2、服务端应用程序
线程池可以帮助服务端应用程序同时处理多个客户端请求
3、I/O 密集型应用程序
线程池可以处理 I/O 操作,允许系统使用空闲时间进行其他任务处理
4、Web 应用程序
线程池可以处理来自 Web 客户端的请求,从而提高了 Web 应用程序的性能
总之,任何需要处理大量任务或需要同时处理多个请求的应用程序都可以使用线程池来提高性能和效率。
四、设计思想
设计某个东西一定要有个目标,这里是要求设计一个线程池,首先要明白线程池的作用,然后根据其作用来展开设计。线程池是通过让线程的数量进行动态的变化来及时处理接受的任务,所以核心就是线程和任务。可以将问题转换为如何组织线程和组织任务?借鉴别人的一幅图
1、如何组织线程
从上图可以看出,线程被创建出来之后,都处于睡眠态,它们实际上是进入了条件变量的等待队列中,而任务都被放入一个链表,被互斥锁保护起来
线程的生命周期如下:(这里省略程序执行的流程图)
(1)、线程被创建
(2)、预防死锁,准备好退出处理函数,防止在持有一把锁的状态中死去
(3)、尝试持有互斥锁(等待任务)
(4)、判断是否有任务,若无则进入条件变量等待队列睡眠,若有则进入第(5)步
(5)、从任务链表中取得一个任务
(6)、释放互斥锁
(7)、弹出退出处理函数,避免占用内存
(8)、执行任务
(9)、执行任务完成后重新回到第(2)步
线程的生命周期其实就是线程要做的事情,把上面的第(2)步到第(8)步写成一个线程的例程函数。
2、如何组织任务
所谓的任务就是一个函数,回想一下创建一条线程时,是不是要指定一个线程例程函数。这里的线程池做法是将函数(准确的说是函数指针)及其参数存入一个任务节点,并将节点链接成一个链表。所以现在的问题变成如何设计一个任务链表?
一个链表的操作有:创建节点,增加节点,删除节点,查询节点这几步。由于这里是任务节点,就不需要查询了,当然也可以查询。所以现在的问题转换为任务节点如何设计?
任务节点的数据域主要有函数指针和函数参数,指针域就是一个简单任务结点指针,也可以是双向循环链表。
设计分析到这里,基本上有了框架了,前面买了这么多关子,下面就来个重头戏。其实前面主要是想描述一种设计思想,遇到新的东西如何设计它?首先要明白设计的目的和作用,然后找出核心点,最后就是把核心点进行不断的分析推理,一步步转换为最基本的问题。学过项目管理的同学应该听过在范围管理里有个叫工作分解结构的东西,这里跟那个差不多。
五、案例
实现一个线程池,并完成线程池的基本操作演示
thread_pool.h(线程池头文件)
#ifndef _THREAD_POOL_H #define _THREAD_POOL_H#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <pthread.h>#define MAX_WAITING_TASKS 1000 // 最大的等待任务数量 #define MAX_ACTIVE_THREADS 20 // 最多的活跃线程数量// 任务节点 typedef struct task {void *(*do_task) (void *arg); // 函数指针void *arg; // 函数参数struct task *next; // 指向下一个任务节点 }task;// 线程池的管理节点 typedef struct thread_pool {pthread_mutex_t task_list_lock; // 任务列表互斥锁,保证同一个时间只有一条线程进行访问pthread_cond_t task_list_cond; // 任务列表条件变量,保证没有任务时,线程进入睡眠task *task_list; // 任务列表pthread_t *tids; // 存放线程号的数组int shutdown; // 线程池销毁的开关unsigned int max_waiting_tasks; // 最大等待的任务数量unsigned int waiting_tasks; // 正在等待被处理的任务数量unsigned int active_threads; // 正在活跃的线程数}thread_pool;// 初始化线程池 int init_pool(thread_pool *pool, unsigned int threads_number);// 往线程池中添加任务 int add_task(thread_pool *pool, void *(*do_task)(void *arg), void *arg);// 往线程池中添加线程 int add_thread(thread_pool *pool, unsigned int additional_threads);// 从线程池中删除线程 int remove_thread(thread_pool *pool, unsigned int removing_threads);// 查询线程池中线程数量 int total_thread(thread_pool *pool);// 销毁线程池 int destroy_pool(thread_pool *pool);// 线程的例程函数 void *routine(void *arg);#endif // _THREAD_POOL_H
thread_pool.c(线程池实现文件)
#include "thread_pool.h"// 初始化线程池 int init_pool(thread_pool *pool, unsigned int threads_number) {if(threads_number < 1){printf("warning: threads_number must bigger than 0\n");return -1;}if(threads_number > MAX_ACTIVE_THREADS){printf("warning: threads number is bigger than MAX_ACTIVE_THREADS(%u)\n", MAX_ACTIVE_THREADS);return -1;}pthread_mutex_init(&pool->task_list_lock, NULL); // 初始化互斥锁pthread_cond_init(&pool->task_list_cond, NULL); // 初始化条件变量pool->task_list = calloc(1, sizeof(task)); // 申请一个任务头节点if(!pool->task_list){printf("error: task_list calloc fail\n");return -1;}pool->task_list->next = NULL; // 让任务头节点指向NULLpool->tids = calloc(MAX_ACTIVE_THREADS, sizeof(pthread_t)); // 申请堆数组用来存放线程号if(!pool->tids){printf("error: tids calloc fail\n");return -1;}pool->shutdown = 0; // 关闭线程池销毁开发pool->waiting_tasks = 0; // 当前等待任务为0pool->max_waiting_tasks = MAX_WAITING_TASKS; // 初始化最大等待任务数量pool->active_threads = threads_number; // 初始化活跃的线程数// 根据活跃的线程数,创建对应数量的线程for(int i = 0; i < pool->active_threads; i++){// 需要判断线程是否创建失败// 线程号用线程号数组,线程属性设置为默认的,例程函数是routine,函数参数是线程池errno = pthread_create(&pool->tids[i], NULL, routine,(void*)pool);if(errno != 0){perror("error: pthread_create fail");return -1;}}return 1; }// 往线程池中添加任务 int add_task(thread_pool *pool, void *(*do_task)(void *arg), void *arg) {if(!pool){printf("warning: thread_pool is null\n");return -1;}// 如果当前等待执行的任务数超过最大等待的任务数量,则退出if(pool->waiting_tasks >= pool->max_waiting_tasks){printf("warning: task_list is full, too many list\n");return -1;}// 尝试给新的任务节点申请空间task *new_task = (task*)malloc(sizeof(task));if(!new_task){printf("error: task malloc fail\n");return -1;}// 初始化任务节点new_task->do_task = do_task;new_task->arg = arg;new_task->next = NULL;// 把任务节点添加到任务列表最后面// 1、先找到任务列表末尾task *tmp = NULL;for(tmp = pool->task_list; tmp->next; tmp = tmp->next);// 2、上锁pthread_mutex_lock(&pool->task_list_lock);// 3、添加新任务到任务列表末尾,同时当前等待任务数+1tmp->next = new_task;pool->waiting_tasks++;// 4、解锁pthread_mutex_unlock(&pool->task_list_lock);// 唤醒一个正在条件变量中睡眠等待的线程,取执行任务pthread_cond_signal(&pool->task_list_cond);return 1; }// 往线程池中添加线程,返回实际添加的线程数量 int add_thread(thread_pool *pool, unsigned int additional_threads) {if(!pool){printf("warning: thread_pool is null\n");return -1;}if(additional_threads == 0){return 0;}// 期望活跃的线程数 = 目前活跃的线程数 + 期望添加的线程数unsigned int total_threads = pool->active_threads + additional_threads;// 如果超过最大的活跃线程数就打印警告if(total_threads > MAX_ACTIVE_THREADS){printf("warning: add too many threads\n");}int actual_add_threads = 0;for(int i = pool->active_threads; i < total_threads && i < MAX_ACTIVE_THREADS; i++){// 需要判断线程是否创建失败// 线程号用线程号数组,线程属性设置为默认的,例程函数是routine,函数参数是线程池errno = pthread_create(&pool->tids[i], NULL, routine,(void*)pool);if(errno != 0){perror("error: pthread_create fail");// 如果一个都没有创建成功,就直接返回if(actual_add_threads == 0){return -1;}// 如果成功创建了多个线程,但是本次创建失败,就跳出循环break;}else{actual_add_threads++; // 线程创建成功就+1}}// 更新线程池中活跃的线程数pool->active_threads += actual_add_threads;return actual_add_threads; // 返回实际添加的线程 }// 从线程池中删除线程,返回实际删除线程的数量 int remove_thread(thread_pool *pool, unsigned int removing_threads) {if(!pool){printf("warning: thread_pool is null\n");return -1;}if(removing_threads == 0){return pool->active_threads;}// 如果要删除的线程数大于线程池中活跃的线程数,则打印警告if(removing_threads >= pool->active_threads){printf("warning: remove too many threads\n");}// 剩余数量 = 活跃数量 - 删除的目标数 int remaining_threads = pool->active_threads - removing_threads;// 目的是为了让线程池中最少保留有一个线程可以用于执行任务remaining_threads = remaining_threads > 0 ? remaining_threads : 1;int actual_remove_threads = 0;;// 循环取消线程直到等于期望线程数for(int i = pool->active_threads-1; i > remaining_threads-1; i--){errno = pthread_cancel(pool->tids[i]);if(errno != 0){printf("[%ld] cancel error: %s\n", pool->tids[i], strerror(errno));break;}else{actual_remove_threads++;}}// 更新线程池中活跃的线程数量pool->active_threads -= actual_remove_threads;return actual_remove_threads; // 返回实际删除的线程 }// 查询线程池中线程数量 int total_thread(thread_pool *pool) {return pool->active_threads; }// 销毁线程池 int destroy_pool(thread_pool *pool) {if(!pool){printf("warning: thread_pool is null\n");return -1;}pool->shutdown = 1; // 启动线程池销毁开关pthread_cond_broadcast(&pool->task_list_cond); // 唤醒所有在线程池中的线程// 循环等待所有线程退出for(int i = 0; i < pool->active_threads; i++){errno = pthread_join(pool->tids[i], NULL);if(errno != 0){printf("join tids[%d] error: %s\n", i, strerror(errno));}else{printf("[%ld] is joined\n", pool->tids[i]);}}// 头删法,从头一个个的删除任务节点for(task *p = pool->task_list->next; p; p = pool->task_list){pool->task_list->next = p->next; // 修改首元节点free(p);}free(pool->task_list);free(pool->tids);free(pool);return 1; }// 线程的取消函数 void pthread_cancel_handler(void *arg) {printf("[%ld] is cancel\n", pthread_self());pthread_mutex_unlock((pthread_mutex_t*)arg); // 解锁 }// 线程的例程函数 void *routine(void *arg) {task *task_p; // 任务指针,用来执行将要执行的任务thread_pool *pool = (thread_pool*)arg;while(1){pthread_cleanup_push(pthread_cancel_handler, (void*)&pool->task_list_lock);// 尝试持有互斥锁pthread_mutex_lock(&pool->task_list_lock);// 判断是否有任务,没有则进入睡眠// 1、没有任务,线程池销毁开关断开,则进入条件变量等待队列while(!pool->waiting_tasks && !pool->shutdown){// 当条件不满足时,会先自动解锁pool->lock,然后等待到条件满足后,会自动上锁pool->lockpthread_cond_wait(&pool->task_list_cond, &pool->task_list_lock);}// 2、没有任务,线程池销毁开发闭合,则先解锁,然后退出if(!pool->waiting_tasks && pool->shutdown){pthread_mutex_unlock(&pool->task_list_lock); // 需要解锁pthread_exit(NULL);}// 3、有任务,线程池销毁开关断开,则取一个任务task_p = pool->task_list->next;pool->task_list->next = task_p->next; // 弹出第一任务节点 pool->waiting_tasks--; // 当前等待的任务数量-1,pthread_mutex_unlock(&pool->task_list_lock); // 解锁// 弹出压入的线程取消函数,运行到这里不执行,但是当线程在前面被意外取消或中断会执行pthread_cleanup_pop(0); // 为了防止死锁,执行任务期间不接受线程取消请求pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);(task_p->do_task)(task_p->arg); // 通过函数指针的方式执行任务// 执行完任务后,接受线程取消请求pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);free(task_p); // 删除任务节点}pthread_exit(NULL); }
main.c(线程池操作演示文件)
// 线程池的测试案例#include <stdio.h> #include <unistd.h> #include "thread_pool.h"void *mytask(void *arg) { int n = rand()%10;printf("[%ld][%s] ==> job will be done in %d sec...\n", pthread_self(), __FUNCTION__, n);sleep(n);printf("[%ld][%s] ==> job done!\n", pthread_self(), __FUNCTION__); }void *func(void *arg ) {printf("this is a test by [%s]\n", (char*)arg);sleep(1);printf("test finish...\n"); }void *count_time(void *arg) {int i = 0;while(1){sleep(1);printf("sec: %d\n", ++i);} }int main(void) {pthread_t a;pthread_create(&a, NULL, count_time, NULL);// 1、初始化线程池thread_pool *pool = malloc(sizeof(thread_pool));init_pool(pool, 2);// 2、投放任务printf("throwing 3 tasks...\n");add_task(pool, mytask, NULL);add_task(pool, mytask, NULL);add_task(pool, mytask, NULL);// 3、查看当前线程池中的线程数量printf("current thread number: %d\n", total_thread(pool));sleep(9);// 4、再次投放任务printf("throwing another 2 tasks...\n");add_task(pool, mytask, NULL);add_task(pool, mytask, NULL);add_task(pool, func, (void *)"Great Macro");// 5、添加2条线程printf("try add 2 threads, actual add %d\n", add_thread(pool, 2));printf("current thread number: %d\n", total_thread(pool));sleep(5);// 6、删除3条线程printf("try remove 3 threads, actual remove: %d\n", remove_thread(pool, 3));printf("current thread number: %d\n", total_thread(pool));sleep(5);// 7、 销毁线程池printf("destroy thread pool\n");destroy_pool(pool);return 0; }
注:编译时,把线程池文件和测试文件放在同一个工作目录下。
实际使用时,只需要把thread_pool.h和thread_pool.c拷贝到自己的工程目录下,然后根据规则操作线程池。
六、总结
线程池是许多线程的集合,它不是线程组。线程池适用于任何需要处理大量任务或需要同时处理多个请求的应用程序的场景,可以提供性能和效率。
至此,Linux系统编程系列,16篇完结撒花,历时5天,这年中秋国庆没有假放!!!
相关文章:
Linux系统编程系列之线程池
Linux系统编程系列(16篇管饱,吃货都投降了!) 1、Linux系统编程系列之进程基础 2、Linux系统编程系列之进程间通信(IPC)-信号 3、Linux系统编程系列之进程间通信(IPC)-管道 4、Linux系统编程系列之进程间通信-IPC对象 5、Linux系统…...
Linux CentOS7 vim多文件与多窗口操作
窗口是可视化的分割区域。Windows中窗口的概念与linux中基本相同。连接xshell就是在Windows中新建一个窗口。而vim打开一个文件默认创建一个窗口。同时,Vim打开一个文件也就会建立一个缓冲区,打开多个文件就会创建多个缓冲区。 本文讨论vim中打开多个文…...
SPI 通信协议
1. SPI通信 1. 什么是SPI通信协议 2. SPI的通信过程 在一开始会先把发送缓冲器的数据(8位)。一次性放到移位寄存器里。 移位寄存器会一位一位发送出去。但是要先放到锁存器里。然后从机来读取。从机的过程也一样。当移位寄存器的数据全部发送完。其实…...
【图像处理】使用各向异性滤波器和分割图像处理从MRI图像检测脑肿瘤(Matlab代码实现)
💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...
5个适合初学者的初级网络安全工作,网络安全就业必看
前言 网络安全涉及保护计算机系统、网络和数据免受未经授权的访问、破坏和盗窃 - 防止数字活动和数据访问的中断 - 同时也保护用户的资产和隐私。鉴于公共事业、医疗保健、金融以及联邦政府等行业的网络犯罪攻击不断升级,对网络专业人员的需求很高,这并…...
Kafka核心原理
1、Topic的分片和副本机制 分片作用: 解决单台节点容量有限的问题,节点多,效率提升,吞吐量提升。通过分片,将一个大的容器分解为多个小的容器,分布在不同的节点上,从而实现分布式存储。 分片…...
探秘前后端开发世界:猫头虎带你穿梭编程的繁忙街区,解锁全栈之路
🌷🍁 博主猫头虎 带您 Go to New World.✨🍁 🦄 博客首页——猫头虎的博客🎐 🐳《面试题大全专栏》 文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺 &a…...
洛谷_分支循环
p2433 问题 5 甲列火车长 260 米,每秒行 12 米;乙列火车长220 米,每秒行 20 米,两车相向而行,从两车车头相遇时开始计时,多长时间后两车车尾相离?已知答案是整数。 计算方式:两车车…...
MySQL数据库入门到精通——进阶篇(3)
黑马程序员 MySQL数据库入门到精通——进阶篇(3) 1. 锁1.1 锁-介绍1.2 锁-全局锁1.3 锁-表级锁1.3.1 表级锁-表锁1.3.2 表级锁元数据锁( meta data lock,MDL)1.3.3 表级锁-意向锁1.3.4 表级锁意向锁测试 1.4 锁-行级锁1.4.1 行级锁-行锁1.4.2…...
Mind Map:大语言模型中的知识图谱提示激发思维图10.1+10.2
知识图谱提示激发思维图 摘要介绍相关工作方法第一步:证据图挖掘第二步:证据图聚合第三步:LLM Mind Map推理 实验实验设置医学问答长对话问题使用KG的部分知识生成深入分析 总结 摘要 LLM通常在吸收新知识的能力、generation of hallucinati…...
[引擎开发] 杂谈ue4中的Vulkan
接触Vulkan大概也有大半年,概述一下自己这段时间了解到的东西。本文实际上是杂谈性质而非综述性质,带有严重的主观认知,因此并没有那么严谨。 使用Vulkan会带来什么呢?简单来说就是对底层更好的控制。这意味着我们能够有更多的手段…...
docker--redis容器部署及地理空间API的使用示例-II
文章目录 Redis 地理位置类型API命令操作示例JAVA使用示例导入依赖RedisTemplate 操作GeoData示例CityInfo实体类Geo操作接口类Geo操作接口实现类SpringBoot测试类RedissonClient 操作GeoData示例docker–redis容器部署及与SpringBoot整合 docker–redis容器部署及地理空间API的…...
Vue中如何进行文件浏览与文件管理
Vue中的文件浏览与文件管理 文件浏览与文件管理是许多Web应用程序中常见的功能之一。在Vue.js中,您可以轻松地实现文件浏览和管理功能,使您的应用程序更具交互性和可用性。本文将向您展示如何使用Vue.js构建文件浏览器和文件管理功能,以及如…...
jenkins利用插件Active Choices Plug-in达到联动显示或隐藏参数,且参数值可修改
1. 添加组件 Active Choices Plug-in 如jenkins无法联网,可在以下两个地址中下载插件,然后放到/home/jenkins/.jenkins/plugin下面重启jenkins即可 Active Choices Active Choices | Jenkins plugin 2. 效果如下: sharding为空时…...
香蕉叶病害数据集
1.数据集 第一个文件夹为数据增强(旋转平移裁剪等操作)后的数据集 第二个文件夹为原始数据集 2.原始数据集 Cordana文件夹(162张照片) healthy文件夹(129张) Pestalotiopsis文件夹(173张照片&…...
天地无用 - 修改朋友圈的定位: 高德地图 + 爱思助手
1,电脑上打开高德地图网页版 高德地图 (amap.com) 2,网页最下一栏,点击“开放平台” 高德开放平台 | 高德地图API (amap.com) 3,在新网页中,需要登录高德账户才能操作。 可以使用手机号和验证码登录。 4,…...
AtCoder Beginner Contest 232(A-G)
A - QQ solver (atcoder.jp)直接按题意模拟即可。 B - Caesar Cipher (atcoder.jp)按题意模拟即可 C - Graph Isomorphism (atcoder.jp)按题意模拟即可 D - Weak Takahashi (atcoder.jp) 一个非常套路的网格dp E - Rook Path (atcoder.jp) (1)题意 有…...
计算机网络(第8版)-第5章 运输层
5.1 运输层协议概述 5.1.1 进程之间的通信 图5-1 中两个运输层之间有一个深色双向粗箭头,写明“运输层提供应用进程间的逻辑通信”。 图5-1 运输层为相互通信的应用进程提供了逻辑通信 5.1.2 运输层的两个主要协议 5.1.3 运输层的端口 请注意,这种…...
AtCoder Beginner Contest 231(D-F,H)
D - Neighbors (atcoder.jp) (1)题意 给出M组关系,问是否有一个排列,能表示A[i]和B[i]相邻 (2)思路 考虑如果有环,显然不能满足排列,因为排列中度数最多为2,若有超过2的显…...
【Python】map
map()函数是Python内置函数之一,它的主要作用是将一个函数应用于可迭代对象中的每个元素,并返回一个包含结果的迭代器。 map()函数的语法如下: map(function, iterable)function参数是一个函数,表示要应用于可迭代对象每个元素的…...
树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法
树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作,无需更改相机配置。但是,一…...
Vue3 + Element Plus + TypeScript中el-transfer穿梭框组件使用详解及示例
使用详解 Element Plus 的 el-transfer 组件是一个强大的穿梭框组件,常用于在两个集合之间进行数据转移,如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。 核心特性与用法 基本属性 v-model:绑定右侧列表的值&…...
Mybatis逆向工程,动态创建实体类、条件扩展类、Mapper接口、Mapper.xml映射文件
今天呢,博主的学习进度也是步入了Java Mybatis 框架,目前正在逐步杨帆旗航。 那么接下来就给大家出一期有关 Mybatis 逆向工程的教学,希望能对大家有所帮助,也特别欢迎大家指点不足之处,小生很乐意接受正确的建议&…...
java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别
UnsatisfiedLinkError 在对接硬件设备中,我们会遇到使用 java 调用 dll文件 的情况,此时大概率出现UnsatisfiedLinkError链接错误,原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用,结果 dll 未实现 JNI 协…...
蓝牙 BLE 扫描面试题大全(2):进阶面试题与实战演练
前文覆盖了 BLE 扫描的基础概念与经典问题蓝牙 BLE 扫描面试题大全(1):从基础到实战的深度解析-CSDN博客,但实际面试中,企业更关注候选人对复杂场景的应对能力(如多设备并发扫描、低功耗与高发现率的平衡)和前沿技术的…...
python爬虫:Newspaper3k 的详细使用(好用的新闻网站文章抓取和解析的Python库)
更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、Newspaper3k 概述1.1 Newspaper3k 介绍1.2 主要功能1.3 典型应用场景1.4 安装二、基本用法2.2 提取单篇文章的内容2.2 处理多篇文档三、高级选项3.1 自定义配置3.2 分析文章情感四、实战案例4.1 构建新闻摘要聚合器…...
三体问题详解
从物理学角度,三体问题之所以不稳定,是因为三个天体在万有引力作用下相互作用,形成一个非线性耦合系统。我们可以从牛顿经典力学出发,列出具体的运动方程,并说明为何这个系统本质上是混沌的,无法得到一般解…...
Rust 异步编程
Rust 异步编程 引言 Rust 是一种系统编程语言,以其高性能、安全性以及零成本抽象而著称。在多核处理器成为主流的今天,异步编程成为了一种提高应用性能、优化资源利用的有效手段。本文将深入探讨 Rust 异步编程的核心概念、常用库以及最佳实践。 异步编程基础 什么是异步…...
MySQL 知识小结(一)
一、my.cnf配置详解 我们知道安装MySQL有两种方式来安装咱们的MySQL数据库,分别是二进制安装编译数据库或者使用三方yum来进行安装,第三方yum的安装相对于二进制压缩包的安装更快捷,但是文件存放起来数据比较冗余,用二进制能够更好管理咱们M…...
[免费]微信小程序问卷调查系统(SpringBoot后端+Vue管理端)【论文+源码+SQL脚本】
大家好,我是java1234_小锋老师,看到一个不错的微信小程序问卷调查系统(SpringBoot后端Vue管理端)【论文源码SQL脚本】,分享下哈。 项目视频演示 【免费】微信小程序问卷调查系统(SpringBoot后端Vue管理端) Java毕业设计_哔哩哔哩_bilibili 项…...


