Как асинхронная поддержка C # 5 поможет с проблемами синхронизации потоков пользовательского интерфейса?

16

Я где-то слышал, что C # 5 async-await будет настолько крутым, что вам не придется беспокоиться об этом:

if (InvokeRequired)
{
    BeginInvoke(...);
    return;
}
// do your stuff here

Похоже, что обратный вызов операции ожидания произойдет в исходном потоке вызывающей стороны. Эрик Липперт и Андерс Хейлсберг неоднократно заявляли, что эта функция возникла из-за необходимости сделать пользовательские интерфейсы (особенно пользовательские интерфейсы сенсорных устройств) более отзывчивыми.

Я думаю, что обычное использование такой функции будет что-то вроде этого:

public class Form1 : Form
{
    // ...
    async void GetFurtherInfo()
    {
        var temperature = await GetCurrentTemperatureAsync();
        label1.Text = temperature;
    }
}

Если используется только обратный вызов, то установка текста метки вызовет исключение, поскольку он не выполняется в потоке пользовательского интерфейса.

До сих пор я не мог найти какой-либо ресурс, подтверждающий, что это так. Кто-нибудь знает об этом? Есть ли какие-либо документы, объясняющие технически, как это будет работать?

Пожалуйста, предоставьте ссылку из надежного источника, а не просто отвечайте "да".

Alex
источник
Это кажется маловероятным, по крайней мере, в части awaitфункциональности. Это просто много синтаксического сахара для продолжения прохождения . Возможно, есть какие-то другие не связанные улучшения WinForms, которые должны помочь? Это относится к самой платформе .NET, а не к C #.
Aaronaught
@ Я согласен, вот почему я задаю вопрос точно. Я отредактировал вопрос, чтобы уточнить, откуда я. Звучит странно, что они создали бы эту функцию и все еще требуют, чтобы мы использовали печально известный стиль кода InvokeRequired.
Алекс

Ответы:

17

Я думаю, вы запутались здесь. То, о чем вы просите, уже возможно с помощью System.Threading.Tasks, asyncи awaitв C # 5 просто предоставим немного более приятного синтаксического сахара для той же функции.

Давайте использовать пример Winforms - перетащите кнопку и текстовое поле на форму и используйте этот код:

private void button1_Click(object sender, EventArgs e)
{
    Task.Factory.StartNew<int>(() => DelayedAdd(5, 10))
        .ContinueWith(t => DelayedAdd(t.Result, 20))
        .ContinueWith(t => DelayedAdd(t.Result, 30))
        .ContinueWith(t => DelayedAdd(t.Result, 50))
        .ContinueWith(t => textBox1.Text = t.Result.ToString(),
            TaskScheduler.FromCurrentSynchronizationContext());
}

private int DelayedAdd(int a, int b)
{
    Thread.Sleep(500);
    return a + b;
}

Запустите его, и вы увидите, что (а) он не блокирует поток пользовательского интерфейса и (б) вы не получите обычную ошибку «операция с несколькими потоками не действительна» - если вы не удалите TaskSchedulerаргумент из последнего ContinueWith, в в каком случае вы будете.

Это стандартное продолжение передачи . Магия происходит в TaskSchedulerклассе и, в частности, в экземпляре, полученном с помощью FromCurrentSynchronizationContext. Передайте это в любое продолжение, и вы скажете, что продолжение должно выполняться в том потоке, который вызвал FromCurrentSynchronizationContextметод - в данном случае, в потоке пользовательского интерфейса.

Ожидатели немного более сложные в том смысле, что они знают, с какого потока они начали и с какого потока должно происходить продолжение. Таким образом, приведенный выше код может быть написан немного более естественно:

private async void button1_Click(object sender, EventArgs e)
{
    int a = await DelayedAddAsync(5, 10);
    int b = await DelayedAddAsync(a, 20);
    int c = await DelayedAddAsync(b, 30);
    int d = await DelayedAddAsync(c, 50);
    textBox1.Text = d.ToString();
}

private async Task<int> DelayedAddAsync(int a, int b)
{
    Thread.Sleep(500);
    return a + b;
}

Эти два должны выглядеть очень похожи, и на самом деле они являются очень похожи. DelayedAddAsyncТеперь метод возвращает Task<int>вместо int, и поэтому awaitпросто хлопая продолжения на каждом из них. Основное отличие состоит в том, что он передает контекст синхронизации в каждой строке, поэтому вам не нужно делать это явно, как мы это делали в последнем примере.

Теоретически различия гораздо более значительны. Во втором примере каждая отдельная строка в button1_Clickметоде фактически выполняется в потоке пользовательского интерфейса, но сама задача ( DelayedAddAsync) выполняется в фоновом режиме. В первом примере все работает в фоновом режиме , за исключением присвоения, к textBox1.Textкоторому мы явно привязали контекст синхронизации потока пользовательского интерфейса.

Вот что действительно интересно await- тот факт, что ожидающий может подключаться и выходить из одного и того же метода без каких-либо блокирующих вызовов. Вы звоните await, текущий поток возвращается к обработке сообщений, и когда это будет сделано, официант подберет именно то место, где остановился, в том же потоке, в котором остановился. Но с точки зрения вашего Invoke/ BeginInvokeконтраста в вопросе, я Мне жаль говорить, что вы должны были перестать делать это давным-давно.

Aaronaught
источник
Это очень интересно @Aaronaught. Я знал о стиле прохождения продолжения, но не знал всего этого «контекста синхронизации». Есть ли документ, связывающий этот контекст синхронизации с C # 5 async-await? Я понимаю, что это существующая функция, но тот факт, что они используют ее по умолчанию, звучит как нечто грандиозное, особенно потому, что она должна оказывать значительное влияние на производительность, верно? Есть еще комментарии по этому поводу? Спасибо за ваш ответ, кстати.
Алекс
1
@Alex: Для ответов на все эти дополнительные вопросы я предлагаю вам прочитать Async Performance: Понимание затрат на Async и Await . Раздел «Забота о контексте» объясняет, как все это относится к контексту синхронизации.
Aaronaught
(Кстати, контексты синхронизации не новы; они были в фреймворке с 2.0. TPL просто сделал их намного проще в использовании.)
Aaronaught
2
Мне интересно, почему до сих пор много обсуждается использование стиля InvokeRequired, и большинство потоков, которые я видел, даже не упоминают контексты синхронизации. Это сэкономило бы мне время, чтобы поставить этот вопрос ...
Алекс
2
@ Алекс: Я думаю, вы не искали в нужных местах . Я не знаю, что тебе сказать; Есть большие части сообщества .NET, которые требуют много времени, чтобы наверстать упущенное. Черт, я все еще вижу некоторых кодеров, использующих ArrayListкласс в новом коде. У меня все еще нет опыта работы с RX. Люди узнают, что им нужно знать, и делятся тем, что они уже знают, даже если то, что они уже знают, устарело. Этот ответ может быть устаревшим через несколько лет.
Aaronaught
4

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

Год назад Эрик Липперт написал серию из восьми статей, рассказывающих об этом: Fabulous Adventures In Coding

РЕДАКТИРОВАТЬ: и вот Андерс // build / presentation: channel9

Кстати, вы заметили, что если вы перевернете "// build /" вверх ногами, вы получите "/ plinq //" ;-)

Николас Батлер
источник
3

Джон Скит (Jon Skeet) дал отличную презентацию, охватывающую методы Async в C # 5, которая может оказаться чрезвычайно полезной:

http://skillsmatter.com/podcast/home/async-methods

Шон Холм
источник
EduAsync того же автора также может быть хорошим источником информации об Async.
Матье