Sdílet prostřednictvím


Osvědčené postupy pro spravované podprocesy

Multithreading vyžaduje pečlivé programování. U většiny úloh můžete snížit složitost tím, že zařadíte požadavky na spuštění ve vláknech fondu vláken. Toto téma se zabývá obtížnějšími situacemi, jako je koordinace práce více vláken nebo zpracování vláken, která blokují.

Poznámka:

Počínaje rozhraním .NET Framework 4 poskytuje paralelní knihovna úloh a PLINQ rozhraní API, která snižují některé složitosti a rizika programování s více vlákny. Další informace naleznete v tématu Paralelní programování v .NET.

Zablokování a podmínky závodu

Multithreading řeší problémy s propustností a odezvou, ale zároveň zavádí nové problémy: deadlocky a podmínky souběhu.

Zablokování

K vzájemnému zablokování dojde, když se každé ze dvou vláken pokusí uzamknout prostředek, který už je uzamčený. Ani jedno vlákno nemůže pokračovat.

Mnoho metod spravovaných tříd vláken poskytuje časové limity, které vám pomůžou rozpoznat zablokování. Například následující kód se pokusí získat zámek objektu s názvem lockObject. Pokud zámek není získán v 300 milisekundách, Monitor.TryEnter vrátí false.

If Monitor.TryEnter(lockObject, 300) Then
    Try
        ' Place code protected by the Monitor here.
    Finally
        Monitor.Exit(lockObject)
    End Try
Else
    ' Code to execute if the attempt times out.
End If
if (Monitor.TryEnter(lockObject, 300)) {
    try {
        // Place code protected by the Monitor here.
    }
    finally {
        Monitor.Exit(lockObject);
    }
}
else {
    // Code to execute if the attempt times out.
}

Soutěžní podmínky

Podmínka závodu je chyba, která nastane, když výsledek programu závisí na tom, které z dvou nebo více vláken dosáhne specifického bloku kódu první. Spuštění programu mnohokrát generuje různé výsledky a výsledek jakéhokoli daného spuštění nelze předpovědět.

Jednoduchým příkladem podmínky časování je zvýšení pole. Předpokládejme, že třída má privátní statické pole (Sdílené v jazyce Visual Basic), které se zvýší při každém vytvoření instance třídy pomocí kódu, například objCt++; (C#) nebo objCt += 1 (Visual Basic). Tato operace vyžaduje načtení hodnoty z objCt registru, zvýšení hodnoty a jeho uložení do objCt.

Ve vícevláknové aplikaci může být vlákno, které načetlo a zvýšilo hodnotu, přerušeno jiným vláknem, které provádí všechny tři kroky; když první vlákno obnoví provádění a uloží svou hodnotu, přepíše objCt aniž by vzalo v úvahu, že se mezitím hodnota změnila.

Tomuto konkrétnímu závodnímu stavu se snadno vyhnete použitím metod třídy Interlocked, například Interlocked.Increment. Další techniky synchronizace dat mezi více vlákny najdete v tématu Synchronizace dat pro vícevláknové zpracování.

Podmínky soutěže mohou nastat také při synchronizaci činností více vláken. Pokaždé, když napíšete řádek kódu, musíte zvážit, co se může stát, pokud by bylo vlákno přerušeno před vykonáním řádku (nebo před vykonáním některé z jednotlivých instrukcí stroje, které tvoří řádek) a jiné vlákno by ho předběhlo.

Statické členy a statické konstruktory

Třída není inicializována, dokud jeho konstruktor třídy (static konstruktor v jazyce C#, Shared Sub New v jazyce Visual Basic) není dokončen. Aby se zabránilo spuštění kódu u typu, který není inicializován, modul CLR blokuje všechna volání z jiných vláken na static členy třídy (Shared členy v jazyce Visual Basic), dokud konstruktor třídy nedokončí spuštění.

Pokud například konstruktor třídy spustí nové vlákno a procedura vlákna volá static člena třídy, nové vlákno blokuje, dokud konstruktor třídy není dokončen.

To platí pro jakýkoli typ, který může mít static konstruktor.

Počet procesorů

Ať už je v systému k dispozici více procesorů, nebo jenom jeden procesor, může ovlivnit architekturu s více vlákny. Další informace naleznete v tématu Počet procesorů.

Environment.ProcessorCount Pomocí vlastnosti určete počet procesorů dostupných za běhu.

Obecná doporučení

Při použití více vláken zvažte následující pokyny:

  • Nepoužívejte Thread.Abort k ukončení jiných vláken. Volání Abort na jiné vlákno je podobné vyvolání výjimky v tomto vlákně, aniž byste věděli, jaký bod vlákno dosáhlo při jeho zpracování.

  • Nepoužívejte Thread.Suspend a Thread.Resume k synchronizaci aktivit více vláken. Používejte Mutex, ManualResetEvent, AutoResetEventa Monitor.

  • Neovládejte spouštění pracovních vláken z hlavního programu (například pomocí událostí). Místo toho navrhněte program tak, aby pracovní vlákna odpovídala za čekání na dostupnost práce, jeho spuštění a upozorňování dalších částí programu po dokončení. Pokud pracovní vlákna neblokují, zvažte použití vláken z fondu vláken. Monitor.PulseAll je užitečná v situacích, kdy pracovní vlákna blokují.

  • Nepoužívejte typy jako objekty zámku. To znamená, že se vyhněte kódu, například lock(typeof(X)) v jazyce C# nebo SyncLock(GetType(X)) v jazyce Visual Basic, nebo použití Monitor.Enter s Type objekty. Pro daný typ existuje pouze jedna instance System.Type pro každou doménu aplikace. Pokud je typ, na který uzamknete, veřejný, může jej také uzamknout jiný kód než ten váš, což vede k zablokování. Další problémy najdete v tématu Osvědčené postupy pro spolehlivost.

  • Při zamykání instancí, například lock(this) v jazyce C# nebo SyncLock(Me) v jazyce Visual Basic, buďte opatrní. Pokud jiný kód v aplikaci, mimo typ, převezme zámek objektu, může dojít k zablokování.

  • Ujistěte se, že vlákno, které vstoupilo do monitoru, monitor vždy opustí, i když během této doby dojde k výjimce. Příkaz lock jazyka C# a příkaz SyncLock jazyka Visual Basic poskytují toto chování automaticky, přičemž k zajištění, že je volána, použije Monitor.Exit. Pokud nemůžete zajistit, že se zavolá Exit, zvažte změnu návrhu tak, aby se používal Mutex. Mutex se automaticky uvolní, když se ukončí vlákno, které ho aktuálně vlastní.

  • Pro úlohy, které vyžadují různé prostředky, používejte více vláken a vyhněte se přiřazování více vláken k jednomu prostředku. Například každá úloha zahrnující vstupně-výstupní operace může mít vlastní vlákno, protože toto vlákno bude blokovat během vstupně-výstupních operací a umožní tak spuštění dalších vláken. Uživatelský vstup je další prostředek, který využívá vyhrazené vlákno. Na počítači s jedním procesorem koexistuje úkol, který zahrnuje náročné výpočty, se vstupem uživatele a úlohami zahrnujícími vstupně-výstupní operace, ale více úloh náročných na výpočty mezi sebou soutěží.

  • Zvažte použití metod Interlocked třídy pro jednoduché změny stavu místo použití lock příkazu (SyncLock v jazyce Visual Basic). Příkaz lock je dobrý nástroj pro obecné účely, ale Interlocked třída poskytuje lepší výkon aktualizací, které musí být atomické. Interně spustí jednu předponu zámku, pokud neexistuje kolize. V kontrolách kódu sledujte kód podobný následujícím příkladům. V prvním příkladu se zvýší stavová proměnná:

    SyncLock lockObject
        myField += 1
    End SyncLock
    
    lock(lockObject)
    {
        myField++;
    }
    

    Výkon můžete zlepšit pomocí Increment metody místo lock příkazu následujícím způsobem:

    System.Threading.Interlocked.Increment(myField)
    
    System.Threading.Interlocked.Increment(myField);
    

    Poznámka:

    Použijte metodu Add pro atomické přírůstky větší než 1.

    V druhém příkladu je proměnná typu odkazu aktualizována pouze v případě, že se jedná o odkaz null (Nothing v jazyce Visual Basic).

    If x Is Nothing Then
        SyncLock lockObject
            If x Is Nothing Then
                x = y
            End If
        End SyncLock
    End If
    
    if (x == null)
    {
        lock (lockObject)
        {
            x ??= y;
        }
    }
    

    Výkon je možné zlepšit použitím CompareExchange metody následujícím způsobem:

    System.Threading.Interlocked.CompareExchange(x, y, Nothing)
    
    System.Threading.Interlocked.CompareExchange(ref x, y, null);
    

    Poznámka:

    Přetížení CompareExchange<T>(T, T, T) metody poskytuje typově bezpečnou alternativu pro odkazové typy.

Doporučení pro knihovny tříd

Při navrhování knihoven tříd pro multithreading zvažte následující pokyny:

  • Pokud je to možné, vyhněte se potřebě synchronizace. To platí hlavně pro silně používaný kód. Například algoritmus může být upraven tak, aby toleroval podmínku soutěže, a ne aby ji eliminoval. Nepotřebná synchronizace snižuje výkon a vytváří možnost zablokování a kolizních stavů.

  • Ve výchozím nastavení zajistěte, aby statická data (Shared v jazyce Visual Basic) byla bezpečná při použití ve vláknech.

  • Ve výchozím nastavení nepoužívejte datové vlákno instance jako bezpečné. Přidání zámků pro vytvoření kódu bezpečného pro přístup z více vláken snižuje výkon, zvyšuje kolize zámků a vytváří možnost, že dojde k zablokování. V běžných modelech aplikací spouští uživatelský kód pouze jedno vlákno, což minimalizuje potřebu bezpečnosti vláken. Z tohoto důvodu nejsou knihovny tříd .NET ve výchozím nastavení bezpečné pro přístup z více vláken.

  • Vyhněte se poskytování statických metod, které mění statický stav. V běžných scénářích serveru se statický stav sdílí napříč požadavky, což znamená, že tento kód může současně spouštět více vláken. To otevírá možnost chyb ve vláknech. Zvažte použití návrhového vzoru, který zapouzdřuje data do instancí, jež nejsou sdíleny mezi jednotlivými požadavky. Kromě toho, pokud jsou statická data synchronizována, mohou volání mezi statickými metodami, které mění stav, vést k zablokování nebo redundantní synchronizaci, což nepříznivě ovlivňuje výkon.

Viz také