Task.Factory.StartNew - ожидание и возврат результата

Рейтинг: 0Ответов: 2Опубликовано: 21.06.2023

Как запустить пачку фоновых потоков и дождаться любой первый с возвратом результата ?

Пробовал так и ещё различным десятком способов после гугла, в том числе и без лямбды.

Task task = Task.Factory.StartNew(async () =>
{
    AnalyseInfo analyseInfo = await myAnalyzer.AnalyseAsync();
    _analyseInfos.Add(analyseInfo);
});


Task<AnalyseInfo> task = Task<AnalyseInfo>.Factory.StartNew(async () =>
{
    return await myAnalyzer.AnalyseAsync();
});


Task<Task<AnalyseInfo>> task = Task<Task<AnalyseInfo>>.Factory.StartNew(async () =>
{
    return await myAnalyzer.AnalyseAsync();
});

Но не удалось получить желаемого результата.

Ожидать хотел любую завершившившуюся задачу с помощью Task.WaitAny

// Дождаться завершения любой задачи
int numTask = Task.WaitAny(_tasks.ToArray());

Все попытки заканчивались тем, что задача получала статус завершения, до завершения..

Ответы

▲ 3Принят

Task.Factory.StartNew создаёт новую задачу оборачивая возвращаемое значение вне зависимости от этого самого значения.

Поэтому, когда вы пишете Task.Factory.StartNew(async …), вы получаете Task<Task<…>> - задачу, значение которой тоже задача.

Рассмотрим асинхронный метод подробнее:

async () {
    // 1
    await Bar();
    // 2
}

Если его вызвать, то он вернёт управление после первого нехолостого оператора await, то есть строчка 1 и вызов Bar будут выполнены синхронно, а ожидание Bar и строчка 2 - асинхронно. Когда вы оборачиваете такой метод в задачу - строчка 1 и вызов Bar выполняются во внешней задаче, а ожидание Bar и строчка 2 - во внутренней.

Именно потому у вас и получалось, что задача завершается слишком рано: вы ждали внешнюю задачу.

Как от этого избавиться? Очень просто: используя Unwrap!

Task<AnalyseInfo> t = Task.Factory.StartNew(async () =>
{
    return await myAnalyzer.AnalyseAsync();
}).Unwrap();

В этом методе нет никакой магии, если бы его не было - его было бы нетрудно написать самостоятельно:

async Task<T> Unwrap<T>(Task<Task<T>> task) {
    return await await task;
}

Второй вариант - можно использовать Task.Run, в отличии от StartNew у него есть специальная перегрузка для асинхронных методов, которая вызывает Unwrap внутри:

Task<AnalyseInfo> t = Task.Run(async () =>
{
    return await myAnalyzer.AnalyseAsync();
});

Наконец, третий вариант - вообще не выносить асинхронный метод в отдельный поток, а просто вызвать. Этот вариант не универсален, но обычно работает:

Task<AnalyseInfo> t = myAnalyzer.AnalyseAsync();
▲ 2

Хоть и ответ уже дан выше, попробую внести свои 5 копеек с таким примером:

var longTask = Task.Delay(10000);
var shortTask = Task.Delay(1000);

var tasks = new[] { longTask, shortTask };

var anyTask = await Task.WhenAny(tasks);

Console.WriteLine(anyTask.Status); //RanToCompeltion
Console.WriteLine(shortTask.Status); //RanToCompletion
Console.WriteLine(longTask.Status); //WaitingForActivation

Console.WriteLine(anyTask == shortTask); //True

В таком примере не используется TaskFactory для создания, а используется напрямую ваш таск (что также убирает лишний async и создание лишних стейт-машин для этого таска). В вашем примере в вопросе не хватило кейворда await перед Task.WhenAny, чтобы задача действительно ожидалась.