【问题标题】:Async/Await and ConfigureAwait - executing task later with 'unknown' method signatureAsync/Await 和 ConfigureAwait - 稍后使用“未知”方法签名执行任务
【发布时间】:2018-11-24 09:04:00
【问题描述】:

我一直在努力寻找解决此问题的最佳方法。我已经在谷歌上搜索了几个小时,并且发现了一些好的资源(其中大部分都在这里),但我仍然无法弄清楚解决我的问题的最佳方法是什么。我发现的大多数提示要么没有提到 ConfigureAwait(false),要么只是在存储任务执行以供以后使用时具有相同的方法签名。

我要做的是首先确定在任务中运行什么方法。然后我想运行其他一些任务,然后确定任务。我编辑了下面的代码以使其更清晰。

在我的库中,我有以下场景(非常简化):

public abstract class BaseClass
{
    public int Foo;
}
public class ClassA : BaseClass { }
public class ClassB : BaseClass
{
    public int Bar;
}

public async Task ProcessVariable(int variable)
{
    BaseClass c = null;  // initialized by child class
    Task t = null;
    switch (variable)
    {
        case 1:
            c = new ClassA(variable);   // parse data for ClassA and BaseClass
            // potentially some special code here
            t = OnClassA(c as ClassA, "foo");
            break;
        case 2:
            c = new ClassB(variable);   // parse data for ClassB and BaseClass
            // potentially some special code here
            t = OnClassB(c as ClassB);
            break;
    }

    // first call different task than the one picked in switch
    // can't call it before switch, as variable c is initialized in the switch body
    if (c != null)
        await OnAnyClass(c).ConfigureAwait(false);
    // finally, call the async task we picked inside switch cases
    if (t != null)
        await t.ConfigureAwait(false);
}

public virtual async Task OnAnyClass(BaseClass c)
{
    // this one does something
    await SendAsync(c.Foo).ConfigureAwait(false);
}

public virtual async Task OnClassA(ClassA c, string additionalInfo)
{
    // this one does nothing, but end user can override
    await Task.CompletedTask;
}

public virtual async Task OnClassB(ClassB c)
{
    // this one does something
    await SendAsync(c.Bar).ConfigureAwait(false);
}

这通常效果很好。但是,OnClassA 和 OnClassB 在 OnAnyClass 之前执行。当然,这是预期的行为,因为该方法会立即执行并返回 Task。但是,我希望 OnAnyClass 在其他类之前执行。我在谷歌上搜索了很多关于如何解决这个问题的提示。我遇到了几个解决方案,但我不确定哪一个最适合我的场景:

1.使用任务构造函数

t = new Task(() => OnClassB(c as ClassB)); 
// and then
t.Start();
await t.ConfigureAwait(false);

但是,通过这种方式,我不确定代码的行为方式。我见过几个例子,但最后我只是感到困惑。我是否仍能从 ConfigureAwait(false) 中获得全部好处(这对我来说很重要,因为它是一个库代码),并且执行方法中的所有等待都会被异步正确调用吗?

2。使用带有异步委托的 Task 构造函数

t = new Task(async () => await OnClassB(c as ClassB).ConfigureAwait(false)); 
// and then
t.Start();
await t.ConfigureAwait(false);

现在,通过这种方法,我知道 ConfigureAwait(false) 将被正确使用。但是,这会将 Task 包装在 Task 中 - 由于库的用户将能够覆盖这些方法,我认为这并不是真正可取的。

3.使用 Func

这种方法会很棒 - 但是,每个方法都有不同的签名,我想保持这种方式,所以如果不让代码比现在更混乱,这对我来说真的不起作用。

4.等待案例中的方法

case 1:
    c = new ClassA(variable);
    await OnAnyClass(c).ConfigureAwait(false);
    await OnClassA(c as ClassA, "foo").ConfigureAwait(false);
    break;

这种方法保证有效。但是,由于我的库的性质,可能有几十种情况 - 因此这会导致代码可维护性降低和大量重复的代码行。

5.等待最后的所有方法

case 1:
    c = new ClassA(variable);
    break;
// and then
if (c != null)
    await OnAnyClass(c).ConfigureAwait(false);
if (c is ClassA)
    await OnClassA(c as ClassA, "foo").ConfigureAwait(false);
if (c is ClassB)
    await OnClassB(c as ClassB).ConfigureAwait(false);

这也肯定会起作用,并且与方法 4 相比会减少重复行的数量,但是此代码维护起来仍然非常烦人。

在最坏的情况下,我可能会保持原样,忽略顺序 - 在大多数情况下,它应该无关紧要。但是,如果可能的话,我宁愿保留订单。哪种方法效果最好?由于它是一个库代码,我需要 ConfigureAwait(false) 才能正确执行。你有什么意见和建议?希望我以一种可以理解的方式解释了我的问题。

提前谢谢你,如果我遗漏了一些明显的东西,我深表歉意 - 我使用线程多年,而 async/await 对我来说相对较新。

【问题讨论】:

  • TestClass 未定义
  • @paparazzo 我跳过了它,因为它不相关 - 它可以是任何东西,我只是展示了构造函数使用一些数据来初始化它们自己和基类。
  • 如果不相关,则传递一个字符串。
  • @paparazzo 我在生产代码中执行 - 然后将字符串解析为 JToken,并根据 Json 值,切换执行不同的情况。不相关是指它与我遇到的问题无关。不过,为了简单起见,我将其编辑为 int。
  • 与您无关。不完整的代码会得到不完整的评论。为什么不努力发布完整的代码?

标签: c# asynchronous async-await c#-7.1


【解决方案1】:

试试这个:

    private List<Task> _tasks = new List<Task>();


    public async Task ProcessVariable(int variable)
    {
        BaseClass c = null;
        switch(variable)
        {
            case 1:
                c = new ClassA(variable);
                _tasks.Add(new Task(async () => await OnAnyClass(c)));
                _tasks.Add(new Task(async () => await OnClassA(c as ClassA, "foo")));
                break;
            case 2:
                //...
                throw new NotImplementedException();
        }
        foreach(var task in _tasks)
        {
            await task;
        }
    }

new Task() 应该创建一个任务而不启动它。

【讨论】:

  • 好的,这就像我的问题中的选项 2。这将为每种方法创建 2 个任务。也许你是对的,这是最好的方法,但是你能告诉我这会如何影响性能吗?它会使图书馆用户的代码更容易出错吗? (这就是我对这个选项有顾虑的原因)
  • 任务有很好的性能,你不应该每次都创建新线程,因为这些开销很大。
  • 我对你的答案做了更多的研究,如果我在任务构造函数之后添加 .Unwrap() 似乎它会是正确的。
【解决方案2】:

可能有一个更易于维护的路径,我们可以设置一组地图,而不是增加圈复杂度。这些地图的行为很像if 或switch,另外还有一个好处是可以以序列化的形式存储以进行配置(但并非总是如此),而且恕我直言,这是一个更清洁的过程。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ClassLibrary1
{
    public class ClassFactory
    {
        private IDictionary<int, Func<int, BaseClass>> ClassMap { get; } = new Dictionary<int, Func<int, BaseClass>>()
        {
            {1, x => new ClassA(x) },
            {2, x => new ClassB(x) }
        };

        private IDictionary<Type, Func<BaseClass, Task>> ClassInitializeMap { get; } = new Dictionary<Type, Func<BaseClass,  Task>>()
        {
            {typeof(ClassA) , cls => Task.Delay(1000) }, //Do Something with "Foo" 
            {typeof(ClassB) , cls => Task.Delay(1000) } 
        };

        public async Task ProcessVariable(int variable)
        {
            var theClass = ClassMap[variable](variable);
            await OnAnyClass(theClass).ConfigureAwait(false);
            await ClassInitializeMap[theClass.GetType()](theClass).ConfigureAwait(false);
        }

        public Task OnAnyClass<T>(T anyClass) => Task.Delay(1000);
    }

    public abstract class BaseClass
    {
        public int Foo;
    }
    public class ClassA : BaseClass
    {
        public ClassA(int variable) => Foo = variable;
    }
    public class ClassB : BaseClass
    {
        public ClassB(int variable) => Bar = variable;
        public int Bar;
    }
}

我认为这可能是您所追求的,并且未经测试,所以如果发现任何疏忽或问题,请告诉我。

此外,要在ClassA/ClassB 初始化方法中获取一组可变参数,您可以使用与 AspNet Core 在注入 IOptions 时使用的类似模式。另一种选择是使用Dictionary&lt;Type, Arg&gt; 执行“Bag Style”,其中Arg 是一个基类,它携带每个类类型的参数。在那种情况下,Arg 实际上只是一个字典的包装器,因为初始化方法将知道键。您甚至可以从基础Arg 派生到ClassAArg 和ClassBArg。但是您可以详细说明,或者如果您想要一个示例,请告诉我。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-04-30
    • 2015-01-20
    • 1970-01-01
    • 2022-12-09
    • 2018-06-25
    • 2014-12-19
    • 2014-10-20
    相关资源
    最近更新 更多