艾伦·考克斯(Alan Cox)曾说过: “计算机是状态机。线程是为无法对状态机进行编程的人的。”
由于直接询问Alan并不是让我谦虚的选择,所以我想在这里问:一个人如何仅使用一个线程和状态机来实现高级语言(例如Java)的多线程功能?例如,如果要执行2个活动(进行计算和执行I / O)并且一个活动可以阻止该怎么办?
使用“仅状态机”方式是否可以替代高级语言中的多线程?
艾伦·考克斯(Alan Cox)曾说过: “计算机是状态机。线程是为无法对状态机进行编程的人的。”
由于直接询问Alan并不是让我谦虚的选择,所以我想在这里问:一个人如何仅使用一个线程和状态机来实现高级语言(例如Java)的多线程功能?例如,如果要执行2个活动(进行计算和执行I / O)并且一个活动可以阻止该怎么办?
使用“仅状态机”方式是否可以替代高级语言中的多线程?
Answers:
一个线程所做的只是交错操作,因此进程的各个部分似乎在时间上重叠。具有多线程的单核计算机只是跳来跳去:它从一个线程执行少量代码,然后切换到另一个线程。一个简单的调度程序决定哪个线程具有最高优先级,并在内核中实际执行。
在单核计算机上,实际上没有 “同时”发生任何事情。只是交错执行。
有许多种方法可以实现交错。许多。
假设您有一个简单的两线程进程,该进程使用一个简单的锁,以便两个线程都可以写入一个公共变量。您有六个代码块。
[这可以是循环的,也可以有更多的锁,等等。它所做的只是变得更长,而不是更加复杂。]
T1的步骤必须按顺序运行(T1-之前,T1-使用,T1-之后),T2的步骤必须按顺序运行(T2-之前,T2-使用,T2-之后)。
除了“按顺序”约束以外,可以任何方式对它们进行交织。随便 它们可以如上所述运行。另一个有效的顺序是(T1-之前,T2-之前,T2-锁定,T1-锁定,T2-之后,T1-之后)。有很多有效的订单。
等待。
这只是一个具有六个状态的状态机。
这是一个不确定的有限状态自动机。T1-xxx状态与T2-xxx状态的顺序是不确定的,无关紧要。因此,在某些情况下,“下一个状态”是抛硬币。
例如,当FSM启动时,T1-before或T2-before都是合法的第一状态。抛硬币。
假设它出现在T1之前。去做。完成后,可以在T1-with和T2-before之间进行选择。抛硬币。
在FSM的每个步骤中,将有两个选择(两个线程-两个选择),掷硬币可以确定遵循哪个特定状态。
编写阻止功能的对象是无法创建状态机的人;)
如果无法绕过阻塞,线程很有用。没有一项基本的计算机活动能够真正阻止,只是为了方便使用,就以这种方式实施了许多活动。读取函数将阻塞直到读取整个缓冲区,而不是返回字符或“读取失败”。连接函数将等待应答,而不是检查队列中的返回消息并返回(如果未找到返回消息)。
您不能在状态机中使用阻止功能(至少一个不允许“冻结”的功能)。
是的,使用状态机是一种可行的选择。在实时系统中,这是唯一的选择,该系统为机器提供框架。使用线程和阻塞函数只是“轻松的出路”,因为通常一个对阻塞函数的调用会替换状态机中的大约3-4个状态。
如何仅使用一个线程和状态机就可以以高级语言(例如Java)实现多线程功能?例如,如果要执行2个活动(进行计算和执行I / O)并且一个活动可以阻止该怎么办?
您所描述的称为协作多任务处理,其中将任务分配给CPU,并期望在一定数量的时间或活动后自动放弃。通过继续使用CPU或阻塞整个工作以及没有硬件看门狗定时器而无法配合的任务,监督任务的代码对此无能为力。
在现代系统中,您看到的被称为抢占式多任务处理,这是任务不必放弃CPU的地方,因为当硬件生成的中断到来时,管理器会为它们执行任务。主管中的中断服务例程将保存CPU的状态,并在下次认为该任务应有时间片时恢复该状态,然后从接下来要运行的任何任务中恢复该状态,并跳回该状态,就像什么都没发生一样。此操作称为上下文切换,可能会很昂贵。
使用“仅状态机”方式是否可以替代高级语言中的多线程?
可行吗?当然。桑 有时。是否使用线程还是某种形式的自制协作式多任务处理(例如状态机),取决于您愿意做出的权衡。
线程简化了任务设计,使您可以将每个任务视为自己的程序,恰好与他人共享数据空间。这使您可以自由地专注于手头的工作,而不必一次执行一个迭代就需要进行所有管理和内务处理。但是,由于没有妥协的善行,因此您需要为上下文切换中的所有这些便利付费。在执行最少的工作后(自愿或通过执行诸如I / O之类会阻塞的事情)拥有许多可以产生CPU的线程会占用大量处理器时间进行上下文切换。如果阻塞操作很少阻塞很长时间,则尤其如此。
在某些情况下,合作路线更有意义。我曾经不得不为一块硬件编写一些用户界面软件,该软件通过需要轮询的内存映射接口流式传输许多数据通道。每个通道都是以这样一种方式构建的对象:我可以让它作为线程运行,也可以重复执行一个轮询周期。
正是由于我上面概述的原因,多线程版本的性能一点也不好:每个线程的工作量最少,然后释放CPU,因此其他通道可能需要一些时间,从而导致大量上下文切换。让线程自由运行直到被抢占,这有助于提高吞吐量,但导致某些通道在硬件经历缓冲区溢出之前无法得到服务,因为它们没有足够的时间片。
单线程版本甚至对每个通道都进行了迭代,运行时就像烫伤的猿猴一样,系统上的负载也像石头一样下降。我为提高性能付出的代价是不得不自己处理任务。在这种情况下,执行此操作的代码非常简单,因此开发和维护它的成本值得提高性能。我想那真的是底线。如果我的线程一直在等待一些系统调用返回,那练习可能是不值得的。
这让我想到了Cox的评论:线程不仅仅适用于无法编写状态机的人。有些人有能力做到这一点,但为了更快或更省事地完成工作,选择使用固定状态机(即线程)。
如果要执行2个活动(进行计算和执行I / O)并且可以阻止一个活动怎么办?
好吧,老实说,我无法想象如何在没有线程的情况下处理阻塞的I / O。毕竟,将其称为“ 阻止”是因为调用它的代码必须这样做wait
。
根据我对Cox原始电子邮件的阅读(如下),他指出,尽管该线程无法很好地扩展。我的意思是,如果有100个I / O请求怎么办?1000?10000?Cox指出拥有大量线程可能会导致严重问题:
发件人:艾伦·考克斯(Alan Cox)(alan@lxorguk.ukuu.org.uk)
日期:2000年1月21日星期五-美国东部时间13:33:52IBM论文),如果您的应用程序依赖大量线程,那么您将始终与调度程序发生冲突吗?很多人在一个问题上抛出很多线程,这确实是不好的设计。
那是您最不用担心的。1000个线程是8Mb的内核堆栈,并且有足够的任务切换以确保您可以关闭大部分缓存。计算机是状态机。线程适用于无法对状态机进行编程的人。
在很多情况下,Linux绝对不能帮助这种情况,特别是异步块I / O。
艾伦
来源:Re:IBM对linux内核线程的有趣分析(linux-kernel邮件列表档案)
在两种情况下,线程是唯一的选择:
第二个原因是为什么大多数人认为线程是进行IO或网络编程所不可避免的,但这通常是因为他们不知道自己的操作系统具有更高级的API(或者不想与之抗争)。
至于易用性和可读性,总是有事件循环(如libev或EventMachine)使状态机的编程几乎与使用线程进行状态循环一样简单,但是却提供了足够的控制权来忘记同步问题。
了解状态机和多线程交互方式的一种好方法是查看GUI事件处理程序。许多GUI应用程序/框架使用单个GUI线程,该线程将轮询可能的输入源并为每个接收到的输入调用一个函数。本质上,这可以写成一个巨大的开关:
while (true) {
switch (event) {
case ButtonPressed:
...
case MachineIsBurning:
....
}
}
现在,很快就可以清楚知道此构造中的高级控制级别不能很高:ButtonPressed的处理程序必须在没有用户交互的情况下完成,并返回到主循环,因为如果没有,则不会再有其他用户事件可以加工。如果有任何状态要保存,则该状态必须保存在全局或静态变量中,但不能保存在堆栈中;即,命令式语言的正常控制流程受到限制。您本质上仅限于状态机。
当您嵌套了必须保存的子例程(例如,递归级别)时,这可能会变得非常混乱。或者正在读取文件,但该文件目前不可用。或者只是在长时间的计算中。在所有这些情况下,保存当前执行的状态并返回到主循环是可取的,这就是多线程。仅此而已。
抢先式多线程的引入使整个事情变得更加复杂(例如,操作系统决定何时应该让线程产生控制权),这就是为什么现在连接尚不清楚的原因。
因此,要回答最后一个问题:是的,状态机是另一种选择,大多数GUI都使用GUI线程进行工作。只是不要将状态机推得太远,它很快就会变得难以维护。
我认为这不是-当然,状态机是一个非常“优雅”的计算概念,但是正如您所说的,它们非常复杂。复杂的事情很难解决。不正确的事情只会被打破,因此除非您是Alan Cox假定的天才的天才,否则请坚持使用您知道的作品-将“聪明的编码”留给学习项目。
您可以说出某人何时进行了徒劳的尝试,因为(假设它运行良好)维护它时,您会发现任务几乎是不可能的。最初的“天才”将使您拥有一堆几乎无法理解的代码(因为这些类型的开发人员不会留下太多注释,更不用说技术文档了)。
在某些情况下,状态机将是一个更好的选择-我现在正在考虑使用嵌入式类型的东西,其中使用了一些状态机模式,并以更正规的方式重复使用(即适当的工程设计:))
线程也可能很难正确使用,但是有一些模式可以帮助您-主要是通过减少在线程之间共享数据的需求。
关于这一点的最后一点是,现代计算机无论如何都在许多内核上运行,因此状态机将不会真正充分利用可用资源。线程可以在这里做得更好。