避免在使用 Task.WhenAll() 等待的任务分配时检查条件两次

Avoid checking condition twice when assigning from task awaited with Task.WhenAll()

提问人:jmn 提问时间:10/2/2021 最后编辑:jmn 更新时间:11/18/2023 访问量:208

问:

有没有更好的方法来编写这个异步代码(例如,这样我就不需要重复两次)?我想避免在这里使用。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;
}
C# .NET 异步等待

评论

0赞 Erik Philips 10/2/2021
如果 t1 和 t2 之间没有相互依赖关系,我不确定你为什么要这样做。
0赞 jmn 10/2/2021
你好!这个想法是同时等待任务Task.WhenAll()
0赞 GSerg 10/2/2021
将第二组检查替换为 etc。if (t1 != null) {}
2赞 Evk 10/2/2021
您也不等待 Task.WhenAll,因此它基本上什么都不做(无论如何都不需要它)。
1赞 GSerg 10/2/2021
@jmn 好吧,我想你可以有一个 ,存储类似的东西,然后你可以摆脱你的 和 并处理执行 lambda 的列表,将 tupled 任务的结果传递给它,但这是否是一种改进可能是有争议的。您不需要,只需要按照它们在列表中的顺序排列任务,它们仍将同时运行中 - 但如上面注释中所述,您当前的代码也是如此,因此这里没有改进。List<(Task<string>, Action<string, result>)>(getAsync(), (s, r) => {r.foo = s;})t1t2WhenAllawait

答:

0赞 Theodor Zoulias 10/2/2021 #1

一种方法是有两个列表,一个任务列表和一个操作列表。这些操作将在完成所有任务后按顺序调用,并将分配对象的属性。例: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());

这样,您就不需要将每个变量存储在单独的变量中,因为每个 lambda 表达式都会在块的内部范围内捕获变量。当 调用 时,将完成,因此不会阻塞。TasktaskifActiontasktask.Result


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

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

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

    public async Task<TObject> Run(object arg = null)
    {
        var getterTasks = _functions.Select(f => f());
        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();

评论

0赞 Theodor Zoulias 10/2/2021
@jmn我添加了一种“只是为了好玩”的方法。这是我几周前在这个问题中发布的类似类的修改版本。
0赞 GSerg 10/2/2021
为什么你把它做成两个列表,而不是一个元组列表?
0赞 Theodor Zoulias 10/3/2021
@GSerg当然,您可以使用一个列表而不是两个列表,但我不确定这是否会使代码更具可读性或更有效率。
0赞 GSerg 10/3/2021
当然会这样,原因与为什么一个数组 of 比两个数组 和 更易于维护的原因相同。struct {int id, string name}int[] idsstring[] names
0赞 Theodor Zoulias 10/3/2021
@GSerg嗯,你能发布一个列表的实现作为答案吗,这样我们就可以直观地比较这两个实现?
1赞 Xerillio 10/3/2021 #2

在不知道您的条件正在检查什么的情况下,我想我通常会将该检查移动到与获取 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();
}

评论

0赞 Theodor Zoulias 10/3/2021
这是一种有趣的方法,但假设将 和 属性设置为 是可以的。事实可能并非如此。在对象构造过程中,这些属性可能已初始化为非值。foobarnullnullresult
1赞 Xerillio 10/3/2021
@TheodorZoulias同意,但如果这是真的,我怀疑存在一些设计缺陷。但这只是基于这个非常假设的例子的猜测。我将调整代码句柄(await fooTask) ?? result.foo