Semafor je jedno ze synchronizačních primitiv, se kterým se nepotkáme tak často. Používá se v situacích, kdy máme k dispozici jen omezené množství nějakých sdílených prostředků a potřebujeme je přidělovat typicky většímu počtu zájemců.
Pro ty, co nevědí, o co jde: Semafor jakožto nástroj pro synchronizaci vláken má na začátku nějaké N, které si můžeme představit jako “počet volných míst na parkovišti”. Vždy když nějaké auto přijede na parkoviště, N se zmenší o jednu, protože jedno volné místo ubyde. Ve chvíli, kdy nějaké auto z parkoviště vyjede, N se zase o jedničku zvětší.
Dokud je N > 0, pak je na semaforu zelená a auta mohou do parkoviště vjíždět. Pokud by ale N byla nula, na semaforu se objeví červená a auta musí čekat – na parkoviště je to nepustí.
V .NETu máme třídu System.Threading.Semaphore, která má metody WaitOne (auto vjíždí na parkoviště) a Release (auto vyjíždí z parkoviště). Jenom si místo aut představte vlákna a vyjde nám z toho nástroj, kterým můžete do určité části kódu vpustit najednou maximálně N vláken.
V jedné aplikaci, na které teď pracujeme, máme objekty, jež provádí poměrně komplikované výpočty – představme si nějakou metodu GetOffers, která počítá na základě nějakých vstupních kritérií nabídky pro zákazníka. Výpočet trvá asi 1 sekundu, ale samotná inicializace třídy zhruba 10 sekund. Výpočet je jednovláknový a nejde dobře paralelizovat.
Tento výpočet se používá ve webové aplikaci, kde může najednou přijít více zákazníků a nechat si spočítat nabídky zároveň.
Vytvářet novou instanci pro každý výpočet není vhodné, protože inicializace trvá dlouho. Ideální by bylo mít instanci již nachystanou a spouštět na ní jen ty výpočty. Vzhledem k tomu, že výpočet samotný paralelizovat nejde, ale server bude mít vícejádrový procesor, můžeme provádět např. čtyři různé výpočty najednou.
To ale znamená, že potřebujeme mít někde nachystané 4 instance té třídy, a tu instanci, která zrovna nic nepočítá, vždycky přidělíme vláknu, které ji zrovna potřebuje. A na to se právě dá použít semafor – máme 4 prostředky a přidělujeme je na určitou dobu několika zájemcům (těch může být víc než 4, v takovém případě budou ti, jež nemohou být odbaveni inhed, muset čekat – i to nám semafor zajistí sám od sebe).
Zde je ukázka, jak by to mohlo vypadat:
/// <summary>
/// Holds several prepared instances of the calculator and assigns them to the threads.
/// </summary>
public class CalculatorExecutionEngine<T> : IDisposable
{
private const int MaxSimultaneousOperationsCount = 16;
private List<ExecutionEngineItem<T>> instances = new List<ExecutionEngineItem<T>>();
private SemaphoreSlim semaphore = new SemaphoreSlim(0, MaxSimultaneousOperationsCount);
private object locker = new object();
/// <summary>
/// Registers the instance.
/// </summary>
public void RegisterInstance(T instance)
{
lock (locker)
{
instances.Add(new ExecutionEngineItem<T>() { Item = instance, IsAvailable = true });
// increase the semaphore if there are some slots available
if (instances.Count <= MaxSimultaneousOperationsCount)
{
semaphore.Release();
}
}
}
/// <summary>
/// Performs the action with any of the instances in the pool.
/// </summary>
public TResult PerformAction<TResult>(Func<T, TResult> operation)
{
ExecutionEngineItem<T> instance = null;
try
{
semaphore.Wait();
// pick a first free instance
lock (locker)
{
instance = instances.First(i => i.IsAvailable);
instance.IsAvailable = false;
}
// perform the operation
return operation(instance.Item);
}
finally
{
// unlock the instance
lock (locker)
{
if (instance != null)
{
instance.IsAvailable = true;
}
}
// release the semaphore
semaphore.Release();
}
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
lock (locker)
{
instances.Clear();
}
semaphore.Dispose();
}
/// <summary>
/// Represents an instance in the inner list.
/// </summary>
class ExecutionEngineItem<T>
{
public bool IsAvailable { get; set; }
public T Item { get; set; }
}
}
Všimněme si metody RegisterInstance – tou do objektu ze začátku nasypeme instance tříd, které umí provádět výpočty. Třída je generická, protože objekty, které můžeme chtít takto vláknům přidělovat, mohou být různého typu.
Dále je zde metoda PerformOperation – ta jako parametr bere Func<T, TResult>. Do tohoto parametru dáme funkci, která na vstupu přijímá objekt typu T (v metodě vybíráme první volnou instanci), a která vrací hodnotu typu TResult (tu si zvolíme, když tuto metodu voláme).
Při jakékoliv manipulaci se seznamem instances zamykáme.
A samozřejmě v okamžiku, kdy dostaneme nějakou práci (PerformOperation), tak snížíme semafor, instanci, na které pracujeme, označíme jako zabranou (aby ji nemohlo dostat jiné vlákno), a jakmile jsme hotovi (ať už skončíme korektně nebo výjimkou; je to ve finally bloku), instanci označíme znovu jako volnou a zvýšíme semafor.
Napadá někoho jiné řešení? Není už něco takového v .NET frameworku samotném?