Blog (22)
Komentarze (619)
Recenzje (0)

Programowanie wielo-UI-wątkowe

@alucosoftwareProgramowanie wielo-UI-wątkowe05.04.2012 12:48

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!

Wybrane dla Ciebie
Szanowna Użytkowniczko! Szanowny Użytkowniku!
×
Aby dalej móc dostarczać coraz lepsze materiały redakcyjne i udostępniać coraz lepsze usługi, potrzebujemy zgody na dopasowanie treści marketingowych do Twojego zachowania. Twoje dane są u nas bezpieczne, a zgodę możesz wycofać w każdej chwili na podstronie polityka prywatności.

Kliknij "PRZECHODZĘ DO SERWISU" lub na symbol "X" w górnym rogu tej planszy, jeżeli zgadzasz się na przetwarzanie przez Wirtualną Polskę i naszych Zaufanych Partnerów Twoich danych osobowych, zbieranych w ramach korzystania przez Ciebie z usług, portali i serwisów internetowych Wirtualnej Polski (w tym danych zapisywanych w plikach cookies) w celach marketingowych realizowanych na zlecenie naszych Zaufanych Partnerów. Jeśli nie zgadzasz się na przetwarzanie Twoich danych osobowych skorzystaj z ustawień w polityce prywatności. Zgoda jest dobrowolna i możesz ją w dowolnym momencie wycofać zmieniając ustawienia w polityce prywatności (w której znajdziesz odpowiedzi na wszystkie pytania związane z przetwarzaniem Twoich danych osobowych).

Od 25 maja 2018 roku obowiązuje Rozporządzenie Parlamentu Europejskiego i Rady (UE) 2016/679 (określane jako "RODO"). W związku z tym chcielibyśmy poinformować o przetwarzaniu Twoich danych oraz zasadach, na jakich odbywa się to po dniu 25 maja 2018 roku.

Kto będzie administratorem Twoich danych?

Administratorami Twoich danych będzie Wirtualna Polska Media Spółka Akcyjna z siedzibą w Warszawie, oraz pozostałe spółki z grupy Wirtualna Polska, jak również nasi Zaufani Partnerzy, z którymi stale współpracujemy. Szczegółowe informacje dotyczące administratorów znajdują się w polityce prywatności.

O jakich danych mówimy?

Chodzi o dane osobowe, które są zbierane w ramach korzystania przez Ciebie z naszych usług, portali i serwisów internetowych udostępnianych przez Wirtualną Polskę, w tym zapisywanych w plikach cookies, które są instalowane na naszych stronach przez Wirtualną Polskę oraz naszych Zaufanych Partnerów.

Dlaczego chcemy przetwarzać Twoje dane?

Przetwarzamy je dostarczać coraz lepsze materiały redakcyjne, dopasować ich tematykę do Twoich zainteresowań, tworzyć portale i serwisy internetowe, z których będziesz korzystać z przyjemnością, zapewniać większe bezpieczeństwo usług, udoskonalać nasze usługi i maksymalnie dopasować je do Twoich zainteresowań, pokazywać reklamy dopasowane do Twoich potrzeb. Szczegółowe informacje dotyczące celów przetwarzania Twoich danych znajdują się w polityce prywatności.

Komu możemy przekazać dane?

Twoje dane możemy przekazywać podmiotom przetwarzającym je na nasze zlecenie oraz podmiotom uprawnionym do uzyskania danych na podstawie obowiązującego prawa – oczywiście tylko, gdy wystąpią z żądaniem w oparciu o stosowną podstawę prawną.

Jakie masz prawa w stosunku do Twoich danych?

Masz prawo żądania dostępu, sprostowania, usunięcia lub ograniczenia przetwarzania danych. Możesz wycofać zgodę na przetwarzanie, zgłosić sprzeciw oraz skorzystać z innych praw wymienionych szczegółowo w polityce prywatności.

Jakie są podstawy prawne przetwarzania Twoich danych?

Podstawą prawną przetwarzania Twoich danych w celu świadczenia usług jest niezbędność do wykonania umów o ich świadczenie (tymi umowami są zazwyczaj regulaminy). Podstawą prawną przetwarzania danych w celu pomiarów statystycznych i marketingu własnego administratorów jest tzw. uzasadniony interes administratora. Przetwarzanie Twoich danych w celach marketingowych realizowanych przez Wirtualną Polskę na zlecenie Zaufanych Partnerów i bezpośrednio przez Zaufanych Partnerów będzie odbywać się na podstawie Twojej dobrowolnej zgody.