2. 线程调度的实现
Resource Acquisition Is Initialization (RAII)
Resource 资源
定义: 持有价值昂贵的,可以被获取和释放的对象为资源,常见的有 Memory, Mutex, File
Lifetime 生命周期
一个变量通过调用构造函数来实现 introduction 和 initialization
在退出 {} 的时候,系统会按照与定义相反的方向进行析构函数
RAII
定义: 利用变量的生命周期来实现资源的构造和析构管理
优点
- 资源可以实现自动析构在退出定义域的时候,尤其是 return 和 throw 的情况下
- 对于一些底层的代码实现,例如根 root 线程对象的销毁处理,如果手动销毁,就会在结束前提前释放 stack 从而导致无法继续执行,而利用析构函数自动销毁就可以实现
- 不用担心手动释放资源
- 对于一个 mutex 结构如果在入口处进行一次 lock 而在每一种退出处 (return, throw) 都必须执行 unlock ,就会导致上锁解锁的不对称性,容易忘记且效果不好, 这时候应该依赖在对象的析构函数来进行解锁(构造函数实现上锁),从而实现上锁-解锁一一对应
ucontext_t 类型与 TCB 状态存储
libc 相关 context 方法集
The ucontext_t structure can store the state of a thread at a particular instance of time.
setcontext
setcontext switches to the context of another thread.
1 | int setcontext(const ucontext_t* ucp); |
setcontext loads the context pointed to by ucp to the CPU registers, effectively switching to that context. The context must have previously been saved (via swapcontext or getcontext) or constructed (via makecontext).
getcontext
getcontext copies the context of the current thread to the context pointed to by ucp:
1 | int getcontext(ucontext_t* ucp); |
但是由于 存储context 和 切换到新的 context 的 pc 是不同的,因此如果按照
1 | getcontext(ucp1); |
如果按照复制逻辑来看,存储的 pc 指向的应该是 setcontext 函数的位置,返回之后还是会再次执行 setcontext 所以要用到下文的 swapcontext 来实现
makecontext
makecontext initializes a new thread context.
1 | void makecontext(ucontext_t* ucp, void (*func)(), int argc, ...); |
ucp must point to an allocated object of type ucontext_t. func is a pointer to the function that this context should begin executing when it starts. argc specifies how many arguments func is expecting, followed by that list of arguments.
swapcontext
swapcontext saves the current context and switches to another.
1 | int swapcontext(ucontext_t* oucp, const ucontext_t* ucp); |
swapcontext saves the current CPU registers to the context structure pointed to by oucp, and loads the saved CPU registers from the context structure pointed to by ucp. This effectively pauses the running thread and resumes execution of another thread. The context in ucp must have previously been saved (via swapcontext or getcontext) or constructed (via makecontext). swapcontext does not return immediately; it only returns when the context saved in oucp is later resumed.
线程对象和执行流的区别 thread object vs. execution flow
区别理解
对象指的是管理一定资源 (定义见上文) 的实体,在线程对象的定义中和真正的执行流 (函数) 是分离定义的,例如采用如下定义法
1 | thread::thread(func, arg); |
其中对象为 thread t1(f1, 0); 之类的语句定义的实体t1, 执行流为 f1(0) 的具体函数,二者的生命周期并不相同
其中,线程对象掌管一定的线程资源,包括自身的 上下文 ucontext ; 执行流本身并不享有固定的资源,但是在执行过程中具有过程性的资源,例如 this_thread 和 cpu::self() 等获取当前执行的线程和cpu的方法, 由于在执行过程中可能是用到不同的线程和cpu,故不能将此作为数据存储
static 属性对执行流的支持
类的静态函数和静态变量的生命周期和整体程序保持一致,即程序一开始就初始化并且直到整个程序结束才析构
类的静态变量为整个类公有的资源
类的静态函数并不能作为具体对象的对应方法,但是可以被具体对象调用,即不能有 t1.static_func() 之类的写法,只能在对应类的成员函数中加上 thread::static_func() 的调用,但是这不表示无法获取对应的对象资源,利用类内额外定义的 static this_thread 等变量指向当前运行的线程可以获取具体的对象而不是真的修改整个类的属性
例如,对于 static 函数 cpu::suspend() 是 cpu 类(一种和 processor 结合更加紧密的特殊 thread) 的静态函数,但是真正实现 suspend 的应该是一个具体的 cpu 执行核心而不是整体 cpu 类,因此应该默认 suspend 会通过类似 cpu::self() 的方法获取当前核心再对其执行
为什么要写作 static 而不是 作为一般成员函数
从设计哲学角度,调用成员函数的是类的具体对象而不是执行流,执行流只能拥有临时成员变量,执行 cpu1.suspend() 固然合理但是这并不利于代码的复用且不满足多线程对一个代码片段 (函数) 的强复用性, 即在多线程公用的一个函数里,不共用的资源 (类成员变量、线程对应的上下文) 应该存储在 类的具体对象里面,而事实上公用的部分 (代码和全局变量) 和临时公用部分 (即可能公用的资源如 cpu 和 thread) 则应该由执行流来进行管理
因此,suspend() 应该是作为代码片段被公用的,且其管理的 cpu 资源并不应该作为一个 cpu 对象或者 thread 对象应该拥有的
生命周期差异
具体对象
从构造函数开始到析构函数或者出定义域 {} 结束
执行流
对应的执行函数开始到最后离开执行函数 (最终的做法可以是 setcontext 或者 return 或者 throw)
差异举例
1 | void func1(int arg){ |
在上图所示中,主线程函数可能在定义对象 t1 和开启执行流 func1 之后就执行 return 了即线程对象事实上已经销毁, 但是执行流是一个很缓慢的过程因此并没有结束也并不会被销毁, 但是作为线程库应该等待执行流结束之后要释放该线程对应的内存空间 (主要是栈空间) 因此并不能将线程的 stack ( ucp) 和 thread 直接绑定,或者说不能在 thread 类的析构中 delete stack 空间,否则会发生冲突; 因此事实上需要一个额外的清理线程来处理执行流结束之后的执行流析构, 虽然会存在时间差异但是并不影响空间的申请和占用
对象与执行流的绑定
为了更好的实现对象和执行流之间的绑定,即让二者生命周期更加重复,至少需要在结束的时候执行析构函数的时间 “基本对齐”, 即需要让执行流先完成任务之后 thread 再完成销毁,这就是 join() 加入函数的实现目的
