news 2026/10/28 23:49:26

pthread_create函数指针参数详解:从参数传递到线程池实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
pthread_create函数指针参数详解:从参数传递到线程池实践

搞C/C++多线程编程的人,迟早会和pthread_create打交道。这个函数是POSIX线程库的入口,几乎所有Linux/Unix下的并发任务都从它开始。可它的签名对新手不太友好,尤其是第三个参数void *(*start_routine)(void *),一堆星号括号混在一起,很多人第一次看到就发怵:到底要我传什么?线程入口函数长什么样?参数又要怎么给?

我在项目里带过不少刚接触多线程的同事,发现真正让人翻车的往往不是锁,也不是条件变量,而是这个最基础的“线程创建+参数传递”环节。有人往pthread_create里传了一个局部变量的地址,程序跑得好好的,换一个优化级别就段错误;有人写个循环批量创建线程,所有线程拿到的编号竟然一模一样。所以这次我把pthread_create函数指针用法从头到尾拆开讲:函数指针是什么、参数怎么传、线程怎么回收,再配合函数指针数组,把批量创建线程和组织任务调度的高级玩法一起讲了。

这篇内容适合两类人:一是刚入门Linux多线程、想系统搞懂pthread_create用法的人;二是项目里要用线程池、任务分发的C/C++工程师,需要把代码组织得易维护。我会尽量用“看得见代码、能直接跑”的方式来写,讲清楚每一步背后的道理。

1. pthread_create函数指针参数:先看懂它在等什么

1.1 从原型里拆出最关键的两个参数

pthread_create完整原型(glibc版本)是这样的:

int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void *), void *restrict arg);

四个参数,前两个只是“属性配置”。thread是一块调用方提供的内存,函数会在创建成功时把新线程的ID写进去,后续join、detach都靠这个ID。attr传NULL就用默认属性,绝大多数场景够用了。

真正决定这个线程干什么的,是后两个参数:start_routine是线程入口函数的指针,arg是传给这个入口函数的参数。一个线程的“灵魂”就是这两样:跑哪段代码、拿什么数据。

用生活类比,线程就是一条流水线工位:start_routine是工位墙上贴的操作步骤(代码),arg是工位上那箱待加工的物料(数据)。同一个工位可以换操作步骤,也可以换物料,线程调度器负责把“工人”派到工位上执行。

1.2 拆解void *(*start_routine)(void *)

要把这个函数指针看懂,先记住一个通用语法:返回类型 (*指针名)(参数列表)。

拿一个普通函数来说:

int add(int a, int b); int (*add_ptr)(int, int); // 声明一个指向该类型函数的指针 add_ptr = add; // 函数名本身代表函数地址

函数指针的规则:函数名被当作地址使用,add和&add本质上一样。这也是为什么pthread_create第三个参数可以直接写函数名。

回到start_routine,拆三大块:

  • start_routine:一个指针变量,它指向某个函数。
  • 前面的void *:被指函数的返回类型是void *。
  • 后面的(void *):被指函数接收一个void *参数。

所以完整含义是:一个“返回void *、接收一个void *参数”的函数的地址。

那为什么选void *?因为void *是C语言里的“万能地址”。线程库在通用层面不知道业务的具体类型,它只负责把入口地址和参数地址交给新线程,具体解释成什么类型完全由你自己完成。返回void *同理,线程结束时可以把结果地址交回,想返回什么返回什么。这种“库不关心类型,使用者自解释”的设计,保证了线程入口对任何业务都适用。

下面是一个最简入口函数:

void *thread_routine(void *arg) { // 参数需要自己强转 int value = (int)(intptr_t)arg; printf("thread got %d\n", value); return NULL; }

注意:如果你有一个业务函数void worker(),直接拿它传给pthread_create,编译器会报“incompatible pointer type”。它和void *(*)(void *)不是一个类型。要么把业务函数签名改成线程入口格式,要么包一层适配函数。我习惯包一层,业务函数保持纯净,线程入口统一做类型转换和异常兜底。

1.3 attr与返回值:创建线程的“隐藏细节”

attr即使传NULL,也要知道它控制什么。最常见的有分离态(PTHREAD_CREATE_DETACHED)、栈大小、调度策略。后面第3章会展开。

而pthread_create的返回值容易被忽视:它不是设置errno,而是直接返回错误码。0表示成功,非0对应一个错误编号。常见错误:

  • EAGAIN:线程数量达到系统上限,或资源暂时不足。
  • EINVAL:attr传了非法参数,比如栈大小小于PTHREAD_STACK_MIN。
  • EPERM:调度策略或权限不满足。

正确检查方式:

int rc = pthread_create(&tid, NULL, thread_routine, arg); if (rc != 0) { fprintf(stderr, "pthread_create failed: %s\n", strerror(rc)); }

从这点可以展开:创建线程是一个真实消耗资源的操作。系统对进程内线程数有上限,用ulimit -u可以看。批量创建线程的程序,必须处理pthread_create失败的情况,而不是默认一定成功。

2. 参数传递的正确姿势:从单值到结构体

2.1 单参数:传值还是传地址

线程入口函数接收的是void *arg,理论上你可以把任何类型地址塞进去。传“地址”还是传“值”,是个原则问题。

最简单的单参数场景,给每个线程传一个编号。常见错误写法是:

for (int i = 0; i < 4; i++) { pthread_create(&tids[i], NULL, worker, &i); // 错! }

这里传的是变量i的地址。所有线程拿到的是同一块栈内存。循环是主线程上的执行流,i在不停变化;而子线程什么时候真正读取这个地址,完全由调度决定。结果就是多个线程读到的编号可能都是3,甚至更糟。我在监控日志里见过一次,4个线程打印全部是7——因为i已经跑到7了,而且这个地址上层的栈帧可能已被别的函数调用覆盖。

正确做法是“传值”——把整数拷贝成指针宽度的值传过去:

pthread_create(&tids[i], NULL, worker, (void *)(intptr_t)i);

线程里再转回来:

int id = (int)(intptr_t)arg;

中间用intptr_t过渡,是因为int不保证和指针同宽,intptr_t才是“能完整保存指针的整数类型”。先转成intptr_t,再转成void *,可以避免64位平台上int被截断的问题。

注意:如果你传的是一个动态分配的整数对象(malloc一个int然后传地址),那线程用完必须有人free,而且这个对象的生命周期必须覆盖线程执行期。相比之下,传值方式更省心,优先用。

2.2 多参数:用结构体打包

线程需要多个参数时,不可能一次传多个,系统只给了一个void *。对多个参数,一定要“打包”,最直观的就是用struct。

typedef struct task_arg { int id; char name[32]; int payload; } task_arg; void *task_worker(void *arg) { task_arg *ta = (task_arg *)arg; printf("task %d/%s payload=%d\n", ta->id, ta->name, ta->payload); return NULL; }

创建时按每个线程分配独立结构体:

task_arg *ta = malloc(sizeof(*ta)); ta->id = next_id; snprintf(ta->name, sizeof(ta->name), "task-%d", next_id); ta->payload = compute_payload(); pthread_create(&tid, NULL, task_worker, ta);

这里有两个容易踩的细节。

第一,结构体不要用同一个栈变量。假如主线程有局部变量task_arg ctx,循环里创建4个线程,四个线程的arg都指向同一个ctx地址,和传&i一样的下场。

第二,内存释放要明确。可以主线程统一在join后free,也可以线程入口里free。我习惯:如果主线程在join之后不再需要结构体,就由线程入口free;因为线程入口可以自己保证“我已经用完了”。但无论哪种,代码里必须写清楚。我在实际代码里经常这样处理入口:

void *task_worker(void *arg) { task_arg *ta = (task_arg *)arg; // 使用参数 // ... free(ta); // 参数结构体由线程自己释放 return NULL; }

如果结构体里还有指针指向另一块动态内存,也要约定释放顺序。先free内部字段,再free结构体本身。

另一种少用malloc的办法:维护一个全局结构体数组,按创建顺序给下标。比如:

static task_arg ctx_pool[64]; pthread_create(&tid, NULL, task_worker, &ctx_pool[i]);

这种方式适合线程总数固定、参数天然按槽位分开的场景,省掉了malloc/free的频繁调用。但它本质是共享数组,如果不是只读字段,多个线程读写同一个slot要加锁。而且槽位不能复用错,否则数据会被覆盖。我自己只有在玩具demo里才这么写,工程里还是malloc结构体稳妥。

2.3 生命周期陷阱:栈变量、字符串、共享缓冲

前面提到栈变量,这里展开串成一个必看清单。

先说最坑的:把局部变量的地址直接传给新线程,父函数返回后,该地址指向的栈帧已经失效。子线程什么时候读,读到什么都不可预测。表现通常是“偶发段错误”或“加了O2优化后必现”。

字符串是另一个重灾区。字符串字面量存放于只读数据区,进程生存期内有效,可以直接传:

pthread_create(&tid, NULL, worker, "hello"); // 安全

但如果字符串是函数内局部数组,比如:

char buf[64]; snprintf(buf, sizeof(buf), "req-%d", id); pthread_create(&tid, NULL, worker, buf); // 危险

buf是栈内存,函数返回即失效。要么等线程用完再返回(join),要么复制到堆上:

char *dup = strdup(buf); // 线程用完后 free(dup) pthread_create(&tid, NULL, worker, dup);

共享缓冲的问题也常被归错类。传一个指针给线程,等于把这块内存的访问权交给了线程。如果另一个线程/主线程也在读写同一块缓冲而没有任何同步,那这不是“传参错误”,而是“数据竞争”。但很多人早期就是栽在:明明arg传对了,为什么数据乱?本质是共享写入缺锁。所以参数传递前先想清楚:这块内存在线程运行期间,还有谁会碰?

我给自己定的通用规则很简单:子线程将要访问的所有内存,要么在创建前保证有效到pthread_join之后,要么在堆上分配并明确释放责任。没有第三条捷径。这个规则帮我躲掉了至少几十次段错误。

3. 线程创建与回收:把资源管明白

3.1 默认是joinable,别让它悄悄泄漏

pthread_create创建的线程,默认处于joinable状态。很多人不知道这句话意味着什么:线程执行完,它的资源不会自动释放,必须有人调pthread_join来回收。如果创建了线程却从不join,也没设置detach,那么每跑完一个线程,就泄漏一份资源(线程内核对象、用户态栈等)。服务型程序如果一直在创建线程不回收,内存只会涨不会跌,跑几天就会被系统杀掉了。

所以要建立清晰的回收策略:

  • 需要等待线程结束的:保留joinable,等任务完成后调用pthread_join。返回值也可以从join里带出来。
  • 纯后台、不关心结果的:创建前通过attr设置detach状态,让线程结束自动回收。

join的典型写法:

pthread_t tids[4]; for (int i = 0; i < 4; i++) { pthread_create(&tids[i], NULL, worker, (void *)(intptr_t)i); } // 其他主线程业务... for (int i = 0; i < 4; i++) { pthread_join(tids[i], NULL); }

很多人刚学时写错成“创建+join在一个循环里”,那样线程根本没有并行,第二线程要等第一线程做完全部再开跑,丧失了并发意义。正确做法是先全部创建,存好tids,再统一join。

detach设置代码:

pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); pthread_t tid; int rc = pthread_create(&tid, &attr, worker, arg); pthread_attr_destroy(&attr);

如果某个线程已经被pthread_join过一次,再join会立刻返回ESRCH。如果两个线程互相join,可能造成死锁,pthread_join在检测到这种死锁时返回EDEADLK(Linux上)。这些返回值都值得检查。

3.2 从线程取返回值

线程入口函数返回void *,这个指针会在线程退出后由pthread_join带出来。用法:

void *worker(void *arg) { int num = (int)(intptr_t)arg; char *result = malloc(64); snprintf(result, 64, "square=%d", num * num); return result; } int main() { pthread_t tid; pthread_create(&tid, NULL, worker, (void *)(intptr_t)5); void *ret = NULL; pthread_join(tid, &ret); if (ret) { printf("result: %s\n", (char *)ret); free(ret); } return 0; }

关键约束是:返回值必须指向“线程结束后依然有效”的内存。最常见的错误是返回入口函数内的栈数组:

void *bad_worker(void *arg) { char buf[64]; snprintf(buf, 64, "hello"); return buf; // 错!函数返回后栈内存失效 }

这种代码在调试时往往能“侥幸”工作,但优化级别一高或者栈帧被复用,打印出来的就是垃圾字符串。正确做法是返回堆指针、全局指针或static数组。static数组又面临多线程共享问题:多个线程写同一个static缓冲区,后面的覆盖前面的。最稳妥的是堆分配,让调用方free。

也可以不通过返回值传数据,而是把结果写进arg指向的结构体字段里。比如请求结构体里放result字段,线程执行完填好,主线程join后直接读结构体。这样参数的单位就是“请求上下文”,比零散的返回值管理更清晰。

3.3 线程属性:栈大小与分离状态

attr里除了分离状态,栈大小是高频项。Linux上默认线程栈通常是8MB虚拟内存(可pthread_attr_getstacksize确认),对大多数任务完全够用。但有些程序在线程里做深度递归,或者声明一个大数组,需要显式加大:

pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setstacksize(&attr, 16 * 1024 * 1024);

设太小(小于PTHREAD_STACK_MIN,常见为16KB左右)则pthread_create返回EINVAL。如果要写可移植代码,用sysconf(_SC_THREAD_STACK_MIN)查询最小值。

需要提醒的是:栈设置是一次性分配给线程的虚拟内存,不是物理内存。不管栈设多大,物理内存是按需分配的,所以设大一点通常不会立刻消耗大量内存,但设太小会直接导致线程内递归越界、栈溢出崩溃。

调度策略和优先级这类attr,普通人不用动。默认SCHED_OTHER就够了。实时调度策略需要权限配合,业务线程贸然调整可能引发系统级问题。

3.4 线程数量不是越多越好

pthread_create本身是消耗型系统调用,一次创建大约几微秒到几十微秒(取决于系统负载和线程栈分配)。如果任务密集且短小,频繁创建销毁线程的开销会十分可观。这也是为什么工程上会引入线程池:预先创建固定数量线程,任务通过队列分发,线程循环取任务执行。

线程数量怎么定?简单估计:

  • CPU密集型:通常设为CPU核心数或核心数+1,再多也没收益,反而增加切换开销。
  • IO密集型:线程数可以明显多于核心数,因为大部分时间在等IO。常用公式:线程数 ≈ 核心数 × (1 + 等待时间/计算时间),如果拿不准,先按核心数的2~3倍起压测再调。
  • 每创建一个线程,默认栈8MB虚拟内存、还要占用内核任务描述符。创建成千上万个线程,进程地址空间会迅速吃紧。用pthread_create返回EAGAIN时,先检查是不是线程数量超了ulimit -u。

在一个需要高并发的服务里,我用线程池之前试过“每请求一线程”,峰值几百个线程时系统还可以,到上千个时响应时间明显恶化。改成固定线程池后,吞吐反而稳定。

4. 函数指针数组:用一张表组织多个线程任务

4.1 先解决语法问题

函数指针数组是本章的主角,也是pthread_create场景里最能体现C语言灵活性的设计。先定义一个指向线程入口格式的函数指针类型:

typedef void *(*thread_func)(void *);

这个typedef等于给“线程入口函数类型”起了一个别名thread_func。然后三个任务函数:

void *task_a(void *arg) { ... return NULL; } void *task_b(void *arg) { ... return NULL; } void *task_c(void *arg) { ... return NULL; }

定义一个数组:

thread_func task_table[] = { task_a, task_b, task_c };

task_table就是函数指针数组:下标0存task_a的地址,下标1存task_b,以此类推。编译期就把函数地址静态初始化好,运行期可以直接按索引取用。

调用方式:

for (int i = 0; i < 3; i++) { pthread_create(&tids[i], NULL, task_table[i], args[i]); }

这个模式将“任务类型”和“线程创建”彻底解耦。以后新增任务,只需在数组里加一个函数,创建线程的循环一行不用改。对代码维护价值很大,特别是一批任务本来就要并行执行时,数组就是最自然的批处理容器。

4.2 用下标映射任务ID:一张分发表

函数指针数组最常见的工程用法是“任务分发表”。比如服务端收到一个协议包,包里有个opcode字段,表示要执行什么操作。用switch-case当然能写,但每加一个操作就要改一遍switch,代码越来越臃肿。函数指针数组可以直接用opcode做下标:

typedef void *(*thread_func)(void *); typedef struct request { int opcode; char data[128]; } request; void *handle_login(void *arg); void *handle_query(void *arg); void *handle_save(void *arg); thread_func op_table[] = { [0] = handle_login, [1] = handle_query, [2] = handle_save, };

C99支持指定初始化器,把opcode和处理器绑定得非常直白。线程入口拿到请求后查表:

void *dispatch(void *arg) { request *req = (request *)arg; if (req->opcode < 0 || req->opcode >= TABLE_SIZE) { return NULL; } thread_func handler = op_table[req->opcode]; if (!handler) { return NULL; } return handler((void *)req); }

有人担心数组下标越界,所以在查表之前必须做范围检查。表里不一定每个槽位都有对应函数,留空设NULL,读取前判空即可。这个模式比一行行switch简洁得多,而且操作码到处理函数的映射关系集中在一个地方,配置化程度更高。

在一些大的异步框架里,甚至不止一层表:外层是“事件类型→处理函数”,内层再分“业务状态→回调函数”。本质都是同一招:函数地址放进数组,用整数索引直接触发对应逻辑。

4.3 综合示例:批量任务分发给线程

最后给一个完整可编译的示例,把函数指针数组和pthread_create串起来。

#include <pthread.h> #include <stdio.h> #include <stdint.h> #include <unistd.h> void *task_compute(void *arg) { int id = (int)(intptr_t)arg; printf("compute task %d start\n", id); sleep(1); printf("compute task %d done\n", id); return NULL; } void *task_io(void *arg) { int id = (int)(intptr_t)arg; printf("io task %d start\n", id); usleep(200 * 1000); printf("io task %d done\n", id); return NULL; } void *task_cleanup(void *arg) { int id = (int)(intptr_t)arg; printf("cleanup task %d start\n", id); sleep(2); printf("cleanup task %d done\n", id); return NULL; } typedef void *(*thread_func)(void *); int main() { thread_func tasks[] = { task_compute, task_io, task_cleanup }; int task_count = (int)(sizeof(tasks) / sizeof(tasks[0])); pthread_t tids[task_count]; for (int i = 0; i < task_count; i++) { pthread_create(&tids[i], NULL, tasks[i], (void *)(intptr_t)i); } for (int i = 0; i < task_count; i++) { pthread_join(tids[i], NULL); } printf("all tasks finished\n"); return 0; }

编译并运行:

gcc task_table_demo.c -o task_table_demo -pthread ./task_table_demo

输出顺序很大概率是交错的,这正是多线程的特征。如果某个任务先执行完,它的done可能先打出来。想验证并发,可以多跑几次,就明白不能依赖线程执行的先后顺序。

注意:pthread_create要求start_routine类型严格匹配。示例中task_compute这些函数都满足“返回void *、接收void *”,数组元素类型thread_func与start_routine完全一致,可以直接传。

5. 常见问题、排查实录与经验提示

5.1 三个让我印象深刻的线上问题

第一个:偶发处理错误。服务每收到一个请求就创建一个线程处理,参数是请求结构体指针。当时这个请求结构体在主线程栈上分配,线程还没处理完,主线程已经处理下一个请求,同一块栈内存被覆盖。结果是10个请求里偶尔有1个处理结果错误。排查了两天,最后发现根本不是业务逻辑问题,是传参生命周期没管好。改成堆分配后立刻消失。

第二个:多个线程打印一样的编号。一个批处理程序创建8个线程处理8块数据,日志里所有线程都打印“processing block 7”。原因就是循环里传了&i。用intptr_t把值固化进去之后,编号立刻正常。这个坑非常经典,几乎所有初学pthread_create的人都会踩一次。

第三个:程序不定期卡住几分钟。排查时发现主线程在join一个早已设置了detach的线程。语义矛盾:线程已经分离,资源会自动回收,join永远等不到。处理后统一了设计规则:所有线程要么走join路径,要么走detach路径,绝不混用。从此我再没遇到过这个奇葩卡顿。

5.2 常见问题速查表

现象可能原因解决办法
创建线程偶发段错误传入了已失效的栈地址改堆分配,或join后再返回
线程参数打印全是同一个值循环里传了共享变量地址传intptr_t值,或用独立结构体
程序报Resource temporarily unavailable线程数量超限或资源不足查ulimit -u,降低并发,补join
join一直阻塞不返回线程内部死循环或死锁gdb attach看线程栈
运行越久内存越高joinable线程未join统一join,或创建前设置detach
线程返回字符串是乱码返回了栈变量地址返回堆指针并约定free
加锁后不能并发创建循环里直接join先全部创建,再统一join
pthread_create返回EINVALattr栈大小小于最小值查PTHREAD_STACK_MIN或sysconf

除表格外再说三个小点。编译时务必加-pthread,不是只加-lpthread,-pthread还会定义必要的编译宏,否则抽象掉的关键宏不一致,某些平台直接运行出错。pthread_create的错误码要用strerror打印,而不是perror,因为它不走errno。调试多线程用gdb的thread apply all bt可以一次性看到所有线程栈,排查卡顿非常高效。

5.3 让线程代码好维护的几条经验

入口函数保持短小。线程入口只做四件事:参数类型转换、防御检查、调用真正的业务函数、记录开始/结束日志。业务逻辑放在普通函数里,单元测试不造线程也能测。

参数结构体好好命名。我习惯统一加task_前缀,字段写清楚含义和所有权,特别是带指针的字段,在注释里写明谁负责free。一个结构体同时被多个线程引用时,只读字段可以放心共享;任何写操作都必须同步。

调试阶段给线程入口加pthread_self()打印。多线程日志混在一起时,线程ID比printf的字符串更精确,能马上定位是哪个线程出了问题。线上版本可以用原子计数或日志框架替代,但开发阶段这招最省钱。

再提一个经常会遇到的需求:线程池。前面讲的函数指针数组加上一个任务队列,就是线程池的最小雏形。工作线程循环从队列取任务编号,查表执行。这样线程数量固定、任务分发灵活,能避免“每请求一线程”带来的资源抖动。哪怕是先写一个能用但简陋的版本,也好过完全没有池化。

pthread_create的函数指针参数,我第一次看完man手册其实也是一知半解,真正搞明白还是靠一次次段错误和调试。现在回头看,它的核心就一句话:给新线程一个入口地址和一份有效数据。函数指针是入口的载体,void *是数据的载体,两者组合,C语言就拥有了通用且优雅的线程调用方式。

在实际项目里我越来越喜欢把入口任务组织成函数指针数组:新增任务时,写一个满足签名的函数,往数组里加一项,线程创建逻辑不用动。很多写着省事的switch-case,最后都被这张表替代了。参数传递方面,我给自己定的红线始终没变——传地址可以,但必须保证这块内存在线程执行窗口内是有效的。守住这条红线,pthread_create这个接口基本不会再给你惹麻烦。

如果后续想继续深入,可以往这几个方向展开:线程池的完整实现、pthread_cond条件变量驱动的多线程协作、以及用__thread关键字做线程本地存储。但这些都是建立在把“线程创建+参数传递”吃透之后的事,基础不牢,高并发代码永远是空中楼阁。希望这份总结能让你避开我当年踩过的坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 23:49:25

基于Spring Boot+Vue的校园勤工俭学平台设计与开发实战

又到毕业设计开题季了。后台私信里问得最多的永远是同一个问题&#xff1a;想做个 Spring Boot 方向的管理系统题目&#xff0c;该选什么业务&#xff1f;该用哪些技术&#xff1f;论文怎么写才能过查重&#xff1f;我自己的毕业设计做的就是基于 Spring Boot Vue 的校园勤工俭…

作者头像 李华
网站建设 2026/10/25 21:33:24

Gitee实战指南:从SSH密钥配置到Pages部署与开源许可证选择

1. Gitee为什么能成为开发者生态的底层设施先说个实际现象。这几年不管是做外包、创业公司做项目&#xff0c;还是传统企业搞内部系统&#xff0c;很多团队第一步不是开会定方案&#xff0c;而是先建仓库。仓库建在哪&#xff1f;国内团队用Gitee的比例越来越高。我的经验是&am…

作者头像 李华
网站建设 2026/10/26 0:28:36

微信小程序+SSM健身项目源码实战:跑通与二次开发

手头这套交付包是典型的“微信小程序SSM&#xff08;SpringSpringMVCMyBatis&#xff09;”项目组合&#xff0c;文件名是“weixin159健身小程序ssm&#xff08;文档源码&#xff09;”。这类资源我接触过不少&#xff0c;健身房预约、食堂订餐、校园二手交易&#xff0c;业务五…

作者头像 李华
网站建设 2026/10/25 22:22:11

JDBC连接数据库全攻略:配置、排错与连接池实战

周一早上刚到工位&#xff0c;隔壁组的小王就抱着笔记本过来了&#xff1a;“哥&#xff0c;我连不上数据库了&#xff0c;报错是CommunicationsException&#xff0c;但昨天还好好的。”这种问题我一年少说碰十几次&#xff0c;每次排查到最后&#xff0c;绝大多数情况都回到同…

作者头像 李华
网站建设 2026/10/28 14:03:03

迈瑞病人数据共享协议开发指南:HL7接口对接与MLLP服务实现

简介&#xff1a;这份《迈瑞病人数据共享协议开发指南》中文电子档面向具备软件开发与网络基础的专业工程师&#xff0c;解决第三方产品与迈瑞监护仪、麻醉机、远望VI中心监护系统及病人数据共享网关之间的数据交换问题。文档围绕HL7底层协议与协议层展开&#xff0c;涵盖物理层…

作者头像 李华
网站建设 2026/10/21 12:26:26

Sickos1.1靶机实战:SQL注入绕过与Shellshock提权全记录

可以做渗透测试练习的时候&#xff0c;我有个习惯&#xff1a;每打完一台靶机&#xff0c;就把WriteUp里容易被一带而过的部分拿出来反复琢磨。Sickos1.1就是这样一台机器&#xff0c;它来自VulnHub&#xff0c;难度标签是Hard&#xff0c;但真正难的地方不是某个漏洞的利用代码…

作者头像 李华