Akka的documentation警告:
当使用未来的回调,例如onComplete,onSuccess和onFailure时,你需要小心避免关闭包含actor的引用,即不要在回调中调用方法或访问封闭actor上的可变状态
在我看来,如果我能够让希望访问可变状态的Future在同一个调度程序上运行,该调度程序安排互斥处理actor消息的线程,那么可以避免这个问题。那可能吗? (为什么不呢?)
ExecutionContext
提供的context.dispatcher
与演员消息调度员无关,但如果是这样的话呢?即。
class MyActorWithSafeFutures {
implicit def safeDispatcher = context.dispatcherOnMessageThread
var successCount = 0
var failureCount = 0
override def receive: Receive = {
case MakeExternalRequest(req) =>
val response: Future[Response] = someClient.makeRequest(req)
response.onComplete {
case Success(_) => successCount += 1
case Failure(_) => failureCount += 1
}
response pipeTo sender()
}
}
}
在Akka有没有办法做到这一点?
(我知道我可以将上面的例子转换成像self ! IncrementSuccess
这样的东西,但是这个问题是关于从Futures而不是通过消息改变actor状态。)
看起来我可以自己实现这一点,使用如下代码:
class MyActorWithSafeFutures {
implicit val executionContext: ExecutionContextExecutor = new ExecutionContextExecutor {
override def execute(runnable: Runnable): Unit = {
self ! runnable
}
override def reportFailure(cause: Throwable): Unit = {
throw new Error("Unhandled throwable", cause)
}
}
override def receive: Receive = {
case runnable: Runnable => runnable.run()
... other cases here
}
}
那会有用吗?为什么阿卡没有提供 - 我有没有看到一些巨大的缺点?
(请参阅https://github.com/jducoeur/Requester以获得以有限方式执行此操作的库 - 仅限Asks,而非所有Future回调。)
答案 0 :(得分:0)
您的演员正在其中一个调度程序的主题下执行receive
,并且您希望剥离一个牢牢依附于此特定主题的Future
?在这种情况下,系统无法重用此线程来运行不同的actor,因为这意味着当您想要执行Future
时线程不可用。如果它碰巧使用相同的线程来执行someClient
,那么您可能会陷入僵局。因此,此线程不能再自由地用于运行其他actor - 它必须属于MySafeActor
。
并且不允许其他线程自由运行MySafeActor
- 如果是,两个不同的线程可能会同时尝试更新successCount
并且您丢失数据(例如,如果值为0且两个线程都尝试执行successCount +=1
,值最终可以为1而不是2)。因此,为了安全地执行此操作,MySafeActor
必须有一个Thread
用于自身及其Future
。所以你最终得到了MySafeActor
而Future
紧紧地,但是无形地耦合了。这两个人不能同时运行,可能会相互僵持。 (对于一个写得不好的演员而言,它仍有可能与自己陷入僵局,但所有代码使用该演员的假想互斥体都在一个地方的事实使得它变得更容易看潜在的问题)。
您可以使用传统的多线程技术 - 互斥体等 - 允许Future
和MySafeActor
同时运行。但你真正想要的是将successCount
封装在可以同时但安全使用的东西中 - 某种......演员?
TL; DR:Future
和Actor
:1)可能无法同时运行,在这种情况下,您可能会死锁2)可能会同时运行,在这种情况下您将损坏数据3)以并发安全的方式访问状态,在这种情况下,您将重新实现Actors。
答案 1 :(得分:0)
您可以为MyActorWithSafeFutures
actor类使用PinnedDispatcher,它将为给定类的每个实例创建一个只包含一个线程的线程池,并使用context.dispatcher
作为执行上下文你的Future
。
要做到这一点,你必须在application.conf
:
akka {
...
}
my-pinned-dispatcher {
executor = "thread-pool-executor"
type = PinnedDispatcher
}
并创建你的演员:
actorSystem.actorOf(
Props(
classOf[MyActorWithSafeFutures]
).withDispatcher("my-pinned-dispatcher"),
"myActorWithSafeFutures"
)
虽然你想要实现的目标完全打破了演员模型的目的。应该封装actor状态,并且应该通过传入消息来驱动actor状态更改。
答案 2 :(得分:0)
这不会直接回答您的问题,而是使用Akka Agents提供替代解决方案:
class MyActorWithSafeFutures extends Actor {
var successCount = Agent(0)
var failureCount = Agent(0)
def doSomethingWithPossiblyStaleCounts() = {
val (s, f) = (successCount.get(), failureCount.get())
statisticsCollector ! Ratio(f/s+f)
}
def doSomethingWithCurrentCounts() = {
val (successF, failureF) = (successCount.future(), failureCount.future())
val ratio : Future[Ratio] = for {
s <- successF
f <- failureF
} yield Ratio(f/s+f)
ratio pipeTo statisticsCollector
}
override def receive: Receive = {
case MakeExternalRequest(req) =>
val response: Future[Response] = someClient.makeRequest(req)
response.onComplete {
case Success(_) => successCount.send(_ + 1)
case Failure(_) => failureCount.send(_ + 1)
}
response pipeTo sender()
}
}
问题在于,如果您想对使用@volatile
时产生的计数进行操作,那么您需要在Future中操作,请参阅doSomethingWithCurrentCounts()
。
如果您可以使用最终一致的值(可能会为代理程序安排挂起的更新),那么doSometinghWithPossiblyStaleCounts()
之类的内容就可以了。
答案 3 :(得分:0)
@rkuhn解释了为什么这会是个坏主意on the akka-user list:
我的主要考虑因素是,这样的调度程序会使得多个并发入口点进入Actor的行为非常方便,其中当前建议只有一个 - 活动行为。虽然经典数据争用被提议的ExecutionContext提供的同步排除,但它仍然允许通过挂起逻辑线程而不控制其他消息的中间执行来进行更高级别的比赛。简而言之,我认为这不会让演员更容易理解,恰恰相反。