纯C协程框架NtyCo
原文是由写的,写的真的很好,原文链接:纯c协程框架NtyCo实现与原理-CSDN博客
1.为什么会有协程,协程解决了什么问题?
网络IO优化
在CS,BS的开发模式下,服务器的吞吐量是一个受关注的参数,吞吐量等于1秒内业务处理的次数,那么这个业务处理其实是 由 网络IO事件 + 业务处理时间 组成的。 业务不同处理时间也就不同,但是网络IO时间是可以进行优化的。
也就是说,如何提升recv和send的性能?以epoll管理百万长连接为例,测试IO同步操作与异步操作的性能差别。
对于响应式服务器来说,所有客户端的操作都是源于这个大循环,对于服务器处理网络IO,有两种方式。第一种,IO同步;第二种,IO异步。
IO同步 操作性能测试
对于IO同步操作来说,handle(sockfd) 函数内部实现如下
同步:检测 IO 与 读写 IO 在同一个流程中
测试出来,每一千个连接,耗时7.5秒左右。
优点:
1.sockfd 管理方便
2.代码逻辑清晰
缺点:
1.服务器程序依赖 epoll_wait 的循环,响应速度慢。
2.程序性能差
IO异步 操作性能测试
对于IO 异步操作来说,将任务push到线程池中,有其他线程进行读写。
异步:检测 IO 与 读写 IO 不在同一个流程中
IO操作与epoll_wait不在一个处理流程中,实现了解耦,这是IO异步操作,每一千个连接耗时2.5秒左右
优点:
1.子模块好规划
2.程序性能高
缺点:
1.管理fd麻烦,需要避免一个fd被多个线程操作的情况发生。
协程的诞生
对比项 | IO 同步操作 | IO 异步操作 | 协程 |
sockfd 管理 | 管理方便 | 多个线程共同管理 | 管理方便 |
代码逻辑 | 程序整体逻辑清晰 | 子模块逻辑清晰 | 程序整体逻辑清晰 |
程序性能 | 响应时间长,性能差 | 响应时间短,性能好 | 响应时间短,性能好 |
从上面我们知道了IO同步操作,写代码逻辑清晰,但是效率低;而IO异步操作,fd管理复杂,但是效率高。由此,协程便出现了。
协程:把两者结合起来,以同步的编程方式,实现异步的性能。
即写代码的时候,同步;运行的逻辑,异步。
2.原语
yield()
让出,将当前的执行流程让出,让出给调度器。
那么什么时候需要yield让出呢?很明显在recv之前,send之前,也就是在io之前,因为我们不知道io是否准备就绪了,所以我们先将fd加入epoll中,然后yield让出,将执行流程给调度器运行。
schedule
schedule调度器做什么事情呢?调度器就是io检测,调度器就是不断的调用epoll_wait,来检测哪些fd准备就绪了,然后就恢复相应fd的执行流程执行现场
。注意schedule不是原语,schedule是调度器。
resume()
从上面我们得知恢复是被schedule恢复的,那么现在恢复到了原来流程的哪里呢?其实是恢复到了yield的下一条代码处
。通常下面的代码都会将fd从epoll中移除,然后执行recv或send操作,因为一旦被resume,就说明肯定是准备就绪的。
如何实现yield和resume
- yield :从io操作流程切换到调度器流程(让出)
- resume : 从调度器流程切换到io操作流程
可以基于以下方法实现yield和resume:
1.setjmp/longjmp
#include <stdio.h>
#include <setjmp.h>jmp_buf env; // 定义一个jmp_buf类型的变量env,用于保存跳转环境void func(int arg) {printf("func: %d\n", arg);longjmp(env, ++arg); // 使用longjmp函数跳转回之前设置的环境,并传递增加后的参数值
}int main() {int ret = setjmp(env); // 调用setjmp函数,将当前环境保存到env中,并返回0if (ret == 0) { // 如果setjmp返回0,表示这是第一次调用setjmpfunc(ret);} else if (ret == 1) { // 如果setjmp返回1,表示这是通过longjmp跳转回来的func(ret);} else if (ret == 2) { // 如果setjmp返回2,表示这是通过longjmp跳转回来的func(ret);} else if (ret == 3) { // 如果setjmp返回3,表示这是通过longjmp跳转回来的func(ret);}return 0;
}
2.ucontext
void func1(void) {while (count ++ < 30) {printf("1\n");//swapcontext(&ctx[0], &ctx[1]); // 注释掉的代码:交换上下文,从ctx[0]切换到ctx[1]swapcontext(&ctx[0], &main_ctx); // 实际执行的代码:交换上下文,从ctx[0]切换到main_ctx,即主程序的上下文printf("4\n");}}
// coroutine2
void func2(void) {while (count ++ < 30) {printf("2\n");//swapcontext(&ctx[1], &ctx[2]);swapcontext(&ctx[1], &main_ctx); // 注释掉的代码:将当前上下文ctx[1]切换到上下文ctx[2]printf("5\n"); // 将当前上下文ctx[1]切换到主上下文main_ctx}
}// coroutine3
void func3(void) {while (count ++ < 30) {printf("3\n");//swapcontext(&ctx[2], &ctx[0]);swapcontext(&ctx[2], &main_ctx); // 注释掉的代码:将当前上下文ctx[2]切换到上下文ctx[0]printf("6\n"); // 将当前上下文ctx[2]切换到主上下文main_ctx}
}char stack1[2048] = {0}; // 定义三个栈,每个栈大小为2048字节,并初始化为0char stack2[2048] = {0};char stack3[2048] = {0};getcontext(&ctx[0]); // 获取当前上下文并保存到ctx[0]ctx[0].uc_stack.ss_sp = stack1; // 设置ctx[0]的栈指针为stack1ctx[0].uc_stack.ss_size = sizeof(stack1); // 设置ctx[0]的栈大小为stack1的大小ctx[0].uc_link = &main_ctx; // 设置ctx[0]的链接上下文为main_ctx,当ctx[0]执行完毕后,会切换到main_ctxmakecontext(&ctx[0], func1, 0); // 创建一个新的上下文ctx[0],并指定其执行的函数为func1,参数个数为0getcontext(&ctx[1]);ctx[1].uc_stack.ss_sp = stack2;ctx[1].uc_stack.ss_size = sizeof(stack2);ctx[1].uc_link = &main_ctx;makecontext(&ctx[1], func2, 0);getcontext(&ctx[2]);ctx[2].uc_stack.ss_sp = stack3;ctx[2].uc_stack.ss_size = sizeof(stack3);ctx[2].uc_link = &main_ctx;makecontext(&ctx[2], func3, 0);printf("swapcontext\n");//int i = 30;while (count <= 30) { // schedulerswapcontext(&main_ctx, &ctx[count%3]);}
3.用汇编代码自己实现切换
//new_ctx[%rdi]:即将运行协程的上下文寄存器列表; cur_ctx[%rsi]:正在运行协程的上下文寄存器列表
int _switch(nty_cpu_ctx *new_ctx, nty_cpu_ctx *cur_ctx);// yield让出
void nty_coroutine_yield(nty_coroutine *co) {_switch(&co->sched->ctx, &co->ctx);
}// resume协程恢复执行
int nty_coroutine_resume(nty_coroutine *co) {//...nty_schedule * sched = nty_coroutine_get_sched();sched->curr_thread = co;_switch(&co->ctx, &co->sched->ctx);//...
}
如何从一个协程切换到另一个协程呢?我们只需要将当前协程的上下文从寄存器组中保存下来;将下一个要运行的协程的上下文放到寄存器组上去,即可实现协程的切换。
3.切换
寄存器介绍
下面介绍的都是x86_64的寄存器。
- %rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数…(这里我们只需关注%rdi和%rsi)
- %rbx,%rbp,%r12,%r13,%14,%15 用作数据存储,遵循被调用者使用规则,简单说就是随便用,调用子函数之前要备份它,以防他被修改
- new_ctx是一个指针,指向一块内存,它现在存在%rid里面,同理cur_ctx存在%rsi里面
- %rsp代表栈顶,%rbp代表栈底,%eip代表cpu下一条待取指令的地址(这也就是为什么resume之后会接着运行代码流程的原因)
//new_ctx[%rdi]:即将运行协程的上下文寄存器列表; cur_ctx[%rsi]:正在运行协程的上下文寄存器列表
int _switch(nty_cpu_ctx *new_ctx, nty_cpu_ctx *cur_ctx);
汇编实现切换
//寄存器 cpu上下文
typedef struct _nty_cpu_ctx {void *rsp;//栈顶void *rbp;//栈底void *eip;//CPU通过EIP寄存器读取即将要执行的指令void *edi;void *esi;void *rbx;void *r1;void *r2;void *r3;void *r4;void *r5;
} nty_cpu_ctx;//new_ctx[%rdi]:即将运行协程的上下文寄存器列表; cur_ctx[%rsi]:正在运行协程的上下文寄存器列表
int _switch(nty_cpu_ctx *new_ctx, nty_cpu_ctx *cur_ctx);
//默认x86_64
__asm__(
" .text \n"
" .p2align 4,,15 \n"
".globl _switch \n"
".globl __switch \n"
"_switch: \n"
"__switch: \n"
" movq %rsp, 0(%rsi) # save stack_pointer \n"
" movq %rbp, 8(%rsi) # save frame_pointer \n"
" movq (%rsp), %rax # save insn_pointer \n"
" movq %rax, 16(%rsi) # save eip \n"
" movq %rbx, 24(%rsi) # save rbx,r12-r15 \n"
" movq %r12, 32(%rsi) \n"
" movq %r13, 40(%rsi) \n"
" movq %r14, 48(%rsi) \n"
" movq %r15, 56(%rsi) \n"" movq 56(%rdi), %r15 \n"
" movq 48(%rdi), %r14 \n"
" movq 40(%rdi), %r13 \n"
" movq 32(%rdi), %r12 \n"
" movq 24(%rdi), %rbx # restore rbx,r12-r15 \n"
" movq 8(%rdi), %rbp # restore frame_pointer \n"
" movq 0(%rdi), %rsp # restore stack_pointer \n"
" movq 16(%rdi), %rax # restore insn_pointer \n"
" movq %rax, (%rsp) # restore eip \n"
" ret # 出栈,回到栈指针,执行eip指向的指令。\n"
);
上下文切换,就是将 CPU 的寄存器暂时保存,再将即将运行的协程的上下文寄存器,分别mov 到相对应的寄存器上。此时上下文完成切换。
4.协程的运行流程
协程如何使用,协程的api
在网络IO编程的时候,如果每次accept返回的时候,为新来的fd单独分配一个线程,这一个fd对应一个线程,就不会存在多个线程共用一个fd的问题了,虽然这样代码逻辑清晰易读,但是这是无稽之谈,线程创建与线程调度的代价是很大的
但是如果把线程换成协程,线程API的思维来使用协程,那不就可行了吗?
NtyCo封装了两类接口
- 协程本身的api
-
//创建协程 int nty_coroutine_create(nty_coroutine **new_co, proc_coroutine func, void *arg); //调度器运行 void nty_schedule_run(void);
- posix api的异步封装协程api
-
//POSIX 异步封装 API int nty_socket(int domain, int type, int protocol);int nty_accept(int fd, struct sockaddr *addr, socklen_t *len);ssize_t nty_recv(int fd, void *buf, size_t len, int flags);ssize_t nty_send(int fd, const void *buf, size_t len, int flags);int nty_close(int fd);int nty_connect(int fd, struct sockaddr *name, socklen_t len);ssize_t nty_recvfrom(int fd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);ssize_t nty_sendto(int fd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
协程工作流程
创建协程
int nty_coroutine_create(nty_coroutine **new_co,proc_coroutine func,void *arg);
- nty_coroutine **new_co:需要传入空的协程对象,这个对象是由内部创建的,并且在函数返回的时候,会返回一个内部创建的协程对象。
- proc_coroutine func:协程的子过程。当协程被调度的时候,就会执行该函数
- void *arg : 需要传入到新协程子过程的参数。
协程不存在亲属关系,都是一致的调度关系,接受调度器的调度。调用 create API就会创建一个新协程,新协程就会加入到调度器的就绪队列中。
回调协程的子过程
在 create 协程后,何时回调子过程?何种方式回调子过程?我们知道CPU的EIP寄存器就是存储cpu下一条指令的地址,我们可以把回调函数的地址存储到 EIP 中。这样在resume之后,就会执行协程的子过程了。
// eip 执行入口
static void _exec(void *lt) {nty_coroutine *co = (nty_coroutine *) lt;co->func(co->arg);
}
// 初始化协程栈
static void nty_coroutine_init(nty_coroutine *co) {void **stack = (void **) (co->stack + co->stack_size);stack[-3] = NULL;stack[-2] = (void *) co;//设置参数co->ctx.rsp = (void *) stack - (4 * sizeof(void *));co->ctx.rbp = (void *) stack - (3 * sizeof(void *));co->ctx.eip = (void *) _exec;//设置回调函数入口co->status = BIT(NTY_COROUTINE_STATUS_READY);
}
协程封装posix api异步原理
在send与recv 调用的时候,如何实现异步操作?
在进行 IO 操作(recv,send)之前,先执行了 epoll_ctl 的 del 操作,将相应的 sockfd 从 epfd中删除掉,在执行完 IO 操作(recv,send)再进行 epoll_ctl 的 add 的动作。这段代码看起来似乎好像没有什么作用。
如果是在多个上下文中,这样的做法就很有意义了。能够保证 sockfd 只在一个上下文中能够操作 IO 的。不会出现在多个上下文同时对一个 IO 进行操作的。协程的 IO 异步操作正式是采用此模式进行的。
// 创建协程recv接口
ssize_t nty_recv(int fd, void *buf, size_t len, int flags) {struct epoll_event ev;ev.events = POLLIN | POLLERR | POLLHUP;ev.data.fd = fd;//加入epoll,然后yieldnty_epoll_inner(&ev, 1, 1);//resumessize_t ret = recv(fd, buf, len, flags);return ret;
}
// 加入epoll,更改状态,加入wait集合,然后yield与resume
static int nty_epoll_inner(struct epoll_event *ev, int ev_num, int timeout) {nty_schedule * sched = nty_coroutine_get_sched();nty_coroutine *co = sched->curr_thread;int i;for (i = 0; i < ev_num; i++) {epoll_ctl(sched->epfd, EPOLL_CTL_ADD, ev->data.fd, ev);co->events = ev->events;//加入wait集合,添加wait状态nty_schedule_sched_wait(co, ev->data.fd, ev->events, timeout);}//yieldnty_coroutine_yield(co);for (i = 0; i < ev_num; i++) {epoll_ctl(sched->epfd, EPOLL_CTL_DEL, ev->data.fd, ev);//移除wait集合,移除wait状态nty_schedule_desched_wait(ev->data.fd);}return ev_num;
}
一个简单的使用案例
可以看到,我们编写代码只需以同步的编程方式,就能实现异步的性能了。
#include "nty_coroutine.h"
#include <arpa/inet.h>void server_reader(void *arg) {int fd = *(int *) arg;ssize_t ret;struct pollfd fds;fds.fd = fd;fds.events = POLLIN;while (1) {char buf[1024] = {0};ret = nty_recv(fd, buf, 1024, 0);if (ret > 0) {nty_send(fd, buf, strlen(buf), 0);}else if (ret == 0) {nty_close(fd);break;}}
}void server(void *arg) {unsigned short port = *(unsigned short *) arg;int fd = nty_socket(AF_INET, SOCK_STREAM, 0);if (fd < 0) return;struct sockaddr_in local, remote;local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;bind(fd, (struct sockaddr *) &local, sizeof(struct sockaddr_in));listen(fd, 128);while (1) {socklen_t len = sizeof(struct sockaddr_in);int cli_fd = nty_accept(fd, (struct sockaddr *) &remote, &len);printf("new client comming\n");nty_coroutine *read_co;nty_coroutine_create(&read_co, server_reader, &cli_fd);}
}int main(int argc, char *argv[]) {nty_coroutine *co = NULL;unsigned short port = 8080;nty_coroutine_create(&co, server, &port);nty_schedule_run(); //runreturn 0;
}
5.协程 与 调度器 结构体定义
协程定义
一个协程会有哪些状态呢?如果协程sleep了,那么就是睡眠状态,如果协程刚创建出来,那它肯定是就绪状态,如果协程在等待数据的到来,那就是等待状态。这里这里定义协程的三个运行状态{就绪,睡眠,等待}。
- 新创建的协程,加入就绪集合等待调度
- io未就绪的协程,加入等待集合等待epoll_wait
- 有sleep操作的协程,加入睡眠集合
- 就绪集合没有设置优先级,所以在就绪集合里面的协程优先级一样,那么就可以用队列来存储,先进先出
- 等待集合就是等待IO准备就绪,这个等待IO是有时间长短的,这里用红黑树来存储
- 睡眠集合需要按照睡眠时间的长短进行唤醒,所以也用红黑树存储,key为睡眠时长
我们描述了每一个协程有自己的上下文环境,需要保存 CPU 的寄存器 ctx;需要有子过程的回调函数 func;需要有子过程回调函数的参数 arg;需要定义自己的栈空stack;需要有自己栈空间的大小 stack_size;需要定义协程的创建时间birth;需要定义协程当前的运行状态 status;需要定当前运行状态的结点(ready_next, wait_node, sleep_node);需要定义协程 id;需要定义调度器的全局对象 sched。
typedef struct _nty_coroutine {//cpu ctxnty_cpu_ctx ctx;// funcproc_coroutine func;void *arg;// create timeuint64_t birth;//stackvoid *stack;size_t stack_size;size_t last_stack_size;//statusnty_coroutine_status status;//rootnty_schedule *sched;//co iduint64_t id;//fd eventint fd;uint16_t events;//sleep timeuint64_t sleep_usecs;//setRB_ENTRY(_nty_coroutine) sleep_node;RB_ENTRY(_nty_coroutine) wait_node;TAILQ_ENTRY(_nty_coroutine) ready_node;
} nty_coroutine;
调度器定义
每个协程所需要使用的,而且不同的,就是协程的属性,那么每个协程所需要的,且相同的,就是调度器的属性。用来管理所有协程的属性,作为调度器的属性。调度器的属性,需要有保存 CPU 的寄存器上下文 ctx,可以从协程运行状态yield 到调度器运行的。从协程到调度器用 yield,从调度器到协程用 resume。
typedef struct _nty_schedule {// create timeuint64_t birth;//cpu ctxnty_cpu_ctx ctx;//stack_sizesize_t stack_size;//coroutine numint spawned_coroutines;//default_timeoutuint64_t default_timeout;//当前调度的协程struct _nty_coroutine *curr_thread;//页大小int page_size;//epoll fdint epfd;//线程通知相关,暂未实现int eventfd;//eventsstruct epoll_event eventlist[NTY_CO_MAX_EVENTS];int num_new_events;//setnty_coroutine_queue ready;nty_coroutine_rbtree_sleep sleeping;nty_coroutine_rbtree_wait waiting;
} nty_schedule;
调度的策略
调度器的实现,有两种方案
1.生产者消费者模式
2.多状态运行
生产者消费者模式
多状态运行
7.协程api 与 hook
需要封装为异步的posix api 分析
所有对io的操作,我们都需要取重新封装一遍。为什么不能用posix api,而是我们需要再去封装一次呢?比如我们调用recv的时候,如果我们调用系统的,那么这个fd怎么yield到调度器上呢,所以我们需要在posix api的基础上封装,当然有些接口需要封装,有些不需要。
就像下面的伪代码一样,从同步的recv变成异步的ney_recv
//伪代码
ney_recv(){epoll add fd;yield();epoll del fd;recv(fd);
}
站在同步封装成异步的角度,如果不需要判断io是否就绪的这些api,则不需要封装为异步的。
需要封装的api,这些api在实现的时候,皆采用上面伪代码的策略
1. accept()
2. connect()
3. recv()
4. read()
5. send()
6. write()
7. recvfrom()
8. sendto()
不需要封装的api,这些api因为不会引起阻塞,所以不用封装。
socket()
listen()
close()
fcntl()
setsockopt()
getsockopt()
hook
hook提供了两个接口;1. dlsym()是针对系统的,系统原始的api。2. dlopen()是针对第三方的库。
connect_f = dlsym(RTLD_NEXT, "connect");
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
#include<mysql/mysql.h>
//
// Created by 68725 on 2022/7/17.
//typedef int (*connect_t)(int, struct sockaddr *, socklen_t);connect_t connect_f;typedef ssize_t (*recv_t)(int, void *buf, size_t, int);recv_t recv_f;typedef ssize_t (*send_t)(int, const void *buf, size_t, int);send_t send_f;typedef ssize_t (*read_t)(int, void *buf, size_t);read_t read_f;typedef ssize_t (*write_t)(int, const void *buf, size_t);write_t write_f;int connect(int fd, struct sockaddr *name, socklen_t len) {printf("in connect\n");return connect_f(fd, name, len);
}ssize_t recv(int fd, void *buf, size_t len, int flags) {printf("in recv\n");return recv_f(fd, buf, len, flags);
}ssize_t send(int fd, const void *buf, size_t len, int flags) {printf("in send\n");return send_f(fd, buf, len, flags);
}
ssize_t read(int fd, void *buf, size_t len) {printf("in read\n");return read_f(fd, buf, len);
}ssize_t write(int fd, const void *buf, size_t len) {printf("in write\n");return write_f(fd, buf, len);
}static int init_hook() {connect_f = dlsym(RTLD_NEXT, "connect");recv_f = dlsym(RTLD_NEXT, "recv");send_f = dlsym(RTLD_NEXT, "send");read_f = dlsym(RTLD_NEXT, "read");write_f = dlsym(RTLD_NEXT, "write");
}void main() {init_hook();MYSQL *m_mysql = mysql_init(NULL);if (!m_mysql) {printf("mysql_init failed\n");return;}if (!mysql_real_connect(m_mysql, "192.168.109.1", "root", "123456", "cdb", 3306, NULL, 0)) {printf("mysql_real_connect failed\n");return;}else {printf("mysql_real_connect success\n");}
}
//gcc -o hook hook.c -lmysqlclient -I /usr/include/mysql/ -ldl
8.多核模式
解决协程多核的问题有两种方式,多线程/多进程 与 CPU核心做亲和性。
- 多进程(实现起来容易,对协程代码本身不用去改)
- 多线程(复杂,需要对调度器进行加锁)
那么做多线程对调度器进行加锁,锁放在哪呢?锁放在调度器结构体里面,因为调度器是全局唯一的,那么要锁哪里呢,很明显,<取协程,恢复协程>,这里需要加锁。
相关文章:
纯C协程框架NtyCo
原文是由写的,写的真的很好,原文链接:纯c协程框架NtyCo实现与原理-CSDN博客 1.为什么会有协程,协程解决了什么问题? 网络IO优化 在CS,BS的开发模式下,服务器的吞吐量是一个受关注的参数&#x…...
16、路由守卫:设置魔法结界——React 19 React Router
一、魔法结界的本质 "路由守卫是霍格沃茨城堡的隐身斗篷,在时空裂隙中精准控制维度跃迁!" 魔法部交通司官员挥舞魔杖,React Router 的嵌套路由在空中交织成星轨矩阵。 ——基于《国际魔法联合会》第7号时空协议,路由守…...
try catch + throw
异常 java语言中将,将程序执行中发生的不正常情况称为“异常”。(1)Error;JVM系统内部错误,资源耗尽严重。(2)Execption:运行时异常,编译时异常。 异常体系图 异常处理…...
【东枫电子】AI-RAN:利用人工智能驱动的计算基础设施变革 RAN
太原市东枫电子科技有限公司,翻译 文章目录 摘要一、引言二、AI-RAN 的用例三、AI-RAN 的需求与赋能因素四、参考架构设计五、AI-RAN 概念验证六、结论与未来工作REFERENCES 摘要 无线接入网络 (RAN) 格局正在经历从传统的以通信为中心的基础设施向融合计算通信平台…...
【Linux】第十四章 提高命令行效率
1. #!/bin/bash 是什么意思? #!/bin/bash 出现在 shell 脚本第一行的开头,意味着使用bash shell 命令解释器来执行脚本。 2. PATH变量有什么重要作用? PATH 的作用是告诉操作系统在哪些目录下寻找需要运行命令程序,从而不需要输…...
Lucene中不同搜索类型的使用方法、基本概念、应用场景、差异对比,并通过表格进行总结
为了详细说明Lucene中不同搜索类型的使用方法、差异对比,并通过表格进行总结,我们首先需要理解每种搜索类型的基本概念和应用场景。以下是针对每种搜索类型的简要说明、差异对比以及最终的表格总结。 1. 多字段搜索(MultiFieldQueryParser&am…...
【自然语言处理与大模型】LangChain大模型应用框架入门①
单独为LangChain花大概五篇文章的内容,来和大家一起入门LangChain,这是第一篇重点放在,了解 LangChain 是什么、它的主要用途以及核心模块。了解 LangChain 的设计哲学:链式调用、模块化、可扩展性。安装并学习其中的一个组件&…...
OpenJDK 1.8中-Xloggc参数下GC日志覆盖与追加模式深度解析
#作者:邓伟 文章目录 一、引言二、覆盖模式详解2.1 默认覆盖行为2.2 覆盖模式的适用场景 三、追加模式(日志轮换)深度解析3.1 追加模式的实现:日志轮换配置3.2 日志轮换的底层逻辑3.3 追加模式(日志轮换)适…...
【Java面试笔记:进阶】28.谈谈你的GC调优思路?
在JVM的GC调优中,没有放之四海而皆准的方案,需要结合业务场景、硬件资源和GC特性进行系统性分析。 1.GC调优的基本思路 1.明确调优目标 内存效率(Footprint):减少JVM的内存占用(如边缘设备部署)。低延迟(Latency):降低GC导致的停顿时间(如API网关要求99.9%请求延迟…...
C#中winform窗体如何捕获键盘按键事件
马工撰写的年入30万+C#上位机项目实战必备教程(点击下方链接即可访问文章目录) 1、《C#串口通信从入门到精通》 2、《C#与PLC通信从入门到精通 》 3、《C# Modbus通信从入门到精通》 4、《C#Socket通信从入门到精通 》 5、《C# MES通信从入门到精通》 6、《winform控件从入门…...
USB 网卡——RNDIS 控制消息流程
文章目录 RNDIS 控制消息流程控制写设备回复控制读注意事项RNDIS 控制消息流程 RNDIS 控制消息流程分为三步 控制写(控制端点 0)设备回复(中断端点)控制读(控制端点 0)数据抓包如下 控制写 设备的控制通道是通过 USB 控制端点完成的。 从主机到设备的控制消息作为 SE…...
【AI News | 20250429】每日AI进展
AI Repos 1、aci ACI.dev是一个开源基础设施层,旨在为AI智能体的工具使用提供支持。它通过统一的模型-上下文-协议(MCP)服务器或轻量级Python SDK,使智能体能够以感知意图的方式访问600多种工具,并具备多租户认证、细…...
JavaScript 中深拷贝浅拷贝的区别?如何实现一个深拷贝?
一、区别:像“复印文件” vs “创建副本文件” 1. 浅拷贝(Shallow Copy) 动作:只复制文件的第一页,其他页用“贴标签”指向原文件。结果: 修改第一层属性不影响原对象,但修改嵌套对象会同时改…...
<四级词汇>2025.4.29
compact---所有的人都拍他 越拍越紧 ---紧凑的 impact 往里拍他 -- 碰撞会产生影响 --- 碰撞 影响 influence -- 影响 flue -- 流感 influenza -- 流感 fluent --- fly enter 鸟儿流利飞进屋子 --- 流利的 fluently affluent --- 一再地生活的很流畅 -- 富裕的 …...
mmap核心原理和用途及其与内存映射段的关系
mmap 是 Linux/Unix 系统中的一个关键系统调用,全称是 Memory Map(内存映射)。它的核心功能是将 文件、设备或匿名内存 直接映射到进程的虚拟地址空间,从而实现高效的内存访问和操作。以下是其核心原理和用途的详细说明࿱…...
深入理解 Web Service:原理、组件与核心技术详解
目录 前言1 Web Service 概述2 Web Service 的三大核心角色2.1 服务提供者(Service Provider)2.2 服务请求者(Service Consumer)2.3 服务注册中心(Service Registry) 3 Web Service 核心技术详解3.1 WSDL&a…...
【动态导通电阻】GaN功率器件中动态导通电阻退化的机制、表征及建模方法
2019年,浙江大学的Shu Yang等人在《IEEE Journal of Emerging and Selected Topics in Power Electronics》上发表了一篇关于GaN(氮化镓)功率器件动态导通电阻(Dynamic On-Resistance, RON)的研究论文。该文深入探讨了GaN功率器件中动态导通电阻退化的机制、表征方法、建模…...
AWS SQS 队列策略配置指南:常见错误与解决方案
在 AWS 云服务中,Simple Queue Service (SQS) 是一种完全托管的消息队列服务,广泛应用于分布式系统组件间的解耦。为了确保队列的安全访问,正确配置队列策略至关重要。本文将详细介绍 SQS 队列策略的配置方法,常见错误及其解决方案。 SQS 队列策略基础 SQS 队列策略是基于…...
【后端】【Docker】 Docker 动态代理 取消代理完整脚本合集(Ubuntu)
🚀 Docker 动态代理 & 取消代理完整脚本合集(Ubuntu) 一、动态设置 Docker 代理脚本 支持自定义输入代理地址和端口! 📄 脚本内容(保存为 set_docker_proxy.sh) #!/bin/bash# 读取用户输入 read -p "请输入…...
Java基础学习内容大纲
Java基础学习内容大纲 第一阶段:建立编程思想 Java概述:如何快速学习Java技术、Java历史、Java特点、Sublime、Java运行机制、JDK、转义字符、Java开发规范、Java API 变量:数据类型、变量基本使用、数据类型转换 运算符:运算符介绍、算数运算符、关系运算符、…...
[USACO08DEC] Hay For Sale S Java
import java.util.*;public class Main {public static void main(String[] args) {Scanner sc new Scanner(System.in);int c sc.nextInt(); // 背包容量int h sc.nextInt(); // 物品数量int[] v new int[h 1];for (int i 1; i < h; i) v[i] sc.nextInt();int[] dp …...
碰到的 MRCPv2 串线以及解决思路
最近碰到了一个朋友的 MRCPv2 串线,表现大约是: fs1 和 fs2 同时发到 mrcp server 的同一个 rtp 端口 目前尚不清楚 mrcp server 是否能避免这种情况(其实是有思路的,接收到 rtp 包时检查网络地址和网络端口跟 sip 协商的是否一…...
AI正当时,国内AI HR领先厂商易路如何从“单点突破”到“全面融合”
所谓AI HR,是指将人工智能(AI)技术(如机器学习、自然语言处理、大数据分析等)应用于人力资源管理的各个环节,以提升效率、优化决策并改善员工体验。典型场景有: 在招聘、考勤、薪酬计算等重复…...
MCP 哪家强?深度分析 Cline、Cursor、Trae、Coze 四大平台
想象一下:你只需一句话,AI就能自动抓取GitHub issue、生成代码、调试UI,甚至帮你写博客发到Substack!这不是科幻,而是**Model Context Protocol (MCP)**的现实。MCP作为AI与工具交互的“通用语言”,自2024年…...
【最新 MCP 战神手册 09】利用资源和提示增强上下文
文章目录 1. 开始啦!2. 第一部分:通过资源解锁应用数据3. 第二部分:用提示引导 AI4. 第三部分:高效上下文管理策略1. 开始啦! 在上一章中,我们深入探讨了 MCP 工具——这些组件使得 AI 模型能够执行各种操作。然而,操作很少在真空中发生。为了让 AI 智能地行动,它需要…...
解决vue3 路由query传参刷新后数据丢失的问题
前言:在页面刷新的时候,路由query数据会被清空,网上很多方法说query传参可以实现,反正我是没有实现 思路:将数据保存到本地,通过 “ ?” 进行判断是否有数据,页面销毁的时候删除本地…...
VUE篇之树形特殊篇
根节点是level:1, level3及其子节点有关联,但是和level2和他下面的子节点没有关联 思路:采用守护风琴效果,遍历出level1和level2级节点,后面level3的节点,采用树形结构进行关联 <template><div :class"…...
Qwen多模态系列论文
From:https://www.big-yellow-j.top/posts/2025/04/28/QwenVL.html 本文主要介绍Qwen-vl系列模型包括:Qwen2-vl、Qwen2.5-vl Qwen2-vl http://arxiv.org/abs/2409.12191 模型结构: Qwen2-vl主要的改进点在于:1、使用动态分辨率(…...
Astro大屏中关于数据流转的数据接入与数据中心之间的逻辑关系梳理
在 Astro 大屏中,「数据接入」与「数据中心」是紧密关联的一对模块,分别承担“数据从哪来”和“数据怎么管”的职责。为了更形象地帮助初学者理解,我可以用“自来水系统”的比喻来解释整个原理与操作逻辑: 🏠 形象比喻…...
【今日三题】小红的ABC(找规律) / 不相邻取数(多状态dp) / 空调遥控(排序+二分/滑动窗口)
⭐️个人主页:小羊 ⭐️所属专栏:每日两三题 很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~ 目录 小红的ABC(找规律)不相邻取数(多状态dp)空调遥控(排序二分/滑动窗口) 小红的ABC(找规律) 小红的ABC 找最短回文串ÿ…...
双重差分模型学习笔记(理论)
【DID最全总结】90分钟带你速通双重差分!_哔哩哔哩_bilibili 一、DID的基本原理 (一) 单重差分 1. Cross-Section Comparison 截面数据 只有某个时间点事件发生后的数据 D1 事件发生后 D0 事件发生前 2. Befor-After Vomparison 时间序列…...
企业级私有化部署,内部聊天软件
如何在激烈的市场竞争中脱颖而出,提升工作效率、降低运营成本,同时保障信息安全,成为众多企业管理者亟待解决的问题。而BeeWorks 企业级私有化部署的内部聊天软件,无疑是为这一难题提供了一把金钥匙。 BeeWorks覆盖即时通讯&#…...
HCIA-Datacom 高阶:VLAN、VLANIF 与静态路由综合实验
拓扑图解读 从拓扑图中可以看到,存在三层交换机 LSW3、普通交换机 LSW4、路由器 R2 以及 PC1 - PC4。LSW4 连接了三个不同 VLAN 的 PC(PC1 属于 VLAN 10、PC2 属于 VLAN 20、PC3 属于 VLAN 30 ),并通过 Ethernet 0/0/1 端口以 tr…...
Android ndk 编译opencv后部分接口std::__ndk1与项目std::__1不匹配
1、opencv-4.11预编译命令(在opencv4.5.0之后兼容免费features2d做特征匹配),NDK版本选用的android-ndk-r23c-linux.zip cmake -G Ninja \ -DCMAKE_TOOLCHAIN_FILE/home/who/Downloads/NDK/android-ndk-r23c/build/cmake/android.toolchain.…...
SQL命令一:SQL 基础操作与建表约束
目录 引言 一、SQL 基础命令 (一)数据库相关操作 (二)表格相关操作 (三)MySQL 常用数据类型 二、增删改查(CRUD)操作 (一)增加数据 (二&a…...
颜色分类,不靠“调色盘”:双指针 VS 计数排序的正面PK
颜色分类,不靠“调色盘”:双指针 VS 计数排序的正面PK 在算法圈混得久了,总有一些题目是面试官的心头好,刷题人绕不过的“鬼门关”。“颜色分类”(LeetCode 75)就是其中之一,看似小儿科…...
Shopify网上商店GraphQL Admin接口查询实战
目录 一、Shopify网上商店 二、个人商店配置接口权限 三、PostMan调用接口测试 四、通过Java服务调用接口 一、Shopify网上商店 Shopify是由Tobi Ltke创办的加拿大电子商务软件开发商,总部位于加拿大首都渥太华,已从一家在咖啡店办公的 5人团队&…...
Laravel基础
Laravel 基础 01.Laravel入门和安装 Composer安装Laravel步骤 要使用 Composer 安装 Laravel,请按照以下步骤操作: 确保已经安装了 Composer。如果还没有安装,请访问 https://getcomposer.org/download/ 下载并安装。 打开命令行或终端。…...
【Leetcode 每日一题 - 补卡】2302. 统计得分小于 K 的子数组数目
问题背景 一个数组的 分数 定义为数组之和 乘以 数组的长度。 比方说, [ 1 , 2 , 3 , 4 , 5 ] [1, 2, 3, 4, 5] [1,2,3,4,5] 的分数为 ( 1 2 3 4 5 ) 5 75 (1 2 3 4 5) \times 5 75 (12345)575。 给你一个正整数数组 n u m s nums nums 和一个整数 k…...
力扣——206.反转链表倒序输出链表
206. 反转链表 - 力扣(LeetCode) 思路(迭代) 设三个指针,前后两个指针都为空,当前指针为输入的头指针 开始循环——判断条件为当前节点不为空 先给下一个节点赋值为——当前节点的下一个 改变当前节点的…...
Arthas在Java程序监控和分析中的应用
Arthas在Java程序监控和分析中的应用 在互联网大厂Java求职者的面试中,经常会被问到关于使用Arthas来监控和分析Java程序的相关问题。本文通过一个故事场景来展示这些问题的实际解决方案。 第一轮提问 面试官:马架构,欢迎来到我们公司的面…...
第13讲:图形尺寸与分辨率设置——适配论文版面,打造专业图稿!
目录 📌 为什么这一讲重要? 🎯 一、先认识几个关键词 ✍️ 二、ggsave() 是导出图的标准方法 📐 三、尺寸设置技巧:对齐目标期刊 🔍 找到目标期刊的图形栏宽 📦 四、多个图组合导出(与 patchwork 搭配) 🧪 五、使用 Cairo / ragg 导出高质量图 🎁 六…...
Docker与Vmware网络模式的对别
前言 在使用了很久的VMware和Docker后,分别独立配置过他们的网络,但是每次配置一方时,总感觉和另一方有点不一样,但是也没有来得及总结。刚好最近有时间可以总结一下。 重点: 1、VMware的桥接模式和Docker的桥接模式完…...
大模型在肾癌诊疗全流程中的应用研究报告
目录 一、引言 1.1 研究背景与意义 1.2 研究目的与方法 1.3 国内外研究现状 二、大模型预测肾癌术前情况 2.1 基于影像组学的肾癌良恶性及分级预测 2.1.1 MRI 影像组学模型预测肾透明细胞癌分级 2.1.2 CT 影像深度学习模型鉴别肾肿物良恶性及侵袭性 2.2 大模型对手术风…...
Springboot使用登录拦截器LoginInteceptor来做登录认证
创建拦截器LoginInteceptor类 interceptors/LoginInteceptor.java package org.example.interceptors;import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.example.utils.JwtUtil; import org.springframework.s…...
2025年- H13-Lc120-189.轮转数组(普通数组)---java版
1.题目描述 2.思路 import java.util.Arrays;public class H189 {public static void main(String[] args) {int[] newArr {1, 2, 3, 4, 5};int[] nums new int[5];System.arraycopy(newArr,0,nums,0,4);System.out.println(Arrays.toString(nums)); } }补充2: 3.…...
Android Framework常见问题
以下是不同难度级别的 Android Framework 面试题,包含答案要点,可帮助你为面试做好准备。 初级难度 1. 请简要解释 Android Framework 是什么。 答案要点:Android Framework 是 Android 系统的核心组成部分,它为开发者提供了一…...
【AI】图片处理的AI工具
博主最近需要给客户展示一下做的一些设备和仪器,随手拍了一些照片,觉的背景不是很好看,于是在网上寻找AI图片处理工具。后来随手用了一下豆包AI,发现很好用,这里把一点使用的心得体会记录一下,并和大家分享…...
Python列表全面解析:从基础到高阶操作
一、为什么需要列表? 在Python中,列表是可变有序序列,用于存储多个元素的容器。相较于单一变量存储独立值,列表能更高效地管理批量数据,其特点包括: 引用存储:列表元素存储的是对象的引用…...
C++调用C动态库编译时报undefined reference to “funcxxx“错误
问题描述:Linux平台上C调用C库进行make编译时报undefined reference to "funcxxx"错误,错误实例如下: /usr/bin/ld: CMakeFiles/dialog.dir/widgets/widget.cpp.o: in function Widget::loadVerificationModule(): /home/zhangxia…...