Лучшая практика для ITargetBlock.Завершение.ContinueWith()


Этот вопрос касается лучших практик при использовании ContinueWith() для обработки завершения блока данных TPL.

ITargetBlock<TInput>.Completion() метод позволяет асинхронно обрабатывать завершение блока данных с помощью ContinueWith().

Рассмотрим следующий код консольного приложения, который демонстрирует очень простое использование:
private static void Main()
{
    test().Wait();
}

static async Task test()
{
    var transform = new TransformBlock<int, double>(i => i/2.0);
    var output    = new ActionBlock<double>(d => Console.WriteLine(d));

    // Warning CS4014 here:
    transform.Completion.ContinueWith(continuation => output.Complete());
    transform.LinkTo(output);

    for (int i = 0; i < 10; ++i)
        await transform.SendAsync(i);

    transform.Complete();
    await output.Completion;
}

Код имеет очень простой вид:TransformBlock который делит целые числа на 2,0 и превращает их в двойники. Преобразованные данные обрабатываются с помощью ActionBlock который просто выводит значения в окно консоли.

Вывод:

0
0.5
1
1.5
2
2.5
3
3.5
4
4.5

Когда TransformBlock будет завершено, я хочу также завершить ActionBlock. Это удобно делать так:

transform.Completion.ContinueWith(continuation => output.Complete());

И здесь кроется проблема. Поскольку это находится внутри метода async и я игнорирую возвращаемое значение из ContinueWith(), я получаю предупреждение компилятора:

Предупреждение CS4014: поскольку этот вызов не ожидается, выполнение текущего метода продолжается до завершения вызова. Рассмотрите возможность применения оператора 'await' к результату вызова.

Ясно, что я не могу await выполнить вызов, как советует предупреждение - если я это сделаю, код зависнет, потому что задача CompleteWith() не может быть завершена, пока не будет вызвана transform.Complete().

Теперь у меня нет проблем с самим предупреждением (я могу просто подавить его или назначить задачу переменной, или использовать расширение задачи .Forget() и так далее) - но вот мой вопросы:

  • безопасно ли таким образом обрабатывать связанное завершение блока данных?
  • есть ли лучший способ?
  • Есть ли здесь что-нибудь важное, что я упустил из виду?

Я думаю, что если бы я делал что - то другое, чем output.Complete() внутри продолжения, я должен был бы обеспечить обработку исключений-но, возможно, даже просто вызов output.Complete() чреват опасностью?

1 5

1 ответ:

В то время как вы можете реализовать распространение завершения самостоятельно, поток данных TPL делает это за вас с помощью DataflowLinkOptions.PropagateCompletion:

transform.LinkTo(output, new DataflowLinkOptions {PropagateCompletion = true});

Если вы все еще хотите реализовать это самостоятельно, вы можете решить проблему, сохранив задачу и ожидая ее, когда это уместно, используя Task.WhenAll:

static async Task test()
{
    var transform = new TransformBlock<int, double>(i => i/2.0);
    var output    = new ActionBlock<double>(d => Console.WriteLine(d));

    // Warning CS4014 here:
    var continuation = transform.Completion.ContinueWith(_ => output.Complete());
    transform.LinkTo(output);

    for (int i = 0; i < 10; ++i)
        await transform.SendAsync(i);

    transform.Complete();
    await Task.WhenAll(continuation, output.Completion)
}

Это позволяет обрабатывать любые исключения в output.Complete и удалять предупреждение.