当前位置: 首页 > news >正文

【Create my OS】6 线程调度

Github代码仓库链接

上一节我们已经实现了线程的基本结构并且能够切换到新的线程,但是这个切换过程是我们手动指定的。这一节我们来实现内核线程调度,使得我们只需要创建线程,处理器就会按照某个调度算法自动调入调出线程,实现并发。

6.1 线程管理

1、线程辅助状态:我们目前的线程 Thread 结构体只存储了线程上下文相关的信息,我们需要更多的信息来用于线程的调度。首先就是线程的状态,这里划分四个状态:

  • Ready,线程就绪
  • Running,线程正在占有 CPU 执行
  • Sleeping,线程等待资源而休眠
  • Exited,线程退出。其实 Exited 状态可有可无,因为一个线程调用 Exit() 退出时就会被直接回收资源,而不会继续存储在线程池中。
// kernel/thread.h/* 线程状态 */
typedef enum {Ready,      // 就绪Running,    // 运行Sleeping,   // 休眠Exited      // 退出
} Status;
  • 接着我们就可以定义存储在线程池中的线程信息了。其实定义的是线程池中的一个线程信息空位。
// kernel/thread.h/* 线程池中的线程信息槽 */
typedef struct {Status status;      // 线程状态int tid;            // 线程IDint occupied;       // 该槽位是否被占用Thread thread;      // 线程
} ThreadInfo;
  • 我们同时定义一个结构 RunningThread,用来表示一个正在运行的线程,其实就是将 tid 和 Thread 封装一下。
// kernel/thread.h// 正在运行的线程
typedef struct {int tid;Thread thread;
} RunningThread;

2、线程池

  • 我们定义一个结构体,用于存储调度算法的一些函数。这相当于一个算法框架,要实现一个调度算法只需要实现其中的函数即可。
// krenel/thread.h// 调度器算法实现(函数指针)
typedef struct {void    (* init)(void);     // 初始化调度器void    (* push)(int);      // 将一个线程加入线程调度int     (* pop) (void);     // 从就绪线程中选择一个运行,如果没有可运行的线程则返回 -1int     (* tick)(void);     // 提醒调度算法当前线程又运行了一个 tick,返回的 int 表示调度算法认为当前线程是否需要被切换出去void    (* exit)(int);      // 告诉调度算法某个线程已经结束
} Scheduler;
  • 接着就可以定义线程池了
// kernel/consts.h// 线程池最大线程数
#define MAX_THREAD          0x40// kernel/thread.h// 线程池
typedef struct {ThreadInfo threads[MAX_THREAD];Scheduler scheduler;
} ThreadPool;

3、线程池相关函数

  • allocTid() 函数用于遍历线程池,寻找一个未被使用的 tid。若所有 tid 都被使用则会进入 panic。
  • addToPool() 函数用于将一个线程添加到线程池中,线程池会为其分配一个 tid,并分配一个空位保存这个线程相关的信息,并通知调度算法让这个线程参与调度(调度算法只会操作 tid)。
  • acquireFromPool() 函数用于向线程池获取一个可以运行的线程,由于调用该函数的下一步就要直接切换到这个线程,所以在线程池中直接标记为 Running 状态。如果线程池中没有可以运行的线程,那么返回的 RunningThread 中的 tid 为 -1。
  • retrieveToPool() 函数会在一个线程停止运行,切换回调度线程后调用,用于修改线程池内的线程信息。线程停止运行有两种情况,一种是线程运行结束,另一种是还没有运行完,但是时间片用尽,这种情况就需要重新将线程加入调度器。
  • tickPool() 函数基本就是对调度器的 tick() 函数的包装,用于查看当前正在运行的线程是否需要切换。
  • exitFromPool() 函数的参数是 tid,用于释放该 tid 线程信息的空位,并且通知调度器,让这个 tid 不再参与调度。
// kernel/thread.c// 遍历线程池,寻找未被使用的tid
int
allocTid(ThreadPool *pool)
{int i;for(i = 0; i < MAX_THREAD; i++) {if(!pool->threads[i].occupied)return i;}panic("Alloc tid failed!\n");return -1;
}// 将线程添加到线程池中
void
addToPool(ThreadPool *pool, Thread thread)
{int tid = allocTid(pool);           // 遍历线程池,寻找未使用tid// 配置线程信息pool->threads[tid].status = Ready;  // 就绪pool->threads[tid].occupied = 1;    // 占用pool->threads[tid].thread = thread; // 线程上下文地址和栈底地址pool->scheduler.push(tid);          // 将线程加入参与调度
}// 向线程池获取一个可以运行的线程,若没有返回-1
RunningThread
acquireFromPool(ThreadPool *pool)
{int tid = pool->scheduler.pop();    // 从就绪线程中获取一个可运行线程RunningThread rt;rt.tid = tid;if(tid != -1) {ThreadInfo *ti = &pool->threads[tid];   // 从线程池取出线程// 修改取出线程在线程池的状态(上行代码用&引用传入的)ti->status = Running;   // 由于调用该函数的下一步就要直接切换到这个线程,所以在线程池中直接标记为 Running 状态ti->tid = tid;          // 线程ID(因为将线程添加到线程池中时没用设置ThreadInfo.tid,所以这里初始化)rt.thread = ti->thread;}return rt;
}// 修改线程池内的线程信息:在一个线程停止运行,切换回调度线程后调用
// 线程停止运行有两种情况
//      一种是线程运行结束
//      一种是还没有运行完,但是时间片用尽,这种情况就需要重新将线程加入调度器
void
retrieveToPool(ThreadPool *pool, RunningThread rt)
{int tid = rt.tid;// 若线程不被占用了,即线程运行结束if(!pool->threads[tid].occupied) {      // 表明刚刚这个线程退出了,回收栈空间(传入栈底地址,根据HEAP维护的二叉树,即可知道回收多大空间)kfree((void *)pool->threads[tid].thread.kstack);return;}// 线程时间片用完,重新加入调度器ThreadInfo *ti = &pool->threads[tid];ti->thread = rt.thread;         // 更新线程上下文、栈地址if(ti->status == Running) {ti->status = Ready;         // 更新线程状态pool->scheduler.push(tid);  // 加入线程调度}
}// 对调度器的 tick() 函数包装,用于查看当前正在运行的线程是否需要切换
int
tickPool(ThreadPool *pool)
{// 提醒调度算法当前线程又运行了一个 tick,返回的 int 表示调度算法认为当前线程是否需要被切换出去return pool->scheduler.tick();
}// 释放该 tid 线程信息的占用位,并且通知调度器让这个 tid 不再参与调度
void
exitFromPool(ThreadPool *pool, int tid)
{pool->threads[tid].occupied = 0;    // 清除占用标志pool->scheduler.exit(tid);          // 告诉调度算法某个线程已经结束
}
6.2 调度线程

1、我们所有的运行流程都是运行在线程中的,如果我们要对所有的线程进行调度,我们还需要另外创建一个线程专门用于调度。调度线程的作用是:

  1. 当没有线程在运行时,调度线程根据一定的策略来选择一个线程来执行;
  2. 当一个线程被调度器判断需要让出 CPU 控制权时,例如运行时间过长或者运行结束,并不是直接切换到另一个线程,而是先切换到这个调度线程,让调度线程根据一定的策略来选择另一个线程执行。
  • 我们定义一个结构,用来保存调度线程参与调度所需要的所有信息
// kernel/thread.h// 调度线程参与调度所需要的所有信息
typedef struct {ThreadPool pool;        // 线程池Thread idle;            // 调度线程RunningThread current;  // 当前运行线程信息int occupied;           // 当前是否有线程(除了调度线程)正在运行
} Processor;
  • 我们需要定义一个全局唯一的 Processor,来进行调度。
// kernel/processor.c// 全局唯一的 Processor 实例
static Processor CPU;

2、我们需要在进入 idle 线程时关闭调度,防止调度过程被时钟打断,并在某个适当的时机恢复。涉及的就是关闭全局中断,通过设置sstatus寄存器实现操作。

// kernel/riscv.h/* 打开异步中断,并等待中断 */
static inline void
enable_and_wfi()
{   // csrsi - 控制状态寄存器某个位, 1<<1 - 置位第二位SIE// wfi - Wait for Interrupt特殊指令,用于暂停 CPU 直到某个中断发生,CPU进入低功耗状态asm volatile("csrsi sstatus, 1 << 1; wfi");
}/* 关闭异步中断并保存原先的 sstatus */
static inline usize
disable_and_store()
{usize x;        // 保存操作后的 sstatus 返回// csrrci - CSR read and clear with Immediate,清除SIE位并存储到%0(即x)asm volatile("csrrci %0, sstatus, 1 << 1" : "=r" (x) );return x;
}/* 用 flags 的值恢复 sstatus */
static inline void
restore_sstatus(usize flags)
{// cars - CSR set with Immediate,用输入变量flags的值设置sstatus寄存器asm volatile("csrs sstatus, %0" :: "r"(flags) );
}

3、线程调度操作相关的函数

  • initCPU() 函数使用 idle 线程和 pool 线程池来对 CPU 进行初始化,参数 pool 主要就是为了指定这个 Processor 所使用的调度算法。
  • addToCPU() 函数主要就是对 addToPool() 函数的包装,不用做其他处理。
  • exitFromCPU() 这个函数由线程主动执行,效果类似于 exit(),用于主动通知 CPU 这个线程运行结束,CPU 会通知线程池释放资源,并切换到 idle 线程进行下一步调度。
  • runCPU() 函数,用于切换到 idle 线程,表示正式由 CPU 进行线程管理和调度,这个函数通常在启动线程中调用,由于启动线程被构造为一个局部变量,我们再也无法切换回启动线程,相当于操作系统的初始化工作已经结束。
// kernel/processor.c// 对CPU(调度线程)初始化
// 使用 idle 线程和 pool 线程池来对 CPU 进行初始化
// 参数 pool 主要就是为了指定这个 Processor 所使用的调度算法
void
initCPU(Thread idle, ThreadPool pool)
{CPU.idle = idle;    // 调度线程CPU.pool = pool;    // 线程池CPU.occupied = 0;   // 当前没有线程在运行
}// 将线程添加到CPU管理的线程池中(对 addToPool() 进行包装)
void
addToCPU(Thread thread)
{addToPool(&CPU.pool, thread);
}// 线程主动退出,通知 CPU 这个线程运行结束
// CPU 会通知线程池释放资源,并切换到 idle 线程进行下一步调度
void
exitFromCPU(usize code)
{disable_and_store();            // 关闭异步中断int tid = CPU.current.tid;      // 当前运行线程tidexitFromPool(&CPU.pool, tid);   // 清除线程池中占用标记,告诉调度算法线程已经结束printf("Thread %d exited, exit code = %d\n", tid, code);switchThread(&CPU.current.thread, &CPU.idle);   // 切换到调度器线程
}// 切换到 idle 线程,表示正式由 CPU 进行线程管理和调度,这个函数通常在启动线程中调用
// 由于启动线程被构造为一个局部变量,我们再也无法切换回启动线程,相当于操作系统的初始化工作已经结束
void
runCPU()
{   Thread boot = {0L, 0L};         // 启动线程switchThread(&boot, &CPU.idle); // 从启动线程切换进 idle,boot 线程信息丢失,不会再回来
}

4、线程调度的入口点函数,idleMain(),是调度线程最核心的函数。调度线程的所有逻辑都在这个函数中循环。

// kernel/processor.c// 线程调度的入口点函数,是调度线程最核心的函数
void
idleMain()
{// 进入 idle 时禁用异步中断disable_and_store();while(1) {// 向线程池获取一个可以运行的线程RunningThread rt = acquireFromPool(&CPU.pool);if(rt.tid != -1) {// 有线程可以运行CPU.current = rt;       // 设置调度器当前线程CPU.occupied = 1;       // 标志线程正在运行printf("\n>>>> will switch_to thread %d in idle_main!\n", CPU.current.tid);// 从调度器线程 切换到 当前线程switchThread(&CPU.idle, &CPU.current.thread);  // 切换回 idle 线程处printf("<<<< switch_back to idle in idle_main!\n");CPU.occupied = 0;       // 标记当前没有线程正在运行// 修改线程池内的线程信息:在一个线程停止运行,切换回调度线程后调用retrieveToPool(&CPU.pool, CPU.current);} else {// 无可运行线程,短暂开启异步中断并处理enable_and_wfi();disable_and_store();}}
}

5、时钟中断引发调度:线程调度,很重要的一个特点就是由时钟中断来触发。

  • tickCPU() 函数在时钟中断时被调用,每当时钟中断发生时,如果当前有正在运行的线程,都会检查一下当前线程的时间片是否用完,如果用完了就需要切换到调度线程。
// kernel/processor.c// 在时钟中断时被调用,每当时钟中断发生时,如果当前有正在运行的线程,
// 都会检查一下当前线程的时间片是否用完,如果用完了就需要切换到调度线程
void
tickCPU()
{// 判断当前是否有正在运行线程(不是 idle)if(CPU.occupied) {// 当前线程运行时间片是否耗尽if(tickPool(&CPU.pool)) {// 关闭中断usize flags = disable_and_store();// 切换到 idle 调度器线程switchThread(&CPU.current.thread, &CPU.idle);// 某个时刻再切回此线程时从这里开始restore_sstatus(flags);}}
}
  • 不要忘了在时钟中断处理函数中调用这个函数。
// kernel/interrupt.c// 时钟中断处理:设置下一次时钟中断时间
void
supervisorTimer()
{extern void tick(); tick();         // 设置下一次时钟中断时间extern void tickCPU(); tickCPU();   // 检查当前线程的时间片是否用完
}
6.3 Round-Robin 调度算法

1、我们在第一节已经实现了一个调度算法的框架,只要实现其中的五个函数即可,本节将实现一个很基础的 Round-Robin 调度算法 wiki(即时间片轮转调度算法)。大致思想(下图来自小林coding图解操作系统6.1 进程调度/页面置换/磁盘调度算法 | 小林coding (xiaolincoding.com)):

在这里插入图片描述

2、我们使用一个双向环形链表来实现队列,链表的节点按照 tid + 1 都存放在数组中,其中下标 0 处为 Dummy Head,用于快速找到队列头。

  • 队列中的元素如下定义:
// kernel/rrscheduler.c// 双向环形链表来实现队列,队列元素如下
// 链表的节点按照 tid + 1 都存放在数组中,其中下标 0 处为 Dummy Head,用于快速找到队列头
typedef struct
{int valid;      // 标记线程是否有效usize time;     // 线程剩余时间片int prev;       // 前一个线程tidint next;       // 后一个线程tid
} RRInfo;
  • 这些元素并不存储 Thread,只存储 tid,这种实现方式侵入性较小,耦合度低,便于替换。
  • 定义一个结构体用于存储调度器相关信息,其中 current 表示当前正在运行的线程的 tid。
// kernel/rrscheduler.c// 调度器信息结构体
struct
{RRInfo threads[MAX_THREAD + 1]; // 优先级调度队列(由于 0 号位有个 Dummy Head,所以 threads 数组的长度为 MAX_THREAD + 1)usize maxTime;                  // 最大时间片int current;                    // 当前正在运行的tid
} rrScheduler;

3、具体的五个调度函数实现,代码中附有详细注释:

// kernel/rrscheduler.c// 初始化调度器
void
schedulerInit()
{rrScheduler.maxTime = 1;        // 设置最大时间片为1rrScheduler.current = 0;        // 当前没有线程运行,设置当前线程为0/* 第 0 个位置为 Dummy head,用于快速找到链表头和尾 */RRInfo ri = {0, 0L, 0, 0};      // 初始化一个无效的线程信息结构rrScheduler.threads[0] = ri;
}// 将一个线程加入线程调度,即加入调度队列尾部
void
schedulerPush(int tid)
{tid += 1;       // 调整索引if(tid + 1 > MAX_THREAD + 1) {panic("Cannot push to scheduler!\n");}// 若线程没有时间片,初始化为最大时间片if(rrScheduler.threads[tid].time == 0) {rrScheduler.threads[tid].time = rrScheduler.maxTime;}// 获取当前队列尾部int prev = rrScheduler.threads[0].prev;// 将线程加入队列尾部rrScheduler.threads[tid].valid = 1;     // 标记线程有效rrScheduler.threads[prev].next = tid;   // 尾部next指向当前线程rrScheduler.threads[tid].prev = prev;   // 当前线程prev指向尾部线程rrScheduler.threads[0].prev = tid;      // 头部prev指向当前线程rrScheduler.threads[tid].next = 0;      // 当前线程next指向头部
}// 从就绪线程中选择一个运行,如果没有可运行的线程则返回 -1
int
schedulerPop()
{// 获取队列一个有效线程int ret = rrScheduler.threads[0].next;  if(ret != 0) {// 若有可用线程,则从队列头部弹出int next = rrScheduler.threads[ret].next;   // 获取该线程的下一个线程int prev = rrScheduler.threads[ret].prev;   // 获取该线程的上一个线程rrScheduler.threads[next].prev = prev;      // 更新下一个线程的prevrrScheduler.threads[prev].next = next;      // 更新上一个线程的nextrrScheduler.threads[ret].prev = 0;          // 清空当前线程的prevrrScheduler.threads[ret].next = 0;          // 清空当前线程的nextrrScheduler.threads[ret].valid = 0;         // 标记当前线程为无效rrScheduler.current = ret;                  // 设置调度器当前线程为弹出线程}return ret-1;   // 调整索引
}// 提醒调度算法当前线程又运行了一个 tick
// 输出:1-表示调度算法认为当前线程需要被切换出去,0-不需要切换出去
int
schedulerTick()
{int tid = rrScheduler.current;  // 获取当前线程tidif(tid != 0) {// 当前线程有效rrScheduler.threads[tid].time -= 1;     // 当前线程时间片-1if(rrScheduler.threads[tid].time == 0) {    return 1;       // 时间片用尽则切换出去} else {return 0;       // 否则不切换}}return 1;   // 如果当前线程也进行切换
}// 告诉调度算法某个线程已经结束
void
schedulerExit(int tid)
{tid += 1;   // 调整索引// 判断结束的线程是否为当前正在运行的线程if(rrScheduler.current == tid) {rrScheduler.current = 0;    // 将当前线程设置为0,表示没有线程在运行}
}
6.4 调度测试

1、我们完成了所有的部分,终于可以开始测试了,我们计划创建一些线程,线程的入口点是这个函数:

// kernel/thread.c// 线程测试函数,作为入口点
void
helloThread(usize arg)
{printf("Begin of thread %d\n", arg);int i;// 将传入的参数输出800遍for(i = 0; i < 800; i ++) {printf("%d", arg);}printf("\nEnd of thread %d\n", arg);exitFromCPU(0);     // 退出while(1) {}
}
  • 会将传入的参数输出 800 遍,之后调用 exitFromCPU() 退出。
  • 初始化线程更新为如下:
// kernel/thread.c// 初始化线程
void
initThread()
{// 1.创建调度函数实现Scheduler s = {schedulerInit,schedulerPush,schedulerPop,schedulerTick,schedulerExit};s.init();   // 初始化调度器// 2.创建线程池ThreadPool pool = newThreadPool(s);// 3.构建idle调度线程Thread idle = newKernelThread((usize)idleMain);// 4.初始化CPU调度器initCPU(idle, pool);// 5.构造线程并添加到CPU中usize i;for(i = 0; i < 5; i ++) {Thread t = newKernelThread((usize)helloThread); // 构造新内核线程usize args[8];args[0] = i;appendArguments(&t, args);   // 为线程传入初始化参数// 6.启动addToCPU(t);    // 将线程添加到调度队列中}printf("***** init thread *****\n");
}
  • main函数中,加入线程初始化和切换到idle调度线程
void main()
{extern void initInterrupt();    initInterrupt();    // 设置中断处理程序入口 和 模式extern void initTimer();        initTimer();        // 时钟中断初始化extern void initMemory();       initMemory();       // 初始化 页分配 和 动态内存分配extern void mapKernel();        mapKernel();        // 内核重映射,三级页表机制extern void initThread();       initThread();       // 初始化线程管理extern void runCPU();           runCPU();           // 切换到 idle 调度线程,表示正式由 CPU 进行线程管理和调度while(1) {}
}
  • 运行输出结果如下:
==== Init Interrupt ====
***** Init Memory *****
***** Remap Kernel *****
***** init thread *****>>>> will switch_to thread 0 in idle_main!
Begin of thread 0
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
End of thread 0
Thread 0 exited, exit code = 0
<<<< switch_back to idle in idle_main!>>>> will switch_to thread 1 in idle_main!
Begin of thread 1
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111<<<< switch_back to idle in idle_main!>>>> will switch_to thread 2 in idle_main!
Begin of thread 2
22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222
End of thread 2
Thread 2 exited, exit code = 0
<<<< switch_back to idle in idle_main!>>>> will switch_to thread 3 in idle_main!
Begin of thread 3
33333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333
End of thread 3
Thread 3 exited, exit code = 0
<<<< switch_back to idle in idle_main!>>>> will switch_to thread 4 in idle_main!
Begin of thread 4
444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444<<<< switch_back to idle in idle_main!>>>> will switch_to thread 1 in idle_main!
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
End of thread 1
Thread 1 exited, exit code = 0
<<<< switch_back to idle in idle_main!>>>> will switch_to thread 4 in idle_main!
44444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444
End of thread 4
Thread 4 exited, exit code = 0
<<<< switch_back to idle in idle_main!
  • 你的输出可能与我不完全一样,但是可以看出,线程 1 在第一次运行时没有来得及运行结束,就被切换到线程 2 了,在线程 3 运行结束后,线程 1 又被调度占用了 CPU 才运行结束。
http://www.xdnf.cn/news/1050643.html

相关文章:

  • logback常用配置
  • 自增id用完怎么办?
  • C# CS_Prj01 串口通信控制台程序
  • Java学习笔记——lambda表达式
  • 【FastAPI高级实战】结合查询参数与SQLModel Joins实现高效多表查询(分页、过滤、计数)
  • 微信开发者工具 插件未授权使用,user uni can not visit app
  • Gartner《Reference Architecture for Federated Analytics》学习心得
  • SAP代理商——哲讯科技,助力企业数字化转型
  • 严格三角形方程组
  • 用c语言实现简易c语言扫雷游戏
  • 解析Buildroot
  • html css js网页制作成品——HTML+CSS+js力学光学天文网页设计(4页)附源码
  • MySQL基础架构
  • 深度学习:PyTorch自动微分模块
  • 【CUDA GPU 支持安装全攻略】PyTorch 深度学习开发者指南
  • AI 社交和AI情绪价值的思考 -延申思考2 -全局记忆
  • MobaXterm首次连接Ubuntu失败
  • Rust语言典型并发模式小结
  • 【Dv3Admin】系统信号机制配置文件解析
  • Photoshop图层蒙版全介绍
  • Go实战项目OneX介绍(2/12):项目功能列表介绍
  • 【编程语言】从C++到Java到Golang,语法特性对比与开发实践指南(基础语法,类型系统,运行机制)
  • 多模态大语言模型arxiv论文略读(123)
  • Go语言--语法基础5--基本数据类型--输入输出(2)
  • GRPO训练布局感知的强化学习多模态文档解析框架-Infinity-Parser
  • HarmonyOSNext性能核弹:用Node-API引爆ArkTS/C++跨语言
  • 贝塞尔曲线的公式推导贝塞尔曲线的公式推导,和 SVG 中 Path 的贝塞尔曲线指令的理解记忆
  • 电感篇---常见作用
  • 2.SQL语句分类
  • css属性:图片使用filter属性使得position: fixed失效的情况