Як підтримка асинхронизації C # 5 допоможе вирішити проблеми з синхронізацією потоку інтерфейсу?


16

Десь я чув, що асинхрон-чекання C # 5 буде настільки приголомшливим, що вам не доведеться турбуватися про це:

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

Схоже, зворотний виклик операції, що очікує, відбудеться в оригінальній потоці абонента. Ерік Ліпперт та Андерс Хейлсберг кілька разів заявляли, що ця функція виникла з необхідності зробити інтерфейси користувача (особливо інтерфейси із сенсорним пристроєм) більш чуйними.

Я думаю, що звичайне використання такої функції було б приблизно таким:

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

Якщо використовується лише зворотний виклик, то встановлення тексту мітки призведе до виключення, оскільки воно не виконується в потоці інтерфейсу користувача.

Поки що я не міг знайти жодного ресурсу, який би підтверджував це. Хтось знає про це? Чи є документи, які пояснюють технічно, як це буде працювати?

Будь ласка, надайте посилання з надійного джерела, не відповідайте просто "так".


Це здається малоймовірним, принаймні, наскільки awaitце стосується функціональності. Це просто багато синтаксичного цукру для продовження проходження . Можливо, є якісь інші незв’язані вдосконалення WinForms, які повинні допомогти? Це підпадало б під рамки самого .NET, а не конкретно на C #.
Aaronaught

@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;
}

Запустіть його, і ви побачите, що (а) він не блокує потік користувальницького інтерфейсу і (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. Я знав про стиль проходження продовження, але не знав про всю цю річ "контексту синхронізації". Чи є якийсь документ, що пов'язує цей контекст синхронізації з C # 5 async-wait? Я розумію, що це вже наявна функція, але те, що вони використовують її за замовчуванням, звучить як велика справа, особливо тому, що вона повинна мати серйозні наслідки на продуктивність, правда? Будь-які подальші коментарі з цього приводу? Дякуємо за вашу відповідь.
Олексій

1
@Alex: Щоб отримати відповіді на всі наступні запитання, пропоную прочитати Async Performance: Розуміння витрат на Асинхронізацію та Очікування . У розділі "Турбота про контекст" пояснюється, як це все стосується контексту синхронізації.
Aaronaught

(До речі, контексти синхронізації не є новими; вони існують в рамках з 2.0. TPL просто полегшив їх використання.)
Aaronaught

2
Мені цікаво, чому існує ще багато дискусій щодо використання стилю InvokeRequired, і більшість потоків, які я бачив, навіть не згадують контексти синхронізації. Це врятувало б мені час поставити це питання ...
Алекс

2
@ Алекс: Я думаю, ви не шукали в потрібних місцях . Я не знаю, що тобі сказати; Є велика частина спільноти .NET, яка потребує тривалого часу. Чорт, я все ще бачу деякі кодери, що використовують ArrayListклас у новому коді. Я досі ледве маю жодного досвіду роботи з RX. Люди дізнаються те, що їм потрібно знати, і поділитися тим, що вони вже знають, навіть якщо те, що вони вже знають, застаріло. Ця відповідь може бути застарілою через кілька років.
Aaronaught

4

Так, для потоку користувальницького інтерфейсу зворотний виклик операції, що очікує, відбудеться у вихідному потоці абонента.

Рік про це Ерік Ліпперт написав з 8 частин рік тому: Казкові пригоди в кодуванні

EDIT: і ось Anders '// збирання / презентація: channel9

До речі, ви помітили, що якщо повернути "// build /" догори дном, ви отримаєте "/ plinq //" ;-)


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.