Grand Central Dispatch (GCD) är en kraftfull teknik för att hantera parallell och seriell exekvering av uppgifter i en iOS- eller macOS-applikation. Genom att använda olika typer av köer kan man effektivt styra exekveringen av uppgifter utan att blockera huvudtråden, vilket är avgörande för att upprätthålla en responsiv användarupplevelse. Här kommer vi att titta närmare på hur parallella och seriella köer fungerar och hur de kan användas i praktiken.
En parallell kö tillåter att flera uppgifter exekveras samtidigt om systemresurserna tillåter det. När vi lägger till uppgifter i en parallell kö kan de exekveras i vilken ordning som helst, beroende på systemets kapacitet och tillgång till resurser. Om vi till exempel skapar en parallell kö och lägger till tre uppgifter med olika beräkningslängder, kommer de inte nödvändigtvis att slutföras i den ordning de lagts till, även om de började exekveras i den ordningen.
Exempel på kod för att skapa en parallell kö och lägga till uppgifter:
I detta exempel ser vi att varje uppgift skickas till den parallella kön och körs samtidigt, vilket gör att beräkningsuppgifterna kan slutföras i olika ordning. Resultaten kan vara som följer:
Här ser vi att trots att "async1" startade först, avslutades den sist. Detta beror på att det är en parallell kö där uppgifter kan exekveras samtidigt om resurserna tillåter det.
En seriell kö, å andra sidan, tillåter endast en uppgift att exekveras i taget. När en uppgift har slutförts, börjar den nästa i kön. Det betyder att alla uppgifter exekveras i exakt den ordning de lagts till. Seriella köer är användbara när vi har uppgifter som måste utföras i sekventiell ordning, exempelvis vid uppdatering av användargränssnittet eller när vi behöver hantera resurser som inte är trådsäkra.
Exempel på kod för att skapa en seriell kö och lägga till uppgifter:
I detta fall kommer uppgifterna att exekveras i den ordning de lagts till, även om vissa uppgifter tar längre tid att köra än andra. Resultatet kan se ut så här:
Det är tydligt att den första uppgiften ("async1") slutförs sist, eftersom den är den första som började och den enda som får köras åt gången.
En viktig aspekt av att använda DispatchQueue.main.async(execute:) är när vi behöver utföra uppgifter på huvudtråden, till exempel uppdateringar av användargränssnittet. Eftersom huvudtråden är en seriell kö är det avgörande att inte blockera den med tunga beräkningsoperationer, vilket kan göra användargränssnittet oresponsivt. Om vi har en tung uppgift som bildbehandling eller nätverksanrop bör dessa utföras på en bakgrundstråd, och endast UI-uppdateringarna bör göras på huvudtråden.
Exempel på kod för att uppdatera UI från en bakgrundstråd:
Här utförs bildstorleksändringen på en bakgrundstråd, vilket förhindrar att huvudtråden blockeras och gör appen mer responsiv. När bildstorleken är ändrad, uppdateras UIImageView med det nya resultatet på huvudtråden.
En annan viktig aspekt att överväga är skillnaden mellan async och sync metoderna i GCD. Den största skillnaden är att async metoden inte blockerar den nuvarande tråden, medan sync metoden blockerar den tills uppgiften är färdig. Medan async oftast är att föredra, kan det finnas situationer där sync är användbart, till exempel när vi vill säkerställa att en tråd exekverar en uppgift innan den fortsätter med något annat.
För att sammanfatta: valet mellan parallella och seriella köer i GCD beror på vilken typ av uppgifter som ska exekveras och om det är viktigt att exekveringen sker i ordning eller parallellt. Seriella köer är bäst för uppgifter som behöver köras i sekventiell ordning, medan parallella köer gör det möjligt att köra flera uppgifter samtidigt, vilket kan förbättra prestanda om systemresurserna tillåter det.
Hur kan vi effektivt använda och hantera uppgifter i Swift?
I Swift används uppgifter (tasks) för att hantera asynkrona operationer, där kod utförs parallellt med huvudtråden. När vi skapar en uppgift genom att använda Task { }, körs den tillhandahållna koden asynkront. Det är viktigt att förstå att Swift inte nödvändigtvis skapar en ny tråd för varje uppgift. I stället hanteras exekveringskontexten av Swift’s runtime-system, vilket innebär att uppgifter kan köras utan att explicit behöva skapa nya trådar.
Ett grundläggande exempel på en uppgift kan se ut så här:
I detta exempel anropas funktionen retrieveUserData() precis som tidigare, men nu inom ramen för en uppgift. För en enkel asynkron operation kan detta verka överflödigt, men uppgifter ger oss mer kontroll över vårt asynkrona flöde. En särskilt användbar funktion är att vi kan "detacha" en uppgift, vilket innebär att den inte längre är bunden till den ursprungliga uppgiften.
Vad innebär att detacha en uppgift?
När vi detachar en uppgift skapar vi en oberoende uppgift som inte är kopplad till den ursprungliga. Detta är användbart när vi vill köra en operation utan att vara beroende av den kontext där uppgiften skapades. Här är ett exempel på hur vi kan göra detta:
Denna uppgift är oberoende, och den kommer inte att ärva kontexten eller aktören från den ursprungliga uppgiften. Eftersom den opererar utanför den aktuella kontexten kan den inte åtkomst till delad data från den ursprungliga aktören, vilket gör den användbar för arbetsuppgifter som inte behöver åtkomst till aktörens gemensamma tillstånd.
Swift använder en kooperativ trådpool för att hantera uppgifter, vilket innebär att systemet själv optimerar utförandet av uppgifter över tillgängliga trådar, utan att vi explicit behöver skapa och hantera trådar.
Avbrytning av uppgifter
En annan viktig funktion hos Swift-uppgifter är möjligheten att avbryta dem. Avbrytning sker genom en kooperativ metod, där uppgiften kan fortsätta köra tills den når en lämplig punkt för att stoppa. Detta gör det möjligt för uppgiften att göra nödvändig städning innan den avslutas. För att kontrollera om en uppgift har avbrutits använder vi egenskapen Task.isCancelled.
I följande exempel avbryts uppgiften manuellt efter en viss tid, och vi ser att de återstående iterationerna avbryts direkt:
I koden ovan, när cancel() anropas, avbryts uppgiften efter tre iterationer. Resten av iterationerna körs omedelbart eftersom alla barnuppgifter, som Task.sleep(nanoseconds:), också avbryts.
Om vi vill kontrollera om uppgiften har avbrutits vid varje iteration kan vi använda Task.isCancelled för att utföra eventuell städning och avsluta uppgiften på ett ordnat sätt:
Här ser vi att när uppgiften avbryts, städas resurser upp och uppgiften avslutas utan att köra de återstående iterationerna.
Skrivande av funktioner för att hantera avbrutna uppgifter
När en uppgift avbryts kan vi också använda ett undantag för att signalera att uppgiften har avbrutits. Detta kan vara användbart när vi vill hantera avbrott utanför själva uppgiften. Här är ett exempel på hur vi omstrukturerar koden för att använda en funktion och kasta ett undantag när uppgiften avbryts:
När vi anropar denna funktion inuti en uppgift och fångar undantaget, kan vi hantera städningen utanför uppgiften:
Synkront startande av uppgifter
Swift 6.2 introducerade en funktion som gör det möjligt att starta en uppgift omedelbart, vilket ger mer kontroll över när uppgiften faktiskt börjar exekvera. För att begära att en uppgift ska starta omedelbart, kan vi använda Task.immediate:
Det är viktigt att förstå att detta inte garanterar att uppgiften verkligen börjar exekvera omedelbart, men systemet försöker starta den så snabbt som möjligt.
Namngivning av uppgifter
För att underlätta felhantering och övervakning har Swift också infört möjlighet att ge uppgifter namn. Detta är särskilt användbart vid felsökning. Här är ett exempel på hur vi kan namnge en uppgift:
Genom att namnge uppgifterna kan vi enkelt identifiera dem vid debugging eller i loggar, vilket förbättrar översikten över systemets aktiviteter.
Det är också möjligt att använda namn när vi skapar uppgifter inom uppgiftsgrupper, vilket vi kommer att utforska i nästa avsnitt.

Deutsch
Francais
Nederlands
Svenska
Norsk
Dansk
Suomi
Espanol
Italiano
Portugues
Magyar
Polski
Cestina
Русский