好的,我会试一试,虽然它会“经济实惠”:)
有点像这样:
操作系统内核调度程序/调度程序是用于管理线程的状态机。一个线程由一个栈(在线程创建时分配)和一个线程控制块(TCB)组成,内核中的结构保持线程状态并可以存储线程上下文(包括用户寄存器,尤其是堆栈指针) )。线程必须有代码才能运行,但代码不是专用于线程的——许多线程可以运行相同的代码。线程有状态,例如。在 I/O 上阻塞,在线程间信号上阻塞,休眠一段时间,准备好,在内核上运行。
线程属于进程——一个进程必须至少有一个线程来运行它的代码,并且在进程启动时由操作系统加载程序为其创建一个线程。然后,“主线程”可以创建也属于该进程的其他线程。
状态机输入是软件中断——来自那些已经在内核上运行的线程的系统调用,以及来自外围设备/控制器(磁盘、网络、鼠标、KB 等)的硬件中断,它们使用处理器硬件特性来停止处理器/s 运行来自线程的指令并“立即”运行驱动程序代码。
状态机的输出是一组在内核上运行的线程。如果就绪线程数少于内核数,操作系统将停止不可用的内核数。如果就绪线程多于内核(即机器过载),则决定运行线程的“调度算法”会考虑几个因素 - 线程和进程优先级,刚刚就绪的线程的优先级提升I/O 完成或线程间信号、前台进程提升等。
操作系统能够停止任何内核上的任何正在运行的线程。它有一个处理器间硬件中断通道和驱动程序,可以强制任何线程进入操作系统并被阻塞/停止,(可能是因为另一个线程刚刚准备好并且操作系统调度算法决定必须立即抢占正在运行的线程) .
来自运行线程的软件中断可以通过请求 I/O 或通过向其他线程发出信号(事件、互斥体、条件变量和信号量)来更改运行线程集。来自外围设备的硬件中断可以通过发出 I/O 完成信号来更改正在运行的线程集。
当操作系统获得这些输入时,它使用该输入以及线程控制块和进程控制块结构的容器中的内部状态来决定接下来要运行哪组就绪线程。它可以通过将其上下文(包括寄存器,尤其是堆栈指针)保存在其 TCB 中而不从中断中返回来阻止线程运行。它可以通过将其上下文从其 TCB 恢复到核心并执行中断返回来运行被阻塞的线程,从而允许线程从它停止的地方恢复。
好处是等待 I/O 的线程根本没有运行,因此不使用任何 CPU,当 I/O 可用时,等待线程“立即”准备好,如果有核心可用,正在运行。
操作系统状态数据和硬件/软件中断的这种组合有效地匹配了可以向前推进的线程与可运行它们的内核,并且没有 CPU 被浪费在轮询 I/O 或线程间通信标志上。
所有这些复杂性,无论是在操作系统中还是对于必须设计多线程应用程序并忍受锁、同步、互斥锁等的开发人员来说,只有一个重要目标——高性能 I/O。没有它,您可能会忘记视频流、BitTorrent 和浏览器——它们都太慢而无法使用。
诸如“CPU 量子”、“放弃剩余的时间片”和“循环”之类的语句和短语让我想吐。
这是一个状态机。硬件和软件中断进来,一组运行线程出来。硬件定时器中断(可以使系统调用超时,允许线程休眠并在过载的机器上共享 CPU)虽然很有价值,但只是众多中断之一。
所以我在线程 16 上,我需要进入线程 1 来修改 UI。我在任何地方随机停止它,“将堆栈移动到线程 1”然后“获取它的上下文并修改它”?
不,是时候“经济与真理”#2了……
线程 1 正在运行 GUI。为此,它需要来自鼠标、键盘的输入。发生这种情况的经典方式是线程 1 在 GUI 输入队列(一个线程安全的生产者-消费者队列)上等待并阻塞,以获取 KB/鼠标消息。它不使用 CPU - 内核停止运行服务和 BitTorrent 下载。你敲击键盘上的一个键,键盘控制器硬件在中断控制器上产生一条中断线,导致内核在完成当前指令后立即跳转到键盘驱动程序代码。驱动程序读取 KB 控制器,组装 KeyPressed 消息并将其推送到具有焦点的 GUI 线程的输入队列 - 您的线程 1. 驱动程序通过调用调度程序中断入口点退出,以便可以执行调度运行和您的 GUI线程被分配一个核心并在其上运行。
因此,线程 1 正在执行:
void* HandleGui{
while(true){
GUImessage message=thread1InputQueue.pop();
switch(message.type){
.. // lots of case statements to handle all the possible GUI messages
..
..
};
};
};
如果线程 16 想要与 GUI 交互,它不能直接进行。它所能做的就是将消息排队到线程 1,以类似于 KB/鼠标驱动程序的方式,指示它做一些事情。
这可能看起来有点限制,但是来自线程 16 的消息可以包含的不仅仅是 POD。它可能有一个“RunMyCode”消息类型,并包含一个函数指针,指向线程 16 想要在线程 1 的上下文中运行的代码。当线程 1 开始处理消息时,它的“RunMyCode”case 语句调用函数指针在消息中。请注意,这种“简单”机制是异步的——线程 16 已发出消息并继续运行——它不知道线程 1 何时开始运行它传递的函数。如果函数访问线程 16 中的任何数据,这可能是一个问题 - 线程 16 也可能正在访问它。如果这是一个问题,(可能不是——函数所需的所有数据都可能在消息中,可以在线程 1 调用它时作为参数传递给函数),可以通过使线程 16 等到线程 1 运行该函数来使函数调用同步。一种方法是让函数将 OS 同步对象作为其最后一行 - 线程 16 在对其“RunMyCode”消息进行排队后立即等待的对象:
void* runOnGUI(GUImessage message){
// do stuff with GUI controls
message.notifyCompletion->signal(); // tell thread 16 to run again
};
void* thread16run(){
..
..
GUImessage message;
waitEvent OSkernelWaitObject;
message.type=RunMyCode;
message.function=runOnGUI;
message.notifyCompletion=waitEvent;
thread1InputQueue.push(message); // ask thread 1 to run my function.
waitEvent->wait(); // wait, blocked, until the function is done
..
..
};
因此,让一个函数在另一个线程的上下文中运行需要合作。线程不能调用其他线程——只能通过操作系统向它们发出信号。任何期望运行这种“外部信号”代码的线程都必须有一个可访问的入口点,可以在其中放置函数,并且必须执行代码以检索函数地址并调用它。