Osvědčené postupy pro spravované podprocesy

Multithreading vyžaduje pečlivé programování. V případě většiny úkolů lze omezit složitost umístěním požadavků do fronty pro spuštění pomocí vláken fondu vláken. Toto téma řeší obtížnější situace, například koordinaci práce více vláken nebo zpracování vláken, která se 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 rychlostí odezvy, ale zároveň přináší nové problémy: zablokování a konflikty časování.

Zablokování

K zablokování dochází tehdy, pokud se každé ze dvou vláken pokusí uzamknout prostředek, který je již zamčený. Ani jedno vlákno nemůže provádět žádný další krok.

Mnoho metod tříd modelu spravovaných vláken poskytuje časové limity, které usnadňují zjišťování případů 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 milisekundách 300 milisekund, 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.  
}  

Podmínky časování

Konflikt časování je chyba, ke které dojde, pokud výstup programu závisí na tom, které ze dvou nebo více vláken dříve dosáhne určitého bloku kódu. Opakované spuštění programu vrací různé výsledky a nelze předpovědět výsledek jakéhokoli daného běhu.

Jednoduchým příkladem konfliktu časování je zvyšování 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 kódu objCt do registru, navýšení hodnoty a její uložení v kódu objCt.

Ve vícevláknových aplikacích platí, že vlákno, které načetlo a zvýšilo hodnotu, může být přerušeno jiným vláknem, jež provede všechny tři kroky. Pokud první vlákno pokračuje v provádění a uloží svou hodnotu, přepíše kód objCt, aniž by zohlednilo, že hodnota se mezitím změnila.

Těmto konkrétním konfliktům časování se dá snadno vyhnout použitím metod třídy Interlocked, jako 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í.

Ke konfliktům časování může docházet také tehdy, pokud synchronizujete aktivity více vláken. Při každém zápisu řádku kódu je nutné zvážit, co může nastat, pokud by vlákno bylo před provedením daného řádku (nebo před provedením jakéhokoliv jednotlivého pokynu počítače, který řádek tvoří) přerušeno a jiné vlákno by jej nahradilo.

Statické členy a statické konstruktory

Třída není inicializována až do té doby, dokud příslušný konstruktor třídy (konstruktor static v jazyce C#, konstruktor Shared Sub New v jazyce Visual Basic) nedokončí provádění. Aby nedošlo ke spuštění kódu pro typ, který není inicializován, blokuje modul CLR všechna volání členů static dané třídy (Shared v jazyce Visual Basic) z jiných vláken až do dokončení konstruktoru třídy.

Pokud například konstruktor třídy začíná nové vlákno a procedura vlákna volá člen static dané třídy, je nové vlákno blokováno do doby, než je konstruktor třídy dokončen.

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

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žívání více vláken zvažte následující pokyny:

  • Pro ukončení ostatních vláken nepoužívejte vlákno Thread.Abort. 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í.

  • Pro synchronizaci činností více vláken nepoužívejte vlákna Thread.Suspend a Thread.Resume. Použijte vlákna Mutex, ManualResetEvent, AutoResetEvent a Monitor.

  • Řízení zpracovávání pracovních vláken neprovádějte z hlavního programu (například pomocí událostí). Namísto toho je vhodné navrhnout program tak, aby pracovní vlákna byla zodpovědná za čekání, dokud není k dispozici činnost, kterou pak vykonají a oznámí ostatním částem programu, že byla dokončena. Pokud nejsou pracovní vlákna blokovací, zvažte používání vláken fondu vláken. Volání metody Monitor.PulseAll je užitečné v situacích, kdy pracovní vlákna provádí blokování.

  • Nepoužívejte typy jako objekty zámku. To znamená, že byste neměli používat kódy, jako je lock(typeof(X)) v jazyce C# nebo SyncLock(GetType(X)) v jazyce Visual Basic, nebo používat kód Monitor.Enter s objekty Type. Pro daný typ existuje pouze jedna instance System.Type v doméně aplikace. V případě, že typ, pro který použijete zámek, je veřejný, může jej jiný kód uzamknout, což povede 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 obezřetní. Pokud jiný kód v aplikaci, který je vůči typu externí, převezme zámek objektu, může dojít k zablokování.

  • Zajistěte, aby vlákno, které se zobrazilo v monitorovacím nástroji, jej opustilo vždy, i pokud dojde k výjimce během zobrazení vlákna v monitorovacím nástroji. 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 Monitor.Exit je volána, použije blok finally. Pokud se nemůžete ujistit, že se bude volat Exit , zvažte změnu návrhu tak, aby používal Mutex. Objekt mutex je automaticky uvolněn, pokud se ukončí vlákno, které jej vlastní.

  • Pro úkoly, jež vyžadují různé prostředky, používejte více vláken a zamezte přiřazení většího počtu vláken jedinému prostředku. Například všechny úkoly zahrnující vstup-výstup těží z toho, že mají vlastní vlákno, protože dané vlákno bude přerušováno během vstupně-výstupních operací, a tím umožní provedení jiných vláken. Uživatelský vstup je dalším prostředkem, který těží z vyhrazeného vlákna. Úkol, který vyžaduje intenzivní výpočty, existuje na počítači s jedním procesorem spolu s uživatelským vstupem a s úkoly, jež se týkají vstupu-výstupu, ale úkoly s vícenásobnými intenzivními výpočty si vzájemně konkurují.

  • Pro jednoduché změny stavu je třeba zvážit použití metod třídy Interlocked namísto používání příkazu lock (SyncLock v jazyce Visual Basic). Příkaz lock je dobrý univerzální nástroj, avšak třída Interlocked poskytuje lepší výkon aktualizací, které musí být atomické. Pokud neexistují žádné kolize, provede interně jedinou předponu zámku. V revizích kódu hledejte stejný kód, jaký je uveden v následujících příkladech. V prvním příkladu je zvýšena stavová proměnná:

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

    Pokud použijete metodu Increment namísto příkazu lock, můžete zlepšit výkon, a to následovně:

    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.

    Ve druhém příkladu je proměnná referenčního typu aktualizována pouze tehdy, pokud má odkaz hodnotu 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 lze zvýšit pomocí metody CompareExchange, jak je zobrazeno dále:

    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é, synchronizaci nepoužívejte. To zejména platí pro velmi vytížený kód. Algoritmus může být například upraven pro tolerování konfliktů časování spíše než pro jejich odstranění. Zbytečná synchronizace snižuje výkon a vytvoří možnosti pro zablokování a konflikty časování.

  • Zajistěte, aby data deklarovaná jako static (Shared v jazyce Visual Basic) byla ve výchozím nastavení bezpečná pro přístup z více vláken.

  • Nenastavujte data instancí ve výchozím nastavení jako bezpečná pro přístup z více vláken. Přidávání zámků za účelem 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 zablokování. V běžných modelech aplikací provádí uživatelský kód v každém okamžiku pouze jedno vlákno, což minimalizuje potřebu zabezpečení pro přístup z více 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.

  • Nepoužívejte poskytování statických metod, které mění stav. V případě běžných použití serverů je statický stav sdílen napříč požadavky, což znamená, že více vláken může současně spustit tento kód. Tato skutečnost otevírá možnosti pro chyby spojené s používáním více vláken. Zvažte používání návrhového vzoru, který zapouzdřuje data do instancí, jež nejsou sdíleny napříč požadavky. Dále platí, že pokud jsou synchronizována statická data, volání mezi statickými metodami, které mění stav, může způsobit zablokování nebo redundantní synchronizaci, což nepříznivě ovlivňuje výkon.

Viz také