Programowanie równoległeCzęść 4 – własne wnioski
Jakub Binkowski
O mnie Jakub Binkowski Senior .NET Developer @ Rule Financial
Microsoft Most Valuable Professional (MVP) MCP, MCTS, MCPD
Lider Łódzkiej Grupy Profesjonalistów IT & .NET
Cykl „Programowanie równoległe w .NET” Część I
Wielowątkowość w .NET 4.0 Część II
Interakcja (synchronizacja) między wątkami, struktury danych i algorytmy równoległe
Część IIIOperacje asynchroniczne
Część IVDobre praktyki, najczęstsze błędy, testowanie
Agenda Błędy Dobre praktyki Ogólne rady Testowanie
Niepewne założenia
Niepewne założenia
var list = new List<int> {1, 3, 4, 5};
Co zwróci instrukcja: list.Contains(5);
wywołana jednocześnie z: list.Insert(index: 1, item: 2);
?
Niepewne założenia Czym jest List<T>?
length = 4
items = 1 3 4 50 1 2 3 4
Niepewne założenia Jak działa list.Contains(5)?
length = 4
items = 1 3 4 50 1 2 3 4
Niepewne założenia Jak działa list.Contains(5)?
length = 4
items = 1 3 4 50 1 2 3 4
FALSE
Niepewne założenia Jak działa list.Contains(5)?
length = 4
items = 1 3 4 50 1 2 3 4
FALSE
Niepewne założenia Jak działa list.Contains(5)?
length = 4
items = 1 3 4 50 1 2 3 4
FALSE
Niepewne założenia Jak działa list.Contains(5)?
length = 4
items = 1 3 4 50 1 2 3 4
TRUE
Niepewne założenia Jak działa list.Insert(index: 1, item: 2)?
length = 4
items = 1 3 4 50 1 2 3 4
Niepewne założenia Jak działa list.Insert(index: 1, item: 2)?
length = 4
items = 1 3 4 50 1 2 3 4
Niepewne założenia Jak działa list.Insert(index: 1, item: 2)?
length = 4
items = 1 3 4 5 50 1 2 3 4
Niepewne założenia Jak działa list.Insert(index: 1, item: 2)?
length = 4
items = 1 3 4 5 50 1 2 3 4
Niepewne założenia Jak działa list.Insert(index: 1, item: 2)?
length = 4
items = 1 3 4 4 50 1 2 3 4
Niepewne założenia Jak działa list.Insert(index: 1, item: 2)?
length = 4
items = 1 3 4 4 50 1 2 3 4
Niepewne założenia Jak działa list.Insert(index: 1, item: 2)?
length = 4
items = 1 3 3 4 50 1 2 3 4
Niepewne założenia Jak działa list.Insert(index: 1, item: 2)?
length = 4
items = 1 3 3 4 50 1 2 3 4
2
Niepewne założenia Jak działa list.Insert(index: 1, item: 2)?
length = 4
items = 1 2 3 4 50 1 2 3 4
2
Niepewne założenia Jak działa list.Insert(index: 1, item: 2)?
length = 5
items = 1 2 3 4 50 1 2 3 4
Niepewne założeniaList.Insert(1,2) i List.Contains(5) razemlength = 4
items = 1 3 4 5 50 1 2 3 4
5? FALSE
Niepewne założeniaList.Insert(1,2) i List.Contains(5) razemlength = 4
items = 1 3 4 4 50 1 2 3 4
5? FALSE
Niepewne założeniaList.Insert(1,2) i List.Contains(5) razemlength = 4
items = 1 3 3 4 50 1 2 3 4
5? FALSE
Niepewne założeniaList.Insert(1,2) i List.Contains(5) razemlength = 4
items = 1 2 3 4 50 1 2 3 4
5? FALSE
2
Niepewne założeniaList.Insert(1,2) i List.Contains(5) razemlength = 5
items = 1 2 3 4 50 1 2 3 4
Niepewne założeniaList.Insert(1,2) i List.Contains(5) razemlength = 5
items =
List.Contains(5) = false!
1 2 3 4 50 1 2 3 4
Dobra praktyka Nie zakładaj, że jakieś 2 operacje są
bezpieczne wielowątkowo
Synchronizuj jednoczesny dostęp: lock (Monitor) ReaderWriterLock(Slim) itp.
Używaj kolekcji bezpiecznych wielowątkowo:System.Collections.Concurrent
Instrukcje (nie)atomowe
Problem: Instrukcje (nie)atomowe
jedna linia !=
jedna instukcja
Co się stało? i++ to 3 instrukcje:
LOAD @i, R0 INCREMENT R0 STORE R0, @i
Co się stało?Wątek 1
R0 = 0
i++
Wątek 2
R0 = 0
i++
Pamięć:
i = 5
Co się stało?Wątek 1
R0 = 0
i++
Wątek 2
R0 = 0
i++
Pamięć:
i = 5
Co się stało?Wątek 1
R0 = 0
i++: LOAD @i, R0
Wątek 2
R0 = 0
i++:
LOAD @i, R0
Pamięć:
i = 5
Co się stało?Wątek 1
R0 = 5
i++: LOAD @i, R0
Wątek 2
R0 = 5
i++:
LOAD @i, R0
Pamięć:
i = 5
Co się stało?Wątek 1
R0 = 6
i++: LOAD @i, R0 INC R0
Wątek 2
R0 = 5
i++:
LOAD @i, R0
Pamięć:
i = 5
Co się stało?Wątek 1
R0 = 6
i++: LOAD @i, R0 INC R0 STORE R0, @i
Wątek 2
R0 = 5
i++:
LOAD @i, R0
Pamięć:
i = 6
Co się stało?Wątek 1
R0 = 6
i++: LOAD @i, R0 INC R0 STORE R0, @i
Wątek 2
R0 = 6
i++:
LOAD @i, R0 INC R0
Pamięć:
i = 6
Co się stało?Wątek 1
R0 = 6
i++: LOAD @i, R0 INC R0 STORE R0, @i
Wątek 2
R0 = 6
i++:
LOAD @i, R0 INC R0 STORE R0, @i
Pamięć:
i = 6
Dobra praktyka Nie zakładaj, że „jedna linia = jedna
instrukcja”
Synchronizuj dostęp (lock, itp.)
Używaj gwarantowanych operacji atomowych, np.:Interlocked.Increment(ref _counter);
Zdarzenia (events)
Zdarzenia (events) - przypomnienie Deklaracja:public event EventHandler SomethingHappened;
Wywołanie:if (SomethingHappened != null)
SomethingHappened(this, EventArgs.Empty);
Subskrypcja:SomethingHappened += HandleSomethingHappened;
SomethingHappened -= HandleSomethingHappened;
Problem: Zdarzenia a wielowątkowość
Czy zdarzenia są bezpiecznewielowątkowo?
Co się stało?
Wątek 1 Wątek 2
SomethingHappened +=
HandleSomethingHappened;
SomethingHappened = null
Co się stało?
Wątek 1 Wątek 2
SomethingHappened +=
HandleSomethingHappened;
SomethingHappened = HandleSomethingHappen
ed
Co się stało?
Wątek 1 Wątek 2
SomethingHappened +=
HandleSomethingHappened;
if (SomethingHappened != null)
SomethingHappened = HandleSomethingHappen
ed
Co się stało?
Wątek 1 Wątek 2
SomethingHappened +=
HandleSomethingHappened;
if (SomethingHappened != null)
SomethingHappened = HandleSomethingHappen
ed
true
Co się stało?
Wątek 1 Wątek 2
SomethingHappened +=
HandleSomethingHappened;
SomethingHappened -=
HandleSomethingHappened;
if (SomethingHappened != null)
SomethingHappened = HandleSomethingHappen
ed
Co się stało?
Wątek 1 Wątek 2
SomethingHappened +=
HandleSomethingHappened;
SomethingHappened -=
HandleSomethingHappened;
if (SomethingHappened != null)
SomethingHappened = null
Co się stało?
Wątek 1 Wątek 2
SomethingHappened +=
HandleSomethingHappened;
SomethingHappened -=
HandleSomethingHappened;
if (SomethingHappened != null)
SomethingHappened(this,
EventArgs.Empty);
SomethingHappened = null
Co się stało?
Wątek 1 Wątek 2
SomethingHappened +=
HandleSomethingHappened;
SomethingHappened -=
HandleSomethingHappened;
if (SomethingHappened != null)
SomethingHappened(this,
EventArgs.Empty);
SomethingHappened = null
NullReferenceException
Przykład
Zdarzenia bezpieczne
wielowątkowo
Dobra praktyka Wzorzec użycia eventów:
public event EventHandler SomethingHappened;
protected void OnSomethingHappened(EventArgs args)
{
var handler = SomethingHappened;
if (handler != null)
{
handler(this, args);
}
}
Dobra praktyka Wzorzec użycia eventów:
public event EventHandler SomethingHappened;
protected void OnSomethingHappened(EventArgs args)
{
var handler = SomethingHappened;
if (handler != null)
{
handler(this, args);
}
}
Dobra praktyka Wzorzec użycia eventów:
public event EventHandler SomethingHappened;
protected void OnSomethingHappened(EventArgs args)
{
var handler = SomethingHappened;
if (handler != null)
{
handler(this, args);
}
}
Dobra praktyka Wzorzec użycia eventów:
public event EventHandler SomethingHappened;
protected void OnSomethingHappened(EventArgs args)
{
var handler = SomethingHappened;
if (handler != null)
{
handler(this, args);
}
}
Dobra praktyka Wzorzec użycia eventów:
public event EventHandler SomethingHappened;
protected void OnSomethingHappened(EventArgs args)
{
var handler = SomethingHappened;
if (handler != null)
{
handler(this, args);
}
}
Snippet„invok
e”
Dobra praktyka Wzorzec użycia eventów:
public event EventHandler SomethingHappened;
protected void OnSomethingHappened(EventArgs args)
{
var handler = SomethingHappened;
if (handler != null)
{
handler(this, args);
}
}
Zdarzenia – pytanie Czy subskrypcja jest bezpieczna
wielowątkowo?SomethingHappened += HandleSomethingHappened;
A co jeśli dodajemy drugą subskrypcję? Czy łączenie dwóch delegatów jest bezpieczne?
SomethingHappened += HandleSomethingHappened;SomethingHappened += HandleSomethingHappened2;
Events internals – C# 1.0-3.5private EventHandler _somethingHappened;
public event EventHandler SomethingHappened
{
add
{
lock (this)
{
_somethingHappened = somethingHappened + value;
}
}
remove {/*kod analogiczny*/}
}
Events internals – C# 4.0private EventHandler _somethingHappened;
public event EventHandler SomethingHappened
{
add
{
/*bezpieczny wielowątkowo kod wolny od lock, który dodaje delegat to _somethingHappened*/
}
remove {/*kod analogiczny*/}
}
Zdarzenia – odpowiedzi Czy subskrypcja jest bezpieczna
wielowątkowo? Tak!
Generowany kod jest wielowątkowo bezpieczny.
Czy łączenie dwóch delegatów jest bezpieczne?
Tak! Złączenie 2 delegatów powoduje powstanie trzeciego. Delegaty są stałe, a więc z definicji wielowątkowo bezpieczne.
Lock escape
Przykład
Ucieczkaz lock’a
Problempublic class SafeList<T>: IEnumerable<T>
{
private List<T> _list = new List<T>();
/*...*/
public IEnumerator<T> GetEnumerator()
{
lock (_list)
{
return _list.GetEnumerator();
}
}
}
Problempublic class SafeList<T>: IEnumerable<T>
{
private List<T> _list = new List<T>();
/*...*/
public IEnumerator<T> GetEnumerator()
{
lock (_list)
{
return _list.GetEnumerator();
}
}
}
Zwrócony enumerator wcale nie jest zabezpieczony!
Dobre praktyki Unikaj zwracania enumeratora kolekcji
„chronionych” przez lock („lock escape”)
GetEnumerator() może: iterować po kopii kolekcji zwracać wielowątkowo bezpieczny iterator
Jak to wygląda w .NET? Enumerator po migawce (snapshot) kolekcji:
ConcurrentQueue ConcurrentStack ConcurrentBag
Enumerator dynamiczny: ConcurrentDictionary
Przykład Poprawna implementacja GetEnumerable():
enumeracja po kopii wielowątkowo bezpieczny enumerator
Lock leak
Przykład
Wyciek lock’a
Lock leak – zły kodprivate object _sync = new object(); public event EventHandler SomethingHappened;
private void DoSomething(){ lock (_sync) { //do something... var handler = SomethingHappened; if (handler != null) { handler(this, EventArgs.Empty); } }}
Lock leak – zły kodprivate object _sync = new object(); public event EventHandler SomethingHappened;
private void DoSomething(){ lock (_sync) { //do something... var handler = SomethingHappened; if (handler != null) { handler(this, EventArgs.Empty); } }}
Wykonujemy nieznany zewnętrzny kod wewnątrz „lock”
Przykład
Poprawiony kod
private object _sync = new object(); public event EventHandler SomethingHappened;
private void DoSomething(){ lock (_sync) { //do something... }
var handler = SomethingHappened; if (handler != null) { handler(this, EventArgs.Empty); }}
Lock leak – poprawiony kod
private object _sync = new object(); public event EventHandler SomethingHappened;
private void DoSomething(){ lock (_sync) { //do something... }
var handler = SomethingHappened; if (handler != null) { handler(this, EventArgs.Empty); }}
Lock leak – poprawiony kod
private object _sync = new object(); public event EventHandler SomethingHappened;
private void DoSomething(){ lock (_sync) { //do something... }
var handler = SomethingHappened; if (handler != null) { handler(this, EventArgs.Empty); }}
Lock leak – poprawiony kod
Wykonujemy nieznany zewnętrzny kod poza „lock”
Kod wielowątkowo bezpieczny
Dobre rady wujka Kuby
Kilka ważnych pytań Czy wiesz gdzie w Twojej aplikacji wątki się
rodzą, a gdzie (i jak) umierają?
Czy interakcja między wątkami odbywa się w ściśle określonych miejscach?
Czy dokumentujesz założenia dot. wielowątkowości kodu?
Czy Twój kod jest prosty?
Jak wygląda start wątku w Twojej aplikacji?
Wątek to nie bomba! Dobrze przemyśl, w którym miejscu
wywoływany jest kod asynchroniczny: interakcje użytkownika (wątek UI) Timer, Task / ThreadPool komunikacja (WCF, messaging, itp.)
Wątek to nie bomba! Zastanów się, co dzieje się z wątkiem (kodem)
później.
Czy łapiesz na końcu (początku) ew. wyjątki?
Czy przy wyjściu z aplikacji czekasz na zakończenie utworzonych wątków?
Podejście „let the shit hit the fan” się nie sprawdza!
Interakcje między wątkami Zastanów się, w którym miejscu się wątki
spotykają.
Czy te klasy i metody są odpowiednio zabezpieczone?
Czy te interakcje są przejrzyste?
Dokumentowanie Czy taki wpis jest Ci obcy?/// <remarks>
/// Any public static (Shared in Visual Basic)
/// members of this type are thread safe.
/// Any instance members are not guaranteed
/// to be thread safe.
/// </remarks>
Albo taki?/// <remarks>
/// This method is thread safe.
/// </remarks>
Co warto dokumentować? Czy klasa została zaprojektowana dla
wielowątkowego użycia? Założenia – czego wymaga i co gwarantuje
dany kod Klasy aktywne (wywołujące kod
asynchroniczny):/// <remarks>/// Event is invoked on ThreadPool thread./// </remarks>public event EventHandler MessageReceived;
Bardziej skomplikowane algorytmy Cykl życia klasy a wielowątkowość
Złoty środek Komentarze sprawiają, że przejrzysty kod
jest bardziej zrozumiały
Jeżeli na 1 linię kodu przypada 5 wyjaśnień, to może lepiej napisać inaczej kod?
Prostota Jedynym sposobem oceny poprawności kodu –
analiza
Wielowątkowość sama z siebie jest skomplikowana i trudna. Niech kod wielowątkowy będzie chociaż prosty. Inaczej nikt nie będzie w stanie ocenić jego poprawności!
Brzydki kod + nieprzemyślana wielowątkowość = spaghetti2
Wielowątkowość a testowalność
Testowanie a kod równoległy Test jednostkowy:
Przewidywalne zachowanie Każde uruchomienie daje taki sam rezultat (sukces
lub porażka)
Wykonanie kodu na wielu wątkach: przypadkowe za każdym razem inne
Trudno jest napisać testy do kodu wielowątkowego
CHESS – projekt Microsoft Research Próba okiełznania przypadkowości Zarządzanie przeplotem między wątkami Uruchomienie testu na różnych kombinacjach
przeplotu Możliwość odtworzenia przeplotu!
Tylko Visual Studio 2008 Prawdopodobnie jako część Visual Studio 11
Jak pisać testy jednostkowe? Kod „jednowątkowy” można łatwo testować
Kod wielowątkowy testować jest trudno
A gdyby tak pozbyć się równoległości?
Przykład
Testowanie koduwielowątkowego
Testowanie – czego można się „pozbyć”? Konstrukcje wielowątkowe:
ThreadPool.QueueUserWorkItem Thread.Start Task (TaskScheduler) Timer
Wzorce: nieskończona pętla warunek
Podsumowanie
Podsumowanie Uwaga na częste błędy! Warto przemyśleć wielowątkowość w aplikacji Kod prosty, przejrzysty i udokumentowany Nie da się przetestować bezpieczeństwa
wielowątkowego Da się testować kod wielowątkowy – wystarczy
pozbyć się wielowątkowości!
Błędy - disclaimer
Nie róbcie tego w domu!Ani w pracy!
Zawodowcy popełnili za Was te błędy,
żebyście Wy nie musieli.
Przydatne odnośniki http://www.albahari.com/threading/ Asynchronous programming design patterns:
http://msdn.microsoft.com/en-us/library/ms228969.aspx
Best practices: http://
msdn.microsoft.com/en-us/library/1c9txz50.aspx http://
www.anticipatingminds.com/content/products/devadvantage/KnowledgePacks/Threading/ThreadingKnowledgePack.aspx
http://stackoverflow.com/questions/660621/threading-best-practices
Przydatne odnośniki UI Thread:
http://msdn.microsoft.com/en-us/magazine/cc163328.aspx
http://msdn.microsoft.com/en-us/library/ms741870.aspx
http://10rem.net/blog/2010/04/23/essential-silverlight-and-wpf-skills-the-ui-thread-dispatchers-background-workers-and-async-network-programming
Przydatne odnośniki Events:
http://blogs.msdn.com/b/cburrows/archive/2010/03/05/events-get-a-little-overhaul-in-c-4-part-i-locks.aspx
http://blogs.msdn.com/b/cburrows/archive/2010/03/08/events-get-a-little-overhaul-in-c-4-part-ii-semantic-changes-and.aspx
http://blogs.msdn.com/b/cburrows/archive/2010/03/18/events-get-a-little-overhaul-in-c-4-part-iii-breaking-changes.aspx
http://blogs.msdn.com/b/cburrows/archive/2010/03/30/events-get-a-little-overhaul-in-c-4-afterward-effective-events.aspx
Dziękuję za uwagę. Pytania?