Hur man multithreadar säkert och effektivt i .NET

0
149

Multithreading kan användas för att drastiskt påskynda programmets prestanda, men ingen speedup är gratis — hantering av parallella trådar kräver noggrann programmering, och utan rätt försiktighetsåtgärder kan du stöta på tävlingsförhållanden, dödlägen och till och med kraschar.

Vad gör multithreading svårt?

Om du inte säger något annat till ditt program körs all din kod på “ huvudtråden. ” Från startpunkten för din applikation kör den igenom och kör alla dina funktioner efter varandra. Detta har en gräns för prestanda, eftersom du uppenbarligen bara kan göra så mycket om du måste bearbeta allt en i taget. De flesta moderna processorer har sex eller fler kärnor med 12 eller fler trådar, så det finns prestanda kvar på bordet om du inte använder dem.

Det är dock inte så enkelt som att bara slå på multithreading. ” Endast specifika saker (t.ex. slingor) kan vara korrekt flertrådade, och det finns många överväganden att ta hänsyn till när du gör det.

Den första och viktigaste frågan är tävlingsförhållanden < /stark>. Dessa uppstår ofta under skrivoperationer, när en tråd ändrar en resurs som delas av flera trådar. Detta leder till beteende där programmets utgång beror på vilken tråd som avslutar eller ändrar något först, vilket kan leda till slumpmässigt och oväntat beteende.

Dessa kan vara väldigt, mycket enkla, till exempel kanske du behöver hålla en löpande räkning av något mellan slingorna. Det mest uppenbara sättet att göra detta är att skapa en variabel och öka den, men det här är inte trådsäkert.

Annonsering

Detta rasförhållande uppstår eftersom det inte bara är att lägga till en till variabeln ” i abstrakt bemärkelse; CPU: n laddar värdet på nummer i registret, lägger till ett till det värdet och lagrar sedan resultatet som det nya värdet för variabeln. Det vet inte att under tiden försökte en annan tråd också göra exakt samma sak och laddade in ett felaktigt värde som snart kommer att bli. De två trådarna strider mot varandra, och i slutet av slingan kan antalet inte vara lika med 100.

.NET tillhandahåller en funktion för att hantera detta: nyckelordet lås. Detta förhindrar inte att göra ändringar direkt, men det hjälper till att hantera samtidighet genom att bara tillåta en tråd i taget att få låset. Om en annan tråd försöker ange ett låsuttalande medan en annan tråd bearbetas, väntar den i upp till 300 ms innan han fortsätter.

Du kan bara låsa referenstyper, så ett vanligt mönster skapas ett låsobjekt i förväg och använda det som ett substitut för att låsa värdetypen.

< /p>

Du kan dock märka att det nu finns ett annat problem: blockeringar . Denna kod är ett värsta exempel, men här är det nästan exakt samma sak som att bara göra en vanlig loop (faktiskt lite långsammare, eftersom extra trådar och lås är extra overhead). Varje tråd försöker få låset, men bara en åt gången kan ha låset, så bara en tråd i taget kan faktiskt köra koden inuti låset. I det här fallet är det hela slingans kod, så låssatsen tar bort alla fördelar med trådning och gör allt långsammare.

Generellt vill du låsa efter behov när du behöver skriva. Du vill dock inte ha samma samtid när du väljer vad du vill låsa, för läsningar är inte alltid trådsäkra heller. Om en annan tråd skriver till objektet kan läsning av den från en annan tråd ge ett felaktigt värde eller orsaka att ett visst villkor ger ett felaktigt resultat.

Lyckligtvis finns det några knep för att göra detta ordentligt där du kan balansera hastigheten på multithreading när du använder lås för att undvika tävlingsförhållanden.

Använd förreglad för atomoperationer

För grundläggande operationer kan användning av låsuttrycket vara överkill. Även om det är mycket användbart för låsning före komplexa ändringar, är det för mycket overhead för något så enkelt som att lägga till eller ersätta ett värde.

Annonsering

Interlocked är en klass som omsluter vissa minnesoperationer som tillägg, byte och jämförelse. De underliggande metoderna implementeras på CPU -nivå och garanteras vara atomiska och mycket snabbare än standardlåset. Du kommer att vilja använda dem när det är möjligt, även om de inte helt kommer att ersätta låsning.

I exemplet ovan kommer det att snabba ut låset med ett samtal till Interlocked.Add () påskyndas operationen mycket. Även om detta enkla exempel inte är snabbare än att bara inte använda Interlocked, är det användbart som en del av en större operation och är fortfarande en speedup.

Det finns också Increment and Decrement för ++ och -operationer, vilket kommer att spara dig rejäla två tangenttryckningar. De slår bokstavligen Add (ref count, 1) under huven, så det finns ingen specifik hastighet för att använda dem.

Du kan också använda Exchange, en generisk metod som anger en variabel som är lika med värdet som skickas till den. Men du bör vara försiktig med det här — om du ställer in det till ett värde som du beräknade med det ursprungliga värdet, är detta inte trådsäkert, eftersom det gamla värdet kunde ha ändrats innan du körde låst .Exchange.

CompareExchange kommer att kontrollera två värden för jämlikhet, och byt ut värdet om de är lika.

Use Thread Safe Collections

Standardsamlingarna i System.Collections.Generic kan användas med multithreading, men de är inte helt trådsäkra. Microsoft tillhandahåller trådsäkra implementeringar av vissa samlingar i System.Collections.Concurrent.

Annonsering

Bland dessa ingår ConcurrentBag, en oordnad generisk samling och ConcurrentDictionary, en trådsäker ordlista. Det finns också samtidiga köer och staplar och OrderablePartitioner, som kan dela upp beställbara datakällor som listor i separata partitioner för varje tråd.

Look to Parallelize Loops < /h2>

Ofta är det enklaste stället att multitråda i stora, dyra slingor. Om du kan utföra flera alternativ parallellt kan du få en enorm hastighet under den totala körtiden.

Det bästa sättet att hantera detta är med System.Threading.Tasks.Parallel. Den här klassen tillhandahåller ersättningar för och för varje slinga som kör loopkropparna på separata trådar. Det är enkelt att använda, men kräver lite olika syntax:

Uppenbarligen är fångsten här att du måste se till att DoSomething () är trådsäkert och inte stör några delade variabler. Det är dock inte alltid så enkelt som att bara byta ut slingan mot en parallell slinga, och i många fall måste du låsa delade objekt för att göra ändringar.

För att lindra några av problemen med blockeringar ger Parallel.For och Parallel.ForEach extra funktioner för att hantera staten. I grund och botten kommer inte varje iteration att köras på en separat tråd — om du har 1000 element kommer det inte att skapa 1000 trådar; det kommer att göra så många trådar som din CPU kan hantera och köra flera iterationer per tråd. Det betyder att om du räknar en summa behöver du inte låsa för varje iteration. Du kan helt enkelt gå runt en subtotalvariabel och i slutet låsa objektet och göra ändringar en gång. Detta minskar drastiskt omkostnaderna på mycket stora listor.

Annonsering

Låt oss ta ett exempel. Följande kod tar en stor lista med objekt och måste seriera var och en separat till JSON och sluta med en List & lt; string & gt; av alla föremål. JSON -serialisering är en mycket långsam process, så att dela varje element över flera trådar är en stor snabbhet.

Det finns en massa argument och mycket att packa upp här:

  • Det första argumentet tar en IEnumerable, som definierar data som den går över. Detta är en ForEach -slinga, men samma koncept fungerar för grundläggande For loopar.
  • Den första åtgärden initierar den lokala subtotalvariabeln. Denna variabel kommer att delas över varje iteration av slingan, men bara inuti samma tråd. Andra trådar kommer att ha egna subtotaler. Här initierar vi det till en tom lista. Om du beräknade en numerisk summa kan du returnera 0 här.
  • Den andra åtgärden är huvudslingans kropp. Det första argumentet är det aktuella elementet (eller indexet i en For -loop), det andra är ett ParallelLoopState -objekt som du kan använda för att anropa .Break (), och det sista är subtotalsvariabeln.
    • I den här slingan kan du styra elementet och ändra delsumman. Värdet du returnerar kommer att ersätta delsumman för nästa slinga. I det här fallet serierar vi elementet till en sträng och lägger sedan till strängen i delsumman, som är en lista.
  • Slutligen tar den sista åtgärden delsumman ‘ resultat ’ efter att alla avrättningar har slutförts, så att du kan låsa och ändra en resurs baserat på den slutliga summan. Denna åtgärd körs en gång, i slutet, men den körs fortfarande på en separat tråd, så du måste låsa eller använda förreglade metoder för att ändra resurser. Här kallar vi AddRange () för att lägga till subtotallistan till den slutliga listan.

Unity Multithreading

One sista noten — om du använder Unity -spelmotorn vill du vara försiktig med multithreading. Du kan inte ringa några Unity API: er, annars kraschar spelet. Det är möjligt att använda det sparsamt genom att göra API -operationer i huvudtråden och växla fram och tillbaka när du behöver parallellisera något.

Oftast gäller detta operationer som interagerar med scenen eller fysiken motor. Vector3 -matematiken påverkas inte, och du kan använda den från en separat tråd utan problem. Du kan också ändra fält och egenskaper för dina egna objekt, förutsatt att de inte kallar några Unity -operationer under huven.