Wielowątkowość w C# – Threading, Locki + SpinLocki

Często zdarza się, że mamy potrzebę wykonania kilku rzeczy w tym samym czasie. Przykładowo, pijąc herbatę możemy równocześnie oglądać telewizję czy czytać książkę. Podobnie w programowaniu – podczas (przykładowo) wykonywania jakiejś dłuższej pętli nie chcemy zawieszać programu (na czas jej wykonania), tylko wykonać w tym samym czasie inne operacje.

W tym poście omówię pewną klasę i strukturę. Pierwsza z nich to Thread zawierający się w System.Threading; Pozwala on na wykonanie innej funkcji równolegle, nie naruszając działania kodu. Przykładowo:

using System;
using System.Threading;

namespace test
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread simpleThread1 = new Thread(new ThreadStart(doLoop));
            Thread simpleThread2 = new Thread(new ThreadStart(doLoop2));
            simpleThread1.Start();
            simpleThread2.Start();
            Console.ReadLine();
        }

        static void doLoop()
        {
            for (int i = 0; i != 10; i++)
            {
                Console.Write(i + " I am from thread doLoop!\n");
                Thread.Sleep(100);
            }
        }
        static void doLoop2()
        {
            for (int i = 0; i != 10; i++)
            {
                Console.Write(i + " I am from thread doLoop2!\n");
                Thread.Sleep(100);
            }
        }
    }
}

Daje wyjście:
Bez tytułu

Jak widać dwa wątki (doLoop() oraz doLoop2()) wykonywane są w tym samym czasie.

Przejdziemy teraz do synchronizacji wątków za pomocą funkcji lock. Funkcja ta gwarantuje, że jeden wątek nie przejdzie do sekcji krytycznej kodu, podczas gdy inny jest w „swojej” sekcji krytycznej. Co więcej, w przypadku pracy z klasami, locki powinny być prywatne lub chronione. Dlaczego? Niektóre operacje muszą być wykonywane po kolei, nie mogą być równoległe. Spójrzmy na ten kawałek kodu:

        private object toSync = new object();
        public void thr()
        {

            lock(toSync)
            {
                //sekcja krytyczna
            }

        }

…i ten:

        private object toSync = new object();
        public void thr()
        {
            System.Threading.Monitor.Enter(toSync);

            try
            {
                //sekcja krytyczna
            }
            finally
            {
                System.Threading.Monitor.Exit(toSync);
            }
        }

są równoważne. Funkcja lock została stworzona jako referencja do klasy Monitor oraz niweluje potrzebę korzystania z bloku try. Teraz – do czego jest to potrzebne? Aby wątki nie przeszkadzały sobie wzajemnie. Wykonanie kodu w różnych wątkach operujących na tej samej zmiennej w pamięci może przynieść niepożądane skutki, takie jak „wykrzaczanie się” programu, co jest potencjalnym punktem zapalnym do exploitacji.

 

Teraz przejdźmy do niskopoziomowej synchronizacji, która jest jednak rzadko używana (lock daje nam podobne możliwości bez zbędnych „bajerów”). Mówię tu o Spinlockach czyli w wolnym tłumaczeniu kręcących się blokadach. Sprawdzają one, czy wątek został zwolniony. SpinLocki używane są zazwyczaj tylko wtedy, gdy wydajność liczy się aż za bardzo. Są rodzajem pętli, która odpytuje, czy wątek jest zwolniony, i jeśli tak, wykonują kod. Jest to często szybsze, bo omija to serializację, nie ma potrzeby ingerowania w rejestry procesora. Trzeba mieć na uwadze to, że SpinLock nie jest klasą, lecz strukturą. Niżej podałem przykładowy kod z użyciem SpinLocka.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace test
{
    class Program
    {

        private static int _count;
        private static SpinLock _sLock = new SpinLock();

        static void Main(string[] args)
        {
            for (int i = 0; i != 10; i++)
            {
                var Thr = new Task(vInc);
                Thr.Start();
                Console.Write(_count + '\n');
            } 
            Console.ReadKey();
        }

        static void vInc()
        {
            bool lockTaken = false;

            try
            {
                _sLock.Enter(ref lockTaken);
                _count++;
            }
            finally
            {
                if (lockTaken) _sLock.Exit();
            }
        }
    }
}

Przy tym, należy zaznaczyć, że Task jest asynchroniczny, a SpinLock właśnie go synchronizuje. I co ważne, przed wejściem w SpinLocka flaga lockTaken powinna być ustawiona na False.

Wady SpinLocka? Wciąż aktywny, wykonuje cykle, „siedzi” na procesorze. W przypadku długich operacji lock jest duży lepszy od jego niskopoziomowego odpowiednika.

Advertisements

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Log Out / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Log Out / Zmień )

Facebook photo

Komentujesz korzystając z konta Facebook. Log Out / Zmień )

Google+ photo

Komentujesz korzystając z konta Google+. Log Out / Zmień )

Connecting to %s