第09章上 内核线程
Posted perfy576
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第09章上 内核线程相关的知识,希望对你有一定的参考价值。
处理器任意时刻只能执行一个任务,真正的并行指的是多个处理器同时工作,一台计算机的病性能力屈居于其物理处理器的数量。当以计算只有1个处理器的时候,非要让他兼顾其他任务,位移的做法就是让每个任务在处理器上执行一小会,然后换下一个任务上处理器,知道所有任务都执行完毕。
这种任务的换上换下工作是有任务调度去完成的,任务调度器是操作系统中用于把任务轮调度上处理器运行的一个软件模块是操作系统的一部分。调度器在内核中维护一个任务表,也称为进程表、线程表、调度表。然后按照一定的算法,在每次时钟中断的时候判断是否需要更换任务,如果需要更换任务就从任务表中选择一个任务然后把该任务放上处理器运行。正式因为有了调度器,多任务操作系统才得以实现,他是多任务的核心,它的好坏直接印象了系统的效率。
这种伪并行的好处是降低了任务的平均响应时间,通俗点就是让那些执行时间段的任务不会应为后到,而等待过长的时间,整体上显得快得多。缺点在于任务切换是有软件完成的,奇幻本身必然要消耗处理器周期,因此切换的越频繁,那么所有任务的总执行时间反而更长了。
指令是有处理器来执行的,每个程序或是功能独立的函数,都有自己的执行方向,或是执行流程,称为执行流,执行流是独立的,各个执行流之间有自己的栈,有自己的一套寄存器印象(就是寄存器的值)和内存资源,这正式执行流的上下文环境。在任务调度器眼里只有执行流才能 独立的上处理器运行。对于执行流,可以理解为就是线程和进程。
任务只是认为划分的,逻辑上的概念,同长吧一个个的执行单元称为任务。而这些执行单元就是彼此独立的执行流。
1 线程
线程是一套机制,此机制可以为代码块创造他所依赖的上下文环境,从而让代码具有独立性,因此在原理上线程能使一端函数称为调度单元,是函数能被调度器认可,从人能够被专门调度到处理器上执行。函数就可以被加入到线程表中作为调度器的调度单元,从而就有机会单独获取处理器资源。也就是说不是吧线程调用的函数和其他指令混在一块执行,或者是不是在执行整个进程是顺表执行了该函数,而是单独、转们执行了此函数。
1.1 线程和进程
程序是直静止的,存储在文件系统上,尚未运行的指令代码,是实际运行时程序的映像。
进程是指在运行中的程序,程序必须获得其运行所需要的各类资源才能成为进程,包括栈,寄存器等。
对处理器来说进程是一种控制集合,集合中至少包含一条执行流。而执行流是相互独立的,但是它们共享进程的所有资源,是处理器的执行单位,或是调度单位,这里的执行流就是线程。
线程是具有执行力的独立的代码块。进程是线程+资源。
1.2 进程表身份证PCB
为了唯一的标识某个进程,操作系统需要为每个进程提供一个PCB(Process Control Block)进程控制块,是进程的身份证,用来记录与进程相关的信息,比如进程状态,PID,优先级等。
每个进程又有自己的PCB,所有的PCB都放在一张表格中维护,称为进程表。调度器可以根据这张表选择进程换上处理器运行进程。
PCB中一般包含的字段有:寄存器映像,栈,栈指针,进程号,进程状态,优先级,时间片,页表,打开的文件描述符,父进程等。
1.3 线程的两种实现方式
起初操作系统中只有进程的概念,人么那时候对并发没有太高的要求,后来有些人想提高程序的并发,这才有了线程。
线程仅仅是个执行流,在用户空间还是在内核空间实现他, 最大的区别是线程表在哪里,有谁来调度他上处理器。如果线程在用户空间实现,线程表就在用户进程中,用户进程需要专门写一个线程用作线程调度器,由他来调度进程内部的其他线程,如果线程在内核空间实现,线程表就在内核中,该线程就会由操作系统的调度器统一调度。
2 内核线程实现
2.1 任务状态
每个进程每时每刻都有一个状态,阻塞,正在运行,等待资源,就绪,结束等。使用一个enum
// 被线程执行的函数
typedef void ThreadFunc(void*);
// 几种任务的状态
enum TaskStatus
{
TASK_RUNING,
TASK_READY,
TASK_BLOCKED,
TASK_WAITING,
TASK_HANGING,
TASK_DIED,
};
2.2 任务上下文
每个任务都是在时钟中断的时候切换状态,当一个任务A在运行的过程中被时钟中断打断,然后中断处理函数改变了执行流去执行其他任务B。此时之前的任务A的寄存器映像(值)就需要保存。同时在中断处理函数中,也应该将任务B的寄存器映像换上去。因此需要一个结构体来记录这些寄存器映像IntrStack
IntrStack
的值由时钟中断的中断处理函数填充或是加载到寄存器
// 由时钟中断保存的任务上下文.
struct IntrStack
{
// 下面的代码是我们idm.asm代码压入的东西
// 这个是在idt.asm汇编的时候,push %1 压入的
// push %1 最后压入,所以在第一个
// 中断向量号,在这里面应该都是时钟中断
uint32_t vec;
uint32_t edi;
uint32_t esi;
uint32_t ebp;
// 这里esp没看懂
// 解释是:虽然pushad,会把esp也压入,但是esp因为压栈一直在变化
// 所以会被popad忽略
uint32_t esp_dummy;
uint32_t ebx;
uint32_t edx;
uint32_t ecx;
uint32_t eax;
uint32_t gs;
uint32_t fs;
uint32_t es;
uint32_t ds;
// 下面是cpu从地特权先搞特权是压入
// 是cpu自己压入的。
uint32_t err_code;
void (*eip) (void);
uint32_t cs;
uint32_t eflags;
void* esp;
uint32_t ss;
};
2.3 线程信息
当线程第一次执行的时候,需要有:
- 要执行的函数地址
- 函数的参数
- 函数返回值所在的地址
因此使用一个结构体ThreadStack
去记录这些值
// 线程第一次执行时候需要的数据
// 例如函数地址,函数参数
struct ThreadStack
{
// 这四个寄存器是因为ABI的缘故,需要压入
// 由我们写的汇编代码 pop
uint32_t ebp;
uint32_t ebx;
uint32_t edi;
uint32_t esi;
// 下面就是 ret 后 eip的值 pop到eip中,是近跳转,所以没cs
// 剩下的都是参数
void (*eip)(ThreadFunc* func,void* arg);
// 这些都是为 kernel_thread(ThreadFunc *func,void* func_arg)
// 准备的参数
void (*unused_retaddr);
ThreadFunc* func; // 这是个参数,以后讲。
void* func_arg;
};
其中的ebp
,ebx
,edi
,esi
需要记录的原因是:,根据官方规范:Interl386硬件体系上的所有寄存器都具有全局性,在函数调用时,这些寄存器对主调函数和被调函数都可见,其中ebp
,ebx
,edi
,esi
,esp
归主调函数所用,其他寄存器归被调函数所用。也就是说,被调函数在执行结束后,这5个寄存器的值必须和调用前一直。因此被调函数需要保存这5个寄存器的值。使用c语言编写程序,那么在编译的时候,编译器会自动满足这些条件,当使用汇编辨析的时候,就需要我们自己来保存。
而线程第一次执行的时候,其实相当于调度函数调用了一个线程中需要执行的函数。因此需要保存这些寄存器的值。当线程运行结束的时候需要将这些寄存器恢复。
2.4 PCB结构
有了上面的结构体,现在就可以来记录完整的PCB结构了,程序控制块,TaskStruct
// 进程的PCB控制块
struct TaskStruct
{
uint32_t *self_kstack;
enum TaskStatus status;
uint8_t priority;
char name[6];
// 这是个魔术,完全使我们自定义的
// 用于检查是否越界
uint32_t stack_magic;
// 还有一下字段,以后陆续添加
};
2.5 任务初始化和执行
上面主要是TaskStruct
和ThreadStack
的初始化
每一个新建的任务,状态都应该是TASK_READY
,但是这里,为了演示创建任务的过程,因此直接设置为TASK_RUNNING
,初始化结束后直接开始运行。
// 这是一个包裹函数
// ThreadStack 结构体本质上是 kernel_thread call 之前的压栈数据
static void kernel_thread(ThreadFunc *func,void* func_arg)
{
func(func_arg);
}
// 3. 初始化需要执行的任务
void thread_create(struct TaskStruct* pthread,ThreadFunc func,void* func_arg)
{
// 当中断发生的时候,在idm.asm代码中会压入一系列的数据,
// 这些数据在栈中,然后我们以压入数据结束时刻的栈的esp指针为首地址的,定义了一个
// 结构提,该结构体就是 IntrStack ,
// 也就是说在此时内核栈不为空,所以要跳过这些数据
pthread->self_kstack-=sizeof(struct IntrStack);
// 然后接下来偏移 ThreadStack 大小,
// 此时esp就指向了 ThreadStack 的首指针
// 然后填充该结构体
pthread->self_kstack-=sizeof(struct ThreadStack);
// 先将指针转换为 ThreadStack 结构体的指针,然后下面开始填充
struct ThreadStack* kthread_stack=(struct ThreadStack*)pthread->self_kstack;
// 本质上是调用 kernel_thread() 时候call之前栈中的数据,当然eip是call自己压入的
// 这主要是为了 ret 时候,弹栈,能够使用
// 填充的地址,pop到cs ip寄存器中,改变执行流
kthread_stack->eip=kernel_thread;
kthread_stack->func=func;
kthread_stack->func_arg=func_arg;
kthread_stack->ebp=0;
kthread_stack->ebx=0;
kthread_stack->esi=0;
kthread_stack->edi=0;
}
// 2. 初始化PCB结构 TaskStruct
void init_thread(struct TaskStruct* pthread,char* name,int prio)
{
// 清零,都是在获取页以后清零,在回收页的时候不会清零
memset(pthread,0,sizeof(*pthread));
// 设置任务的名字
strcpy(pthread->name,name);
// 设置任务的状态
pthread->status=TASK_RUNING;
// 设置任务的优先级
pthread->priority=prio;
// 设置任务的内核栈,内核栈的基址是PCB所在页高地址开始,向下是栈
pthread->self_kstack=(uint32_t*)((uint32_t)pthread+PG_SIZE);
// 因为 stack_magic 字段后面到该页的结束都是该任务的内核栈
// 因此为了防止栈溢出以后破坏PCB结构,因此需要这魔数
pthread->stack_magic=0x19870196;
}
struct TaskStruct* thread_start(char* name,int prio,ThreadFunc func,void* func_arg)
{
put_str("\n thread_start start");
// 1. 首先是去获取一页,作为存储PCB结构的页
struct TaskStruct* thread=get_kernel_pages(1);
// 2. 然后初始化该PCB结构
init_thread(thread,name,prio);
// 3. 初始化需要执行的任务
thread_create(thread, func, func_arg);
// 然后 认为的pop四个寄存器
// 之后 ret 的时候 ThreadStack 结构体中的eip 和func 的内容
// 就会被pop到cs,ip寄存器中
// 接下来,从高特权级想低特权级转换,
asm volatile ("movl %0, %%esp; pop %%ebp; pop %%ebx; pop %%edi; pop %%esi; ret" : : "g" (thread->self_kstack) : "memory");
return thread;
}
3 代码
目录结构:
└── bochs
├── 02.tar.gz
├── 03.tar.gz
├── 04.tar.gz
├── 05a.tar.gz
├── 05b.tar.gz
├── 06a.tar.gz
├── 07a.tar.gz
├── 07b.tar.gz
├── 07c.tar.gz
├── 08.tar.gz
├── 09a
│?? ├── boot
│?? │?? ├── include
│?? │?? │?? └── boot.inc
│?? │?? ├── loader.asm
│?? │?? └── mbr.asm
│?? ├── build
│?? │?? ├── bitmap.o
│?? │?? ├── debug.o
│?? │?? ├── idt.o
│?? │?? ├── init.o
│?? │?? ├── interrupt.o
│?? │?? ├── kernel.bin
│?? │?? ├── kernel.o
│?? │?? ├── loader.bin
│?? │?? ├── mbr.bin
│?? │?? ├── memory.o
│?? │?? ├── print.o
│?? │?? ├── string.o
│?? │?? ├── thread.o
│?? │?? └── timer.o
│?? ├── device
│?? │?? ├── timer.c
│?? │?? └── timer.h
│?? ├── kernel
│?? │?? ├── debug.c
│?? │?? ├── debug.h
│?? │?? ├── global.h
│?? │?? ├── idt.asm
│?? │?? ├── init.c
│?? │?? ├── init.h
│?? │?? ├── interrupt.c
│?? │?? ├── interrupt.h
│?? │?? ├── main.c
│?? │?? ├── memory.c
│?? │?? └── memory.h
│?? ├── lib
│?? │?? ├── kernel
│?? │?? │?? ├── bitmap.c
│?? │?? │?? ├── bitmap.h
│?? │?? │?? ├── io.h
│?? │?? │?? ├── print.asm
│?? │?? │?? ├── print.h
│?? │?? │?? ├── string.c
│?? │?? │?? └── string.h
│?? │?? └── libint.h
│?? ├── makefile
│?? ├── start.sh
│?? └── thread
│?? ├── thread.c
│?? └── thread.h
└── hd60m.img
3.1 thread.h/thread.c
thread.h暴露接口
#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H
#include "libint.h"
// 被线程执行的函数
typedef void ThreadFunc(void*);
// 几种任务的状态
enum TaskStatus
{
TASK_RUNING,
TASK_READY,
TASK_BLOCKED,
TASK_WAITING,
TASK_HANGING,
TASK_DIED,
};
// 由时钟中断保存的任务上下文.
struct IntrStack
{
// 下面的代码是我们idm.asm代码压入的东西
// 这个是在idt.asm汇编的时候,push %1 压入的
// push %1 最后压入,所以在第一个
// 中断向量号,在这里面应该都是时钟中断
uint32_t vec;
uint32_t edi;
uint32_t esi;
uint32_t ebp;
// 这里esp没看懂
// 解释是:虽然pushad,会把esp也压入,但是esp因为压栈一直在变化
// 所以会被popad忽略
uint32_t esp_dummy;
uint32_t ebx;
uint32_t edx;
uint32_t ecx;
uint32_t eax;
uint32_t gs;
uint32_t fs;
uint32_t es;
uint32_t ds;
// 下面是cpu从地特权先搞特权是压入
// 是cpu自己压入的。
uint32_t err_code;
void (*eip) (void);
uint32_t cs;
uint32_t eflags;
void* esp;
uint32_t ss;
};
// 线程第一次执行时候需要的数据
// 例如函数地址,函数参数
struct ThreadStack
{
// 这四个寄存器是因为ABI的缘故,需要压入
// 由我们写的汇编代码 pop
uint32_t ebp;
uint32_t ebx;
uint32_t edi;
uint32_t esi;
// 下面就是 ret 后 eip的值 pop到eip中,是近跳转,所以没cs
// 剩下的都是参数
void (*eip)(ThreadFunc* func,void* arg);
// 这些都是为 kernel_thread(ThreadFunc *func,void* func_arg)
// 准备的参数
void (*unused_retaddr);
ThreadFunc* func; // 这是个参数,以后讲。
void* func_arg;
};
// 进程的PCB控制块
struct TaskStruct
{
uint32_t *self_kstack;
enum TaskStatus status;
uint8_t priority;
char name[6];
// 这是个魔术,完全使我们自定义的
// 用于检查是否越界
uint32_t stack_magic;
// 还有一下字段,以后陆续添加
};
void thread_create(struct TaskStruct* pthread, ThreadFunc function, void* func_arg);
void init_thread(struct TaskStruct* pthread, char* name, int prio);
struct TaskStruct* thread_start(char* name, int prio, ThreadFunc function, void* func_arg);
void thread_init(void);
#endif
thread.c 函数实现
#include "thread.h"
#include "string.h"
#include "print.h"
#include "memory.h"
#define PG_SIZE 4096
// 这是一个包裹函数
// ThreadStack 结构体本质上是 kernel_thread call 之前的压栈数据
static void kernel_thread(ThreadFunc *func,void* func_arg)
{
func(func_arg);
}
// 3. 初始化需要执行的任务
void thread_create(struct TaskStruct* pthread,ThreadFunc func,void* func_arg)
{
// 当中断发生的时候,在idm.asm代码中会压入一系列的数据,
// 这些数据在栈中,然后我们以压入数据结束时刻的栈的esp指针为首地址的,定义了一个
// 结构提,该结构体就是 IntrStack ,
// 也就是说在此时内核栈不为空,所以要跳过这些数据
pthread->self_kstack-=sizeof(struct IntrStack);
// 然后接下来偏移 ThreadStack 大小,
// 此时esp就指向了 ThreadStack 的首指针
// 然后填充该结构体
pthread->self_kstack-=sizeof(struct ThreadStack);
// 先将指针转换为 ThreadStack 结构体的指针,然后下面开始填充
struct ThreadStack* kthread_stack=(struct ThreadStack*)pthread->self_kstack;
// 本质上是调用 kernel_thread() 时候call之前栈中的数据,当然eip是call自己压入的
// 这主要是为了 ret 时候,弹栈,能够使用
// 填充的地址,pop到cs ip寄存器中,改变执行流
kthread_stack->eip=kernel_thread;
kthread_stack->func=func;
kthread_stack->func_arg=func_arg;
kthread_stack->ebp=0;
kthread_stack->ebx=0;
kthread_stack->esi=0;
kthread_stack->edi=0;
}
// 2. 初始化PCB结构 TaskStruct
void init_thread(struct TaskStruct* pthread,char* name,int prio)
{
// 清零,都是在获取页以后清零,在回收页的时候不会清零
memset(pthread,0,sizeof(*pthread));
// 设置任务的名字
strcpy(pthread->name,name);
// 设置任务的状态
pthread->status=TASK_RUNING;
// 设置任务的优先级
pthread->priority=prio;
// 设置任务的内核栈,内核栈的基址是PCB所在页高地址开始,向下是栈
pthread->self_kstack=(uint32_t*)((uint32_t)pthread+PG_SIZE);
// 因为 stack_magic 字段后面到该页的结束都是该任务的内核栈
// 因此为了防止栈溢出以后破坏PCB结构,因此需要这魔数
pthread->stack_magic=0x19870196;
}
struct TaskStruct* thread_start(char* name,int prio,ThreadFunc func,void* func_arg)
{
put_str("\n thread_start start");
// 1. 首先是去获取一页,作为存储PCB结构的页
struct TaskStruct* thread=get_kernel_pages(1);
// 2. 然后初始化该PCB结构
init_thread(thread,name,prio);
// 3. 初始化需要执行的任务
thread_create(thread, func, func_arg);
// 然后 认为的pop四个寄存器
// 之后 ret 的时候 ThreadStack 结构体中的eip 和func 的内容
// 就会被pop到cs,ip寄存器中
// 接下来,从高特权级想低特权级转换,
asm volatile ("movl %0, %%esp; pop %%ebp; pop %%ebx; pop %%edi; pop %%esi; ret" : : "g" (thread->self_kstack) : "memory");
return thread;
}
3.2 main.c
调用一下,调用的时候改变了执行流
#include "print.h"
#include "init.h"
#include "debug.h"
#include "interrupt.h"
#include "memory.h"
#include "thread.h"
// 这里一定要先声明,后面定义
// 不然会出错,我也不知道为啥,应该是因为改变了地址?
// 就是在ld中
void k_thread_a(void* arg);
int main(int argc, char const *argv[])
{
set_cursor(880);
put_char('k');
put_char('e');
put_char('r');
put_char('n');
put_char('e');
put_char('l');
put_char('\n');
put_char('\r');
put_char('1');
put_char('2');
put_char('\b');
put_char('3');
put_str("\n put_char\n");
init_all();
put_str("interrupt on\n");
intr_enable();
void* addr = get_kernel_pages(3);
put_str("\n get_kernel_page start vaddr is ");
put_int((uint32_t)addr);
// 改变执行流
thread_start("k_thread_a",31,k_thread_a,"argA");
// 下面这段代码不会执行到了,因为我们没有加任务调度
// 所以执行流一直卡在上面那个执行流了
ASSERT(1==2);
while (1)
{
}
return 0;
}
void k_thread_a(void* arg)
{
char* para=arg;
put_str("\n\n");
put_str(para);
put_str("\n\n");
intr_disable();
while(1)
{
}
}
3.3 makefile
添加了thread.c 的编译,和链接。
注意main.c引用了thread.h 因此INCLUDE
变量要加上thread.h所在的目录
BUILD_DIR=./build
AS=nasm
NASM_ELF=-f elf
INCLUDE=-I./lib -I ./lib/kernel -I./kernel -I./thread/
ASINCLUDE=-I./boot/include/
CFLAGS=-m32 -fno-builtin
LDFLAGS=-Ttext 0xc0001500 -m elf_i386 -e main
CC=gcc
# 注意这里: $(BUILD_DIR)/kernel.o 一定要放在第一个上,因此,这个变量是为连接器准备的.如果不放在第一项,保证错
OBJ=$(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o $(BUILD_DIR)/idt.o $(BUILD_DIR)/debug.o \
$(BUILD_DIR)/interrupt.o $(BUILD_DIR)/init.o $(BUILD_DIR)/memory.o $(BUILD_DIR)/string.o \
$(BUILD_DIR)/bitmap.o $(BUILD_DIR)/thread.o
# 最终要生成的文件
all: $(BUILD_DIR)/kernel.bin $(BUILD_DIR)/loader.bin $(BUILD_DIR)/mbr.bin
# 编译 并刻录 loader.bin
$(BUILD_DIR)/loader.bin: ./boot/loader.asm
$(AS) -o [email protected] $^ $(ASINCLUDE)
dd if=$(BUILD_DIR)/loader.bin of=./hd60m.img bs=512 count=4 seek=2 conv=notrunc
# 编译 并刻录 mbr.bin
$(BUILD_DIR)/mbr.bin: ./boot/mbr.asm
$(AS) -o [email protected] $^ $(ASINCLUDE)
dd if=$(BUILD_DIR)/mbr.bin of=./hd60m.img bs=512 count=1 conv=notrunc
# 编译 print.asm
$(BUILD_DIR)/print.o:./lib/kernel/print.asm
$(AS) $(NASM_ELF) -o [email protected] $^ $(ASINCLUDE)
# 编译 idt.asm
$(BUILD_DIR)/idt.o:./kernel/idt.asm
$(AS) $(NASM_ELF) -o [email protected] $^ $(ASINCLUDE)
# 编译 interrupt.c
$(BUILD_DIR)/interrupt.o:./kernel/interrupt.c
$(CC) -o [email protected] $(CFLAGS) -fno-stack-protector -c $^ $(INCLUDE)
# 编译 debug.c
$(BUILD_DIR)/debug.o:./kernel/debug.c
$(CC) -o [email protected] $(CFLAGS) -c $^ $(INCLUDE)
# 编译 init.c
$(BUILD_DIR)/init.o:./kernel/init.c
$(CC) -o [email protected] $(CFLAGS) -c $^ $(INCLUDE)
# 编译 kernel/memory.c
$(BUILD_DIR)/memory.o:./kernel/memory.c
$(CC) -o [email protected] $(CFLAGS) -c $^ $(INCLUDE)
# 编译 lib/kernel/string.c
$(BUILD_DIR)/string.o:./lib/kernel/string.c
$(CC) -o [email protected] $(CFLAGS) -c $^ $(INCLUDE)
# 编译 lib/kernel/bitmap.c
$(BUILD_DIR)/bitmap.o:./lib/kernel/bitmap.c
$(CC) -o [email protected] $(CFLAGS) -c $^ $(INCLUDE)
# 编译 thread/thread.c
$(BUILD_DIR)/thread.o:./thread/thread.c
$(CC) -o [email protected] $(CFLAGS) -c $^ $(INCLUDE)
# 编译 main.c
$(BUILD_DIR)/kernel.o:./kernel/main.c
$(CC) -o [email protected] $(CFLAGS) -c $^ $(INCLUDE)
# 最终链接
$(BUILD_DIR)/kernel.bin:$(OBJ)
$(LD) -Ttext 0xc0001500 -m elf_i386 -e main -o ./build/kernel.bin $(OBJ)
dd if=./build/kernel.bin of=./hd60m.img bs=512 count=40 seek=9 conv=notrunc
clean:
rm -rf ./build/*
3.4 运行
以上是关于第09章上 内核线程的主要内容,如果未能解决你的问题,请参考以下文章
LINUX PID 1和SYSTEMD PID 0 是内核的一部分,主要用于内进换页,内核初始化的最后一步就是启动 init 进程。这个进程是系统的第一个进程,PID 为 1,又叫超级进程(代码片段
什么是在 C++ 中获取总内核数量的跨平台代码片段? [复制]
JUC并发编程 共享模式之工具 JUC CountdownLatch(倒计时锁) -- CountdownLatch应用(等待多个线程准备完毕( 可以覆盖上次的打印内)等待多个远程调用结束)(代码片段