等待抽象异步任务

3

我是一个对async/await使用非常新手的人。我试图在UI中抽象出异步性和await条件。我有一个抽象的基类:

public abstract class Base
{
    public abstract bool IsRunning { get; }
    public abstract Task<bool> Run();
}

并且从它中派生一些实例,第一个是同步的:

internal class Derived1 : Base
{
    private readonly Base baseCase;
    private Task<bool> task;
    public Derived1(Base baseCase)
    {
        this.baseCase = baseCase;
    }
    public override bool IsRunning
    {
        get { return false; }
    }
    public override Task<bool> Run()
    {
        task = new Task<bool>(() => 
        {
            bool ok = DoSomething();
            return ok;
        });
        return task;
    }
}

以及一个异步实现的派生类:

internal class Derived2 : Base
{
    private readonly Base baseCase;
    private Task<bool> task;
    public Derived2(Base baseCase)
    {
        this.baseCase = baseCase;
    }
    public override bool IsRunning
    {
        get { return task != null && task.Status == TaskStatus.Running; }
    }
    public override Task<bool> Run()
    {
        task = new Task<bool>(() => 
        {
            bool ok = DoSomething();
            return ok;
        });
        return task;
    }
}

然后在用户界面中,如果用户在运行时配置中指定了,我希望能够await一个asynchronous任务,如下所示:

internal class CaseMenuHandler
{
    private async void OnRun(object sender, EventArgs args)
    {
        foreach (var case in Cases)
        {
            Base baseCaseRunner = GetCaseRunner(case);
            try
            {
                bool ok = true;
                if( something_holds ) {
                    ok = await baseCaseRunner.Run();
                }
                else {
                    ok = baseCaseRunner.Run().Result;
                }
            }
            catch (Exception e)
            {
                LogError(...);
            }
        }
    }

希望这很清楚。 我能否在if块内等待条件? 理想情况下,我希望使Base类仅返回bool而不是Task<bool>Run方法,并且仅让Derived2类重写以返回Task<bool>,但我不确定如何做到这一点。也许我应该在Derived2Run方法中返回task.Result?如果有更好的方式包括抽象或任何其他更正,请告诉我。感谢任何想法。
编辑#1
下面的响应已经阐明了同步实现中Derived1Run方法形式。尽管如此,我不能更改DoSomething方法的签名,因此鉴于此,我的Derived2(异步实现)中的Run方法现在如下所示(感谢@Stripling的评论):
    public override async Task<bool> Run()
    {
        task = new Task<bool>(() => 
        {
            bool ok = DoSomething();
            return ok;
        });
        task.Start();
        return await task;
    }

编辑 #2:

当我尝试上述方法(也尝试在task定义之后加入task.Start()调用),我会得到以下错误提示:

Cross-thread operation not valid: Application accessed domain object from a thread other than a legal thread.

你能用高层次的术语解释一下你试图做什么吗? - Yacoub Massad
让我试试。我正在尝试创建一个抽象类和一个派生类,其中一个派生类异步实现一些方法,而另一个派生类则不是,然后在UI中根据情况有条件地等待。 - squashed.bugaboo
2
看一下这个链接:http://blog.stephencleary.com/2013/01/async-oop-1-inheritance-and-interfaces.html - Yacoub Massad
关于第二次编辑:您正在尝试使用只想被特定线程访问的对象进行某些操作。这种情况经常发生在具有指定UI线程的应用程序中。如果不知道哪行代码导致错误,很难确切地帮助您解决问题。 - StriplingWarrior
@squashed.bugaboo:为了简单起见,我会使用return await Task.Run(() => DoSomething());。但是,如果DoSomething确实需要在单独的线程上运行,那么你的想法是正确的。 - StriplingWarrior
显示剩余2条评论
4个回答

7
我可以翻译上述内容,特别是在if语句块内有条件地等待。但你不应该这样做。如果你正确操作,特意阻塞式地调用一个同步任务并没有太大的优势:通常情况下,你只需await返回的Task,如果它代表一个同步任务,则await将被同步解析,开销非常小。当我说“如果你正确操作”的时候,这是正确的方式:
// synchronous
public override Task<bool> Run()
{
    var result = DoSomething();
    return Task.FromResult(result);
}


// asynchronous
public override async Task<bool> Run()
{
    var result = await DoSomethingAsync();
    return result;
}

await等待第一个示例的结果不会进行任何线程切换或类似操作。等待第二个示例的结果可能会进行线程切换,这取决于DoSomethingAsync()的实现方式。没有必要添加抽象层:您始终可以检查Task是否已完成,而等待已经完成的任务将立即返回值。


1
谢谢,知道我可以在任何任务上使用await,太好了。那么我甚至不需要if条件语句。 - squashed.bugaboo
但是如果对于你的示例中的异步 Run 方法,我无法像你一样在 DoSomethingAsync 上使用 await(即,我不想使用 async 关键字而是返回 Task<bool>),因为我的 Task 是使用 lambda 函数定义的。我想问的问题是,如果我需要使用 lambda 函数来实例化 Task,如何使用 async/await 关键字? - squashed.bugaboo
我找到了上面问题的答案。请看我的编辑#1。我会测试一下,很快回来的..谢谢 - squashed.bugaboo
1
@squashed.bugaboo:有没有办法将导致错误的操作与执行长时间运行操作的代码分开?此外,我猜你会想使用 Task.Run(() => DoSomething())。这比创建一个新任务然后记得启动它要简单。 - StriplingWarrior
1
我终于搞定了,但是不得不重构内部以隔离像你和StephenCleary提到的同步组件。现在它可以工作了。 - squashed.bugaboo
显示剩余2条评论

2
我不明白你的同步版本为什么被称为同步,因为你仍然使用了Task.Run(,我本来期望看到的是:
internal class Derived1 : Base
{
    private readonly Base baseCase;
    private Task<bool> task;
    public Derived1(Base baseCase)
    {
        this.baseCase = baseCase;
    }
    public override bool IsRunning
    {
        get { return false; }    

    }

    public override Task<bool> Run()
    { 
        bool ok = DoSomething(); 
        return Task.FromResult(ok);
    }
}

如果您使用这种方法而不是您当前的方法,您的其他代码将变得更简单。
private async void OnRun(object sender, EventArgs args)
{
    foreach (var case in Cases)
    {
        Base baseCaseRunner = GetCaseRunner(case);
        try
        {
            bool ok = true;
            ok = await baseCaseRunner.Run();
        }
        catch (Exception e)
        {
            LogError(...);
        }
    }
}

异步版本将以异步方式运行,同步版本将以同步方式运行。

你的“异步版本”实际上也不是异步的,请参见 Stripling的答案 了解该方法的正确使用方式。


2
首先,我必须说这是一个非常糟糕的想法。有些东西应该是可配置的,有些则不应该。操作的异步性不应该是可配置的。这是一个可怕的设计,我会坚决反对这种荒谬的“要求”。这就像为是否抛出异常设置可配置标志一样毫无意义。

话虽如此,它确实可以做到。这很痛苦,难以维护,最终完全没有用处。但是,我猜你为此付费了,所以没关系,对吧?

如果您出于政治原因(绝对没有有效的技术原因)必须这样做,那么我建议您使用我的文章“棕地异步”的布尔参数技巧。仅阻止(Result)的一个问题是,如果Run使用await(有关在我的博客上描述的原因),它将无效。
布尔参数技巧只是添加了一个布尔参数,指示方法是否预计同步完成。
public abstract class Base
{
  public abstract Task<bool> RunAsync(bool sync);
}

这里的语义是,如果synctrue,则从RunAsync返回的任务必须已经完成。
然后您的实现看起来像:
internal class Derived1 : Base
{
  public override async Task<bool> RunAsync(bool sync)
  {
    IsRunning = true;
    try
    {
      if (sync)
        return DoSomething();
      return await Task.Run(() => DoSomething());
    }
    finally
    {
      IsRunning = false;
    }
  }
}

它被称为:
private async void OnRun(object sender, EventArgs args)
{
  foreach (var case in Cases)
  {
    Base baseCaseRunner = GetCaseRunner(case);
    try
    {
      bool sync = !something_holds;
      bool ok = await baseCaseRunner.RunAsync(sync);
    }
    catch (Exception e)
    {
      LogError(...);
    }
  }
}

请注意,它总是可以使用await调用,但如果synctrueOnRun实际上将是同步的。这是由于"等待快捷路径" - await首先检查任务是否已经完成,如果完成了,它会继续同步执行(如我在异步介绍博客文章中所述)。

在一个类中将Task对象作为私有成员是个不好的主意吗?在我看到的所有示例中,都像你展示的那样,并且似乎能够捕获IsRunning状态。但是这是否等同于拥有一个私有的Task对象来检查IsRunning状态呢?此外,使用你的方法可能会修复我在Edit#2中遇到的错误吗?当从Run方法返回时发生错误。 - squashed.bugaboo
@squashed.bugaboo:在成员中拥有Task并不是坏事,但是拥有一个Task成员来表示同一类中方法的执行是相当奇怪的。对我来说似乎是不必要的。跨线程错误可能仍会发生;如果您无法使用线程池线程(如错误所示),则需要修改DoSomething为异步(并带有一个bool sync参数)。 - Stephen Cleary
@squashed.bugaboo:看起来你有两种不同的方法,正确的解决方案取决于哪种方法是必要的。如果你有一个接口,其中一个方法可以同步或异步实现,那么Task.FromResult是同步实现的好方法。如果你有一个可配置参数,调用代码需要强制执行被调用代码是同步还是异步,则布尔参数hack是适当的。 - Stephen Cleary
我以为我已经修复了它,但说得太早了。我甚至剥离了DoSomething的所有内容,只是为了调试,并在DoSomething内部简单地返回true以进行调试,但即使如此,在Run()方法中返回await task时仍会立即出现跨线程错误。DoSomething方法在其签名中没有async。它必须有吗?我们想避免的一件事就是将异步级联到所有被调用者的最底层。 - squashed.bugaboo
@squashed.bugaboo:跨线程异常只会由线程相关组件引起。 - Stephen Cleary
显示剩余4条评论

0
使用Task.FromResult返回一个包含同步计算结果的任务,并使用await。
bool ok = DoSomething();
return Task.FromResult(ok);

作为附注,我不会将同步代码放入通常被认为是异步的方法中(或者在其他类/位置中是同步的),反之亦然。对于同步和异步实现,我会使用不同的接口/基类。
interface IRunnable
{
    bool IsRunning;
    bool Run();
}

interface IRunnableAsync
{
    bool IsRunning;
    Task<bool> RunAsync();
}

task.Result 不是必需的,可以像普通的 Task 一样等待它并获取结果。 - Scott Chamberlain

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接