使用 Task.WhenAll() 等待任务完成后,避免重复检查条件。

3
有没有更好的方式来编写这个异步代码(比如,我不需要重复两次 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之间没有相互依赖关系,我不确定为什么你要这样做。 - Erik Philips
嗨!这个想法是使用Task.WhenAll()并行等待任务。 - jmn
2
你也不需要等待Task.WhenAll,因为它基本上什么也不做(反正也不需要)。 - Evk
1
@jmn 说实话,你可以有一个 List<(Task<string>, Action<string, result>)>,像 (getAsync(),(s,r) => {r.foo = s;})这样的东西存储在其中,然后你就可以摆脱你的t1t2,只需处理执行lambda传递Tupled任务结果即可。但这可能是值得商榷的改进。您不需要 WhenAll,只需按列表中的顺序等待任务,它们仍将同时进行 - 但这也适用于您当前的代码,如上面的评论所述,因此没有改进。 - GSerg
@TheodorZoulias 如果您已经开始了任何任务,它们将在列表中。在调用 lambda 之前,您显然会单独等待列表中的每个任务。没有未等待的任务。如果您说在创建所有任务之前可能会发生异常,那么同样的异常将阻止您进行 WhenAll,如果您正确处理这些异常以便无论如何都能到达 WhenAll,那么同样的处理将允许您到达等待每个任务的步骤。 - GSerg
显示剩余7条评论
2个回答

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值。 - Theodor Zoulias
1
@TheodorZoulias 同意,但如果是这样的话,我怀疑设计上存在一些缺陷。但这只是根据这个非常假设性的例子所猜测的。我将调整代码来处理 (await fooTask) ?? result.foo - Xerillio

0
一种方法是拥有两个列表,一个任务列表和一个操作列表。在所有任务完成后,操作将按顺序被调用,并将分配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表达式捕获内部作用域中的task变量。当调用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 我增加了一个“只是为了好玩”的方法。这是一个修改过的类的版本,我几周前在这个问题中发布过类似的内容。 - Theodor Zoulias
你为什么要把它分成两个列表,而不是一个元组列表? - GSerg
@GSerg 当然,你可以使用一个列表代替两个,但我不确定这样做是否会使代码更易读或更高效。 - Theodor Zoulias
出于同样的原因,一个 struct {int id, string name} 数组比两个数组 int[] idsstring[] names 更易于维护。 - GSerg
@TheodorZoulias,你觉得写actions.Add(async () => result.Bar = await task);作为第一个答案是可行的吗?这样可以养成避免使用Task.Result的习惯,但这是个好主意吗? - jmn
显示剩余4条评论

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