DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • A Developer’s Guide to Multithreading and Swift Concurrency
  • Building a Skill-Based Agentic Reviewer with Claude Code: A Practical Guide Using Skills.MD, MCP Servers, Tools, and Tasks
  • Stop Using the ATM-Didn’t-Kill-Jobs Story to Reassure Developers About AI
  • AI Agents vs LLMs: Choosing the Right Tool for AI Tasks

Trending

  • Docker Hardened Images Are Free Now — Here's What You Still Need to Build
  • Why SAP S/4HANA Landscape Design Impacts Cloud TCO More Than Compute Costs
  • How to Prevent Data Loss in C#
  • How AI Is Rewriting Full-Stack Java Systems: Practical Patterns with Spring Boot, Kafka and WebSockets
  1. DZone
  2. Coding
  3. Languages
  4. Swift Concurrency, Part 2: Parent/Child Relationship, Automatic Cancellation, Task Groups

Swift Concurrency, Part 2: Parent/Child Relationship, Automatic Cancellation, Task Groups

Learn all about structured concurrency in Swift: parent/child relationship, automatic cancellation, task groups, and more.

By 
Nikita Vasilev user avatar
Nikita Vasilev
·
Apr. 10, 26 · Analysis
Likes (2)
Comment
Save
Tweet
Share
2.7K Views

Join the DZone community and get the full member experience.

Join For Free

In part one of this series, we considered the foundation of Swift concurrency: a multithreading technique that underlies Swift concurrency, the definition of the Task, and the difference between Task, Task.detached, and managing priorities. If you haven’t read this, check it out here. In this part, we are going to explore Structured Concurrency, the relationship between tasks, how to execute multiple simultaneous tasks, working with TaskGroup, and more.

Lightweight Structured Concurrency

In the first part, we considered how the await is a potential suspension point that stops the current execution and brings control to another object until it finishes. But if operations are independent and can be run simultaneously.

Swift concurrency provides a simple solution for this case. Let’s imagine that we need to perform multiple requests that aren’t dependent on each other; for this, we can use async let construction.

Swift
 
func loadData() async throws {
    async let profile = try await userService.profile()
    async let configuration = try await configurationService.configuration()

    dashboardService.load(await profile, await configuration)
}


In this case, these two operations will run simultaneously. The dashboardService will wait for the results of the previous requests before proceeding.

Task Cancellation

One important aspect of working with concurrency is the ability to cancel operations. Swift concurrency provides built-in support for cancelling tasks, allowing developers to efficiently manage resources and respond to changing program conditions. In this section, we will explore how task cancellation works in Swift.

If one of these operations runs into an error, all of them will be cancelled. If you want to manually cancel these operations, you can call the cancel() method on Task.

Swift
 
let task = Task { [weak self] in
  self?.loadData()
}
task.cancel()


The cancel() method doesn’t immediately stop an operation. It works similarly to cancel in OperationQueue: it marks a task as cancelled, but you must handle this case manually.

Swift
 
Task {
    let task = Task {
        if Task.isCancelled {
            print("Task is cancelled, \(Thread.currentThread)")
        }

        print("Starting work on: \(Thread.currentThread)")
        try? await Task.sleep(nanoseconds: NSEC_PER_SEC)
        print("Still running? Cancelled: \(Task.isCancelled)")
    }
    
    task.cancel()
}

// Task is cancelled, <_NSMainThread: 0x600000ff0580>{number = 1, name = main}
// Starting work on: <_NSMainThread: 0x600000ff0580>{number = 1, name = main}


To handle task cancellation, you can either check the Task.isCancelledproperty or call try Task.checkCancellation(), which throws a general cancellation error that can be propagated to the user.

Swift
 
Task {
  let task = Task {
    print("Task is cancelled: \(Task.isCancelled), \(Thread.current)")
    if Task.isCancelled {
      print("Task was cancelled sorry, \(Task.isCancelled)")
    } else {
      try? await Task.sleep(nanoseconds: NSEC_PER_SEC * 1)
      print("Job done \(Thread.current)")
    }
  }

  task.cancel()

// Task is cancelled: true, <_NSMainThread: 0x600002164580>{number = 1, name = main}
// Task was cancelled sorry, true
}


Swift
 
Task {
  let task = Task {
    print("Task is cancelled: \(Task.isCancelled), \(Thread.current)")
    do {
      try Task.checkCancellation()
      try? await Task.sleep(nanoseconds: NSEC_PER_SEC * 1)
      print("Job done \(Thread.current)")
    } catch {
      print("\(error), \(Thread.current) ")
    }
    print("end of task \(Thread.current)")
  }

  task.cancel()

// Task is cancelled: true, <_NSMainThread: 0x600000ec4580>{number = 1, name = main}
// CancellationError(), <_NSMainThread: 0x600000ec4580>{number = 1, name = main} 
// end of task <_NSMainThread: 0x600000ec4580>{number = 1, name = main}
}


In this example, the inner task does not automatically respond to cancellation:

Swift
 
Task {
  let parentTask = Task {
    print("Parent is cancelled: \(Task.isCancelled), \(Thread.currentThread)")

    Task {
      print("Nested task is cancelled: \(Task.isCancelled), \(Thread.currentThread)")
    }
  }

  parentTask.cancel()
}

// Parent is cancelled: true, <_NSMainThread: 0x600001960580>{number = 1, name = main}
// Nested task is cancelled: false, <_NSMainThread: 0x600001960580>{number = 1, name = main}


Here, we create a task and launch another Task inside it. When we call task.cancel(), the outer task is marked as cancelled, but the inner task continues to run. This happens because cancellation does not automatically propagate to nested tasks.

Swift
 
Task {
    let task = Task {
        await withTaskCancellationHandler {
            try? await Task.sleep(nanoseconds: NSEC_PER_SEC * 1)
            print("Print job done: \(Task.isCancelled), \(Thread.currentThread)")
        } onCancel: {
            print("Task cancelled: \(Task.isCancelled), \(Thread.currentThread)")
        }
    }
    
    task.cancel()
}

// Task cancelled: true, <_NSMainThread: 0x600003d80580>{number = 1, name = main}
// Print job done: true, <_NSMainThread: 0x600003d80580>{number = 1, name = main}


TaskLocal

Task-local values allow you to store and access data within the scope of specific tasks. This can be useful when you need to propagate contextual information — for example, tracking a user role, request ID, or configuration setting across a chain of tasks without explicitly passing it as a parameter.

Just like task priorities, detached tasks do not inherit the task context. That’s why task-local values revert to their default state when accessed from a detached task.

Swift
 
private enum Context {
  @TaskLocal static var locale: String = "en_US"
}

func performTask() async {
  print("Outer locale: \(Context.locale)")

  Task {
    print("Before change: \(Context.locale)")

    Context.$locale.withValue("fr_FR") {
      print("Within withValue: \(Context.locale)")

      Task.detached {
        print("Detached locale: \(Context.locale)")
      }
    }
  }
}

// Outer locale: en_US
// Before change: en_US
// Within withValue: fr_FR
// Detached locale: en_US


In this example, the task-local variable locale initially has the value “en_US.”

When the value is temporarily overridden with “fr_FR” using withValue(_:), the new value is visible only within that specific task hierarchy. However, when a detached task is created, it doesn’t inherit this context and therefore prints the default “en_US” role again.

Task Group

Task groups are useful when dealing with a dynamic number of tasks.

Unlike async let, which is designed for a fixed number of concurrent tasks known at compile time, task groups allow you to create and manage tasks dynamically — for example, when processing a collection of items or running parallel network requests of unknown count.

Conceptually, a TaskGroup is similar to a DispatchGroup, but with native support for Swift concurrency features such as structured cancellation, error propagation, and task priorities. All child tasks in the group inherit the priority of their parent task, unless explicitly overridden.

Swift
 
Task(priority: .background) {
  await withTaskGroup(of: Void.self) { group in
    for i in 0..<5 {
      let p: TaskPriority = i % 2 == 0 ? .high : .low

      group.addTask(priority: p) {
        print("\(i), p: \(Task.currentPriority), base: \(Task.basePriority)")
      }
    }
  }
}

// 0, p: TaskPriority.background, base: Optional(TaskPriority.high)
// 1, p: TaskPriority.background, base: Optional(TaskPriority.low)
// 2, p: TaskPriority.background, base: Optional(TaskPriority.high)
// 3, p: TaskPriority.background, base: Optional(TaskPriority.low)
// 4, p: TaskPriority.background, base: Optional(TaskPriority.high)


In this example, a parent task with .background priority creates several child tasks inside a group. Each child task explicitly sets its own priority (.high or .low), which can be observed through Task.currentPriority and Task.basePriority.

This demonstrates how task groups provide flexible and dynamic control over concurrent workloads, while maintaining structured task management and cancellation behavior.

Conclusion

In this article, we explored the core ideas behind structured concurrency in Swift from lightweight parallelism with async let, to advanced techniques like TaskGroup, TaskLocal, and cooperative cancellation.

Swift’s concurrency model provides not only performance and safety, but also a clean, declarative way to reason about concurrent code. Each feature — whether it’s Task, TaskGroup, or @TaskLocal — is designed to work seamlessly together under the same structured model, ensuring that your asynchronous operations remain predictable and maintainable.

Swift (programming language) Task (computing)

Published at DZone with permission of Nikita Vasilev. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • A Developer’s Guide to Multithreading and Swift Concurrency
  • Building a Skill-Based Agentic Reviewer with Claude Code: A Practical Guide Using Skills.MD, MCP Servers, Tools, and Tasks
  • Stop Using the ATM-Didn’t-Kill-Jobs Story to Reassure Developers About AI
  • AI Agents vs LLMs: Choosing the Right Tool for AI Tasks

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook