Можно ли ожидать операции ввода-вывода, которая не объявлена ​​как асинхронная? Если нет, что мне делать?

Я новичок в асинхронном программировании на С#, и я все еще не понимаю несколько вещей. Я читал, что после .NET 4.5 APM и EAP больше не рекомендуются для новой разработки, поскольку предполагается, что TAP заменит их (источник).

Думаю, я понял, как работает async/await, и смогу использовать их для выполнения операций ввода-вывода с асинхронными методами. Например, я мог бы написать асинхронный метод, ожидающий результата GetStringAsync HttpWebClient, поскольку он объявлен как асинхронный метод. Замечательно.

Мой вопрос: что, если у нас есть операция ввода-вывода, которая происходит в методе, который не объявлен как асинхронный? Вот так: предположим, у меня есть API с методом

string GetResultFromWeb()

который запрашивает что-то из Интернета. У меня есть много разных запросов, и я должен использовать для этого этот метод. И затем мне нужно обработать каждый результат запроса. Я понимаю, что сделал бы это, если бы это был асинхронный метод:

Task<string> getResultTask = GetResultFromWeb(myUrl); 
// Do whatever I need to do that doesn't need the query result
string result = await getResultTask;
Process(result);

Но так как это не так, я не могу его дождаться - он говорит мне, что строка не ожидается. Итак, мой вопрос: есть ли способ выполнять эти операции ввода-вывода асинхронно без необходимости создавать один поток для каждого запроса? Если бы я мог, я хотел бы создавать как можно меньше потоков, не блокируя ни один из потоков.

Один из способов, который я обнаружил, заключался в реализации APM, следуя этой статье. от Джеффри Рихтера, а затем в моем методе Begin я вызываю ThreadPool.QueueWorkItem(GetResultFromWeb, asyncResult). Как это:

public class A {
    private void DoQuery(Object ar){
        AsyncResult<string> asyncResult = (AsyncResult<string>) ar;
        string result = GetResultFromWeb();
        asyncResult.SetAsCompleted(result, false);
    }

    public IAsyncResult BeginQuery(AsyncCallback){
        AsyncResult<string> asyncResult = new AsyncResult<string>(callback, this);
        ThreadPool.QueueUserWorkItem(DoQuery, asyncResult);
        return asyncResult;
    }

    public string EndQuery(IAsyncResult ar){
        AsyncResult<string> asyncResult = (AsyncResult<string>)ar;
        return asyncResult.EndInvoke();
    }
}

Затем я использую AsyncEnumerator и начинаю (BeginQuery) несколько запросов и обрабатываю результаты по мере их завершения (используя yield return/EndQuery). Кажется, это работает хорошо. Но, прочитав так много о том, что APM устарел, мне стало интересно, как я могу сделать это с помощью TAP. Кроме того, есть ли какие-либо проблемы с этим подходом APM?

Спасибо!


person Derek Patton    schedule 06.01.2015    source источник


Ответы (3)


что, если у нас есть операция ввода-вывода, которая выполняется в методе, который не объявлен как асинхронный?

В этом случае операция ввода-вывода блокируется. Другими словами, GetResultFromWeb блокирует вызывающий поток. Имейте это в виду, когда мы пройдемся по остальным...

Я должен использовать этот метод, чтобы сделать это.

Из этого я делаю вывод, что вы не можете написать метод GetResultFromWebAsync, который является асинхронным. Таким образом, любой поток, выполняющий веб-запросы, должен быть заблокирован.

есть ли способ выполнять эти операции ввода-вывода асинхронно без необходимости создавать один поток для каждого запроса?

Самый естественный подход — написать метод GetResultFromWebAsync. Поскольку это невозможно, ваши варианты: заблокировать вызывающий поток или заблокировать какой-либо другой поток (например, поток пула потоков). Блокирование потока из пула — это метод, который я называю «поддельной асинхронностью», поскольку он выглядит асинхронным (т. е. не блокирует поток пользовательского интерфейса), но на самом деле это не так (т. е. вместо этого он просто блокирует поток из пула).

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

Это невозможно, учитывая ограничения. Если вы должны использовать метод GetResultFromWeb, и этот метод блокирует вызывающий поток, тогда поток должен быть заблокирован.

Один из способов, который я нашел для этого, заключался в реализации APM, следуя этой статье Джеффри Рихтера, а затем в моем методе Begin я вызываю ThreadPool.QueueWorkItem(GetResultFromWeb, asyncResult).

В этом случае ваш код предоставляет асинхронный API (начало/конец), но в реализации он просто вызывает GetResultFromWeb в потоке пула потоков. То есть это фальшивая асинхронность.

Кажется, это работает хорошо.

Это работает, но это не совсем асинхронно.

Но, прочитав так много о том, что APM устарел, мне стало интересно, как я могу сделать это с помощью TAP.

Как отмечали другие, есть гораздо более простой способ запланировать работу с пулом потоков: Task.Run.

Настоящая асинхронность невозможна, потому что у вас есть метод блокировки, который вы должны использовать. Итак, все, что вы можете сделать, это обходной путь - фальшивая асинхронность, также известная как блокировка потока пула потоков. Самый простой способ сделать это:

Task<string> getResultTask = Task.Run(() => GetResultFromWeb(myUrl)); 
// Do whatever I need to do that doesn't need the query result
string result = await getResultTask;
Process(result);

(гораздо более чистый код, чем APM и AsyncEnumerator)

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

Другими словами, как я более подробно описываю в своем блоге, используйте Task.Run для вызова метода, а не для реализации метода.

person Stephen Cleary    schedule 06.01.2015
comment
Отличный ответ! Спасибо! - person Derek Patton; 06.01.2015

Ваш API является асинхронным с использованием старой модели Begin/End. Это вписывается в TPL через

Task.Factory.FromAsync<string>(BeginQuery, EndQuery)

который возвращает Task<string>, который вы можете await.

person Ben Voigt    schedule 06.01.2015
comment
Помимо упрощения кода, есть ли какое-либо преимущество (производительность) в этом вместо того, чтобы просто придерживаться старого метода Begin/End? - person Derek Patton; 06.01.2015
comment
@DerekPatton: гораздо проще чередовать другие асинхронные вызовы. - person Ben Voigt; 06.01.2015
comment
@DerekPatton: Кроме того, я не знаю, почему вы думаете, что может быть преимущество в производительности. Обертка никогда не бывает быстрее, чем основа, на которой она держится. - person Ben Voigt; 06.01.2015
comment
Вы сказали, что мой API использует модель Begin/End. Нет. Обратите внимание, что я сам создал методы Begin/End. Означает ли это, что мне придется создавать методы Begin/End, а затем использовать этот вызов Task.Factory.FromAsync каждый раз, когда я хочу ожидать неасинхронных методов? Кроме того, создает ли это новый поток для каждого вызова? - person Derek Patton; 06.01.2015
comment
@DerekPatton: если ваш API соответствует стандартным подписям, вы можете использовать предложение Бена. Обратите внимание, что не существует перегрузки, точно соответствующей вашему методу BeginQuery (насколько я могу судить... их много, и я мог что-то упустить :) ), но вы можете вызвать метод и передать результат в FromAsync: Task.Factory.FromAsync<string>(BeginQuery(MyQueryCallback), EndQuery); (где MyQueryCallback — ваш метод обратного вызова, который вы бы передали BeginQuery() в любом случае). - person Peter Duniho; 06.01.2015
comment
@DerekPatton: Нет, вы не хотели бы писать методы Begin/End только для того, чтобы их обернуть. Но есть много существующих API, которые используют Begin/End. Чтобы иметь хорошую производительность, вам нужно переписать GetResultFromWeb, чтобы использовать какой-то асинхронный API для сетевого ввода-вывода. Будь то Begin/End, асинхронный TPL или что-то еще, все они могут быть превращены в асинхронные. В вашем конкретном случае может быть даже лучше проксировать запрос с помощью асинхронных API и вызывать сторонний API с локальным URL-адресом, который отвечает немедленно. - person Ben Voigt; 06.01.2015

Более простой способ сделать то, что вы ищете, — это вызвать метод, используя класс Task. В вашем случае это будет выглядеть примерно так:

Task<string> getResultTask = Task.Run<string>(()=>GetResultFromWeb(myUrl));
// Do whatever I need to do that doesn't need the query result
string result = await getResultTask;
Process(result);

Хотя это создаст еще один поток, как это делает ваша опция IAsyncResult, это значительно упрощает процесс.

person Jacob Lambert    schedule 06.01.2015
comment
Хороший ответ, однако наличие метода async, который просто делегирует Task.Run, не рекомендуется, поскольку методы async обычно не запускают новый поток. - person NeddySpaghetti; 06.01.2015
comment
Хорошо, но разве это не создает новый поток для каждого вызова GetResultFromWebAsync? Я думал, что APM будет более эффективным, поскольку он не будет создавать отдельный поток для каждого вызова, а будет ставить задачи в очередь и чередовать гораздо меньшее количество потоков для этих задач, тем самым устраняя накладные расходы на создание новых потоков. Я ошибся? - person Derek Patton; 06.01.2015
comment
@DerekPatton Я считаю, что класс Task использовал ThreadPool внутри, если я помню, что я читал правильно. - person Jacob Lambert; 06.01.2015
comment
@NedStoyanov, что тогда было бы лучшим способом реализовать это? Это лучший из известных мне на данный момент способов заставить неасинхронный метод действовать как асинк-метод, но я всегда стараюсь его улучшить. - person Jacob Lambert; 06.01.2015
comment
Я думаю, что рекомендуется не добавлять метод, заканчивающийся на xxxxAsync, просто использовать Task.Run напрямую. - person NeddySpaghetti; 06.01.2015
comment
@DerekPatton Я был прав, думая, что Task использует пул потоков CLR внутри. - person Jacob Lambert; 06.01.2015