Strona używa cookies (ciasteczek). Dowiedz się więcej o celu ich używania i zmianach ustawień. Korzystając ze strony wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki.    X

Programowanie wielo-UI-wątkowe

Możliwość tworzenia aplikacji wykorzystujących wątki robocze (ang. Worker Threads) w .NET to żadna nowość. Istnieje wiele dobrze udokumentowanych sposobów radzenia sobie z tak zdefiniowanym zagadnieniem wielowątkowości:Threading in C# by Joseph Albahari

Jednakże prawie wszystkie dostępne w sieci artykuły poruszają (i opisują) zagadnienie wielowątkowości wyłącznie w kontekście wątków wykonujących żmudne (czyt. długie) obliczenia w tle bądź operacje wejścia/wyjścia o nieprzewidywalnym czasie ukończenia. Mówiąc inaczej, metody te opisują sytuacje, w których wątek główny programu (często ten, na którym funkcjonuje graficzny interfejs użytkownika) powołuje do życia wątki obliczeniowe po to, aby nie utracić responsywności (popularne "Brak odpowiedzi...") w trakcie wykonywania tychże obliczeń. Brak jest natomiast prostych odpowiedzi na pytania jak tworzyć i komunikować się pomiędzy dwoma, trzema itd. niezależnymi wątkami, które same pełnią rolę "wątków głównych" (ang. UI Threads) w obrębie jednej domeny dotnetowej aplikacji (ang. Application Domain). No dobra, odpowiedzi są, ale najczęściej w stylu "po co Tobie drugi wątek UI?", "uważam, że źle zaprojektowałeś aplikację" lub "myślę, że chodzi Tobie o InvokeRequired". Jak widać niewiele mają one wspólnego z tematem...

Wiadomo (na przykładzie Windows Forms), że dostęp do i modyfikacja elementu interfejsu użytkownika może nastąpić wyłącznie z wątku, który ten element utworzył. Inaczej bowiem każdego nieroztropnego programistę czeka przykra niespodzianka w postaci wyjątku InvalidOperationException i nieunikniony zawał serca. Nie można ot tak sobie zmieniać stanu "kontrolek" z poziomu dowolnego miejsca w wielowątkowym programie. A szkoda :)

Application.Run + SynchronizationContext

Podniesienie rangi wątku do rangi "głównego" (w ogromnym uproszczeniu!) w typowej aplikacji Windows Forms, ale także i każdej okienkowej aplikacji, dzieje się z chwilą uruchomienia pętli przetwarzającej zdarzenia pochodzące od użytkownika oraz wiadomości systemowe. Przetwarzanie takowej pętli zdarzeń rozpoczyna się z chwilą wywołania w danym wątku statycznej metody Application.Run (patrz -> Program.cs). Konsekwencją takiego modelu będą wszelkie nieudane próby tworzenia i zarządzania interfejsem graficznym w wątkach (implementowanych np. za pomocą klasy System.Threading.Thread) pozbawionych takiej pętli. Dodatkowo, i tu bardzo ważne!, wątki, w których tworzone są jakiekolwiek elementy pełniące rolę interfejsu użytkownika i dziedziczące po klasie System.Windows.Forms.Control "automagicznie" otrzymują tzw. kontekst umożliwiający synchronizację obiektów należących do danego wątku (ang. WindowsFormsSynchronizationContext). Dzięki temu, wykorzystując unikatowy obiekt SynchronizationContext (zwracany przez System.Threading.SynchronizationContext.Current) możemy sprawnie wywoływać dowolne metody, z dowolnego miejsca, na właściwym wątku. W dużym skrócie, wystarczy metodom Send bądź Post, udostępnianym przez SynchronizationContext, przekazać wskaźnik do interesującej nas funkcji (delegat, yuk :P) a jej rychłe wywołanie zostanie zakolejkowane na przynależnym wątku. Przy czym druga ze wspomnianych metod jest metodą asynchroniczną. Zainteresowanych tematem odsyłam (jak zwykle) do źródeł z pierwszej ręki:Parallel Computing - It's All About the SynchronizationContext

Na nieszczęście wszystkich programistów dokumentacja .NET Framework nie jest już pod tym względem szczególnie bogata, dlatego też dla wielu - czasem doświadczonych programistów - temat ten mógł stanowić wielką niewiadomą. Było minęło, prawda?

Przykład

Kończąc powyższe przemyślenia zaprezentuję drobny przykład (pisany na kolanie), który - tu zaznaczę - pod żadnym pozorem NIE może być brany za kompletny czy nawet książkowo poprawny. Z pewnych oczywistych względów taki nie jest. Z drugiej strony, na pewno odnajdziecie w nim ciekawe rozwiązania, które z pewnością podsuną Wam rozwiązania problemów programowania wielo-UI-wątkowego. Bo programowanie wielo-UI-wątkowe to coś więcej niż InvokeRequired...

using System; using System.Collections.Generic; using System.Threading; using System.Text; using System.Windows.Forms; using System.Drawing; namespace MultipleUIThreads { class Program { // Referencje do obiektów klasy Test static Test test1; static Test test2; static Test test3; static void Main(string[] args) { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); // null -> brak nadrzędnego wątku test1 = new Test(null, "Okno 1"); test1.Show(); test1.DispatchWork<string>(new string[] { "raz", "dwa", "trzy" }); Thread thread2 = new Thread((obj2) => { // obj2 -> Id nadrzędnego wątku test2 = new Test(obj2, "Okno 2"); test2.Show(); test2.DispatchWork<int>(new int[] { 1, 2, 3, 4 }); Thread thread3 = new Thread((obj3) => { // obj3 -> Id nadrzędnego wątku, w tym wypadku thread2 test3 = new Test(obj3, "Okno 3"); test3.Show(); Application.Run(); }); thread3.Start(Thread.CurrentThread.ManagedThreadId); Application.Run(); }); thread2.Start(Thread.CurrentThread.ManagedThreadId); Application.DoEvents(); Thread.Sleep(3000); // Wywołanie metody w innym wątku niż obecny test2.DispatchChangeSizeAndBackColor(new Size(500, 240), Color.Yellow); Thread.Sleep(3000); // Wywołanie metody w innym wątku niż obecny test3.DispatchWork<int>(new int[] { 22, 222, -23, -56 }); Thread.Sleep(3000); // Wywołanie metody w innym wątku niż obecny test3.DispatchAnything<Point, Color, Size>(test3.ChangeLocationColorSize, new Point(200, 300), Color.Blue, new Size(350, 390)); // Z zasady operacje na oknie nie powinny się odbywać przed uruchomieniem pętli // przetwarzającej wiadomości systemowe oraz zdarzenia użytkownika! Application.Run(); } } class Test { private Form form; SynchronizationContext context; public SynchronizationContext Context { get { return this.context; } private set { this.context = value; } } // ManagedThreadId nadrzędnego wątku private int parentThreadId; public Test(object obj, string name) { form = CreateForm(name); if (obj is int) parentThreadId = (int)obj; else parentThreadId = -1; // Zapisz aktualny kontekst umożliwiający synchronizację // SynchronizationContext.Current == null w przypadku gdy wcześniej // nie utworzono obiektu : System.Windows.Forms.Control context = SynchronizationContext.Current; } private Form CreateForm(string name) { Form form = new Form(); form.Text = name; form.Size = new Size(480, 140); form.Paint += new PaintEventHandler(Paint); return form; } private void Paint(object sender, PaintEventArgs e) { Graphics g = e.Graphics; StringFormat format = new StringFormat() { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; g.DrawString(String.Format( "\"ParentThread\".ManagedThreadId = {0}\n"+ "CurrentThread.ManagedThreadId = {1}", parentThreadId, Thread.CurrentThread.ManagedThreadId), new Font("Arial", 18), Brushes.Black, ((Form)sender).DisplayRectangle, format); } public void DispatchWork<T>(params T[] args) { Console.WriteLine("Metoda DispatchWork wywołana z wątku: " + Thread.CurrentThread.ManagedThreadId.ToString()); if (SynchronizationContext.Current != this.Context) { Console.WriteLine("Wywołanie metody nastąpiło z innego wątku. Nastąpi \"przekierowanie\" do poprawnego wątku"); // Metoda asynchroniczna SynchronizationContext.Post natychmiast zwraca sterowanie // Obsługa wyjątków w wątku, do którego należy metoda Work(), dlatego też // try { Post } catch { } nie przechwyci wyjątku! To jest generalna // zasada przy projektowaniu aplikacji wykorzystujących metody asynchroniczne this.Context.Post(delegate { Work<T>(args); }, null); return; } Console.WriteLine("Prawidłowy SynchronizationContext"); Work<T>(args); } public void Work<T>(params T[] args) { Console.WriteLine("Metoda Work wywołana z wątku: " + Thread.CurrentThread.ManagedThreadId); Console.WriteLine("args.Length = {0}, typeof(T) = {1}", args.Length, typeof(T)); int i = 0; foreach (T t in args) Console.WriteLine("args[{0}] = {1}", i++, t.ToString()); if (typeof(T).Equals(typeof(int))) { int sum = 0; for (int j = 0; j < args.Length; j++) sum += Convert.ToInt32(args[j]); Console.WriteLine("Suma = {0}", sum); } Console.WriteLine(new string('-', 50)); } public void Show() { form.Show(); } public void DispatchChangeSizeAndBackColor(Size size, Color color) { Console.WriteLine("Metoda DispatchChangeSizeAndBackColor wywołana z wątku: " + Thread.CurrentThread.ManagedThreadId.ToString()); if (SynchronizationContext.Current != this.Context) { Console.WriteLine("Wywołanie metody nastąpiło z innego wątku. Nastąpi \"przekierowanie\" do poprawnego wątku"); this.Context.Post(delegate { ChangeSizeAndBackColor(size, color); }, null); return; } Console.WriteLine("Prawidłowy SynchronizationContext"); ChangeSizeAndBackColor(size, color); } public void ChangeSizeAndBackColor(Size size, Color color) { Console.WriteLine("Metoda ChangeBackgroundColor wywołana z wątku: " + Thread.CurrentThread.ManagedThreadId); form.Size = size; form.BackColor = color; Console.WriteLine(new string('-', 50)); } // Tu już mamy prawdziwy zawrót głowy :) public void DispatchAnything<T, T2, T3>(Action<T, T2, T3> function, T param1, T2 param2, T3 param3) { Console.WriteLine("Metoda DispatchAnything wywołana z wątku: " + Thread.CurrentThread.ManagedThreadId.ToString()); if (SynchronizationContext.Current != this.Context) { Console.WriteLine("Wywołanie metody nastąpiło z innego wątku. Nastąpi \"przekierowanie\" do poprawnego wątku"); this.Context.Post(delegate { function(param1, param2, param3); }, null); return; } Console.WriteLine("Prawidłowy SynchronizationContext"); function(param1, param2, param3); } public void ChangeLocationColorSize(Point point, Color color, Size size) { Console.WriteLine("Metoda ChangeLocationColorSize wywołana z wątku: " + Thread.CurrentThread.ManagedThreadId); form.SetDesktopLocation(point.X, point.Y); form.BackColor = color; form.Size = size; Console.WriteLine(new string('-', 50)); } } }

I jeszcze mały zrzut treści wyświetlanych w konsoli. Kolorowe okienka WinForms pominę milczeniem :)

Powodzenia!
 

windows porady programowanie

Komentarze

0 nowych
djfoxer   18 #1 05.04.2012 17:35

Ciekawe. Powiem szczerze, że chyba kiedyś z tego korzystałem (aczkolwiek nie założył bym się o nic), ale było to na zasadzie znajdź-zrób-zapomnij :) Jak coś wiem gdzie szukać, dzięki :)

djfoxer   18 #2 05.04.2012 17:49

Jak kiedyś można było programować bez internetu? ;) Żeby ogarnąć cały temat i znaleźć rozwiązania do wielu problemów, trzeba było mieć, albo kupę kasy na książki, albo pod bokiem jakąś nieźle wyposażoną bibliotekę techniczną. Dobrze, że to już przeszłość :)

alucosoftware   7 #3 05.04.2012 19:25

Na szczęście jestem młody wiekiem i z trudem przypominam sobie czasy bez Internetu... Czekaj, czekaj... tak! sztuczki z dialup'em i uziemieniem (tu: kaloryfer) jeszcze pamiętam...

Ech, na mojej półce tylko jedna pozycja: Microsoft Visual C# .NET Language Reference, 2002, Microsoft Corporation :)

Internet, w niektórych miejscach, to bardzo dobre źródło wiedzy (nawet programistycznej).

patryk9200   8 #4 05.04.2012 22:34

Dzięki wielkie! z pewnością się przyda, właśnie planowałem stworzenie aplikacji wielowątkowej. Jeśli chodzi o internet to muszę się zgodzić, chociaż trzeba wiedzieć co się szuka, i nie wybierać pierwszego lepszego przykładu ale zapoznać się z większą grupą rozwiązań. Natomiast z książek to polecam "Visual C# 2010" wydawnictwa Microsoft Press (jest polski przekład).

alucosoftware   7 #5 05.04.2012 23:42

@patryk9200
Nie ma za co. Jeśli dopiero zaczynasz swoich sił w programowaniu wielowątkowym to dokładnie przemyśl założenia projektu. Czasem lepszą drogą jest wykorzystanie implementacji BackgroundWorker, starych, dobrych asynchronicznych delegat bądź biblioteki Task Parallel Library (tu już wymagany .NET 4.0+).

TPL jest rekomendowany przez większość guru Microsoftu jako "optymalna droga", ale na razie jakoś nie mogę się przekonać...

  #6 06.04.2012 09:51

@alucosoftware
świetny wpis. do tej pory nie znałem tego sposobu. rzeczywiście w dokumentacji nie ma dużo na ten temat. teraz już będzie o wiele łatwiej, dzięki! fajnie też, że kilka sposobów pokazałeś :)

alucosoftware   7 #7 06.04.2012 11:33

@darekj
Bardzo dziękuję. Jak to podkreśliłem we wpisie jest to tylko szybka i "brudna" implementacja. Ale otwiera oczy na prezentowane zagadnienie :)

Wiele rzeczy można (i trzeba) poprawić np. zarządzanie wspólną listą referencji do poszczególnych obiektów na wątkach, zarządzanie wyjątkami. Dodatkowo, na etapie debugowania kodu, możesz zastąpić stałe wartości w łańcuchach znaków opisujące metody (np. Metoda X wywołana z ...) na bardziej niezależne, wykorzystując do tego celu mechanizm refleksji (StackFrame -> GetMethod). Ale jak wiadomo, mechanizm refleksji do najszybszych nie należy.

Kiedyś już napisałem, że najlepszym pomysłem jest takie tworzenie fragmentów oprogramowania, które szybko można wykorzystać w innych miejscach, aplikacjach itp. (ang. reusable). Szkoda życia na powtarzanie tych samych czynności.

W razie problemów służę pomocą.

patryk9200   8 #8 08.04.2012 00:07

@alucosoftware właśnie tak jak napisałeś, planuję użyć BackgroundWorker który znam. Jednak warto poszerzać horyzonty i poznawać inne rozwiązania. Dzięki za wpis.

alucosoftware   7 #9 08.04.2012 23:13

@patryk9200
Opisz pokrótce czym będzie się zajmował dodatkowy wątek to pomogę Tobie dobrać - moim zdaniem - "optymalną drogę" (czyt. wygodną) ;)

  #10 10.04.2012 11:38

Co za bezsensowna nazwa: wielo - UI - wątkowe. Czy autor chodził do szkoły i potrafi się normalnie wysławiać ? Można to nazwać po prostu: Obsługa interfejsu (lub UI) w środowisku wielowątkowym. Wiem że niektórzy chcą iść na skróty, ale nie za cenę chorych dziwactw.

alucosoftware   7 #11 10.04.2012 17:42

@smerf2
Tytuł przez Ciebie zaproponowany nie mówi wiele o przedstawionym w niniejszym wpisie zagadnieniu. Może masz problem ze zrozumieniem przesłania (podkreślam, że wiele wątków implementuje UI, każdy z osobna).

Autor chodził do szkoły.