【Create my OS】6 线程调度

December 15, 2024

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;            // 线程ID
    int 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);     // 从就绪线程中选择一个运行,如果没有可运行的线程则返回 -1
    int     (* 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;      // 当前运行线程tid
    exitFromPool(&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)):

alt text

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

  • 队列中的元素如下定义:
// kernel/rrscheduler.c

// 双向环形链表来实现队列,队列元素如下
// 链表的节点按照 tid + 1 都存放在数组中,其中下标 0 处为 Dummy Head,用于快速找到队列头
typedef struct
{
    int valid;      // 标记线程是否有效
    usize time;     // 线程剩余时间片
    int prev;       // 前一个线程tid
    int 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;        // 设置最大时间片为1
    rrScheduler.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;      // 更新下一个线程的prev
        rrScheduler.threads[prev].next = next;      // 更新上一个线程的next
        rrScheduler.threads[ret].prev = 0;          // 清空当前线程的prev
        rrScheduler.threads[ret].next = 0;          // 清空当前线程的next
        rrScheduler.threads[ret].valid = 0;         // 标记当前线程为无效
        rrScheduler.current = ret;                  // 设置调度器当前线程为弹出线程
    }
    return ret-1;   // 调整索引
}

// 提醒调度算法当前线程又运行了一个 tick
// 输出:1-表示调度算法认为当前线程需要被切换出去,0-不需要切换出去
int
schedulerTick()
{
    int tid = rrScheduler.current;  // 获取当前线程tid
    if(tid != 0) {
        // 当前线程有效
        rrScheduler.threads[tid].time -= 1;     // 当前线程时间片-1
        if(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 才运行结束。

Profile picture

Written by JokerDebug who works at Southeast University, Nanjing, China You can follow me on Github