【问题标题】:Avoid checking condition twice when assigning from task awaited with Task.WhenAll()从 Task.WhenAll() 等待的任务分配时避免检查条件两次
【发布时间】:2021-11-23 18:02:01
【问题描述】:

有没有更好的方法来编写这个异步代码(例如,这样我就不需要重复 if (myCondition) 两次)?我想避免在这里使用Task.Run

var tasks = new List<Task>();
Task<String> t1 = null;
Task<String> t2 = null;

if (myCondition) {
    t1 = getAsync();
    tasks.Add(t1);
}

if (myOtherCondition) {
    t2 = getAsync2();
    tasks.Add(t2);
}

await Task.WhenAll(tasks)

if (myCondition) {
    result.foo = await t1;
}

if (myOtherCondition) {
    result.bar = await t2;
}

【问题讨论】:

  • 如果 t1 和 t2 之间没有相互依赖关系,我不知道你为什么要这样做。
  • 嗨!这个想法是等待与Task.WhenAll() 并行的任务
  • if (t1 != null) {}等替换第二组检查
  • 你也不需要等待 Task.WhenAll 所以它基本上什么都不做(不是无论如何都需要它)。
  • @jmn 好吧,我想你可以有一个List&lt;(Task&lt;string&gt;, Action&lt;string, result&gt;)&gt;,在那里存储(getAsync(), (s, r) =&gt; {r.foo = s;}) 之类的东西,然后你可以摆脱你的t1t2 并处理执行 lambda 的列表将元组任务的结果传递给它,但它是否是一种改进可能是有争议的。您不需要WhenAll,只需await 任务按它们在列表中的顺序排列,它们仍将同时运行 - 但您当前的代码也是如此,如上面 cmets 中所述,所以不需要在这里改进。

标签: c# .net async-await


【解决方案1】:

在不知道你的条件在检查什么的情况下,我想我通常会将该检查移到与获取 foo 或 bar 实际相关的方法中。这似乎是你的一种方法做的比它应该做的更多的例子。

另一种方法:

var fooTask = GetFoo();
var barTask = GetBar();

await Task.WhenAll(new [] { fooTask, barTask });
result.foo = (await fooTask) ?? result.foo;
result.bar = (await barTask) ?? result.bar;

// ...
async Task<string> GetFoo()
{
    if (!myCondition) {
        return Task.FromResult((string)null);
    }
    return await DoHeavyWorkFoo();
}

async Task<string> GetBar()
{
    if (!myOtherCondition) {
        return Task.FromResult((string)null);
    }
    return await DoHeavyWorkBar();
}

【讨论】:

  • 这是一种有趣的方法,但假设将foobar 属性设置为null 是可以的。情况可能并非如此。在构造result 对象期间,这些属性可能已经初始化为非null 值。
  • @TheodorZoulias 同意,但如果这是真的,我怀疑存在一些设计缺陷。但这只是基于这个非常假设的例子的猜测。我将使用(await fooTask) ?? result.foo 调整句柄的代码
【解决方案2】:

一种方法是创建两个列表,一个任务列表和一个操作列表。这些操作将在所有任务完成后依次调用,并将分配result 对象的属性。示例:

var tasks = new List<Task>();
var actions = new List<Action>();
var result = new MyClass();

if (myCondition)
{
    var task = getAsync();
    tasks.Add(task);
    actions.Add(() => result.Foo = task.Result);
}

if (myOtherCondition)
{
    var task = getAsync2();
    tasks.Add(task);
    actions.Add(() => result.Bar = task.Result);
}

await Task.WhenAll(tasks);

actions.ForEach(action => action());

这样您就不需要将每个 Task 存储在单独的变量中,因为每个 lambda 表达式 captures task 变量都在 if 块的内部范围内。当Action被调用时,task就完成了,所以task.Result不会阻塞。


只是为了好玩:如果你想变得花哨,你可以把这个“并行对象初始化”功能封装在一个ObjectInitializer类中,它会同时调用所有的异步方法,然后创建一个新对象并按顺序分配其每个属性的值:

public class ObjectInitializer<TObject> where TObject : new()
{
    private readonly List<Func<object, Task<Action<TObject>>>> _functions = new();

    public void Add<TProperty>(Func<object, Task<TProperty>> valueGetter,
        Action<TObject, TProperty> propertySetter)
    {
        _functions.Add(async arg =>
        {
            TProperty value = await valueGetter(arg);
            return instance => propertySetter(instance, value);
        });
    }

    public async Task<TObject> Run(object arg = null)
    {
        var getterTasks = _functions.Select(f => f(arg));
        Action<TObject>[] setters = await Task.WhenAll(getterTasks);
        TObject instance = new();
        Array.ForEach(setters, f => f(instance));
        return instance;
    }
}

使用示例:

var initializer = new ObjectInitializer<MyClass>();
if (myFooCondition) initializer.Add(_ => GetFooAsync(), (x, v) => x.Foo = v);
if (myBarCondition) initializer.Add(_ => GetBarAsync(), (x, v) => x.Bar = v);
MyClass result = await initializer.Run();

【讨论】:

  • @jmn 我添加了一个“只是为了好玩”的方法。这是我几周前在this question 中发布的类似课程的修改版本。
  • 你为什么把它做成两个列表,而不是一个元组列表?
  • @GSerg 当然,您可以使用一个列表而不是两个,但我不确定这是否会使代码更具可读性或更高效。
  • 当然可以,原因与struct {int id, string name} 的数组比int[] idsstring[] names 的两个数组更易于维护的原因相同。
  • @GSerg hmm,您能否发布一个列表实现作为答案,以便我们可以直观地比较这两种实现?
猜你喜欢
  • 2016-01-30
  • 2012-11-06
  • 2015-03-07
  • 2021-07-17
  • 2018-02-03
  • 2023-03-26
  • 1970-01-01
  • 2016-08-22
  • 1970-01-01
相关资源
最近更新 更多