我知道PipeTo
,但是some stuff, like synchronous waiting on nested continuation, seems to go against the async & await way.
所以,我的第一个问题[1]将是:是否有任何魔法'在这里,我们可以在延续中同步等待嵌套任务,并且它最终仍然是异步的吗?
虽然我们在async& amp;等待差异,如何处理失败?
让我们创建一个简单的例子:
public static class AsyncOperations
{
public async static Task<int> CalculateAnswerAsync()
{
await Task.Delay(1000).ConfigureAwait(false);
throw new InvalidOperationException("Testing!");
//return 42;
}
public async static Task<string> ConvertAsync(int number)
{
await Task.Delay(600).ConfigureAwait(false);
return number + " :)";
}
}
在&#39;常规&#39;,async&amp;等待方式:
var answer = await AsyncOperations.CalculateAnswerAsync();
var converted = await AsyncOperations.ConvertAsync(answer);
异常会从第一次操作中冒出来,就像你期望的那样。
现在,让我们创建一个与这些异步操作一起工作的角色。为了论证,让我们说CalculateAnswerAsync
和ConvertAsync
应该一个接一个地用作一个完整的操作(类似于StreamWriter.WriteLineAsync
和{ {1}}如果您只想将一行写入流中。)
StreamWriter.FlushAsync
如果没有例外,我仍然会public sealed class AsyncTestActor : ReceiveActor
{
public sealed class Start
{
}
public sealed class OperationResult
{
private readonly string message;
public OperationResult(string message)
{
this.message = message;
}
public string Message
{
get { return message; }
}
}
public AsyncTestActor()
{
Receive<Start>(msg =>
{
AsyncOperations.CalculateAnswerAsync()
.ContinueWith(result =>
{
var number = result.Result;
var conversionTask = AsyncOperations.ConvertAsync(number);
conversionTask.Wait(1500);
return new OperationResult(conversionTask.Result);
})
.PipeTo(Self);
});
Receive<OperationResult>(msg => Console.WriteLine("Got " + msg.Message));
}
}
没有任何问题,这会让我回到“魔术”中。点以上[1]。
此外,示例中提供的Got 42 :)
和AttachedToParent
标志是可选的,还是几乎要求所有内容都按预期工作?他们似乎对异常处理没有任何影响......
现在,如果ExecuteSynchronously
抛出异常,这意味着CalculateAnswerAsync
抛出了result.Result
,那么它几乎被吞没了。
我应该怎么做,如果可能的话,在异步操作中制作异常会使演员成为常规的&#39;例外吗?
答案 0 :(得分:10)
TPL中错误处理的乐趣:)
Task
时,该任务将独立于您的演员的ThreadPool
。这意味着你在Task
内所做的任何事情都已经与你的演员异步 - 因为它在不同的线程上运行。这篇is why I made a Task.Wait
call inside the PipeTo
sample you linked to位于您帖子的顶部。对演员没有任何影响 - 它看起来像是一个长期运行的任务。conversionTask.Result
属性将抛出在运行期间捕获的异常,因此您希望在Task
内添加一些错误处理以确保你的演员被告知出了什么问题。请注意我在这里做到了:https://github.com/petabridge/akkadotnet-code-samples/blob/master/PipeTo/src/PipeTo.App/Actors/HttpDownloaderActor.cs#L117 - 如果你把你的例外变成你的演员可以处理的信息:鸟儿开始唱歌,彩虹闪耀,TPL错误不再是痛苦和痛苦的根源。现在,如果CalculateAnswerAsync抛出异常,这意味着 result.Result抛出AggregateException,它几乎被吞噬了 没有踪影。
AggregateException
将包含其中包含的内部异常列表 - TPL具有聚合错误概念的原因是(a)您有一个任务是多个任务的延续总的来说,即Task.WhenAll
或(b)您将错误传播到ContinueWith
链回到父级。您还可以调用AggregateException.Flatten()
调用,以便更轻松地管理嵌套异常。
处理TPL中的例外是一件令人讨厌的事情,这是真的 - 但解决问题的最佳方式是try..catch..
内的Task
例外情况并将其转换为邮件类别演员可以处理。
此外,在示例可选中提供了AttachedToParent和ExecuteSynchronously标志,还是他们几乎需要让所有内容按预期工作?
这主要是一个问题,当你继续延续 - PipeTo
自动使用这些标志。它对错误处理没有任何影响,但可确保您的连续在与原始Task
相同的线程上立即执行。
我建议只在你进行大量嵌套延续时才使用这些标志 - 一旦你进行了超过1次延续,TPL开始对如何安排你的任务采取一些自由(实际上,像OnlyOnCompleted这样的标志超过1次继续后不再被接受。)
答案 1 :(得分:7)
只是为了增加亚伦所说的话。 截至昨天,我们确实在使用Task调度程序时支持在actor内部安全的异步等待。
public class AsyncAwaitActor : ReceiveActor
{
public AsyncAwaitActor()
{
Receive<string>(async m =>
{
await Task.Delay(TimeSpan.FromSeconds(1));
Sender.Tell("done");
});
}
}
public class AskerActor : ReceiveActor
{
public AskerActor(ActorRef other)
{
Receive<string>(async m =>
{
var res = await other.Ask(m);
Sender.Tell(res);
});
}
}
public class ActorAsyncAwaitSpec : AkkaSpec
{
[Fact]
public async Task Actors_should_be_able_to_async_await_ask_message_loop()
{
var actor = Sys.ActorOf(Props.Create<AsyncAwaitActor>()
.WithDispatcher("akka.actor.task-dispatcher"),
"Worker");
//IMPORTANT: you must use the akka.actor.task-dispatcher
//otherwise async await is not safe
var asker = Sys.ActorOf(Props.Create(() => new AskerActor(actor))
.WithDispatcher("akka.actor.task-dispatcher"),
"Asker");
var res = await asker.Ask("something");
Assert.Equal("done", res);
}
}
这不是我们的默认调度程序,因为它确实带来了性能/吞吐量的代价。
如果触发阻止任务(例如使用task.Wait()
或task.Result
),也存在死锁风险
所以PipeTo
模式仍然是首选方法,因为它更适合于actor模型。
但是,如果您真的需要进行一些TPL集成,async await支持就可以作为额外的工具。
此功能实际上使用了PipeTo
。
它将使每个任务继续并将其包装在特殊消息中并将该消息传递回actor并在actor自己的并发上下文中执行该任务。