Как отменить пользовательское ожидание

Я читал блог Стивена Туба о создании пользовательского ожидания для SocketAsyncEventArgs. Все это работает нормально. Но что мне нужно, так это ожидаемое отмену, а блог не освещает эту тему. Также Стивен Клири, к сожалению, не описывает в своей книге, как отменить асинхронные методы, которые не поддерживают отмену. Я пытался реализовать это сам, но у меня не получилось с TaskCompletionSource и Task.WhenAny, потому что с ожидаемым я на самом деле не ожидаю задачи. Это то, что я хотел бы иметь: возможность использовать Socket ConnectAsync с форматом TAP и возможность отменить его, сохраняя при этом возможность повторного использования SocketAsyncEventArgs.

public async Task ConnectAsync(SocketAsyncEventArgs args, CancellationToken ct) {}

И это то, что у меня есть из блога Стивена Туба (и реализация SocketAwaitable):

public static SocketAwaitable ConnectAsync(this Socket socket,
    SocketAwaitable awaitable)
{
    awaitable.Reset();
    if (!socket.ConnectAsync(awaitable.m_eventArgs))
        awaitable.m_wasCompleted = true;
    return awaitable;
}

Я просто не могу понять, как перевести это в формат TAP и сделать его отменяемым. Любая помощь приветствуется.

Редактировать 1: Это пример того, как я обычно делаю отмену:

private static async Task<bool> ConnectAsync(SocketAsyncEventArgs args, CancellationToken cancellationToken)
{
    var taskCompletionSource = new TaskCompletionSource<bool>();

    cancellationToken.Register(() =>
    {TaskCompletionSource.Task
        taskCompletionSource.TrySetCanceled();
    });

    // This extension method of Socket not implemented yet
    var task = _socket.ConnectAsync(args);

    var completedTask = await Task.WhenAny(task, taskCompletionSource.Task);

    return await completedTask;
}

person Josh    schedule 06.12.2019    source источник
comment
Вы пытались проверить этот поток?   -  person Pavel Anikhouski    schedule 06.12.2019


Ответы (1)


настраиваемый ожидаемый объект для SocketAsyncEventArgs.

Это выполнимо, но SocketAsyncEventArgs специально предназначен для сценариев, чрезвычайно чувствительных к производительности. Подавляющему большинству (я имею в виду >99,9%) проектов он не нужен и вместо него можно использовать TAP. TAP удобен тем, что хорошо взаимодействует с другими методами... такими как отмена.

что мне нужно, так это ожидаемое отмену, и блог не охватывает эту тему. Также Стивен Клири, к сожалению, не описывает в своей книге, как отменить асинхронные методы, которые не поддерживают отмену.

Итак, здесь есть пара вещей. Начиная с «как отменить асинхронные методы, которые не поддерживают отмену»: краткий ответ: вы не можете. Это потому, что отмена является кооперативной; поэтому, если одна сторона не может сотрудничать, это просто невозможно. Более длинный ответ заключается в том, что это возможно, но обычно больше проблем, чем того стоит.

В общем случае «отменить неотменяемый метод» вы можете запустить метод синхронно в отдельном потоке, а затем прервать этот поток; это самый эффективный и самый опасный подход, поскольку прерванные потоки могут легко повредить состояние приложения. Чтобы избежать этой серьезной проблемы с повреждением данных, наиболее надежным подходом является запуск метода в отдельном процессе, который можно корректно завершить. Тем не менее, это обычно излишне и приносит больше проблем, чем оно того стоит.

Достаточно общего ответа; конкретно для сокетов вы не можете отменить одну операцию, потому что это оставляет сокет в неизвестном состоянии. Возьмем пример Connect в вашем вопросе: между соединением и отменой есть состояние гонки. Ваш код может оказаться с подключенным сокетом после того, как запросит отмену, и вы не хотите, чтобы этот ресурс остался. Таким образом, нормальный и общепринятый способ отменить подключение к сокету — закрыть сокет.

То же самое касается любой другой операции с сокетом: если вам нужно прервать чтение или запись, то, когда ваш код выдает отмену, он не может знать, какая часть чтения/записи завершена, и это оставит сокет в неизвестном состоянии. состояние в отношении протокола, который вы используете. Правильный способ "отменить" любую операцию с сокетом — закрыть сокет.

Обратите внимание, что эта «отмена» находится в другом масштабе, чем обычно наблюдаемая отмена. CancellationToken и друзья представляют собой отмену одной операции; закрытие сокета отменяет все операции для этого сокета. По этой причине API-интерфейсы сокетов не принимают CancellationToken параметров.

Стивен Клири, к сожалению, не описывает в своей книге, как отменить асинхронные методы, которые не поддерживают отмену. Я пытался реализовать это сам

Вопрос "как отменить неотменяемый метод" почти никогда не решается должным образом с помощью "использовать этот блок кода". В частности, используемый вами подход отменяет ожидание, а не операцию. После запроса на отмену ваше приложение все еще пытается подключиться к серверу и в конечном итоге может завершиться успешно или неудачно (оба результата будут проигнорированы).

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

private static async Task MyMethodAsync(CancellationToken cancellationToken)
{
  var connectTask = _socket.ConnectAsync(_host);
  await connectTask.WaitAsync(cancellationToken);
  ...
}

Я предпочитаю этот тип API, потому что ясно, что отменяется асинхронное ожидание, а не операция подключения.

Я терплю неудачу с TaskCompletionSource и Task.WhenAny, потому что с ожидаемым я на самом деле не ожидаю задачи.

Что ж, чтобы сделать что-то более сложное, вам нужно преобразовать ожидаемый объект в Task. Возможно, есть более эффективный вариант, но это действительно лезет в сорняки. И даже если вы разберетесь со всеми деталями, вы все равно получите «фальшивую отмену»: API, который выглядит так, как будто отменяет операцию, но на самом деле это не так — он просто отменяет ожидание .

Это то, что я хотел бы иметь: возможность использовать Socket ConnectAsync с форматом TAP и возможность отменить его, сохраняя при этом возможность повторного использования SocketAsyncEventArgs.

TL;DR: вы должны закрыть сокет вместо отмены операции подключения. Это самый чистый подход с точки зрения быстрого восстановления ресурсов.

person Stephen Cleary    schedule 06.12.2019
comment
Ух ты. Спасибо, что нашли время объяснить мне это. Как вы сказали, я думаю, что усилия по реализации этого не оправданы в моем случае. Но эта тема все равно очень интересна. Облом, что нет идеального решения, но я посмотрю в вашей библиотеке повнимательнее. Но на данный момент я думаю, что просто оберну методы *Async с помощью TaskCompletionSource. Насколько я понимаю, единственный недостаток этого (без пользовательского ожидания) заключается в том, что EventArgs нельзя использовать повторно, я прав? - person Josh; 06.12.2019
comment
@Джош: Правильно; Task нельзя использовать повторно. EventArgs была попыткой сделать их многоразовыми, что было хорошим подходом, но в значительной степени заменяется ValueTask, если вы используете .NET Core. - person Stephen Cleary; 06.12.2019
comment
спасибо за упоминание ValueTask. У меня их не было на радаре. Я только что нашел эту статью Стивена Туба: devblogs.microsoft.com/dotnet/, и он упоминает перегрузки для SendAsync и ReceiveAsync для использования с ValueTask. Но я не вижу их упоминаний в документации Socket. Доступны ли они для .Net Core 3? И я думаю, что это был бы лучший вариант при рассмотрении распределения объектов. Но все таки. Мои потребности не оправдывают использование ValueTask. - person Josh; 06.12.2019
comment
@Josh: они методы расширения. Обратите внимание, что некоторые перегрузки возвращают Task<T>, а другие возвращают ValueTask<T>. - person Stephen Cleary; 06.12.2019