7

我试图绕过协作多任务系统的概念以及它在单线程应用程序中的工作原理。

我的理解是,这是一种“多任务处理的形式,其中多个任务通过在每个任务中的程序员定义的点处自愿将控制权让给其他任务来执行”。

因此,如果您有一个任务列表并且一个任务正在执行,您如何确定将执行传递给另一个任务?当您将执行返回给先前的任务时,如何从先前的位置恢复?

我觉得这有点令人困惑,因为我不明白如果没有多线程应用程序如何实现这一点。

任何建议都会非常有帮助:)

谢谢

4

4 回答 4

7

在单个进程(或执行线程)使用协作多任务处理的特定场景中,您可以使用诸如 Windows 的纤程或 POSIX setcontext 系列函数之类的东西。我将在这里使用术语纤维。

基本上,当一个纤程完成执行一大块工作并希望自愿允许其他纤程运行(因此称为“合作”术语)时,它要么手动切换到另一个纤程的上下文,要么更典型地执行某种 yield() 或scheduler() 调用跳转到调度程序的上下文,然后调度程序找到要运行的新纤程并切换到该纤程的上下文。

这里的上下文是什么意思?基本上是堆栈和寄存器。堆栈没有什么神奇之处,它只是堆栈指针恰好指向的一块内存。程序计数器也没有什么神奇之处,它只是指向下一条要执行的指令。切换上下文只是将当前寄存器保存在某处,将堆栈指针更改为不同的内存块,将程序计数器更新为不同的指令流,将该上下文保存的寄存器复制到 CPU 中,然后进行跳转。Bam,您现在正在使用不同的堆栈执行不同的指令。通常上下文切换代码是用汇编语言编写的,该汇编代码的调用方式不会修改当前堆栈或取消更改,无论哪种情况,它都不会在堆栈或寄存器中留下任何痕迹,因此当代码恢复执行时,它不知道发生了什么。(再次,主题:我们假设方法调用摆弄寄存器,将参数推送到堆栈,移动堆栈指针等,但这只是 C 调用约定。没有什么需要你维护一个堆栈或有任何特定的方法调用在堆栈上留下任何自己的痕迹)。

由于每个堆栈都是独立的,因此您不会有一些看似随机的方法调用的连续链最终溢出堆栈(如果您天真地尝试使用连续调用彼此的标准 C 方法来实现此方案,这可能是结果)。您可以使用状态机手动实现这一点,其中每个纤程都保留其工作位置的状态机,定期返回调用调度程序的方法,但是当实际的纤程/协程支持广泛可用时,为什么还要麻烦呢?

还要记住,协作多任务处理与进程、受保护的内存、地址空间等是正交的。见证 Mac OS 9 或 Windows 3.x。他们支持分离流程的想法。但是当您让步时,上下文更改为操作系统上下文,允许操作系统调度程序运行,然后可能选择另一个进程切换到。从理论上讲,您可以拥有一个仍然使用协作多任务处理的完全受保护的虚拟内存操作系统。在那些系统中,如果一个错误的进程永远不会让步,那么操作系统调度程序就永远不会运行,因此系统中的所有其他进程都会被冻结。**

下一个自然的问题是什么使某些东西先发制人......答案是操作系统与 CPU 调度一个中断计时器来停止当前正在执行的任务并切换回操作系统调度程序的上下文,而不管当前任务是否愿意释放CPU 与否,因此“抢占”它。如果操作系统使用 CPU 特权级别,则(内核配置的)定时器不能被较低级别(用户模式)代码取消,但理论上如果操作系统不使用此类保护,错误的任务可以屏蔽或取消中断定时器和劫持CPU。还有一些其他场景,例如 IO 调用,其中可以在计时器之外调用调度程序,并且调度程序可能会决定没有其他进程具有更高的优先级并将控制权返回给同一进程而无需切换......实际上大多数操作系统都没有

** 你可能会问,如果在某个时间段内没有调用 yield,为什么不直接触发一个计时器。答案在于多线程同步。在合作系统中,您不必费心去锁、担心重新进入等,因为您只有在事情处于已知良好状态时才会让步。如果这个神秘的计时器触发了,那么您现在可能已经破坏了被中断程序的状态。如果必须编写程序来处理这个问题,恭喜……你现在拥有了一个半途而废的先发制人的多任务系统。还不如做对了!而且,如果您无论如何都在进行更改,不妨添加线程、受保护的内存等。这几乎就是主要操作系统的历史。

于 2013-12-31T18:44:40.310 回答
3

协作多任务处理背后的基本思想是信任——每个子任务将自行及时放弃控制,以避免占用其他任务的处理器时间。这就是为什么协作式多任务系统中的任务需要非常彻底的测试,并且在某些情况下需要经过认证才能使用。

我并不声称自己是专家,但我认为协作任务可以作为状态机来实现,将控制权传递给任务会导致它运行所需的绝对最短时间以取得任何进展。例如,文件读取器可能会读取文件的下几个字节,解析器可能会解析文档的下一行,或者传感器控制器可能会读取一次,然后将控制权返回给协作调度程序,该调度程序将检查任务完成。

每个任务都必须将其内部状态保留在堆上(在对象级别),而不是像传统的阻塞函数或线程那样在堆栈帧(在函数级别)上。

与依赖硬件计时器来触发上下文切换的传统多任务处理不同,协作式多任务处理依赖于以这样一种方式编写的代码,即每个长时间运行的任务的每一步都保证在可接受的少量时间内完成.

于 2013-03-30T15:17:56.210 回答
2

任务将执行显式等待暂停让步操作,从而调用调度程序。可能有不同的操作来等待 IO 完成或在繁重的计算中显式让步。在应用程序任务的主循环中,它可以有一个 *wait_for_event* 调用而不是忙轮询。这将暂停任务,直到它有输入要处理。

捕获失控任务可能还有超时机制,但它不是切换的主要手段(否则它不会合作)。

于 2013-05-11T05:02:01.447 回答
2

考虑协作多任务的一种方法是将任务拆分为步骤(或状态)。每个任务都会跟踪它需要执行的下一步。轮到任务时,它只执行那一步并返回。这样,在程序的主循环中,您只需按顺序调用每个任务,并且因为每个任务只占用少量时间来完成一个步骤,我们最终得到一个允许所有任务执行的系统共享 cpu 时间(即合作)。

于 2013-11-15T20:28:42.463 回答