状态机与线程


24

艾伦·考克斯(Alan Cox)曾说过: “计算机是状态机。线程是为无法对状态机进行编程的人的。”
由于直接询问Alan并不是让我谦虚的选择,所以我想在这里问:一个人如何仅使用一个线程和状态机来实现高级语言(例如Java)的多线程功能?例如,如果要执行2个活动(进行计算和执行I / O)并且一个活动可以阻止该怎么办?
使用“仅状态机”方式是否可以替代高级语言中的多线程?


4
计算机也是图灵机。尽管如此,像图灵机一样对它进行编程并不一定有用。命令式语言中的堆栈是非常有用的东西,多线程程序允许同时在内存中保留多个堆栈。在状态机中做相同的事情肯定是可能的,但更麻烦的是。
thiton 2011年

10
Alan是一名OS内核开发人员。这是他的家。因此,他的报价应在这种情况下使用。他本来会“针对金属”进行编程,在这种情况下,使用这种模型更为合理。一旦操作系统抽象出了硬件及其固有的特性(即“一台计算机就是一个状态机...”),您就有机会并受益于能够使用在您的领域中更有意义的其他模型。几乎每个游戏都大量使用状态机。
史蒂文·埃弗斯

2
线程只是操作系统的一项功能,可以自动管理某些状态机开关。显然,您可以创建一个巨大的状态机,该状态机将自己管理所有内容,但是更加复杂。关于过程也可以这样说。您可以说过程是针对不能编程状态机的人的。但是抽象为您提供了更简单,更不易出错的界面。我认为这只是现实中应该听到,考虑并随后忽略的另一个“很酷的报价”。
Yam Marcovic

2
“但是[thread]抽象为您提供了更简单,更不易出错的界面。” 这似乎是错误的。出现线程安全错误的人数表明它似乎引起错误。
S.Lott

1
这里的许多评论和答案通常都将报价解释为反多任务处理。我相信Alan Cox只是反线程,并且会提倡使用多个进程来实现人们使用线程的许多目标。请记住,他是Unix黑客:fork FTW。我没有直接从报价中找到他的任何评论,但这是Linux内核邮件列表中来自Larry McVoy的评论,它朝这个方向发展:lkml.indiana.edu/hypermail/linux/kernel/0106.2/0405.html
马丁B

Answers:


25

一个线程所做的只是交错操作,因此进程的各个部分似乎在时间上重叠。具有多线程的单核计算机只是跳来跳去:它从一个线程执行少量代码,然后切换到另一个线程。一个简单的调度程序决定哪个线程具有最高优先级,并在内核中实际执行。

在单核计算机上,实际上没有 “同时”发生任何事情。只是交错执行。

有许多种方法可以实现交错。许多。

假设您有一个简单的两线程进程,该进程使用一个简单的锁,以便两个线程都可以写入一个公共变量。您有六个代码块。

  • T1-锁前
  • T1-带锁
  • T1后锁
  • T2-锁前
  • T2-带锁
  • T2后锁

[这可以是循环的,也可以有更多的锁,等等。它所做的只是变得更长,而不是更加复杂。]

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的每个步骤中,将有两个选择(两个线程-两个选择),掷硬币可以确定遵循哪个特定状态。


谢谢,很好的解释。那多核机器呢?我想,有没有明确的方法来利用Java状态机中的内核?为此,需要依靠操作系统,对吗?
Victor Sorokin

5
多核计算机使调度稍微复杂一些。但是,所有内核都写入单个公共内存,因此,两个内核之间的“内存写顺序”意味着-从本质上讲,我们回到了内存写的交错执行。操作系统利用内核,而JVM利用内核。无需三思。
S.Lott

9

编写阻止功能的对象是无法创建状态机的人;)

如果无法绕过阻塞,线程很有用。没有一项基本的计算机活动能够真正阻止,只是为了方便使用,就以这种方式实施了许多活动。读取函数将阻塞直到读取整个缓冲区,而不是返回字符或“读取失败”。连接函数将等待应答,而不是检查队列中的返回消息并返回(如果未找到返回消息)。

您不能在状态机中使用阻止功能(至少一个不允许“冻结”的功能)。

是的,使用状态机是一种可行的选择。在实时系统中,这是唯一的选择,该系统为机器提供框架。使用线程和阻塞函数只是“轻松的出路”,因为通常一个对阻塞函数的调用会替换状态机中的大约3-4个状态。


从根本上说,单执行上下文程序中的代码页错误实际上是阻塞的。根据定义,只有流程中的下一个代码块可用时,具有单一执行上下文的代码才能向前推进。
大卫·史瓦兹

1
@David Schwartz:是的,这从根本上说是封锁;但这不是“操作”,因为它不是受阻止的代码执行的事情,而是发生的事情。
哈维尔

1
文件读取从根本上来说并不是阻塞-它可以始终拆分成请求读取给定位置并在满足请求后从缓冲区获取数据。页面错误是偶然使用/启发式交换使用的一种解决方法。如果在执行状态所需的所有数据都可用之前进入了给定状态,就会发生这种情况-缺乏远见,这与状态机的概念背道而驰。如果换出和换入操作是状态机的一部分,则不会发生页面错误。
SF。

1
@David Schwartz:“阻止”行为的定义始终受“实时”要求的约束。例如:对于需要几百毫秒量级响应性的应用程序,代码页错误被认为是非阻塞的。另一方面,如果应用程序对实时性有严格的要求,则根本不会使用虚拟内存。
2011年

1
@Mar:...或使用确定性交换算法,以确保在必要数据之前将其提取。
SF。

9

如何仅使用一个线程和状态机就可以以高级语言(例如Java)实现多线程功能?例如,如果要执行2个活动(进行计算和执行I / O)并且一个活动可以阻止该怎么办?

您所描述的称为协作多任务处理,其中将任务分配给CPU,并期望在一定数量的时间或活动后自动放弃。通过继续使用CPU或阻塞整个工作以及没有硬件看门狗定时器而无法配合的任务,监督任务的代码对此无能为力。

在现代系统中,您看到的被称为抢占式多任务处理,这是任务不必放弃CPU的地方,因为当硬件生成的中断到来时,管理器会为它们执行任务。主管中的中断服务例程将保存CPU的状态,并在下次认为该任务应有时间片时恢复该状态,然后从接下来要运行的任何任务中恢复该状态,并跳回该状态,就像什么都没发生一样。此操作称为上下文切换,可能会很昂贵。

使用“仅状态机”方式是否可以替代高级语言中的多线程?

可行吗?当然。桑 有时。是否使用线程还是某种形式的自制协作式多任务处理(例如状态机),取决于您愿意做出的权衡。

线程简化了任务设计,使您可以将每个任务视为自己的程序,恰好与他人共享数据空间。这使您可以自由地专注于手头的工作,而不必一次执行一个迭代就需要进行所有管理和内务处理。但是,由于没有妥协的善行,因此您需要为上下文切换中的所有这些便利付费。在执行最少的工作后(自愿或通过执行诸如I / O之类会阻塞的事情)拥有许多可以产生CPU的线程会占用大量处理器时间进行上下文切换。如果阻塞操作很少阻塞很长时间,则尤其如此。

在某些情况下,合作路线更有意义。我曾经不得不为一块硬件编写一些用户界面软件,该软件通过需要轮询的内存映射接口流式传输许多数据通道。每个通道都是以这样一种方式构建的对象:我可以让它作为线程运行,也可以重复执行一个轮询周期。

正是由于我上面概述的原因,多线程版本的性能一点也不好:每个线程的工作量最少,然后释放CPU,因此其他通道可能需要一些时间,从而导致大量上下文切换。让线程自由运行直到被抢占,这有助于提高吞吐量,但导致某些通道在硬件经历缓冲区溢出之前无法得到服务,因为它们没有足够的时间片。

单线程版本甚至对每个通道都进行了迭代,运行时就像烫伤的猿猴一样,系统上的负载也像石头一样下降。我为提高性能付出的代价是不得不自己处理任务。在这种情况下,执行此操作的代码非常简单,因此开发和维护它的成本值得提高性能。我想那真的是底线。如果我的线程一直在等待一些系统调用返回,那练习可能是不值得的。

这让我想到了Cox的评论:线程不仅仅适用于无法编写状态机的人。有些人有能力做到这一点,但为了更快或更省事地完成工作,选择使用固定状态机(即线程)。


2

如果要执行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:52

IBM论文),如果您的应用程序依赖大量线程,那么您将始终与调度程序发生冲突吗?很多人在一个问题上抛出很多线程,这确实是不好的设计。

那是您最不用担心的。1000个线程是8Mb的内核堆栈,并且有足够的任务切换以确保您可以关闭大部分缓存。计算机是状态机。线程适用于无法对状态机进行编程的人。

在很多情况下,Linux绝对不能帮助这种情况,特别是异步块I / O。

艾伦

来源:Re:IBM对linux内核线程的有趣分析(linux-kernel邮件列表档案)


1
  • 从理论上讲,这是真的。在现实生活中,线程只是用于对这种状态机进行编程的有效抽象。它们是如此高效,以至于也可以用于对Statecharts和Petri网进行编程(即,并行行为,其中状态机基本上是顺序的)。

  • 状态机的问题是组合爆炸。具有4G RAM的计算机的状态数为2 ^(2 ^ 32)状态(不包括2T磁盘驱动器)。

  • 对于一个唯一的工具是锤子的男人来说,每个问题都像钉子。


1

在两种情况下,线程是唯一的选择:

  • 使用多核而没有内存分离。
  • 以应对阻塞的外部代码。

第二个原因是为什么大多数人认为线程是进行IO或网络编程所不可避免的,但这通常是因为他们不知道自己的操作系统具有更高级的API(或者不想与之抗争)。

至于易用性和可读性,总是有事件循环(如libevEventMachine)使状态机的编程几乎与使用线程进行状态循环一样简单,但是却提供了足够的控制权来忘记同步问题。


1
两全其美:线程中的小型阻塞状态机使应用程序代码非常简单。如果有线程,线程可以很好地分配给内核。如果一切都没有至少两个核心,那么它将很快。即基于四核臂的手机将在2012
推出。– Tim Williscroft 2011年

0

了解状态机和多线程交互方式的一种好方法是查看GUI事件处理程序。许多GUI应用程序/框架使用单个GUI线程,该线程将轮询可能的输入源并为每个接收到的输入调用一个函数。本质上,这可以写成一个巨大的开关:

while (true) {
    switch (event) {
        case ButtonPressed:
        ...
        case MachineIsBurning:
        ....
    }
}

现在,很快就可以清楚知道此构造中的高级控制级别不能很高:ButtonPressed的处理程序必须在没有用户交互的情况下完成,并返回到主循环,因为如果没有,则不会再有其他用户事件可以加工。如果有任何状态要保存,则该状态必须保存在全局或静态变量中,但不能保存在堆栈中;即,命令式语言的正常控制流程受到限制。您本质上仅限于状态机。

当您嵌套了必须保存的子例程(例如,递归级别)时,这可能会变得非常混乱。或者正在读取文件,但该文件目前不可用。或者只是在长时间的计算中。在所有这些情况下,保存当前执行的状态并返回到主循环可取的,这就是多线程。仅此而已。

抢先式多线程的引入使整个事情变得更加复杂(例如,操作系统决定何时应该让线程产生控制权),这就是为什么现在连接尚不清楚的原因。

因此,要回答最后一个问题:是的,状态机是另一种选择,大多数GUI都使用GUI线程进行工作。只是不要将状态机推得太远,它很快就会变得难以维护。


0

询问使用状态机是否可以使用高级语言,有点像询问使用汇编程序编写是否可以替代使用高级语言。在正确的情况下,他们俩都有自己的位置。

使用线程的抽象使更复杂的并行系统更易于实现,但是最终所有并行系统都有相同的问题要处理。对于基于状态机的系统,死锁/活锁优先级倒置之类的经典问题尽可能地多,如果它们具有足够复杂的共享内存并行NUMA甚至基于CSP的系统,那么它们就尽可能。


0

我认为这不是-当然,状态机是一个非常“优雅”的计算概念,但是正如您所说的,它们非常复杂。复杂的事情很难解决。不正确的事情只会被打破,因此除非您是Alan Cox假定的天才的天才,否则请坚持使用您知道的作品-将“聪明的编码”留给学习项目。

您可以说出某人何时进行了徒劳的尝试,因为(假设它运行良好)维护它时,您会发现任务几乎是不可能的。最初的“天才”将使您拥有一堆几乎无法理解的代码(因为这些类型的开发人员不会留下太多注释,更不用说技术文档了)。

在某些情况下,状态机将是一个更好的选择-我现在正在考虑使用嵌入式类型的东西,其中使用了一些状态机模式,并以更正规的方式重复使用(即适当的工程设计:))

线程也可能很难正确使用,但是有一些模式可以帮助您-主要是通过减少在线程之间共享数据的需求。

关于这一点的最后一点是,现代计算机无论如何都在许多内核上运行,因此状态机将不会真正充分利用可用资源。线程可以在这里做得更好。


2
状态机一点也不复杂!复杂的状态机很复杂,但是所有复杂的系统也是如此:o)
MaR

2
-1为“请勿尝试”。那是您可以提供的最糟糕的建议。
哈维尔

1
-1“不要尝试”?那真是愚蠢。我还要挑战您关于状态机很难的主张。一旦进入了类似“ Hirarchal Fuzzy State Machine”的系统,那么,那就更加复杂了。但是一个简单的状态机?那是我上学时每2年学到的非常基本的东西。
史蒂文·埃弗斯

让我改写“不要尝试”位...
gbjbaanb

0

正确使用状态机而不使用线程的一个很好的例子:nginx vs apache2。通常,您可以假设nginx在一个线程中处理所有连接,而apache2为每个连接创建一个线程。

但是对我来说,使用状态机vs线程与使用完全手工制作的asm vs java十分相似:您可能会获得难以理解的结果,但是这需要大量的程序员,大量的纪律,使得项目更加复杂,并且只有在被使用时才值得很多其他程序员。因此,如果您是想制造快速Web服务器的人,请使用状态机和异步IO。如果要编写项目(而不是要在所有地方使用的库),请使用线程。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.