Dnes nás čeká poměrně krátká kapitola, a to synchronizační kontexty.
Kromě manuálního zamykání (jiný způsob locking jsme ani nebrali) můžeme zamykat i deklarativně, automaticky. Docílíme toho tím, že třídu odvodíme od třídy ContextBoundObject a pak na ni aplikujeme atribut Synchronization. Přikážeme tak běhovému prostředí CLR, aby zamykal automaticky:
using System;
using System.Threading;
using System.Runtime.Remoting.Contexts;
[Synchronization]
public class AutoLock : ContextBoundObject
{
public void Demo()
{
Console.Write("Start...");
Thread.Sleep(1000); // Nestane se, že by se sem dostala dvě vlákna,
Console.WriteLine("konec"); // díky automatickému lockingu
}
}
public class Test
{
public static void Main()
{
AutoLock safeInstance = new AutoLock();
// Zavoláme 3× metodu Demo
new Thread(safeInstance.Demo).Start();
new Thread(safeInstance.Demo).Start();
safeInstance.Demo();
}
}
CLR zajistí, že jen jedno vlákno může spustit kód uvnitř „safeInstance“. Docílí toho vytvořeným svého synchronizačního objektu, který zamkne okolo každého volání metody nebo vlastnosti ze „safeInstance“. Oblast, kterou zámek dokáže pokrýt, označujeme za synchronizační kontext.
Jak přesně tohle ale celé funguje? Klíč je v v namespace atributu Synchronization, který je System.Runtime.Remoting.Contexts. Instanci ContextBoundObject si můžeme představit jako „vzdálený“ objekt. Tím je myšleno, že všechny volané metody jsou zachyceny. Zachycení probíhá přes „prostředníka“ – když vytvoříme instanci naší třídy AutoLock, CLR vytvoří něco jako prostředníka – objekt se stejnými metodami a vlastnostmi jako AutoLock. Právě přes tohoto prostředníka se vykonává automatické zamykání. Ohledně výkonu – automatické zamykání prodlouží volání metody o pár mikrosekund.
Automatické zamykání nemůže být použito na statické členy (vždy je nutná instance, aby se mohl vytvořit prostředník), ani na třídy, které nejsou odvozené od ContextBoundObject (příkladem budiž Windows formulář odvozený od třídy Form).
Synchronizační kontext může přesáhnout rámec jednoho objektu (instance třídy). Pokud byl synchronizovaný objekt vytvořen z jiné třídy, obě třídy pak sdílejí stejný kontext (jeden velký zámek). Toto chování se dá upravit pomocí nastavení atributu Synchronization přes třídu SynchronizationAttribute (ta obsahuje několik pojmenovaných „int“ konstant, uvedených v tabulce):
Konstanta | Význam |
NOT_SUPPORTED | Totéž, jako když nepoužijeme atribut vůbec |
SUPPORTED | Spojí existující kontext s právě vytvořeným, pokud žádný další neexistuje, nic se nestane |
REQUIRED (výchozí) | Spojí existující kontext s právě vytvořeným, pokud žádný další neexistuje, vytvoří se nový |
REQUIRES_NEW | Vždy vytvoří nový kontext |
Takže pokud přes instanci třídy SynchronizaceA vytvoříme instanci třídy SynchronizaceB, oba budou mít svůj vlastní synchronizační kontext (pokud je SynchronizaceB deklarována jako v příkladu níže).
[Synchronization (SynchronizationAttribute.REQUIRES_NEW)]
public class SynchronizaceB : ContextBoundObject {}
Je logické, že čím větší rozsah kontextu, tím je vše snadnější pro správu, ale jsme více omezení možnostmi. Na druhou stranu – při použití hodně kontextů zase hrozí „deadlocky“ (pro připomenutí – zamezení práce dvou vláken navzájem):
[Synchronization]
public class Deadlock : ContextBoundObject
{
public Deadlock Other;
public void Demo()
{
Thread.Sleep(1000);
Other.Ahoj();
}
void Ahoj()
{
Console.WriteLine("ahoj");
}
}
public class Test
{
static void Main()
{
var dead1 = new Deadlock();
var dead2 = new Deadlock();
dead1.Other = dead2;
dead2.Other = dead1;
new Thread(dead1.Demo).Start();
dead2.Demo();
}
}
Protože je každá instance třídy Deadlock vytvořená uvnitř třídy Test (bez atributu Synchronization), každá instance bude mít svůj synchronizační kontext, a tím pádem i svůj lock. Když se dva objekty různě volají navzájem, jako to dělají v příkladu výše, netrvá dlouho a dojde k deadlocku. Na tento „jev“ je nutné dávat si trochu pozor, u automatického lockingu nemusejí být příčiny vždy tak zřejmé.
Reentrancy
S tímto pojmem jsme se setkali ve třetím díle, označuje metodu, která je absolutně thread-safe – jedno vlákno ji může zavolat hned po zavolání jiným vláknem bez jakýchkoliv neočekávaných efektů atd. Poměrně často mají pojmy „thread-safe“ a „reentrant“ stejný význam.
Avšak nedají se úplně zaměnit. Pravá „reentrant“ metoda vznikne, když atribut Synchronization rozšíříme o klíčové slovo true:
[Synchronization(true)]
Pokud běh aplikace opustí na chvíli ten „svůj“ kontext, dojde k jeho dočasnému zaniknutí. Toto by zabránilo v příkladu nahoře deadlockům! Na druhou stranu, kterékoliv vlákno je pak schopné zavolat kteroukoliv metodu v označené třídě („znovu-vstupovat“ do kontextu; reentering), následkem toho ale mohou být další problémy, kterým se celou tu dobu snažíme vyhnout.
Protože je atribut [Synchronization(true)] aplikován na úrovni třídy, každá metoda v označené třídě se tak stává náchylná na „volné“ volání!
Uvedli jsme si několik příkladů, které ukazují některé nevýhody automatického lockingu. Pokud ho použijeme, můžou vyjít na povrch problémy, které by jindy ani nevznikly. Díky tomu je ve složitějších aplikacích výhradně používané manuální zamykání.
Příště nás čeká téma, které spousta z vás očekává – přístup k prvkům na formuláři pomocí metod Control.Invoke, Control.BeginInvoke a hlavně třídy BackgroundWorker.