在上一篇文章中 ,我谈到了构建并发代码(无论是否在Scala中)的重要性,以及开发人员在这样做时面临的一些问题,包括不要锁得太多,锁得太少,避免死锁,避免旋转太多线程,依此类推。 令人沮丧的清单。
我不是一个让事情陷入如此绝望的状态的人,然后我开始与您一起探索一些Scala的并发构造,首先是直接从Scala内部直接使用Java语言的并发库,然后转到MailBox
类型。来自Scala API。 尽管这两种方法当然都是可行的,但这并不是Scala和并发的真正“热度”所在。
输入Scala 演员 ...离开舞台。
什么是“演员”?
从本质上讲,“参与者”实现是一种在执行实体(称为参与者)之间使用消息传递来协调工作的方法(请注意,有意拒绝使用“过程”,“线程”或“机器”之类的词)。 如果这听起来像是RPC机制,那确实是,不是。 如果RPC调用(如Java RMI调用)将在调用方阻塞,直到服务器端完成其处理并发送回某种响应(返回值或异常),则消息传递方法故意不会阻塞调用方,从而巧妙地避免了死锁的机会。
就其本身而言,仅传递消息并不能避免所有并发错误代码的问题。 此外,这种方法还促进了“无共享”编程风格,在这种风格中,不同的参与者无法访问共享数据结构(顺便说一句,有助于促进对该参与者是否是此JVM本地或整个世界的封装) )-这完全消除了同步的需要。 毕竟,正如我们之前所看到的,如果没有共享,则不需要同步任何并发执行。
这几乎不是参与者模型的正式描述,而且毫无疑问,那些具有更正式计算机科学背景的人会发现各种各样的方法,使这种描述无法捕获参与者的全部细节。 但是出于本文的目的,它是一个良好的基础。 可以在Web上找到更详细和正式的描述,包括几篇详细介绍演员背后概念的学术论文(我将由您自行决定以后再跟进)。 现在,我们准备看一下Scala actors API。
斯卡拉演员
从根本上讲,与actor合作并不是那么困难,最简单的方法就是使用Actor
类的actor
方法简单地创建一个actor,如清单1所示:
清单1.还有...动作!
import scala.actors._, Actor._
package com.tedneward.scalaexamples.scala.V4
{
object Actor1
{
def main(args : Array[String]) =
{
val badActor =
actor
{
receive
{
case msg => System.out.println(msg)
}
}
badActor ! "Do ya feel lucky, punk?"
}
}
}
同时发生了几件事。
首先,我们从自命名包中导入Scala Actors库,然后直接从该库中导入Actor
类的成员。 第二步不是严格必需的,因为我们可以继续使用Actor.actor
在代码中使用Actor.actor
而不是actor
,但是这样做可以给人以为actor
是该语言的内置构造,并且(在某些观点上)使代码更具可读性。
下一步是使用actor
方法创建actor本身,该方法将代码块作为参数。 在这种情况下,该代码块将执行一个简单的receive
,我们将在稍后进行介绍。 结果是一个actor,已存储到值引用中,可以使用。
请记住,参与者不使用交流方法,而是使用消息。 当我说下一行使用!
时,可能有点违反直觉!
实际上是一种(实际上)将消息发送到badActor
的方法(实际上)。 内幕深深地埋藏在Actor
特质中,是我们上次查看的那些MailBox
元素中的另一个; !
方法采用传递的参数(在这种情况下为String),将其转储到邮箱中并立即返回。
一旦消息已传递给角色,角色便通过调用其receive
方法来处理该消息,该方法完全如其名称所暗示的那样-从信箱中提取第一个可用消息并将其传递给隐式模式匹配块。 请注意,由于您没有为模式匹配的大小写指定类型,因此任何内容都将匹配,并且消息将绑定到msg
名称(您需要打印该消息)。
请仔细注意一个事实,即可以发送的类型没有任何类型限制-您不仅限于前面的示例可能暗示的字符串。 实际上,基于参与者的设计经常使用Scala案例类来传达实际的消息本身,提供隐式的“命令”或“动作”以根据类型执行,并以案例类的参数/成员作为参数或动作数据。
例如,假设您希望参与者响应发送的消息而执行几个不同的动作; 新的实现看起来类似于清单2:
清单2.嘿,我是导演!
object Actor2
{
case class Speak(line : String);
case class Gesture(bodyPart : String, action : String);
case class NegotiateNewContract;
def main(args : Array[String]) =
{
val badActor =
actor
{
receive
{
case NegotiateNewContract =>
System.out.println("I won't do it for less than $1 million!")
case Speak(line) =>
System.out.println(line)
case Gesture(bodyPart, action) =>
System.out.println("(" + action + "s " + bodyPart + ")")
case _ =>
System.out.println("Huh? I'll be in my trailer.")
}
}
badActor ! NegotiateNewContract
badActor ! Speak("Do ya feel lucky, punk?")
badActor ! Gesture("face", "grimaces")
badActor ! Speak("Well, do ya?")
}
}
到目前为止,一切都很好,但是在运行时,仅协商了新合同。 之后,JVM终止。 刚开始时,可能会感觉到生成的线程对消息的响应速度不够快,但是请记住,在参与者模型中,您本身并不处理线程,而只是处理消息传递。 取而代之的是,这里的问题要简单得多:一个接收会生出一条消息,因此您已经将多条消息排队的事实并不重要,因为只有一个接收而只有一条消息传递,而不管可能排队了多少等待处理。
要解决此问题,需要对代码进行以下更改,如清单3所示:
- 将
receive
块放入一个无限循环内。 - 创建一个新的case类,以指示何时完成整个处理。
清单3.现在我是一个更好的导演!
object Actor2
{
case class Speak(line : String);
case class Gesture(bodyPart : String, action : String);
case class NegotiateNewContract;
case class ThatsAWrap;
def main(args : Array[String]) =
{
val badActor =
actor
{
var done = false
while (! done)
{
receive
{
case NegotiateNewContract =>
System.out.println("I won't do it for less than $1 million!")
case Speak(line) =>
System.out.println(line)
case Gesture(bodyPart, action) =>
System.out.println("(" + action + "s " + bodyPart + ")")
case ThatsAWrap =>
System.out.println("Great cast party, everybody! See ya!")
done = true
case _ =>
System.out.println("Huh? I'll be in my trailer.")
}
}
}
badActor ! NegotiateNewContract
badActor ! Speak("Do ya feel lucky, punk?")
badActor ! Gesture("face", "grimaces")
badActor ! Speak("Well, do ya?")
badActor ! ThatsAWrap
}
}
谁说制作电影很难,显然没有演员Scala演员。
同时行动
从这段代码中看不到的一件事是并发(如果有的话)来自哪里—到目前为止,我已经向您展示了这很可能是方法调用的另一种同步形式,您将无法分辨区别。 (从技术上讲,您可以推断出第二个示例中存在一些并发性,然后再进行近乎无限的循环,但这更多是偶然的证明,显然不是铁定的证明。)
为了证明所有这些线程都在下面,请更深入地研究前面的示例:
清单4.我准备特写,DeMille先生
object Actor3
{
case class Speak(line : String);
case class Gesture(bodyPart : String, action : String);
case class NegotiateNewContract;
case class ThatsAWrap;
def main(args : Array[String]) =
{
def ct =
"Thread " + Thread.currentThread().getName() + ": "
val badActor =
actor
{
var done = false
while (! done)
{
receive
{
case NegotiateNewContract =>
System.out.println(ct + "I won't do it for less than $1 million!")
case Speak(line) =>
System.out.println(ct + line)
case Gesture(bodyPart, action) =>
System.out.println(ct + "(" + action + "s " + bodyPart + ")")
case ThatsAWrap =>
System.out.println(ct + "Great cast party, everybody! See ya!")
done = true
case _ =>
System.out.println(ct + "Huh? I'll be in my trailer.")
}
}
}
System.out.println(ct + "Negotiating...")
badActor ! NegotiateNewContract
System.out.println(ct + "Speaking...")
badActor ! Speak("Do ya feel lucky, punk?")
System.out.println(ct + "Gesturing...")
badActor ! Gesture("face", "grimaces")
System.out.println(ct + "Speaking again...")
badActor ! Speak("Well, do ya?")
System.out.println(ct + "Wrapping up")
badActor ! ThatsAWrap
}
}
当这个新示例运行时,很明显地涉及到两个不同的线程:
-
main
线程(启动每个Javamain
线程的同一线程) -
Thread-2
线程是由Scala Actors库在幕后衍生出来的
所以是的,从根本上说,我们在启动第一个参与者时一直在执行多线程执行。
但是,习惯于这种新的执行模型可能会有些尴尬,这仅仅是因为它代表了一种完全不同的并发性思维方式。 例如,考虑上一篇文章中的生产者/消费者模型。 那里有很多代码,尤其是在Drop
类中,使我们可以很清楚地了解线程相互之间以及与使所有内容保持同步所需的监视器之间的关系。 我在这里重复上一篇文章的V3代码以供参考:
清单5. ProdConSample,v3(Scala)
package com.tedneward.scalaexamples.scala.V3
{
import concurrent.MailBox
import concurrent.ops._
object ProdConSample
{
class Drop
{
private val m = new MailBox()
private case class Empty()
private case class Full(x : String)
m send Empty() // initialization
def put(msg : String) : Unit =
{
m receive
{
case Empty() =>
m send Full(msg)
}
}
def take() : String =
{
m receive
{
case Full(msg) =>
m send Empty(); msg
}
}
}
def main(args : Array[String]) : Unit =
{
// Create Drop
val drop = new Drop()
// Spawn Producer
spawn
{
val importantInfo : Array[String] = Array(
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
);
importantInfo.foreach((msg) => drop.put(msg))
drop.put("DONE")
}
// Spawn Consumer
spawn
{
var message = drop.take()
while (message != "DONE")
{
System.out.format("MESSAGE RECEIVED: %s%n", message)
message = drop.take()
}
}
}
}
}
有趣的是,Scala如何简化了部分代码,但实际上与原始Java版本在概念上并没有太大不同。 但是,现在让我们看一下如果您将其简化为最基本的要点,那么基于actor的Producer / Consumer示例版本将是什么样子:
清单6.采取1.然后……行动! 生产! 消耗!
object ProdConSample1
{
case class Message(msg : String)
def main(args : Array[String]) : Unit =
{
val consumer =
actor
{
var done = false
while (! done)
{
receive
{
case msg =>
System.out.println("Received message! -> " + msg)
done = (msg == "DONE")
}
}
}
consumer ! "Mares eat oats"
consumer ! "Does eat oats"
consumer ! "Little lambs eat ivy"
consumer ! "Kids eat ivy too"
consumer ! "DONE"
}
}
这个第一个版本肯定会很简洁,在某些情况下,也许可以做所有需要做的事情,但是运行代码并将其与早期版本进行比较发现了一个重要的区别–基于actor的版本是一个多位置缓冲区,而不是像我们以前使用过的那样单槽下降。 在某些人看来,这似乎是一种增强,而不是缺点,但是让我们确保将其进行“比较”-让我们来回过头来创建基于actor的Drop
版本,在该版本中,对put()
每次调用都必须以调用take()
。
幸运的是,Scala Actors库可以很容易地模仿此功能。 从根本上讲,您希望生产者阻止,直到消费者收到消息为止。 最简单的方法是让生产者阻止,直到生产者收到消费者的确认,即已收到消息。 从某种意义上讲,这是以前的基于监视器的代码使用锁定对象周围的监视器进行该信号传递的方式。
在Scala Actors库中最简单的方法是使用!?
方法而不是!
方法(在收到确认之前将一直阻塞)。 (如果您很好奇,在Scala Actors实现中,每个Java线程已经是一个actor,因此答复将到达与main
线程隐式关联的邮箱。)这意味着使用者需要发送某种确认信息; 它使用隐式继承的reply
方法(以及receive
方法)来执行此操作,如清单7所示:
清单7.采取2 ...行动!
object ProdConSample2
{
case class Message(msg : String)
def main(args : Array[String]) : Unit =
{
val consumer =
actor
{
var done = false
while (! done)
{
receive
{
case msg =>
System.out.println("Received message! -> " + msg)
done = (msg == "DONE")
reply("RECEIVED")
}
}
}
System.out.println("Sending....")
consumer !? "Mares eat oats"
System.out.println("Sending....")
consumer !? "Does eat oats"
System.out.println("Sending....")
consumer !? "Little lambs eat ivy"
System.out.println("Sending....")
consumer !? "Kids eat ivy too"
System.out.println("Sending....")
consumer !? "DONE"
}
}
或者,如果您更喜欢使用spawn
来将Producer启动到与main()
分开的线程中的版本(与原始版本最接近),则它可能类似于清单8:
清单8.采取4 ...行动!
object ProdConSampleUsingSpawn
{
import concurrent.ops._
def main(args : Array[String]) : Unit =
{
// Spawn Consumer
val consumer =
actor
{
var done = false
while (! done)
{
receive
{
case msg =>
System.out.println("MESSAGE RECEIVED: " + msg)
done = (msg == "DONE")
reply("RECEIVED")
}
}
}
// Spawn Producer
spawn
{
val importantInfo : Array[String] = Array(
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too",
"DONE"
);
importantInfo.foreach((msg) => consumer !? msg)
}
}
}
无论您以哪种方式查看,基于actor的版本都比原始版本简单得多...只要读者可以使actor和隐式邮箱保持一致即可。
这不是小事。 参与者模型严重颠覆了并发性和线程安全性思考的整个过程; 它从专注于共享数据结构( 数据并发 )的模型变为专注于作用于数据的代码本身的结构( 任务并发 )的模型,并尽可能少地共享数据。 请注意,先前代码中的示例生产者/消费者中的反转。 在前面的示例中,并发是围绕Drop
类(有界缓冲区)显式编写的。 在本文的此版本中, Drop
甚至没有出现,并且焦点仍然集中在两个参与者(线程)及其通过无共享消息进行的交互上。
自然地,仍然可以与参与者建立以数据为中心的并发构造。 您只需要采取稍微不同的方法即可。 考虑这个简单的“ counter”对象,该对象使用参与者消息来传达“ increment”和“ get”操作,如清单9所示:
清单9.用5 ...计数!
object CountingSample
{
case class Incr
case class Value(sender : Actor)
case class Lock(sender : Actor)
case class UnLock(value : Int)
class Counter extends Actor
{
override def act(): Unit = loop(0)
def loop(value: int): Unit = {
receive {
case Incr() => loop(value + 1)
case Value(a) => a ! value; loop(value)
case Lock(a) => a ! value
receive { case UnLock(v) => loop(v) }
case _ => loop(value)
}
}
}
def main(args : Array[String]) : Unit =
{
val counter = new Counter
counter.start()
counter ! Incr()
counter ! Incr()
counter ! Incr()
counter ! Value(self)
receive { case cvalue => Console.println(cvalue) }
counter ! Incr()
counter ! Incr()
counter ! Value(self)
receive { case cvalue => Console.println(cvalue) }
}
}
为了更符合Producer / Consumer示例的目的,清单10是内部使用actor的Drop
的版本(可能是为了允许其他Java类使用Drop
而不用担心如何调用actors方法)直):
清单10.放下,见演员
object ActorDropSample
{
class Drop
{
private case class Put(x: String)
private case object Take
private case object Stop
private val buffer =
actor
{
var data = ""
loop
{
react
{
case Put(x) if data == "" =>
data = x; reply()
case Take if data != "" =>
val r = data; data = ""; reply(r)
case Stop =>
reply(); exit("stopped")
}
}
}
def put(x: String) { buffer !? Put(x) }
def take() : String = (buffer !? Take).asInstanceOf[String]
def stop() { buffer !? Stop }
}
def main(args : Array[String]) : Unit =
{
import concurrent.ops._
// Create Drop
val drop = new Drop()
// Spawn Producer
spawn
{
val importantInfo : Array[String] = Array(
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
);
importantInfo.foreach((msg) => { drop.put(msg) })
drop.put("DONE")
}
// Spawn Consumer
spawn
{
var message = drop.take()
while (message != "DONE")
{
System.out.format("MESSAGE RECEIVED: %s%n", message)
message = drop.take()
}
drop.stop()
}
}
}
如您所见,它需要更多的代码(和额外的线程,因为每个actor都在线程池中运行),但是此版本与以前构建的版本在API上等效,因此将所有并发问题放在Java的Drop
类中传统上,开发人员期望如此。
演员还有更多。
在诸如大规模系统内部的情况下,让每个actor受到Java线程的支持将太繁琐和浪费,特别是如果每个actor要花费更多的时间等待而不是处理。 在这种情况下,基于事件的参与者可能是合适的; 它实际上位于一个封闭的内部,该封闭捕获了演员的其余动作。 也就是说,现在不必通过线程状态和寄存器等来表示代码块(函数)。 一旦向参与者发送消息(显然需要一个活动线程),就会触发该关闭,因此该关闭在活动期间借用了一个活动线程,然后终止或通过回叫使其自身进入另一个“等待”状态本身,有效地释放线程以供其他用途。 (请参阅“ 相关主题”中的Haller / Odersky论文。)
在Scala Actors库中,这是通过react
方法完成的,而不是如我在本文中所示的receive
。 使用react
的关键是正式的react
无法返回,因此react
内部的实现必须重新调用包含react
块的代码块。 这是loop
构造派上用场的地方,它创建了一个接近无限的循环(顾名思义)。 这意味着清单10中的Drop
实现实际上可以纯粹通过借用调用者的线程来进行操作,从而减少了执行所有所需操作所需的线程数。 (在实践中,我从未见过这样简单的例子,所以我想我们必须听懂Scala设计师的话。)
在某些情况下,您可以选择从基本Actor
特性继承(在这种情况下,必须定义act
方法或该类保持抽象),以便创建一个隐式充当actor的新类。 话虽如此,这个想法在Scala社区中已不受欢迎; 通常,我勾勒出的方法(使用Actor
对象中的actor
方法)是创建新actor的首选方法。
结论
由于在actor中进行编程所需要的样式与在“传统”对象中进行编程所需要的样式稍有不同,因此在与actor合作时需要牢记一些注意事项。
首先,请记住,参与者的大部分力量来自消息传递样式,而不是代表其他命令式编程世界的阻塞调用样式。 (有趣的是,以消息传递为核心原理的面向对象的语言已经存在。最广泛认可的两种语言是Objective-C和Smalltalk,此外,ThoughtWorker Ola同事创建了一个新块Ioke。 Bini。)如果您创建直接或间接扩展Actor
类,请尝试确保通过消息传递来完成对所述对象的所有调用。
其次,由于消息可以在任何给定的时间点传递(更重要的是)可能在发送和接收之间存在相当大的延迟,因此请确保消息具有正确处理它们可能需要的所有状态。 这种方法将导致:
- 使代码更易于理解(因为消息将带有它需要处理的所有状态)
- 减少参与者在其他地方访问共享状态的机会,从而减少僵局或其他并发噩梦的机会
第三,尽管这在对话中可能会很明显,但值得一提的是,演员不应受到阻碍。 从本质上讲,阻塞是导致死锁的原因。 您的代码可以避免阻塞的越多,死锁的机会就越多。
有趣的是,如果您熟悉Java消息服务(JMS)API,您会在我提出的这些建议中发现强烈的相似之处-毕竟,参与者的消息传递风格就是消息之间的传递实体(例如JMS消息传递就是在实体之间传递的消息)。 不同之处在于,JMS消息倾向于更大的规模,并且在层和进程级别上运行,而参与者消息倾向于较小的规模,并且在对象和线程级别上运行。 如果您拥有JMS,那么您将拥有演员。
Actor并不是解决您的代码可能遇到的每一个并发问题的灵丹妙药,但是Actor肯定提供了一种使用外观和操作方式相当简单明了的构造来对应用程序或库代码进行建模的新方法。 这并不意味着它们将始终按照您期望的方式运行,但是其中某些行为是可以预期的-毕竟,对象可能没有按照您首次遇到它们时所期望的方式运行。
这是本期的内容; 直到下一次,尽情享受!
翻译自: https://www.ibm.com/developerworks/java/library/j-scala04109/index.html