• Nie Znaleziono Wyników

Wykład

N/A
N/A
Protected

Academic year: 2021

Share "Wykład"

Copied!
103
0
0

Pełen tekst

(1)

Programowanie Współbieżne

(2)

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/

(3)
(4)

4

(5)
(6)

6

(7)

Pierwsze kroki

Z zestawu narzędzi wybieramy przycisk

(8)

8

Pierwsze kroki

(9)

Pierwsze kroki

We właściwościach zmieniamy nazwę przycisku jego tekst

(10)

10

Pierwsze kroki

Po dwukrotnym kliknięciu otworzy nam się wygenerowany automatycznie kod programu do

(11)

Pierwsze kroki

Następnie uzupełniamy nasz kod jedną linią:

MessageBox

.Show("Hello world");

(12)

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.ProcessorCount

(13)

Wą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)

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

(15)

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)

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ść";

(17)

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)

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();

(19)

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)

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();

(21)

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)

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.

(23)

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)

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

(25)

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)

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.

(27)

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)

28

Sekcja krytyczna

Kilka wątków (n_wątków) robi to samo zadanie:

for (int ii = 0; ii < 1000000; ii++) {

{

licznik++; }

}

(29)

Sekcja krytyczna

lock

private 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)

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) {

(31)

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)

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

(33)

Sekcja krytyczna

Blokowanie zagnieżdżone

static 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)

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

(35)

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)

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.

(37)

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)

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); }

(39)

Stany wątku

Unstarted WaitSleepJoin Abort Requested Running Stopped Aborted Start Wątek

blokowany odblokowanyWątek

Abort Zakończenie wątku Zakończenie wątku Teoretycznie Zakończenie wątku ResetAbort Abort

(40)

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

(41)

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)

42

Wait Handles

AutoResetEvent

EventWaitHandle 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.

(43)

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)

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

(45)

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)

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();

(47)

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)

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(); } }

(49)

Wait Handles

ManualResetEvent

EventWaitHandle 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)

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ę

(51)

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)

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.

(53)

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)

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:

(55)

Wait Handles

Wait, wait, wait...

WaitHandle

.WaitAll(WaitHandle[] waitHandles)-

Czekaj na pozwolenie od wszystkich z waitHandles

WaitHandle

.WaitAny(WaitHandle[] waitHandles)

(56)

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(); }

}

(57)

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)

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();

(59)

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)

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:

(61)

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)

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}");

(63)

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)

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

(65)

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)

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 Synchronized

SUPPORTED

- dołącz do istniejącego kontekstu synchronizacji jeżeli jest stworzony z innego obiektu, w innym przypadku będzie niesynchronizowany

REQUIRED

- (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 synchronizacji

(67)

Delegaty 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)

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(); }

(69)

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)

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);

}

(71)

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)

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ą.

(73)

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)

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}

(75)

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)

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; }

} }

}

(77)

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)

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

(79)

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)

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ć

(81)

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)

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

(83)

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)

84

Background Worker

Klasa parametry:

class ParametryDwa {

public int czasUspienia,iloscIteracji;

public ParametryDwa(int _czasUspienia, int _iloscIteracji) {

czasUspienia = _czasUspienia; iloscIteracji = _iloscIteracji; }

(85)

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)

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); }

(87)

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)

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();

(89)

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)

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.

(91)

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)

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.

(93)

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)

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); } }

(95)

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)

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);

(97)

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)

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.

(99)

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)

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

(101)

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)

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() {

...

(103)

Apartment Threading

Gdy nasza aplikacja to czysty kod .Net Apartment nie

ma znaczenia.

Typy z System.Windows.Forms korzystają z kodu

Win32 przeznaczonego do pracy w Single Thread

Apartment. Z tego powodu w programach tego typu

wymagane jest [STAThread].

Cytaty

Powiązane dokumenty

procedura P(S)  polega na dekrementacji 2 zmiennej semaforowej i sprawdzeniu czy jej warto±¢ jest mniejsza od zera; je±li tak to proces, który próbowaª przej±¢ przez semafor

W pierwszym przypadku sonda wydziela sygnaá taktujący, zapewnia separacjĊ galwaniczną zarówno od Ĩródáa sygnaáu, jak i od urządzenia odbiorczego oraz dostarcza

ħródáo sygnaáu czasu i czĊstotliwoĞci z termostatowanym generatorem kwarcowym lub rubidowym, synchronizowane z satelitarnego globalnego systemu pozycjonowania GPS.. Unikatowe

Proces pozorny jest procesem sekwencyjnym (założenie sekwencyjności zapewnia realizację wzajemnego wykluczania procesów realnych) i cyklicznym, który podczas spotkania z

Problem rozważany w niniejszej pracy sprowadza się do wyznaczania takich ogólnych mechanizmów synchronizacji o- biektów systemu produkcyjnego, które zapewniają

R eferat ten stanow i kontynuację tem atyki przedstaw ionej na V Krajowej Konferencji A utom atyzacji Dyskretnych Procesów Przemysłowych.. Schemat ro zw iązan ia zagad

Natomiast przy częstotliwości około 30 Hz wartość współczynnika korelacji zmniejsza się do wartości -0.7, co oznacza, że pęcherze odrywają się w sposób skorelowany - na

Miarę tego jak trudne w egzekucji pisarskiej, a następnie typograficz- nej, było odtwarzanie tablicy synchronistycznej Marcina Polaka najlepiej ilustruje fakt, że żadne z