Що робить SynchronizationContext?


135

У книзі Програмування на C # є приклад зразка про SynchronizationContext:

SynchronizationContext originalContext = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(delegate {
    string text = File.ReadAllText(@"c:\temp\log.txt");
    originalContext.Post(delegate {
        myTextBox.Text = text;
    }, null);
});

Я початківець у нитках, тому, будь ласка, відповідайте детально. По-перше, я не знаю, що означає контекст, що програма зберігає в програмі originalContext? І коли Postметод буде запущений, що зробить потік інтерфейсу?
Якщо я запитаю якісь дурні речі, виправте мене, дякую!

EDIT: Наприклад, що робити, якщо я просто напишу myTextBox.Text = text;в методі, яка різниця?


1
Хороший посібник має це сказати . Мета моделі синхронізації, реалізованої цим класом, - дозволити внутрішнім операціям асинхронної / синхронізації загальної мови виконувати належну поведінку з різними моделями синхронізації. Ця модель також спрощує деякі вимоги, яким повинні були керуватися керовані програми, щоб правильно працювати в різних середовищах синхронізації.
ta.speot.is


7
@RoyiNamir: Так, але вгадайте, що: async/ awaitпокладається SynchronizationContextпід ним.
stakx - більше не вносяться повідомлення

Відповіді:


170

Що робить SynchronizationContext?

Простіше кажучи, SynchronizationContextпредставляє місце "де" код може бути виконаний. Делегати, які передаються в його Sendабо Postметод буде викликатися в цьому місці. ( Postце неблокуюча / асинхронна версія Send.)

Кожен потік може мати SynchronizationContextпримірник, пов'язаний з ним. Виконаний потік може бути пов'язаний з контекстом синхронізації за допомогою виклику статичного SynchronizationContext.SetSynchronizationContextметоду , а поточний контекст запущеного потоку можна запитувати через SynchronizationContext.Currentвластивість .

Незважаючи на те, що я щойно написав (кожен потік, що має пов'язаний контекст синхронізації), a SynchronizationContextне обов'язково представляє певний потік ; він також може пересилати виклик переданих до нього делегатів до будь-якого з декількох потоків (наприклад, до ThreadPoolробочої нитки) або (принаймні теоретично) до конкретного ядра процесора або навіть до іншого хоста мережі . Там, де ваші делегати закінчуються, залежить від типу SynchronizationContextвикористовуваного.

Windows Forms встановить WindowsFormsSynchronizationContextна тему, на якій створена перша форма. (Цей потік прийнято називати "потоком інтерфейсу".) Цей тип контексту синхронізації викликає переданих йому делегатів саме на цей потік. Це дуже корисно, оскільки Windows Forms, як і багато інших фреймворків інтерфейсу користувача, дозволяє лише маніпулювати елементами управління на тій же темі, на якій вони були створені.

Що робити, якщо я просто пишу myTextBox.Text = text;методом, яка різниця?

Код, який ви передали, ThreadPool.QueueUserWorkItemбуде запускатися на робочу нитку пулу потоків. Тобто він не буде виконуватись на потоці, на якому myTextBoxстворено ваш , тому Windows Forms рано чи пізно (особливо у версіях версій) створює виняток, повідомляючи, що ви не можете отримати доступ myTextBoxчерез інший потік.

Ось чому вам доведеться якось "переключитися" з робочої нитки на "потік інтерфейсу" (де myTextBoxстворено) перед цим конкретним завданням. Робиться це так:

  1. Поки ви все ще знаходитесь у потоці користувальницького інтерфейсу, захопіть SynchronizationContextтам «Форми Windows» та збережіть посилання на нього у змінній ( originalContext) для подальшого використання. Ви повинні запитувати SynchronizationContext.Currentв цьому пункті; якщо ви попросили його всередині коду, який передається ThreadPool.QueueUserWorkItem, ви можете отримати будь-який контекст синхронізації, пов'язаний з робочою ниткою пулу потоків. Після збереження посилання на контекст Windows Forms ви можете використовувати його будь-де та в будь-який час для "надсилання" коду до потоку інтерфейсу користувача.

  2. Кожного разу, коли вам потрібно буде маніпулювати елементом інтерфейсу (але вже не є або може не бути в потоці користувальницького інтерфейсу), відкрийте контекст синхронізації Windows Forms через originalContextі передайте код, який маніпулює користувальницьким інтерфейсом Sendабо Post.


Заключні зауваження та підказки:

  • Те, що контексти синхронізації не робитимуть для вас, говорить про те, який код повинен працювати у певному місці / контексті, а який код можна просто виконувати нормально, не передаючи його в а SynchronizationContext. Для того, щоб вирішити це, ви повинні знати правила та вимоги рамки, проти якої програмуєте - Windows Forms у цьому випадку.

    Тому пам’ятайте це просте правило для Windows Forms: НЕ звертайтеся до елементів керування чи форм з потоку, відмінного від того, який створив їх. Якщо вам потрібно зробити це, використовуйте SynchronizationContextмеханізм, як описано вище, або Control.BeginInvoke(що є специфічним для Windows Forms способом робити саме те саме).

  • Якщо ви програмуєте на .NET 4.5 або більш пізньої версії, ви можете зробити ваше життя набагато простіше шляхом перетворення коду , який явно використовує SynchronizationContext, ThreadPool.QueueUserWorkItem, control.BeginInvokeі т.д. до нових async/ awaitключових слів і Task Parallel Library (TPL) , тобто API навколишнього Taskі Task<TResult>класи. Вони, в дуже високій мірі, подбають про захоплення контексту синхронізації потоку користувальницького інтерфейсу, запустивши асинхронну операцію, потім повернувшись до потоку інтерфейсу користувача, щоб ви могли обробити результат операції.


Ви кажете, що Windows Forms, як і багато інших фреймворків інтерфейсу користувача, дозволяє лише маніпулювати елементами управління на одній темі, але всі вікна в Windows повинні мати доступ до тієї самої нитки, яка його створила.
користувач34660

4
@ user34660: Ні, це невірно. Ви можете мати кілька потоків, які створюють елементи керування Windows Forms. Але кожен елемент управління пов'язаний з одним потоком, який його створив, і до нього має бути доступний лише один. Елементи керування з різних потоків інтерфейсу також дуже обмежені у взаємодії один з одним: один не може бути батьком / дочіркою іншого, зв’язування даних між ними неможливо тощо. Нарешті, кожен потік, який створює елементи управління, потребує власного повідомлення цикл (який починається з Application.Run, IIRC). Це досить просунута тема, а не щось випадково зроблене.
stakx - більше не вносять внесок

Мій перший коментар пояснюється тим, що ви говорите "як і багато інших фреймворків інтерфейсу", маючи на увазі, що деякі вікна дозволяють "маніпулювати елементами управління" з іншого потоку, але жодного Windows не робити. У вас не може бути "декілька потоків, які створюють елементи керування Windows Forms" для одного і того ж вікна, "а доступ до них має бути той самий потік", "а доступ до цього лише одного потоку" говорять те саме. Я сумніваюся, що для одного і того ж вікна можливо створити "Керування з різних потоків інтерфейсу". Все це не є вдосконаленим для тих, хто досвідчений з програмуванням Windows раніше.
користувач34660

3
Вся ця розмова про «вікна» та «вікна Windows» робить мене дуже запамороченою. Чи згадував я якесь із цих «вікон»? Я не думаю, що так ...
stakx - більше не вносить допису

1
@ibubi: Я не впевнений, що розумію ваше запитання. Контекст синхронізації будь-якого потоку або не встановлений ( null), або екземпляр SynchronizationContext(або його підклас). Суть цієї цитати не в тому, що ви отримуєте, а в тому, що ви не отримаєте: контексті синхронізації потоку інтерфейсу.
stakx - більше не вносяться повідомлення

24

Я хотів би додати до інших відповідей, SynchronizationContext.Postпросто чергає зворотний виклик для подальшого виконання цільового потоку (як правило, протягом наступного циклу циклу повідомлень цільового потоку), а потім виконання продовжується в потоці виклику. З іншого боку, SynchronizationContext.Sendнамагається негайно виконати зворотний виклик на цільовому потоці, що блокує викликовий потік і може призвести до тупикової ситуації. В обох випадках існує можливість повторної передачі коду (введення методу класу в той самий потік виконання до повернення попереднього виклику цього ж методу).

Якщо ви знайомі з моделлю програмування Win32, дуже близькою буде аналогія PostMessageта SendMessageAPI, які ви можете зателефонувати, щоб відправити повідомлення з потоку, відмінного від цільового вікна.

Ось дуже вдале пояснення, що таке контексти синхронізації: Все про SynchronizationContext .


16

У ньому зберігається постачальник синхронізації - клас, похідний від SynchronizationContext. У цьому випадку, ймовірно, це буде екземпляр WindowsFormsSynchronizationContext. Цей клас використовує методи Control.Invoke () та Control.BeginInvoke () для реалізації методів Send () та Post (). Або це може бути DispatcherSynchronizationContext, він використовує Dispatcher.Invoke () та BeginInvoke (). У програмі Winforms або WPF цей постачальник автоматично встановлюється, як тільки ви створюєте вікно.

Коли ви запускаєте код на іншому потоці, як, наприклад, нитка пулу потоків, що використовується у фрагменті, ви повинні бути обережними, щоб ви не використовували безпосередньо об'єкти, які не є безпечними для потоку. Як і будь-який об’єкт інтерфейсу користувача, ви повинні оновити властивість TextBox.Text з потоку, який створив TextBox. Метод Post () гарантує, що ціль делегата працює на цьому потоці.

Слідкуйте за тим, що цей фрагмент є трохи небезпечним, він буде працювати правильно лише тоді, коли ви викликаєте його з потоку інтерфейсу користувача. СинхронізаціяContext.Current має різні значення в різних потоках. Тільки потік інтерфейсу має корисне значення. І є причиною того, що код мав його скопіювати. Більш зрозумілий і безпечний спосіб зробити це в додатку Winforms:

    ThreadPool.QueueUserWorkItem(delegate {
        string text = File.ReadAllText(@"c:\temp\log.txt");
        myTextBox.BeginInvoke(new Action(() => {
            myTextBox.Text = text;
        }));
    });

Що має перевагу в тому, що він працює при виклику з будь-якого потоку. Перевага використання SynchronizationContext.Current полягає в тому, що він все ще працює, чи використовується код у Winforms або WPF, він має значення в бібліотеці. Це, звичайно, не хороший приклад такого коду, ви завжди знаєте, який тип TextBox у вас тут, і ви завжди знаєте, чи використовувати Control.BeginInvoke або Dispatcher.BeginInvoke. Насправді використання SynchronizationContext.Current - це не так часто.

Книга намагається навчити вас про нитку, тому використовувати цей хибний приклад добре. У реальному житті в кількох випадках, коли ви могли б розглянути можливість використання SynchronizationContext.Current, ви все одно залишите це до ключових слів асинхронізації / очікування C # або TaskScheduler.FromCurrentSynchronizationContext (), щоб зробити це за вас. Але зауважте, що вони все ще неправильно поводяться так, як робить фрагмент, коли ви використовуєте їх у неправильній нитці з точно тієї ж причини. Тут дуже поширене питання, додатковий рівень абстракції є корисним, але ускладнює з'ясування, чому вони не працюють правильно. Сподіваємось, книга також говорить вам, коли її не використовувати :)


Вибачте, чому дозвольте ручці потоку користувальницького інтерфейсу захистити нитки? тобто я думаю, що потік користувальницького інтерфейсу може використовувати myTextBox, коли повідомлення Post () запускається, це безпечно?
хмарноФан

4
Вашу англійську важко розшифрувати. Ваш оригінальний фрагмент працює правильно лише тоді, коли він викликається з потоку інтерфейсу користувача. Що є дуже поширеним випадком. Тільки тоді він буде опублікований назад до потоку інтерфейсу користувача. Якщо він викликається з робочої нитки, то ціль делегата Post () буде працювати на потоці нитки пулу. Кабум. Це те, що ви хочете спробувати на собі. Запустіть нитку і дозвольте потоку викликати цей код. Ви зробили це правильно, якщо код виходить з ладу з NullReferenceException.
Ганс Пасант

5

Мета контексту синхронізації тут - переконатися, що myTextbox.Text = text;виклик стає в основному потоці інтерфейсу користувача.

Windows вимагає, щоб до керування графічним інтерфейсом було доступно лише тему, з якою вони були створені. Якщо ви спробуєте призначити текст у фоновому потоці без попередньої синхронізації (будь-яким із декількох засобів, наприклад, цього або шаблону Invoke), буде викинуто виняток.

Для цього потрібно зберегти контекст синхронізації до створення фонового потоку, тоді фоновий потік використовує контекст. Метод пошти виконує код GUI.

Так, показаний вами код в основному марний. Навіщо створювати фонову нитку, лише щоб негайно повернутися до основної нитки інтерфейсу? Це просто приклад.


4
"Так, код, який ви показали, в основному марний. Навіщо створювати фонову нитку, лише негайно потрібно повернутися до основного потоку інтерфейсу? Це лише приклад." - Читання з файлу може бути довгим завданням, якщо файл великий, те, що може блокувати потік інтерфейсу та зробити його
безвідповідальним

У мене дурне питання. Кожен потік має Id, і я вважаю, що у потоку інтерфейсу також є ID = 2, наприклад. Потім, коли я перебуваю на потоці пулу ниток, чи можу я зробити щось подібне: var thread = GetThread (2); thread.Execute (() => textbox1.Text = "foo")?
Іван

@John - Ні, я не думаю, що це працює, тому що нитка вже виконується. Ви не можете виконати вже виконаний потік. Виконати роботи лише тоді, коли нитка не запущена (IIRC)
Erik Funkenbusch

3

До Джерела

Кожен потік має пов'язаний з ним контекст - це також відомий як "поточний" контекст - і ці контексти можна поділити між потоками. ExecutionContext містить відповідні метадані поточного середовища або контексту, в якому програма виконується. SynchronizationContext являє собою абстракцію - вона позначає місце, де виконується код вашої програми.

SynchronizationContext дозволяє вам поставити чергу в інший контекст. Зауважте, що кожен потік може мати власний SynchronizatonContext.

Наприклад: Припустимо, у вас є дві нитки, Thread1 і Thread2. Скажімо, Thread1 виконує певну роботу, і тоді Thread1 бажає виконати код на Thread2. Один з можливих способів зробити це - попросити Thread2 про її об'єкт SynchronizationContext, надати його Thread1, і тоді Thread1 може зателефонувати SynchronizationContext.Send для виконання коду на Thread2.


2
Контекст синхронізації не обов'язково прив'язується до певного потоку. Можна для декількох потоків обробляти запити в одному контексті синхронізації, а один потік обробляти запити для кількох контекстів синхронізації.
Сервіс

3

SynchronizationContext надає нам спосіб оновити інтерфейс користувача з іншого потоку (синхронно за допомогою методу Send або асинхронно за допомогою методу Post).

Погляньте на наступний приклад:

    private void SynchronizationContext SyncContext = SynchronizationContext.Current;
    private void Button_Click(object sender, RoutedEventArgs e)
    {
        Thread thread = new Thread(Work1);
        thread.Start(SyncContext);
    }

    private void Work1(object state)
    {
        SynchronizationContext syncContext = state as SynchronizationContext;
        syncContext.Post(UpdateTextBox, syncContext);
    }

    private void UpdateTextBox(object state)
    {
        Thread.Sleep(1000);
        string text = File.ReadAllText(@"c:\temp\log.txt");
        myTextBox.Text = text;
    }

SynchronizationContext.Current поверне контекст синхронізації потоку інтерфейсу. Звідки я це знаю? На початку кожної форми або програми WPF контекст буде встановлений у потоці інтерфейсу користувача. Якщо ви створите додаток WPF і запустите мій приклад, ви побачите, що при натисканні на кнопку він спить приблизно 1 секунду, тоді він покаже вміст файлу. Ви можете очікувати, що цього не вийде, тому що виклик методу UpdateTextBox (який є Work1) - це метод, переданий до потоку, тому він повинен спати цю тему не головним потоком інтерфейсу, NOPE! Незважаючи на те, що метод Work1 передається в потік, зауважте, що він також приймає об'єкт, який є SyncContext. Якщо ви подивитесь на це, ви побачите, що метод UpdateTextBox виконується через метод syncContext.Post, а не метод Work1. Погляньте на наступне:

private void Button_Click(object sender, RoutedEventArgs e) 
{
    Thread.Sleep(1000);
    string text = File.ReadAllText(@"c:\temp\log.txt");
    myTextBox.Text = text;
}

Останній приклад і цей виконує те саме. Обидва не блокують інтерфейс користувача, поки він працює.

На закінчення подумайте про SynchronizationContext як нитку. Це не нитка, вона визначає потік (зауважте, що не у всіх потоках є SyncContext). Кожного разу, коли ми зателефонуємо на метод публікації чи надсилання, щоб оновити інтерфейс користувача, це подібно до того, щоб оновити інтерфейс користувача звичайно з основної нитки інтерфейсу користувача. Якщо з якихось причин вам потрібно оновити інтерфейс користувача з іншого потоку, переконайтесь, що в потоці є SyncContext основної нитки користувальницького інтерфейсу і просто зателефонуйте на метод відправки або публікації на ньому методом, який потрібно виконати, і ви всі набір.

Сподіваюся, це допоможе тобі, приятелю!


2

SynchronizationContext - це, головним чином, постачальник виконання делегатів зворотного виклику, головним чином відповідальний за те, щоб делегати запускалися у заданому контексті виконання після того, як певна частина коду (інкапсульована у завданні .Net TPL) програми завершила її виконання.

З технічної точки зору SC - це простий клас C #, орієнтований на підтримку та надання своєї функції спеціально для об’єктів паралельної бібліотеки завдань.

Кожна програма .Net, за винятком консольних додатків, має конкретну реалізацію цього класу на основі конкретних базових рамок, тобто: WPF, WindowsForm, Asp Net, Silverlight, ecc ..

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

І слово "контекст" означає контекст виконання, тобто поточний контекст виконання, де буде виконаний той код очікування, а саме синхронізація між кодом асинхронізації та кодом очікування відбувається в конкретному контексті виконання, таким чином цей об'єкт називається SynchronizationContext: він представляє контекст виконання, який буде доглядати за синхронізацією коду асинхронізації та виконанням коду очікування .


1

Цей приклад наведено з прикладів Linqpad Джозефа Альбахарі, але він справді допомагає зрозуміти, що робить контекст синхронізації.

void WaitForTwoSecondsAsync (Action continuation)
{
    continuation.Dump();
    var syncContext = AsyncOperationManager.SynchronizationContext;
    new Timer (_ => syncContext.Post (o => continuation(), _)).Change (2000, -1);
}

void Main()
{
    Util.CreateSynchronizationContext();
    ("Waiting on thread " + Thread.CurrentThread.ManagedThreadId).Dump();
    for (int i = 0; i < 10; i++)
        WaitForTwoSecondsAsync (() => ("Done on thread " + Thread.CurrentThread.ManagedThreadId).Dump());
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.