DEV Community

Priya Raman
Priya Raman

Posted on • Updated on

Swift - Structured Concurrency - Notes about Task

Task - Independent chunk of work in async context.

  • async let doSomething = funcCall() is syntactic sugar for a creating a task underneath + await result.

  • Task Priority - High[~userInitiated], medium to low[~utility] and then, background.
    -- Child task inherits from parent, UI task from Main thread inherits userInitiated, if neither, gets nil or Swift queries to figure out the priority.
    -- Task.currentPriority gives current priority

  • Task priority escalation scenarios.
    -- Task B with low priority gets invoked by Task A with high priority, Task B priority gets elevated to Task A's priority. Task.currentPriority for Task B is also high.
    -- Task B with low priority is getting executed by an actor which has enqueued Task A with high priority, Task B priority gets elevated to Task A's priority. Task.currentPriority is still the same but executes faster though.

  • Task Syntax

let fetchTask = Task(priority: .high) {() -> returnValueType  in 
try ...
return returnValueType
} 
do {  
  let val = try await fetchTask.value // .value gives the   
                                      return value.
} catch {
  print("Error")
}
Enter fullscreen mode Exit fullscreen mode
  • Inferences: -- try await task.value gives the return value with specified return type as part of Task closure. -- Another way to retrieve value is through .result and .get(), it returns the Result struct with Return type and Error as values, so, let result = await task.result() but it could be TRIED when the result is accessed as let val = try result.get(), this is helpful to the caller as it could decide when exactly to throw and deal with the error and result leading to just pass around the encapsulated result as a whole to the caller chain up further. -- Tasks get kickstarted the moment we initialize, it runs concurrently with other code as below. fetchUpdates Started 14:46:48:1850 - actual async function call headlinesTask Started 14:46:48:1850 ScoreTask Started 14:46:48:1850 scoreTask Value Started 14:46:48:1850 headlinesTask API Ended 14:46:48:2120 ScoreTask API ended 14:46:48:2120 scoreTask Value Ended 14:46:48:2130 headlinesTask Value Started 14:46:48:2130 headlinesTask Value Ended 14:46:48:2130 fetchUpdates Ended 14:46:48:2520 We can see that tasks started at the same time and value retrieval was hit at the same time. once API ended, value retrieval ended and moved on to 2nd value retrieval.
  • @State property could be accessed from any thread, the view will be refreshed on Main thread.
  • Task and .taskmodifier to view - modifier is intelligent enough to be bound to life time of the view so, will be cancelled after view is moved out of memory. Task runs to completion until its either complete or cancelled. if async API call is attached to .task modifier, and if view tends to get recreated, API gets called though with URLSession, it might be cached. Dynamic identifier conforming to Equatable can be applied to .task modifier, so API could get called based on equatable value.
  • Task Cancellation
    -- Task.isCancelled -> Bool Static method within Task definition
    -- try Task.checkCancellation : Static method within Task definition that throws TaskCancellation Error.
    -- .cancel() method on task instance.
    -- URLSession throws cancellation Errors if call is cancelled.

  • Task vs detachedTask
    -- Task/ detachedTask is differentiated by its priority, local values and actor context
    --- Priority: Task is inherently defined by its context in which its executed, meaning it inherits the parent priority, if button action initiates the task, it gains userinitiated priority and runs the task with that priority. WHILE, Task.detached does not have any parent to inherit priority so, its nil or something we prescribe.
    --- local values: defined as static types within task letting us share the values within the specific task. WHILE, Task.detached does not have any parent to inherit local values so, no local values will be inherited.
    ---- Task Local value: Struct/Class/Enum/Actor with static dataType associated @TaskLocal property wrapper. It can have different meaning ~Values according to the Task in which its associated with, similar to local and global variables scope. Task starts and ends with try await Type.$staticVar.withValue(customVal) and can be nested as well, with global scope still retaining original declared value.
    --- actor isolation: When a task is defined within actor,its isolated to that actor - meaning other parts of the code could be used synchronously within the task. Everything within a task executes sequentially and synchronously though it might entail other API calls. Task has the control of code execution flow.WHILE, Task.detached does not have any parent or any control over how the code execution flows, so, within detachedTask, any code run requiring async context runs concurrently, so, to sequence the task, we would have to await the async function call to make it the way we want the task.detached to run. As it does not have a parent, it has greater restriction towards executing other pieces of code even within that actor.

  • Task Sleep - try await Task.sleep(nanoSeconds: 3_000_000_000) , requires try as if the task is cancelled, it will throw a cancellation error inherently, requires await as it causes the execution to be suspended. It wont block the underlying thread, it will go on to execute something else.

  • Voluntarily suspend Task - Task.yield() , gives Swift a chance to run other priority tasks if present by suspending current task. This is a suggestion to Swift runtime, so, the current task could or need not be suspended.

My Notes as understood from HackingWithSwift Structured Concurrency series.

Top comments (0)