如果我"get back to the main thread"那么到底发生了什么,中断如何处理线程?



背景:我使用了Beej的指南,他提到了分叉和确保你"得到僵尸"。我拿到的一本《操作系统》书解释了操作系统是如何创建"线程"的(我一直认为这是一本更基本的书),引用它,我的意思是,操作系统几乎决定了一切。基本上,它们共享所有外部资源,但它们划分了寄存器和堆栈空间(我认为这是第三件事)。

所以我得到了waitpid函数http://www.qnx.com的开发人员文档解释得很好。事实上,我阅读了关于线程的整个部分,减去了Processes和threads谷歌之后的所有类型的条件。

事实上,我可以将代码拆分并重新组合在一起,这并没有让我感到困惑。如何我可以做到这一点令人困惑。

在C和C++中,您的程序是一个Main()函数,它继续前进,调用其他函数,可能永远循环(等待输入或渲染),然后最终退出或返回。在这个模型中,我看不出有什么理由让它停在"我在等什么"之外,在这种情况下,它只是循环。

好吧,它似乎可以通过设置某些东西来循环,比如"我在等待信号量"、"响应"或"中断"。或者它可能会在没有等待的情况下被打断。这就是让我困惑的地方。

处理器对进程和线程进行时间切片。这一切都很好,但它是如何决定何时停止的呢?我知道你进入轮询功能后会说"嘿,我在等待输入、时钟滴答声或用户做什么"。不知怎么的,它告诉了操作系统?我不确定。但更多:

它似乎能够完全随机地中断或插入,即使是在单线程应用程序上也是如此。所以你正在运行一个线程,突然waitpid()说:"嘿,我完成了一个进程,让我打断一下,我们都讨厌僵尸,我必须这么做。"而你仍然在循环计算。那么,刚刚发生了什么???我不知道,不知何故,它们都在运行,你的计算也没有被打乱,因为它是单线程的,但这并不意味着当你还在做其他应用程序的事情时,它不会停止在同一线程内运行waitpid()。

同样令人困惑的是,如何通知你,比如iOSes通知,并说"嘿,我有一些UI更改,让我退出16,然后让我回到1,这样我就可以更改这件事"。但和上一段一样的问题是,它是如何中断正在运行的线程的?

我想我理解这种分裂,但这次加入完全令人困惑。这就像教科书上有我应该接受的"兔子从帽子里出来"的步骤。其他SO帖子告诉我,他们不共享同一个堆栈,但这没有帮助,现在我想象一个slinky(堆栈)向另一个slink倾斜,但不确定它是如何重组以更改数据的。

谢谢你的帮助,我很抱歉这太长了,但我知道如果我在这里过于简洁,会有人误解这一点,并给我"它们是不同的堆栈"的答案。

谢谢,

好吧,我会试试,尽管它会"经济实惠":)

有点像这样:

操作系统内核调度程序/调度器是一个用于管理线程的状态机。线程包括一个堆栈(在线程创建时分配)和一个线程控制块(TCB),该结构位于内核中,用于保存线程状态并可以存储线程上下文(包括用户寄存器,尤其是堆栈指针)。一个线程必须有要运行的代码,但该代码不是专门用于该线程的——许多线程可以运行相同的代码。线程有状态,例如在I/O上被阻塞,在线程间信号上被阻塞、睡眠一段时间、准备就绪、在内核上运行。

线程属于进程——进程必须至少有一个线程来运行其代码,并且在进程启动时由OS加载器为其创建一个线程。然后,"主线程"可能会创建其他也属于该进程的线程。

状态机输入是软件中断——来自已经在内核上运行的线程的系统调用,以及来自全管道设备/控制器(磁盘、网络、鼠标、KB等)的硬件中断,这些设备/控制器使用处理器硬件功能来停止处理器从线程运行指令,并"立即"运行驱动程序代码。

状态机的输出是在内核上运行的一组线程。如果就绪线程少于内核,操作系统将停止不可用的内核。如果准备好的线程比核心多(即机器过载),那么决定运行线程的"sheduling算法"会考虑几个因素——线程和进程优先级、刚刚在I/O完成或线程间信号时准备好的进程的比例提升、前台进程提升等。

操作系统能够停止任何内核上任何正在运行的线程。它有一个处理器间硬件中断通道和驱动程序,可以强制任何线程进入操作系统并被阻止/停止(可能是因为另一个线程刚刚准备好,操作系统调度算法决定必须立即抢占正在运行的线程)。

运行线程的软件中断可以通过请求I/O或向其他线程发出信号(事件、互斥、条件变量和信号量)来更改运行线程集。来自外围设备的硬件中断可以通过发出I/O完成的信号来改变正在运行的线程集。

当操作系统获得这些输入时,它会使用该输入以及线程控制块和进程控制块结构容器中的内部状态来决定下一步运行哪组就绪线程。它可以通过将线程的上下文(包括寄存器,尤其是堆栈指针)保存在TCB中而不从中断中返回来阻止线程运行。它可以通过将上下文从TCB恢复到核心并执行中断返回来运行被阻塞的线程,从而允许线程从中断的地方恢复。

这样做的好处是,没有一个等待I/O的线程可以运行,因此不使用任何CPU,并且当I/O变得可导航时,一个等待的线程会"立即"准备就绪,如果有可用的核心,则会运行。

操作系统状态数据和硬件/软件中断的这种组合,有效地将能够向前推进的线程与能够运行它们的内核相匹配,并且CPU不会浪费在轮询I/O或线程间通信标志上。

所有这些复杂性,无论是在操作系统中,还是对于必须设计多线程应用程序并忍受锁、同步、互斥等的开发人员来说,都只有一个至关重要的目标——高性能I/O。如果没有它,你可能会忘记视频流、BitTorrent和浏览器——它们都太慢了,无法使用。

像"CPU量子"、"放弃剩余的时间片"one_answers"循环"这样的语句和短语让我想吐。

这是一台状态机。硬件和软件中断进入,一组正在运行的线程出现。硬件定时器中断(可以使系统调用超时,允许线程睡眠并在过载的盒子上共享CPU的中断)虽然很有价值,但只是其中之一。

所以我在线程16上,我需要到达线程1来修改UI。我随机将其停止在任何位置,"将堆栈移到线程1",然后"获取其上下文并修改它"?

不,是时候"经济实惠"#2了…

线程1正在运行GUI。要做到这一点,它需要来自鼠标和键盘的输入。发生这种情况的经典方式是线程1在GUI输入队列上等待(被阻止)KB/鼠标消息,该队列是线程安全的生产者-消费者队列。它不使用CPU-核心停止运行服务和BitTorrent下载。你按下键盘上的一个键,键盘控制器硬件在中断控制器上弹出一条中断线,导致核心在完成当前指令后立即跳转到键盘驱动程序代码。驱动程序读取KB控制器,组装一条KeyPressed消息,并将其推送到具有焦点的GUI线程的输入队列中——即线程1。驱动程序通过调用调度程序中断入口点退出,这样就可以执行调度运行,并为GUI线程分配一个核心运行。对于线程1,它所做的只是对队列进行阻塞"弹出"调用,最终返回一条消息进行处理。

因此,线程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交互,它就不能直接进行交互。它所能做的就是以类似于KB/鼠标驱动程序的方式将消息排队到线程1,以指示它执行某些操作。

这可能看起来有点限制,但来自线程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
..
..
};    

因此,让一个函数在另一个线程的上下文中运行需要合作。线程不能调用其他线程,只能通过操作系统向它们发出信号。任何预期运行这种"外部信号"代码的线程都必须有一个可访问的入口点,在那里可以放置函数,并且必须执行代码来检索函数地址并调用它。

最新更新