Programowanie Współbieżne
2
Materiały
http://www.microsoft.com/express/download/
http://msdn.microsoft.com/en-us/library/aa645740(VS.71).aspx
http://www.albahari.com/threading/
http://www.centrumxp.pl/
4
6
Pierwsze kroki
Z zestawu narzędzi wybieramy przycisk
8
Pierwsze kroki
Pierwsze kroki
We właściwościach zmieniamy nazwę przycisku jego tekst
10
Pierwsze kroki
Po dwukrotnym kliknięciu otworzy nam się wygenerowany automatycznie kod programu do
Pierwsze kroki
Następnie uzupełniamy nasz kod jedną linią:
MessageBox
.Show("Hello world");
12
Wątki
Kiedy przydają nam się wątki?
Gdy chcemy by nasz program reagował gdy wykonuje w tle
jakieś „ciężkie” zadanie
Różnego rodzaju procesy – serwery. Podczas oczekiwania na
dane na jednym wątku, program może coś wykonywać na
innym.
Gdy mamy program, który wykonuje sporo obliczeń (np.
kompresja plików multimedialnych) i chcemy je w jakiś sposób
zrównoleglić. Efekt będzie odczuwalny gdy fizycznie będziemy
dysponować wieloma rdzeniami. Liczbę tę można sprawdzić za
pomocą
Environment.ProcessorCountWątki
Kiedy wątki mogą nam szkodzić?
●
Gdy będzie ich za dużo. Czas przełączania i alokacji zbyt
kosztowny,
●
Gdy zadanie wykonywane przez wątek będzie krócej trwało niż
powołanie danego wątku.
●
Gdy w pełni nie przewidzimy interakcji pomiędzy wątkami,
debugowanie jest bardzo kłopotliwe.
●
Gdy używamy dużo dysku, nie powinniśmy powoływać wiele
wątków, a raczej jeden, dwa i szeregować zadania odczytu i
zapisu. (ktoś próbował skopiować z płyty CD/DVD kilka plików
14
Wątki
●
Gdy operujemy na wątkach musimy dodać do programu
using System.Threading;
●
Każdy program ma przynajmniej jeden wątek, zwany wątkiem
głównym
●
Każdy wątek ma swój oddzielny stos więc zmienne lokalne są
modyfikowane niezależnie.
●
Zmienne globalne są współdzielone przez wątki (często
Wątki
Przykład uruchomienia metod działających jako wątki.
private void naszWatekBezParametrow() {
MessageBox.Show("wątek ZUPELNIE bez parametrów"); }
private void naszWatek(object o) {
MessageBox.Show("jestem sobie " +
Thread.CurrentThread.Name + "\nWiadomość to: " + (string)o);
}
private void naszWatek() {
16
Wątki
Przykład uruchomienia bez parametrów
Thread watek = new Thread(new
ThreadStart(naszWatekBezParametrow)); //Thread watek = new Thread(naszWatekBezParametrow);
//kompilator sam sobie resztę doda
//Thread watek = new Thread(new ThreadStart(naszWatek)); //Thread watek = new Thread(naszWatek); //Gdy mamy
przeciążone metody (bez i z parametrami kompilator nie wie czy zastosować ThreadStart czy ParameterizedThreadStart
watek.Start();
//jako wątek bez parametrów przy próbie Start(wiadomosc) zakończy się błędem podczas uruchomienia
//string wiadomosc = "jakaś wiadomość";
Wątki
Przykład uruchomienia z parametrami
private void naszWatekZParametrem(object wiadomosc) {
MessageBox.Show((string)wiadomosc); }
Thread watek = new Thread(new
ParameterizedThreadStart(naszWatekZParametrem)); //Thread watek = new Thread(naszWatekZParametrem); //Thread watek = new Thread(new
//ParameterizedThreadStart(naszWatek));
//watek.Start(); //Możliwy taki start ale nie będzie parametrów
18
Wątki
Przykład uruchomienia anonimowego
private void naszWatekZKonkretnymParametrem(string
wiadomosc) {
MessageBox.Show("Nasz wątek z konkretnym parametrem dostał wiadomość: \n" + wiadomosc);
}
string zmiennaWiadomosc;
zmiennaWiadomosc = "Wiadomość przed utworzeniem wątku"; Thread watek = new Thread(delegate()
{ naszWatekZKonkretnymParametrem(zmiennaWiadomosc); }); // zastosowanie anonimowej metody, nie musimy podawać
parametru object tylko możemy konkretnego typu np. string
zmiennaWiadomosc = "Wiadomość po utworzeniu wątka"; // Thread watek = new Thread(delegate()
{ MessageBox.Show("wiadomość 1");
MessageBox.Show("Wiadomość 2"); }); watek.Start();
Wątki
Przykład uruchomienia anonimowego z lambdą
private void button3b_Click(object sender, EventArgs e) {
Thread watek = new Thread((s) => { //kod wątku
MessageBox.Show(s as string); });
watek.Start("jakaś wiadomość będąca parametrem w wątku anonimowym wystartowanym z zapisem lambda");
20
Wątki
Przykład uruchomienia z obiektu
public class RozneWatki {
public void watek1() {
MessageBox.Show("jestem sobie wątek1"); }
public void watek2() {
MessageBox.Show("jestem sobie wątek2"); }
}
RozneWatki rozneWatki = new RozneWatki();
Thread watek1 = new Thread(rozneWatki.watek1); Thread watek2 = new Thread(rozneWatki.watek2); watek1.Start();
Wątki
Nazywanie wątków – pomoc w debugowaniu
Thread watek = new Thread(naszWatek);
watek.Name = "Ot taka nazwa";
private void naszWatek() {
MessageBox.Show(Thread.CurrentThread.Name); }
22
Wątki
Wątki pierwszoplanowe i w tle
Thread watek = new Thread(naszWatek);
watek.IsBackground = true; //gdy true zamknięcie
głównego zamyka też potomny watek.Start();
private void naszWatek() {
MessageBox.Show(Thread.CurrentThread.Name); }
Domyślnie IsBackground = false dlatego po zamknięciu głównej aplikacji nadal widzimy wątki z niej powstałe. Gdy ustawimy na true wyjście z wątka głównego powoduje natychmiastowe zakończenie wątków potomnych. Blok
finaly jest pomijany. Jest to sytuacja nie pożądana dlatego powinniśmy
poczekać na koniec wątków potomnych.
Wątki
Priorytetowość
enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }
● Oznacza jak dużo czasu procesora przyznane jest dla danego wątka w grupie
wątków jednego procesu.
● Ustawienie na Highest wcale nie oznacza, że będzie to wątek czasu
rzeczywistego. Trzeba by było również ustawić priorytet dla procesu.
Process.GetCurrentProcess().PriorityClass =
ProcessPriorityClass.High;
● Jest jeszcze wyższy priorytet Realtime, wtedy nasz proces będzie działał
nieprzerwanie, jednak gdy wejdzie w pętle nieskonczoną nie odzyskamy kontroli nad systemem.
24
Wątki
Wyjątki
Try { watek.Start(); } catch {MessageBox.Show("błąd przy uruchamianiu"); }
● Takie uruchomienie zwróci nam jedynie wyjątek przy uruchamianiu, nie
przechwycimy wyjątku "rzuconego" w wątku.
● Wyjątki z wątków mogą zakończyć aplikację, trzeba je przechwytywać na
Synchronizacja
Blokowanie
● Procesy zablokowane z powodu oczekiwania na jakieś zdarzenie, np.
Sleep, Join, lock, Semaphore itp.
Natychmiastowo zrzekają się czasu procesora, dodają WaitSleepJoin do właściwości ThreadState i nie kolejkują się do czasu odblokowania.
● Odblokowanie może nastąpić z 4 przyczyn:
– Warunek odblokowania został spełniony – Minął timeout
– Został przerwany przez Thread.Interrupt – Zosatł przerwany przez Thread.Abort
26
Synchronizacja
Oczekiwanie Sleep i SpinWait
static void Main() {
Thread.Sleep (0); // zrzeknięcie się przydzielonego kwantu czasowego
Thread.Sleep (1000); // uśpij na 1000 ms
Thread.Sleep (TimeSpan.FromHours (1)); // uśpij na 1 godzinę
Thread.Sleep (Timeout.Infinite); // śpij wiecznie :) czyli do czasu przerwania.
}
● Ogólnie Sleep powoduje rezygnację wątku z czasu procesora. Wątek taki nie
jest kolejkowany przez podany czas.
Thread.SpinWait (100); // nic nie rób przez 100 cykli
● Wątek nie rezygnuje z procesora, jednak wykonuje na nim puste operacje. Nie
jest w stanie WaitSleepJoin i nie może być przerwany przez Interrupt. Można zastosować gdy chcemy czekać bardzo krótko.
Synchronizacja
Oczekiwanie Join
Thread watek1 = new Thread(new
ParameterizedThreadStart(naszWatekZParametrem)); watek1.Start("Wątek z Join.");
watek1.Join();
● Czekamy na zakończenie watku. Mechanizm, zbierania komunikatów nie jest
zatrzymany, więc jak klikniemy jakiś guzik w wątku głównym to ostatecznie doczekamy się reakcji.
28
Sekcja krytyczna
Kilka wątków (n_wątków) robi to samo zadanie:for (int ii = 0; ii < 1000000; ii++) {
{
licznik++; }
}
Sekcja krytyczna
lockprivate object blokowacz = new object(); ...
for (int ii = 0; ii < 1000000; ii++) { lock (blokowacz) { licznik++; } }
● W danym momencie tylko jeden wątek, może przebywać w chronionym
obszarze inne będą czekały w kolejce FIFO
30
Sekcja krytyczna
Wybór obiektu który będzie blokował● Musi to być typ referencyjny
● Zwykle jest związany z obiektami na których działamy np.: class Bezpieczna {
List <string> list = new List <string>();
void Test() {
lock (list) {
list.Add ("Item 1"); ...
● Powinniśmy stosować obiekty które są private by uniknąć
niezamierzonej interakcji z zewnątrz
● Z tego samego powodu nie powinniśmy stosować np. lock (this){}
lub lock (typeof (Widget)) { ... }
● Użycie obiektu do zablokowania fragmentu kodu nie powoduje
automatycznie blokowania danego obiektu.
lock(list1) {
Sekcja krytyczna
Monitor – rozwinięcie lock●Lock faktycznie jest skrótem składniowym czegoś takiego:
Monitor.Enter(blokowacz); try { licznik++; } finally { Monitor.Exit(blokowacz); }
●Wywołanie Monitor.Exit bez uprzedniego Monitor.Enter spowoduje
rzucenie wyjątku
32
Sekcja krytyczna
Interlocked – operacje atomowe for (int ii = 0; ii < 1000000; ii++)
{
Interlocked.Increment(ref licznik); }
●Dodatkowo mamy do dyspozycji
– Add Dodadawanie do dwóch liczb
– CompareExchange porównanie i ewentualna podmiana – Decrement zmniejszenie
– Equals czy równe – Exchange Zamiana – Read odczyt liczby 64b
Sekcja krytyczna
Blokowanie zagnieżdżonestatic object x = new object();
static void Main() {
lock (x) {
Console.WriteLine ("Zablokowałem"); Nest();
Console.WriteLine ("Odblokowałem"); }
//Tutaj odblokowane zupełnie
}
static void Nest() {
lock (x) {
…//Tu podwójny lock
}
//Tu odblokowane tylko ostatnie zagnieżdżenie
34
Sekcja krytyczna
● Kiedy blokować
– Wszędzie tam, gdzie wiele wątków może mieć dostęp do wspólnych
zmiennych
– Wszędzie tam, gdzie chcemy mieć niepodzielność operacji, np.
sprawdzenie warunku i wykonanie czegoś
● Na co uważać
– Nie powinniśmy zbyt dużo blokować bo ciężko analizować taki kod a
łatwo spowodować DeadLock
– Zbyt duże fragmenty kodu wykonywane przez pojedynczy proces
Przerwanie wątku
● Thread.Interrupt – przerywa bieżące czekanie i powoduje rzucenie
wyjątku ThreadInterruptedException
private void watekNieskonczony() {
try {
Thread.Sleep(Timeout.Infinite); }
catch (ThreadInterruptedException ex) {
MessageBox.Show("przechwycony wyjątek:"+ex.Message); }
MessageBox.Show("A tu koniec"); }
● Należy pamiętać, że przerywanie w ten sposób może być niebezpieczne,
36
Przerwanie wątku
● Thread.Abort – Działa podobnie jak Interrupt z tą różnicą, że rzuca
wyjątkiem ThreadAbortException oraz wyjątek jest ponownie rzucany pod koniec bloku catch, chyba że w bloku catch zastosujemy
Thread.ResetAbort();
● Działanie jest podobne, jednak w przypadku Interrupt wątek przerywany
jest tylko w momencie czekania, Abort może tego dokonać w dowolnym miejscu wykonywania, nawet w nienaszym kodzie.
Stany wątku
● ThreadState – kombinacja bitowa trzech warstw.
● Uruchomienie, blokada, przerwanie wątku (Unstarted, Running,
WaitSleepJoin, Stopped, AbortRequested)
● Pierwszoplanowość i drugoplanowość wątku (Background,
Foreground)
● Postęp w zawieszeniu wątku (SuspendRequested, Suspended )
używane przez przestarzałe metody
● Ostateczny stan wątku określa się przez sumę bitową tych trzech „Warstw”. I
tak, może być np wątek
Background, Unstarted lub
38
Stany wątku
● W enumeracji ThreadState są też nigdy nie używane dwa stany: StopRequested i Aborted
● By jeszcze bardziej skomplikować, Running ma wartość 0
więc porównanie
if ((t.ThreadState & ThreadState.Running) > 0)... nic nam nie da
● Można się wspomóc IsAlive jednak zwraca false tylko przed startem i gdy się
zakończy. Gdy jest zablokowany też jest true.
● Najlepiej napisać sobie swoją metodę:
public static ThreadState SimpleThreadState (ThreadState ts) {
return ts & (ThreadState.Aborted |
ThreadState.AbortRequested |
ThreadState.Stopped |
ThreadState.Unstarted |
Thr eadState.WaitSleepJoin); }
Stany wątku
Unstarted WaitSleepJoin Abort Requested Running Stopped Aborted Start Wątekblokowany odblokowanyWątek
Abort Zakończenie wątku Zakończenie wątku Teoretycznie Zakończenie wątku ResetAbort Abort
40
Wait Handles
● Win32 Api dostarcza trzech klas
– EventWaitHandle – Mutex
– Semaphore
● Wszystkie 3 bazują na abstrakcyjnej klasie WaitHandle ● EventWaitHandle ma dwie podklasy
– AutoResetEvent – ManualResetEvent
● Różnią się one tylko sposobem wywołania konstruktora.
● WaitHandles pozwalają na nazwanie klas i używanie pomiędzy odrębnymi
Wait Handles
AutoResetEvent● Można porównać do bramki która przepuszcza tylko jeden proces za
naciśnięciem jednego guzika.
● Gdy bramka jest otwarta proces lub wątek który wywoła metodę WaitOne()
przechodzi przez bramkę jednocześnie ją zamykając
● Gdy bramka jest zamknięta proces ustawia się w kolejce.
● Każdy inny nie zablokowany proces może odblokować bramkę za pomocą
wywołania metody Set()
● Jedno wywołanie Set() wpuści tylko jeden proces.
● Gdy nie będzie procesów w kolejce, Set() otworzy bramkę ● Gdy bramka jest już otwarta, następne Set() są ignorowane.
42
Wait Handles
AutoResetEventEventWaitHandle czekaczka = new EventWaitHandle (false, EventResetMode.Auto);
EventWaitHandle czekaczka = new AutoResetEvent (false); Powyższe dwa wywołania są równoważne.
Pierwszy parametr określa czy bramka ma być podczas utworzenia otwarta.
Wait Handles
EventWaitHandle - międzyprocesowe
EventWaitHandle czekaczka = new EventWaitHandle (false,
EventResetMode.Auto,"Nasza nazwa czekaczki");
● Trzecim parametrem może być nazwa widziana przez wszystkie inne
procesy w systemie.
● Gdy podczas tworzenia okaże sie że obiekt o podanej nazwie istnieje
dostaniemy tylko referencję a czwarty parametr będzie false;
EventWaitHandle (false, EventResetMode.Auto,"Nasza nazwa czekaczki", out czyNowy);
44
Ready Go
Załóżmy, że mamy taki scenariusz● Główny proces ma co chwilę nowe zadania do wykonania ● Zadania te mają być wykonane przez wątek
● Za każdym razem uruchamiany jest nowy wątek ● Przekazywane jest zadanie
● Po wykonaniu pracy wątek jest kończony
By zmniejszyć obciążenie wynikające z tworzenia wątków (czy nawet innych procesów) możemy postępować według poniższego algorytmu:
● Główny proces tworzy wątek ● Wątek czeka na zadanie
● Wykonuje zadanie
Ready Go
Najprostsza wersja producenta i konsumenta
static EventWaitHandle ready = new AutoResetEvent(false); static EventWaitHandle go = new AutoResetEvent(false); static volatile string zadanie;
static void Main(string[] args) {
new Thread(Konsument).Start();
for (int i = 1; i <= 5; i++) //przekaż 5 razy zadanie
{
ready.WaitOne(); // Czekamy na gotowość konsumenta
zadanie = "a".PadRight(i, 'a'); // przygotowujemy zadanie
go.Set(); // mówimy że dane gotowe do odbioru
}
ready.WaitOne(); zadanie = null; go.Set(); // każemy skończyć
Console.ReadKey(); }
static void Konsument() {
while (true) {
ready.Set(); // Informujemy producenta że jesteśmy gotowi
46
Kolejka producent - konsument
● Wykorzystanie procesu drugoplanowego ● Producent kolejkuje elementy
● Konsument dekolejkuje elementy
● Rozwiązanie podobne do poprzedniego tylko nie blokujące class ProducentKonsument : Idisposable
{
EventWaitHandle czekaczka = new AutoResetEvent(false); Thread konsumentWatek;
Queue<string> kolejka = new Queue<string>(); public ProducentKonsument()
{
konsumentWatek = new Thread(konsument); konsumentWatek.Name = "konsumentWatek"; konsumentWatek.Start();
Kolejka producent - konsument
void konsument() {
while (true) {
string mesg = null; lock (kolejka)
if (kolejka.Count > 0) {
mesg = kolejka.Dequeue(); if (mesg == null) return; }
if (mesg != null) {
Console.WriteLine("odebralem: " + mesg); Thread.Sleep(1000);
} else
{
Console.WriteLine("no to czekam..."); // Jeżeli nie ma więcej zadań to czekaj
48
Kolejka producent - konsument
public void zakolejkuj(string mesg) { lock (kolejka) { kolejka.Enqueue(mesg); } czekaczka.Set(); }
public void Dispose() { zakolejkuj(null); konsumentWatek.Join(); czekaczka.Close(); } }
Wait Handles
ManualResetEventEventWaitHandle czekaczka = new EventWaitHandle (false,
EventResetMode.Manual);
EventWaitHandle czekaczka = new ManualResetEvent (false);
● Powyższe dwa wywołania są równoważne.
● Pierwszy parametr określa czy bramka ma być podczas utworzenia otwarta. ● Metoda Set wpuszcza wszystkich czekających lub wołających WaitOne
50
Wait Handles
Mutex● Działa tak samo jak lock z tym że może być używany pomiędzy procesami i
jest około 100 razy wolniejszy (przy założeniu że nie blokujemy)
● Tak samo jak lock zapewnia wyłączny dostęp do bloku programu pomiędzy
wywołaniem WaitOne a ReleaseMutex i musi być wywołany z tego samego wątka.
● Zaletą jest automatyczne zwolnienie mutexa nawet gdy aplikacja się
Wait Handles
Mutex
static Mutex mutex = new Mutex(false, "tu.kielce.pl mutex");
private void naszWatekZMutex(object o) {
for (int ii = 0; ii < 1000000; ii++) { mutex.WaitOne(); licznik++; mutex.ReleaseMutex(); } }
52
Wait Handles
Semaphore
Semafor jest jak licznik, który nigdy nie może być mniejszy od 0.
Operacja
WaitOne
zmniejsza ten licznik o 1, jeżeli jest 0 to dany
wątek czeka, aż inny zwiększy za pomocą
Release
.
W przypadku semafora, podnieść go może każdy inny wątek, nie
tylko ten który go opuścił, tak jak to jest w przypadku lock czy
Mutex.
Wait Handles
Semaphore
static Semaphore semafor = new Semaphore(1, 1);
private void naszWatekZSemaphore(object o) {
for (int ii = 0; ii < 1000000; ii++) { semafor.WaitOne(); licznik++; semafor.Release(); } }
54
Wait Handles
Wait, wait, wait...
WaitHandle
.SignalAndWait – Jednoczesne wysłanie
sygnału i czekanie. Można w ten sposób zrealizować np.
spotkania.
private
static
EventWaitHandle
wh1 =
new
EventWaitHandle
(
false
,
EventResetMode
.AutoReset);
private
static
EventWaitHandle
wh2 =
new
EventWaitHandle
(
false
,
EventResetMode
.AutoReset);
Jeden z wątków wywołuje:
WaitHandle
.SignalAndWait(wh1, wh2);
Drugi z wątków wywołuje:
Wait Handles
Wait, wait, wait...
WaitHandle
.WaitAll(WaitHandle[] waitHandles)-
Czekaj na pozwolenie od wszystkich z waitHandles
WaitHandle
.WaitAny(WaitHandle[] waitHandles)
56
Bariera
Bariera jest stosowana do synchronizacji pracy wątków w
pewnych etapach. Przykładowo w algorytmach genetycznych
gdzie czekamy, aż wszystkie wątki zakończą pracę w danej
iteracji. Poniżej niezsynchronizowane praca kilku wątków.
static void printString(string inputstring) {
ThreadStart watek = () => {
for (int i = 0; i < inputstring.Length; i++)
{
Console.Write(inputstring.ToArray()[i]); }
};
Thread[] watki = new Thread[liczbaWatkow];
for (int i = 0; i<liczbaWatkow; ++i) {
watki[i] = new Thread(watek); watki[i].Start();
}
//Tu poczekamy aż zakończą się wszystkie uruchomione w tej metodzie wątki for (int i = 0; i < liczbaWatkow; ++i)
{
watki[i].Join(); }
}
Bariera
Próba zastosowania Monitora
static void printStringWaitPulse(string inputstring) {
object o = new object();
int licznikWywolan = 0;
ThreadStart watek = () => {
for (int i = 0; i < inputstring.Length; i++) {
lock (o) { Console.Write(inputstring.ToArray()[i]); licznikWywolan++; if (licznikWywolan < liczbaWatkow) { Monitor.Wait(o); } else {
Monitor.PulseAll(o); //Zasygnalizuj wszystkim że działamy dalej
licznikWywolan = 0; }
} }
};
Thread[] watki = new Thread[liczbaWatkow];
for (int i = 0; i < liczbaWatkow; ++i) { watki[i] = new Thread(watek);
58
Bariera
Zastosowanie Barrier oraz CountdownEvent
static System.Threading.Barrier bariera = new
System.Threading.Barrier(liczbaWatkow, (b) => { Console.WriteLine(" Bariera w fazie: {0}", b.CurrentPhaseNumber); });
static void printStringBarrier(string inputstring) {
ThreadStart watek = () => {
for (int i = 0; i < inputstring.Length; i++) {
Console.Write(inputstring.ToArray()[i]); bariera.SignalAndWait();
}
ce.Signal();//gdy zadanie jest wykonane zgłaszamy to by obniżyć licznik
};
Thread[] watki = new Thread[liczbaWatkow]; for (int i = 0; i < liczbaWatkow; ++i)
{
watki[i] = new Thread(watek); watki[i].Start();
}
//Tu poczekamy aż licznik dotrze do 0 (wszystkie wątki wywołają ce.Signal())
ce.Wait();
Kolekcje
Dodawanie do listy przez wiele wątków
List<Thread> watki = new List<Thread>();
List<int> liczby = new List<int>(10000); for (int i = 0;i<100;i++)
{
var watek = new Thread(() => { for (int l = 0; l<100; l++) liczby.Add(i*l); }); watki.Add(watek); watek.Start(); }
foreach (var watek in watki) {
watek.Join(); }
60
Kolekcje
Zastosowanie ConcurrentBag
Przykład: Kolekcje
ConcurrentBag<int> bag = new ConcurrentBag<int>(); List<Thread> watki2 = new List<Thread>();
for (int i = 0; i < 100; i++) {
var watek = new Thread(() => { for (int l = 0; l < 100; l++) bag.Add(i * l); }); watki2.Add(watek); watek.Start(); }
foreach (var watek in watki2) {
watek.Join(); }
Console.WriteLine($"Liczba elementów w concurrent bag:
Kolekcje
Problem z unikalnymi wartościami
ConcurrentBag<int> bag2 = new ConcurrentBag<int>(); List<Thread> watki3 = new List<Thread>(); for (int i = 0; i < 100; i++)
{
var watek = new Thread(() => {
for (int l = 0; l < 100; l++) {
if (!bag2.Any(x => x == l)) //po sprawdzeniu tego warunku może nastąpić niekorzystny przeplot.
bag2.Add(l); } }); watki3.Add(watek); watek.Start(); }
foreach (var watek in watki3) {
62
Kolekcje
Zastosowanie ConcurrentDictionary
Przykład: Kolekcje
var dictionary = new ConcurrentDictionary<int, object>(); //sztuczka by klucz był wartością a wartość może być null
List<Thread> watki4 = new List<Thread>(); for (int i = 0; i < 100; i++)
{
var watek = new Thread(() => { for (int l = 0; l < 100; l++) { dictionary.TryAdd(l, null); } }); watki4.Add(watek); watek.Start(); }
foreach (var watek in watki4) {
watek.Join(); }
Console.WriteLine($"Liczba elementów w ConcurrentDictionary unikalnych: {dictionary.Count}");
Kolekcje
Zastosowanie BlockingCollection
var blockingCollection = new BlockingCollection<int>();
var producent = new Thread(() => { for (int l = 0; l < 100; l++) { blockingCollection.TryAdd(l); Thread.Sleep(10); } blockingCollection.CompleteAdding(); });
var konsument = new Thread(() => {
for (int l = 0; l < 100; l++) {
blockingCollection.TryTake(out var result2); //co się stanie gdy użyjemy funkcji nieblokującej?
// var result2 = blockingCollection.Take( ); //a co gdy będzie to funkcja blokująca
64
Wait Handles
ContextBoundObject
Automatyczne blokowanie wywołań metod z jednej instancji
klasy.
using
System.Runtime.Remoting.Contexts;
[
Synchronization
]
public class
JakasKlasa
:
ContextBoundObject
{
...
}
CLR (Common Language Runtime) zapewnia, że tylko jeden
wątek może wywołać kod tej samej instancji obiektu w tym
samym czasie. Sztuczka polega na tym, że podczas
tworzenia obiektu klasy
JakasKlasa
tworzony jest obiekt
proxy, przez którego przechodzą wywołania metod klasy
Wait Handles
ContextBoundObject
Automatyczna synchronizacja nie może być stosowana do
pól protect static ani klas wywodzących się od
ContextBoundObject np. Windows Form
Trzeba też pamiętać, że nadal nie rozwiązuje nam to
problemu gdy wywołamy dla kolekcji coś takiego:
BezpiecznaKlasa
bezpieka =
new
BezpiecznaKlasa
();
...
66
Wait Handles
ContextBoundObject
Jeżeli z bezpiecznego obiektu tworzony jest kolejny obiekt to
automatycznie jest on też bezpieczny w tym samym kontekscie,
chyba, że postanowimy inaczej za pomocą atrybutów.
[Synchronization (SynchronizationAttribute.REQUIRES_NEW)]
public class JakasKlasaB : ContextBoundObject { ...
NOT_SUPPORTED
- równoważne z nieużywaniem SynchronizedSUPPORTED
- dołącz do istniejącego kontekstu synchronizacji jeżeli jest stworzony z innego obiektu, w innym przypadku będzie niesynchronizowanyREQUIRED
- (domyślny) dołącz do istniejącego kontekstu synchronizacji jeżeli jest stworzony z innego obiektu, w innym przypadku stwórz swój nowy kontekst synchronizacjiDelegaty i zdarzenia
Obiektowy odpowiednik wskaźnika do funkcji z c/c++
klasa pochodną z klasy System.Delegate
Deklaracja wygląda jak deklaracja funkcji:
delegate void PrzykladowyDelegat(); //deklaracja delegatu która jest równoważna z deklaracją klasy
By wykorzystać delegat musimy stworzyć nowy obiekt tej klasy.
delegate ddd = new PrzykladowyDelegat(jakasFunkcja);
jakasFunkcja musi być tego samego typu co delegat.
Funkcjonalnie zachowuje się jak klasy wewnętrzne w javie z tym
że w javie trzeba było tworzyć całą klasę tu tylko metodę.
68
Delegaty i zdarzenia
//PrzykladDelegate
class Program
{
delegate void JakisDelegat();
// metoda zgodna z deklaracją delegatu
static void jakasFunkcja() {
System.Console.WriteLine("Jakas Funkcja!? Przecieże to był delegat!");
}
static void Main(string[] args) {
JakisDelegat ddd = new
JakisDelegat(jakasFunkcja);
// w tym miejscu delegujemy wywołanie metody jakasFunkcja()
ddd();
Console.ReadLine(); }
Delegaty i zdarzenia
Zdarzenia (events) są formą komunikacji informowania innych, że
wystąpiła jakaś sytuacja.
Realizowane są przy pomocy delegatów. Przykład eventa wraz
argumentami:
public delegate void TikTakEventHandler(object sender,
TikTakEventArgs e);
public event TikTakEventHandler cykEvent;
public class TikTakEventArgs : EventArgs
{
public int ktoreCykniecie; public bool czyWiecznie; }
70
Delegaty i zdarzenia
Zasygnalizowanie zdarzenia:
TikTakEventArgs
tta =
new
TikTakEventArgs
();
tta.czyWiecznie =
true
;
for
(
int
ii = 0;ii<ileRazy;ii++)
{
tta.ktoreCykniecie = ii;
if
(cykEvent!=
null
)
cykEvent(
this
,tta);
Thread
.Sleep(1000);
}
WinForms i wątki
Cross-Thread
W wielowątkowej aplikacji WinForms nie można używać metod ani
pól kontrolek, które nie zostały stworzone w danym wątku.
Gdy chcemy zapisać coś na formatce z innego wątku
private void niebezpieczneLiczydlo() {
for (int liczba = 0; liczba < 20; liczba++) labelLicznik.Text = liczba.ToString(); }
...
Thread watekLicz = new Thread(niebezpieczneLiczydlo); watekLicz.Start();
72
WinForms i wątki
Komponenty WinForms zawierają własność
Control.InvokeRequired – oznaczającą, że dane przypisanie
wartości należy zlecić do wątka będącego twórcą kontrolki.
W tym celu należy użyć metody Control.Invoke wskazując delegata
z odpowiednią metodą.
WinForms i wątki
private void liczydloBezpieczne()
{ZapiszTekst zapDel = new ZapiszTekst(zapiszTekst);
for (int liczba = 0; liczba < 20; liczba++) {
if (this.labelLicznik.InvokeRequired) {
// jest w innym wątku więc wymaga
//wywołuje delegata zapDel przekazując mu tablicę //parametrów w tym przypadku jeden parametr
this.Invoke(zapDel, new object[] {liczba.ToString()});
} else
{
// Gdy jest w tym samym wątku
this.labelLicznik.Text = liczba.ToString(); }
74
WinForms i wątki
Bardziej eleganckim rozwiązaniem będzie stworzenie klasy
zawierającej bezpieczne metody:
static class ThreadSafeCalls
{
private delegate void SetLabelDelegate(Label label, string tekst);
public static void SetLabel(Label label, string tekst) {
//sprawdzanie czy wywołanie jest niebezpieczne //(spoza wątku który utworzył tę kontrolkę)
if (label.InvokeRequired) {
// jest w innym wątku więc wymaga
// wywołuje delegata zapDel przekazując mu tablicę parametrów // w tym przypadku jeden parametr
label.Invoke(new SetLabelDelegate(SetLabel), new object[] { label, tekst });
//przekazanie do wątku będącego właścicielem kontrolki, żądania jej zmiany
} else
{
// Gdy jest w tym samym wątku
label.Text = tekst; }
} s}
WinForms i wątki
Ustawienie tekst etykietki będzie prostrze i czytelniejsze:
private void liczydloBezpieczne2() {
for (int liczba = 0; liczba < 21; liczba++) {
ThreadSafeCalls.SetLabel(labelLicznik,liczba.ToString()); Thread.Sleep(500);
} }
76
WinForms i wątki
Kolejna metoda to napisanie zmodyfikowanej klasy komponentu:
public class LabelThreadSafe : Label {
public new string Text {
get
{
return base.Text; }
set
{
if (this.InvokeRequired) {
this.Invoke(new Action(() => { this.Text = value; })); }
else
{
base.Text = value; }
} }
}
WinForms i wątki
Umieszczamy swój komponent na formatce i normalnie używamy.
private void liczydloBezpieczne3()
{
for (int liczba = 0; liczba < 21; liczba++) {
labelThreadSafeLicznik.Text = liczba.ToString(); Thread.Sleep(500);
} }
78
WinForms i wątki
Kontekst synchronizacji
●
W .Net wątki mogą posiadać kontekst synchronizacji. Są to obiekt
klasy SynchronizationContext. Można sprawdzić badając
statyczną własność SynchronizationContext.Current
●
Aplikacje desktopowe mają tworzony automatycznie kontekst
synchronizacji dla wątków interfejsu użytkownika.
–
WindowsFormsSynchronizationContext (WinForms),
–DispatcherSynchronizationContext (WPF),
–
AspNetSynchronizationContext (ASP.NET)
●
Możliwe jest przekazanie kontekstu przez referencję do innego
wątku.
●
Wątek, który otrzymał kontekst synchronizacji może wywołać metody
lub wyrażenia lambda w wątku z którego kontekst pochodzi
WinForms i wątki
Kontekst synchronizacji
●
Mechanizm ten, można użyć do zmiany własności kontrolek wątku
okna przez inne wątki.
●
Kontekst posiada dwie metody do uruchamiania kodu w wątku
właściciela kontekstu
–
Send – podobna do Invoke, blokująca
–
Post – metoda asynchroniczna podobna do
BeginInvoke/EndInvoke
●
Powyższe metody asynchrocniczne nie przekazują wyjątku „na
zewnątrz”,
●
Metoda Send w WPF też nie przekaże wyjątku
80
Background Worker
●
Jest pomocną klasą z System.ComponentModel, która
dostarcza nam następującej funkcjonalności
–
Flaga cancel do zasygnalizowania końca, zamiast
Abort
–
Standardowy protokół do do raportowania postępu,
zakończenia i przerwania pracy
–
Implementacja IComponent pozwalająca na
umieszczenie w VS Designerze.
–
Łapanie wyjątków w wątku workera
–
Możliwość zapisywania postępu bezpośrednio na
formatkę w innym wątku (nie ma problemu z
wywołaniem Cross-thread), nie musimy używać
Background Worker
●
Model tego typu wykorzystuje identyczną składnię, jak
asynchroniczne delegaty
●
Aby użyć BacgroundWorkera wystarczy poinformować go
obsługując zdarzenie DoWork jaka metoda ma być
wykonana w tle i wywołać RunWorkerAsync()
●
Wątek główny kontynuuje działanie, a w tle wykonywana
jest funkcja zgłoszona do BackgroundWorkera.
●
BW sygnalizuje postęp prac za pomocą zdarzenia
ProgressChanged – w jego obsłudze można
aktualizować np. ProgressBar
82
Background Worker
●Zróbmy sobie formatkę jak niżej:
●
Pola nazwane będą textBoxCzas oraz textBoxIlosc
●Guziki – buttonStart i buttonCancel
●
ProgressBar - progressBarLiczydlo
●
Dodajemy backgroundWorkera przeciągając go z toolsów
Background Worker
●
Dwukrotnie klikając w BW otworzy nam się kod wykonywany
podczas zdarzenia DoWork
●
Uzupełniamy go
private void backgroundWorkerLiczydlo_DoWork(object sender, DoWorkEventArgs e) {
//uzyskaj obiekt wejsciowy
ParametryDwa parametry = (ParametryDwa)e.Argument;
for (int i = 0; i < parametry.iloscIteracji; i++) {
Console.WriteLine("właśnie wykonuję iterację " + i); System.Threading.Thread.Sleep(parametry.czasUspienia); backgroundWorkerLiczydlo.ReportProgress((i + 1)*100 /parametry.iloscIteracji); if (backgroundWorkerLiczydlo.CancellationPending) { e.Cancel = true; return; } } Uwaga: 100/parametry.iloscIteracji*(i+1) nie zadziała dla większej liczby iteracji niż 100
84
Background Worker
●
Klasa parametry:
class ParametryDwa {
public int czasUspienia,iloscIteracji;
public ParametryDwa(int _czasUspienia, int _iloscIteracji) {
czasUspienia = _czasUspienia; iloscIteracji = _iloscIteracji; }
Background Worker
●
Dwukrotnie klikając w buttonStart otwiera nam się kod
wykonany po kliknięciu w guzik (do uzupełnienia):
private void buttonStart_Click(object sender, EventArgs e) {
try
{
//uzyskaj dane z formatki
int czas = int.Parse(textBoxCzas.Text); int ilosc = int.Parse(textBoxIlosc.Text); //umieść to w obiekcie do wysyłki
ParametryDwa param = new ParametryDwa(czas, ilosc); backgroundWorkerLiczydlo.RunWorkerAsync(param);
}
catch (Exception ex) {
MessageBox.Show(ex.Message); }
86
Background Worker
●
Gdy chcemy się dowiedzieć o końcu zadania i przekazać jakieś
parametry wystarczy obsłużyć zdarzenie:
RunWorkerCompleted .
private void backgroundWorkerLiczydlo_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
MessageBox.Show("przerwano"); else if (e.Error != null)
MessageBox.Show("Błąd: " + e.Error.ToString()); else
MessageBox.Show("Praca została wykonana\n" + e.Result); }
Background Worker
●
Jeszcze tylko obsługa eventów kliknięcia cancel oraz zmiana
statusu progress bar
private void backgroundWorkerLiczydlo_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBarLiczydlo.Value = e.ProgressPercentage; }
private void buttonCancel_Click(object sender, EventArgs e) {
backgroundWorkerLiczydlo.CancelAsync(); }
88
Wątki w WPF
●
Jeżeli w aplikacji WPF chcemy zmienić stan kontrolki spoza
wątku, który ją utworzył także będziemy mieli problem
podobnie jak z aplikacją Win Forms:
Przykład: WPFThread
private void niebezpieczneLiczydlo() {
for (int liczba = 0; liczba < 21; liczba++) {
labelLicznik.Content = liczba.ToString(); Thread.Sleep(200);
} }
private void buttonStart_Click(object sender, RoutedEventArgs e) {
Thread watekLicz = new Thread(niebezpieczneLiczydlo); watekLicz.Start();
Wątki w WPF
●
Druga wersja już poprawna:
delegate void ZapiszTekst(string tekst);
private void zapiszTekst(string tekst) {
labelLicznik.Content = tekst; }
private void liczydloBezpieczne() {
ZapiszTekst zapDel = new ZapiszTekst(zapiszTekst); for (int liczba = 0; liczba < 21; liczba++)
{
if (!labelLicznik.Dispatcher.CheckAccess())
{
labelLicznik.Dispatcher.Invoke(zapDel, new object[] { liczba.ToString() });
} else
90
Wątki w WPF
●
Komponenty wizualne kontrolowane są przez wątek okna
aplikacji. Wszelkie zmiany zgłaszane są jako żądania z
innych wątków do wątku kontrolującego za pośrednictwem
obiektu Dispatcher, po wcześniejszym sprawdzeniu za
pomocą CheckAccess.
●
Metodę Invoke można przeciążyć podając priorytet wywołania
label.Dispatcher.Invoke(new Action(() => { label.Content = content; }
),System.Windows.Threading.DispatcherPriority.ApplicationIdle);
●
Jest też możliwość zgłaszania, żądań asynchronicznie z
pomocą BeginInvoke.
Wątki w WPF
●
Trzecia wersja poprawna, osobna klasa statyczna z
bezpiecznymi metodami
class ThreadSafeCallsWPF {
public static void setLabelContent(Label label, object content) {
if (!label.Dispatcher.CheckAccess())
{
label.Dispatcher.Invoke(new Action(() => { label.Content = content; })); } else { label.Content = content; } } }
private void liczydloBezpieczne2() {
for (int liczba = 0; liczba < 21; liczba++) {
92
Wątki w WPF
●
Do trzeciej wersji dodajemy canvas
●
Przy próbie narysowania elipsy na kontrolce canvas bez
synchronizacji dostajemy następujący błąd:
●
Singe Threaded Apartment – model wywodzący się z
architektury aplikacji wielowątkowych wykorzystujących
obiekty COM (technologia tworzenia obiektów rejestrowanych
globalnie w systemie operacyjnym dzięki czemu mogą być
wykorzystywane przez inne aplikacje. COM jest podstawą
Microsoft OLE, COM+, DCOM czy ActiveX.
Wątki w WPF
●
Czwarta wersja (włączamy STA)
●
Dostaniemy błąd, który już znamy i wiemy jak sobie z nim
poradzić.
Thread watekLicz = new Thread(liczydloNiebezpieczne2); watekLicz.SetApartmentState(ApartmentState.STA);
94
Wątki w WPF
●
Dodajemy bezpieczną metodę rysującą elipsy na canvasie:
Przykład: WPFThread
private delegate void drawEllipseDelegate(Canvas canvas, double w, double h,
double x, double y, Brush brush);
public static void drawEllipse(Canvas canvas, double w, double h, double x,
double y, Brush brush) {
if (!canvas.Dispatcher.CheckAccess())
{
canvas.Dispatcher.Invoke(new drawEllipseDelegate(drawEllipse), new object[] { canvas, w, h, x, y, brush });
} else
{
Ellipse ellipse = new Ellipse(); ellipse.Width = w; ellipse.Height = h; ellipse.Fill = brush; canvas.Children.Add(ellipse); Canvas.SetLeft(ellipse, x); Canvas.SetTop(ellipse, y); } }
Wątki w WPF
●
Teraz powinno wszystko działać (wersja piąta).
●
Jednak gdy chcielibyśmy tworzyć różne kolory a nie używać
statycznego:
●
Pojawia się kolejny problem: „The Application is in break
mode”, z konsoli można odczytać błąd:
„Nie można użyć
obiektu DependencyObject należącego do innego
wątku niż nadrzędny obiekt Freezable.”
●
Na szczęście rozwiązanie jest proste. Trzeba wykonać
metodę Freeze na obiekcie scb przed jego użyciem.
ThreadSafeCallsWPF.drawEllipse(canvas1, w, h, x, y, Brushes.Red);
SolidColorBrush scb = new SolidColorBrush(losujKolor()); ThreadSafeCallsWPF.drawEllipse(canvas1, w, h, x, y, scb);
96
Wątki w WPF
●
Szósta wersja użycie kontekstu synchronizacji
Przykład: WPFThread
private void liczydloBezpieczne3(object par) {
DispatcherSynchronizationContext context = par as
DispatcherSynchronizationContext;
for (int liczba = 0; liczba < 21; liczba++) { context.Send((object s) => { labelLicznik.Content = s as string; } , liczba.ToString()); Thread.Sleep(200); } }
Thread watekLicz = new Thread(liczydloBezpieczne3); watekLicz.Start(SynchronizationContext.Current);
Wątki w WPF
●
Uwaga na niebezpieczeństwo blokady. Nie powinno się tak
robić (efekt taki jak byśmy wykonali funkcję sekwencyjnie),
ale przy dodaniu oczekiwania na wątek
●
●
W momencie wykonywania funkcji obsługi kliknięcia
buttonStart_Click startujemy wątek potomny, który w pętli
stara się zakolejkować zgłoszenie czynności do wykonania
przez wątek macierzysty (Control.Invoke lub
SynchronizeContext.Send).
●
Zatrzymują one dalsze działania do momentu zakończenia
czynności wątku interfejsu. Niestety wątek macierzysty jest
Thread watekLicz = new Thread(liczydloBezpieczne3); watekLicz.Start(SynchronizationContext.Current); Watek.Join();
98
Wątki w WPF
●
Zamiast Send, można użyć do kolejkowania zgłoszeń
metody asynchronicznej Post
●
Spowoduje to wykonanie całej pętli jednak obsługą zgłoszeń
interfejs użytkownika zajmie się dopiero po zakończeniu
wątku potomnego. Efekt będzie taki, że wszystkie żądania
mogą wykonać się jednocześnie, czyli zobaczymy
ostateczny wynik licznika. Nie jest to poprawne działanie
ale przynajmniej nie mamy już blokady.
Apartment Threading
Jest logicznym kontenerem zawierającym:
Jeden wątek (Single-Threded Apartment)
Wiele wątków (Multi-Threded Apartment)
●
Apartment może zawierać zarówno wątki jak i obiekty
●Kontekst Synchronizacji mógł tylko obiekty
●
Obiekty takie są przypisane do Apartment'u przez cały
okres trwania.
●
Obiekty w kontekście synchronizacji mogą być wołane
przez dowolne wątki (ale w trybie exclusive)
100
Apartment Threading
Jeżeli można by było przedstawić obrazowo kontener
jako bibliotekę, obiekty jako książki a wątki jako osoby
to:
●
W przypadku kontekstu synchronizacji do biblioteki
wchodziłby sobie ktokolwiek ale zawsze pojedynczo
(nie może być dwóch osób w bibliotece)
●
W przypadku apartmentów mamy pracowników
przypisanych do biblioteki.
–
Single. Jest jeden bibliotekarz i do niego
odwołujemy się by coś przeczytał i nam
powiedział co w danej książce jest
Apartment Threading
●
Sygnalizowanie bibliotekarzowi , że chcemy
skorzystać z jakiejś książki (obiektu) nosi nazwę
„marshalling”.
●
Metoda jest przekazywana (marshal) poprzez
pracownika biblioteki i wykonywana na obiektach w
bibliotece
●
Marshalling jest zaimplementowany w „bibliotekarzu”
przez system komunikatów w Windows Forms i jest
przekazywane automatycznie
●
Jest to mechanizm, który cały czas sprawdza
102
Apartment Threading
●
Wątki .Net są automatycznie przypisane do
multi-thread-appartment chyba, że chcemy by było
inaczej wtedy:
Thread
t =
new
Thread
(...);
t.SetApartmentState (
ApartmentState
.STA);
Lub
class
Program
{
[
STAThread
]
static void
Main() {
...
Apartment Threading
●
Gdy nasza aplikacja to czysty kod .Net Apartment nie
ma znaczenia.
●