2

Есть функция, которая в бесконечном цикле ждет longpolling ответ от сервера. Допустим ответ приходит каждые 100 секунд (или раньше, если произойдет какое-то событие на сервере), и каждый ответ обрабатывается процессором 5 секунд

Чтоб постоянно не выделять поток, который будет просто ждать, я написал асинхронную функцию, которая будет брать поток из пула потоков для обработки (те 5 секунд), каждый раз когда приходит ответ (каждые 100 секунд):

    async void LongPolling(string server, CancellationToken ct)
    {
        var task = _vk.GetLongPollingAnswerAsync(server);
        while (true)
        {
            string answer = await task;
            if (ct.IsCancellationRequested) return;
            task = _vk.GetLongPollingAnswerAsync(server);
            Handler(answer); //обработка answer, которая длится 5 секунд
        }
    }

Но если вызывать эту функцию из UI контекста, то, как я понимаю, UI будет каждые 100 секунд повисать на 5 секунд, что не есть хорошо, а request context (asp.net) и вовсе выбросит исключение

Итого 3 вопроса:

  1. Можно ли как-то реализовать это не через async-await?
  2. Как не захватывать контекст синхронизации, чтоб каждые 100 секунд UI не подвисал?
  3. Можно ли как-то выполнить среднюю часть вне контекста синхронизации, а последнюю внутри, т.е.

    async void LongPolling(string server, CancellationToken ct)
    {
        var task = _vk.GetLongPollingAnswerAsync(server);
        while (true)
        {
            string answer = await task;
            if (ct.IsCancellationRequested) return;
            task = _vk.GetLongPollingAnswerAsync(server);//1
    
            string result = await Handler(answer);//2
    
            UITextBox.text = result;//3
        }
    }
    

    1 и 2 выполняются вне контекста UI (чтоб 5 секунд форма не зависала), а 3 внутри (чтоб был доступ к контролам)?

Qutrix
  • 1,214
  • 1
    await function.ConfigureAwait(false) для всех асинхронных методов или запуск через, к примеру, Task.Run всей задачи. Это по поводу контекста – Vladislav Khapin Nov 21 '16 at 16:11
  • Нужно понимать, что само по себе наличие Task, или тем более await, не гарантирует выполнение операции в фоновом потоке. Если вызов _vk.GetLongPollingAnswerAsync блокирующий, то всему виной криворукость разработчиков VK API. Отправьте его принудительно на выполнение в Thread Pool с помощью Task.Run. А проблемы с переключением контекста я здесь не вижу. – Raider Nov 21 '16 at 16:40
  • 1
    @VladislavKhapin, ConfigureAwait(false), если поставить после _vk.GetLongPollingAnswerAsync(server), этот вызов пройдет в контексте, только продолжение будет вне контекста, да и смотрите вопрос 3.

    Task.Run - создастся поток, который будет постоянно ждать, никаких асинхронных плюшек, смотрите начало вопроса

    – Qutrix Nov 21 '16 at 17:09
  • @VladislavKhapin .ConfigureAwait(false) в данной ситуации - костыль, к тому же частично нерабочий. Он не гарантирует переключение на фоновой поток. – Pavel Mayorov Nov 22 '16 at 12:48
  • @PavelMayorov да, понял, что сглупил. Пример без await на MSDN есть, кстати – Vladislav Khapin Nov 22 '16 at 13:37
  • http://ru.stackoverflow.com/questions/512968/win10-universal-app-async-%d0%b7%d0%b0%d0%b4%d0%b5%d1%80%d0%b6%d0%ba%d0%b0/513241#513241 – Serginio Nov 23 '16 at 14:35

2 Answers2

3

Предположим, что GetLongPollingAnswerAsync не блокирует вызов. Тогда проблема в Handler(answer): на нём UI подвисать не должно. Думаю, что у вас в нём что-то неправильно.

Для справки: если вам реально нужно, чтобы контекст не захватывался на два внутренних метода, делайте так:

async void LongPolling(string server, CancellationToken ct)
{
    while (true)
    {
        var result = await GetHandledStrignAsync(server, ct);
        if (ct.IsCancellationRequested) return;
        UITextBox.text = result;
    }
}

async Task<string> GetHandledStrignAsync(string server, CancellationToken ct)
{
    string answer = await _vk.GetLongPollingAnswerAsync(server).ConfigureAwait(false);
    if (ct.IsCancellationRequested) return null;
    return await Handler(answer);
}

Но это не должно помонать, если Handler делает всё правильно.


Реализовать не через async/await можно, но не нужно. Будет сложнее и вручную.


Если Handler у вас CPU-bound, то не оформляйте его как Task, и у вас будет вот что:

async void LongPolling(string server, CancellationToken ct)
{
    while (true)
    {
        string answer = await _vk.GetLongPollingAnswerAsync(server);
        if (ct.IsCancellationRequested) return;
        UITextBox.text = await Task.Run(() => Handler(answer));
    }
}

Если у вас есть подозрения, что и _vk.GetLongPollingAnswerAsync(server); CPU-bound, отправьте и его на пул потоков:

        string answer = await Task.Run(() => _vk.GetLongPollingAnswerAsync(server));

Если вы хотите вручную перебросить ваше выполнение на пул потоков, можно попробовать такой трюк (отсюда):

public struct ThreadPoolRedirector : INotifyCompletion
{
    public ThreadPoolRedirector GetAwaiter() => this;
    public bool IsCompleted => Thread.CurrentThread.IsThreadPoolThread;
    public void OnCompleted(Action continuation) =>
        ThreadPool.QueueUserWorkItem(o => continuation());
    public void GetResult() { }
}

public static class AsyncExtensions
{
    public static ThreadPoolRedirector RedirectToThreadPool() => new ThreadPoolRedirector();
}

Ваш код будет таким:

async void LongPolling(string server, CancellationToken ct)
{
    while (true)
    {            
        string text = await GetText(server);
        // тут мы в UI-потоке
        if (ct.IsCancellationRequested) return;
        UITextBox.text = text;
    }
}

async Task<string> GetText(string server)
{
    await AsyncExtensions.RedirectToThreadPool();
    // тут на на пуле потоков
    // вызываем не-CPU-bound-метод через await
    string answer = await _vk.GetLongPollingAnswerAsync(server);
    if (ct.IsCancellationRequested) null;
    // мы на пуле потоков, можно выполнять CPU-bound-код
    return Handler(answer);
}
VladD
  • 206,799
2

Кратко: поскольку у вас проблема в долгой работе Handler и вы желаете вынести его из UI-потока - вам надо воспользоваться методом, который специально был создан для выноса кода в фоновой поток. Я говорю про Task.Run:

await Task.Run(() => Handler(answer));

В коде выше не важно, синхронный Handler или асинхронный - перегрузки Task.Run корректно обрабатывают оба случая.


В качестве альтернативы, можно модифицировать Handler так, чтобы он всегда выполнялся в фоновом потоке (использовал код из ответа VladD):

public struct ThreadPoolRedirector : INotifyCompletion
{
    public ThreadPoolRedirector GetAwaiter() => this;
    public bool IsCompleted => Thread.CurrentThread.IsThreadPoolThread;
    public void OnCompleted(Action continuation) =>
        ThreadPool.QueueUserWorkItem(o => continuation());
    public void GetResult() { }

    public static ThreadPoolRedirector RedirectToThreadPool() => default(ThreadPoolRedirector);
}

async Task Handler(string answer)
{
    await ThreadPoolRedirector.RedirectToThreadPool();

    // дальше что-то, что выполняется 5 секунд в фоне
}
Pavel Mayorov
  • 58,537
  • Чем отличается второй (альтернативный) способ от Task.Run? – Qutrix Nov 22 '16 at 13:03
  • @Qutrix на 1 лямбду и уровень вложенности отступов меньше. В общем-то, никто не запрещает внутри Handler писать Task.Run – Pavel Mayorov Nov 22 '16 at 13:05
  • http://ru.stackoverflow.com/questions/593978/async-await-как-принудительно-НЕ-захватывать-контекст?noredirect=1#comment800377_594107 и http://ru.stackoverflow.com/questions/593978/async-await-как-принудительно-НЕ-захватывать-контекст?noredirect=1#comment800378_594107 – Qutrix Nov 22 '16 at 13:09
  • @Qutrix это вы зачем мне написали?.. – Pavel Mayorov Nov 22 '16 at 13:46
  • Сама суть вопроса, на который я хочу услышать ответ: можно ли как-то задать, чтоб контекст не передавался, чтоб три раза не выделять поток, вместо одного, кроме ConfigureAwait(false)? Task.Run хорошо, но уж лучше ConfigureAwait(false) - в тех комментах описал почему – Qutrix Nov 22 '16 at 13:56
  • @Qutrix вы пытаетесь сделать нечто противоположное тому, что вам нужно. Вам надо остаться в потоке UI - иначе вы не сможете обновить UITextBox. Выделение же фоновых потоков ничего не стоит. – Pavel Mayorov Nov 22 '16 at 14:26
  • Нет-нет, на долгое время, большое кол-во вычислений мне надо сделать не в потоке UI и вернутся к нему только тогда, когда уже вычисления сделаны. Это можно сделать разными способами – Qutrix Nov 22 '16 at 14:30
  • 1
    @Qutrix вынесите их в отдельный метод. – Pavel Mayorov Nov 22 '16 at 16:04