K čemu se mimo jiné v praxi hodí Semaphore?

Tomáš Herceg       29.04.2012       C#, Offtopic       13290 zobrazení

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?

 

hodnocení článku

0       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

Diskuse: K čemu se mimo jiné v praxi hodí Semaphore?

Nebylo by jednodušší použít ManualResetEvent a položky z List<T> přímo odebírat a vracet je tam, až se práce dokončí?

ManualResetEvent by se nastavoval podle toho, zda je v List<T> nějaké položka, či ne.

Update: Když jsem se nad tím trochu zamyslel, tak by složitost řešení byla + - stejná.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Složitost by asi fakt byla stejná.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Diskuse: K čemu se mimo jiné v praxi hodí Semaphore?

Čau,

Podle mě by na tohle stačilo použít BlockingCollection<T>, která se naplní volnými instancemi. Při provádění operace se používaná instance fyzicky z kolekce výjme (pokud není žádná volná, Take bude automaticky blokovat) a po ukončení používání opět přidá.

Jediný to tam jednoduše nejde je ta kontrola na současně probídající výpočty, ale tady by asi stačilo prostě do tý kolekce na začátku víc instancí nepřidat.

public class CalculatorExecutionEngine<T>
{
    private BlockingCollection<T> instances = new BlockingCollection<T>(new ConcurrentBag<T>());

    public void RegisterInstance(T instance)
    {
        instances.Add(instance);
    }

    public TResult PerformAction<TResult>(Func<T, TResult> operation)
    {
        T instance = instances.Take();
        try
        {
            return operation(instance);
        }
        finally
        {
            instances.Add(instance);
        }
    }
}
nahlásit spamnahlásit spam 0 odpovědětodpovědět

BlockingCollection jsem nikdy nepoužil - fakt ten Take bude čekat?

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Já věřim, že jo a MSDN taky.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Diskuse: K čemu se mimo jiné v praxi hodí Semaphore?

Nejsem žádný expert na multithreading, ale neodvedl by stejnou práci ThreadPool?

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Ne, threadpool je úplně něco jiného - tedy princip je podobný, ale tam máte nachystaná vlákna, kterým můžete dávat úkoly. Ale výše uvedený problém to neřeší.

nahlásit spamnahlásit spam 0 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:
Heslo:

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback