Swift Task.init vs Task.detached

Mar 31, 2023#swift#concurrency

A Task in Swift is a unit of asynchronous work that can run concurrently with other tasks. A task can run in the background and communicate with the UI thread when needed. To create and run a task in Swift, you can use 2 following initializers:

  • Task.init creates a top-level task that runs on behalf of the current actor.
  • Task.detached creates an unstructured task that’s not part of the current actor.

An actor is a type that protects access to its mutable state by serializing all interactions with its properties and methods. The main difference between top-level tasks and unstructured tasks is that top-level tasks inherit the actor context and priority of the current task, while unstructured tasks do not.

Task.init

A task created by Task.init inherits the priority, task-local values, and actor context of the caller, which means it can access and modify the data and methods of the actor synchronously. However, it is not cancelled when its parent task is cancelled, so you need to keep a reference to it and cancel it manually if needed.

import Foundation

// Define a function that simulates a long-running operation
func longRunningOperation() async -> Int {
    // Sleep for 3 seconds
    await Task.sleep(3 * 1_000_000_000)
    // Return a random number
    return Int.random(in: 1...10)
}

// Define an actor that performs some calculations
actor Calculator {
    // Define a property to store the result
    var result = 0
    
    // Define a function that adds a number to the result
    func add(_ number: Int) {
        result += number
    }
    
    // Define a function that creates and runs an unstructured task using Task.init
    func runTask() {
        // Create a new task with the same priority and actor context as the caller
        Task {
            // Call the long-running operation and await its result
            let number = await longRunningOperation()
            print("Task finished with number \(number)")
            // Add the number to the result synchronously
            self.add(number)
            print("Result is now \(result)")
        }
    }
}

// Create an instance of the calculator actor
let calculator = Calculator()

// Call the runTask function on the actor
await calculator.runTask()

// Print a message after calling the function
print("Function called")

The output of this code is:

Function called
Task finished with number 7
Result is now 7

As you can see, the task created by Task.init runs asynchronously on the same actor as the caller, and can access and modify its data synchronously. The task also inherits the priority of the caller, which is not shown in this example. The task is not cancelled when the parent task finishes, which is why it prints its messages after the “Function called” message.

Task.detached

Swift detached tasks, created by Task.detached, are a way of creating and running asynchronous tasks that are not part of the structured concurrency hierarchy. They are independent from their parent context and do not inherit its priority, task-local storage, or cancellation behavior. Detached tasks are useful when you need to perform a task that is completely independent of the parent task and does not need to communicate with it or return any value to it.

Here is an example of using a detached task to download an image from a URL and cache it to disk:

import Foundation
import UIKit

// Define a struct that represents an image downloader
struct ImageDownloader {
    // Define a function that downloads an image from a URL
    func downloadImage(from url: URL) async throws -> UIImage {
        // Create a URL session
        let session = URLSession.shared
        // Await for the data task to complete
        let (data, _) = try await session.data(from: url)
        // Convert the data to an image
        guard let image = UIImage(data: data) else {
            throw ImageDownloadError.invalidData
        }
        // Return the image
        return image
    }
    
    // Define a function that caches an image to disk
    func cacheImage(_ image: UIImage, withName name: String) throws {
        // Get the cache directory URL
        let cacheDirectory = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        // Create a file URL with the name
        let fileURL = cacheDirectory.appendingPathComponent(name)
        // Convert the image to JPEG data
        guard let data = image.jpegData(compressionQuality: 0.8) else {
            throw ImageCacheError.invalidData
        }
        // Write the data to disk
        try data.write(to: fileURL)
    }
}

// Define some errors for image downloading and caching
enum ImageDownloadError: Error {
    case invalidData
}

enum ImageCacheError: Error {
    case invalidData
}

// Define a function that creates and runs a detached task to download and cache an image
func downloadAndCacheImage(from url: URL) -> Task<Void, Never> {
    // Create an instance of the image downloader
    let downloader = ImageDownloader()
    
    // Create and return a detached task that downloads and caches the image
    return Task.detached {
        do {
            // Download the image
            let image = try await downloader.downloadImage(from: url)
            print("Image downloaded")
            // Cache the image
            try downloader.cacheImage(image, withName: "image.jpg")
            print("Image cached")
        } catch {
            // Handle any errors
            print("Error: \(error)")
        }
    }
}

// Define a URL for an image
let imageURL = URL(string: "https://example.com/image.jpg")!

// Call the function to create and run the detached task
let task = downloadAndCacheImage(from: imageURL)

// Optionally cancel the task if needed
//task.cancel()

The output of this code is the same as before:

Image downloaded
Image cached

As you can see, the detached task runs independently from the main thread and does not return any value or communicate with it. It performs two asynchronous operations in sequence: downloading and caching the image. It also handles any errors that may occur during these operations.

You should be careful when using detached tasks, as they can introduce unstructured concurrency and make it harder to reason about your code. Detached tasks can also create retain cycles or memory leaks if they capture strong references to objects that are no longer needed. You should prefer using structured concurrency features like child tasks or async let bindings whenever possible.