Using Threads in Swift

Swift provides DispatchQueue as an excellent layer above raw threads. But sometimes you want to use a low-level thread API

Alex Dremov

Swift provides DispatchQueue as an excellent layer above raw threads. But sometimes you want to create a new thread dedicated to some specific task. Or maybe implement your own concurrent executor. Swift gives you access to raw threads and in this article, I'll show how to use it.

Thread

Creating a thread in Swift is pretty simple using Thread class. You can either specify objc function through a selector as a starting point, or pass a closure, and, more convenient way, subclass Thread.

class MyThread: Thread {
    override func main() { // Thread's starting point
        print("Hi from thread")
    }
}

let thread = MyThread()
thread.start()
Simple thread

The thread is not started when the initializer is called. You need to call start()  method explicitly to start the thread.

The thread runs despite its handle returned by Thread initializer. That's it — the variable can no longer exist and the thread will still run. That's fine, but you will lose the ability to control the thread: check if it's completed, wait for its completion, cancel it, etc.

Wait for completion, join a thread

Swift does not provide a way to wait for the thread's completion.

💡
The main thread can finish before the new thread. In this case, the latter is also terminated

To wait for thread completion, we can join threads using DispatchGroup

class MyThread: Thread {
    let waiter = DispatchGroup()

    override func start() {
        waiter.enter()
        super.start()
    }

    override func main() {
        task()
        waiter.leave()
    }

    func task() {
        print("Hi from thread")
    }

    func join() {
        waiter.wait()
    }
}

let thread = MyThread()
thread.start()

thread.join() // Waits for thread completion

Terminate the thread

The thread terminates automatically after reaching main's end. To exit the thread in advance, you can call Thread.exit() function from the thread. To use it correctly with created DispatchGroup, it's better to create a custom exit method:

class MyThread: Thread {
    ...
	func exit() {
        waiter.leave()
        Thread.exit()
    }
    ...
}
Subscribe and don't miss posts!

Cancel the thread

Apart from terminating the thread, you can cancel it, by calling cancel() method on the thread's handle or inside the thread itself. This sets isCancelled property to true.

For this feature to work, in the thread you need to check this flag periodically and then call exit if the flag is true.

💡
Calling cancel does not stop the thread but rather notifies it that it must be stopped. You can even ignore it, but it's not a good practice

In the example below, we can use cancel to notify the thread about a timeout.

class MyThread: Thread {
	let waiter = DispatchGroup()

	override func start() {
		waiter.enter()
		super.start()
	}

	override func main() {
		task()
		waiter.leave()
	}

	func exit() {
		waiter.leave()
		Thread.exit()
	}

	func task() {
		let start = Date.now
		for _ in 0...100500 {
			if isCancelled {
				let seconds = Double(Date.now.timeIntervalSince(start))
				print("Cancelled after \(seconds) seconds")
				exit()
			}
			Thread.sleep(forTimeInterval: 0.01) // Long task
		}
	}

	func join() {
		waiter.wait()
	}
}

let thread = MyThread()
DispatchQueue.global().asyncAfter(
	deadline: .now().advanced(by: .seconds(5))) {
	thread.cancel()
}
thread.start()
thread.join()

Output: Cancelled after 5.509860992431641 seconds

Concurrency must be safe

Mind that in case of using several threads, shared data and structures must be thread-safe.

I recently released an article on Actors model in Swift. Actors is an architectural approach to concurrency

Conquer Data Races with Swift Actors | Alex Dremov
Unleash the power of Swift concurrency with Actors! Get all the information you need in this comprehensive article

Use cases

  • A long-running task in your app

If there is a long-running task in your app, then consider creating a dedicated thread for it.  

  • Creating concurrent executors

Swift's DispatchQueue and OperationQueue are powerful tools, but even their functions are limited.

For example, there is no strand executor or explicit thread pool.

  • Writing your own Thread Pool as a practice and a pet-project

Why not? The best way to understand how DispatchQueue works is to write your own!

Finally

Check out my quick guide to async/await in Swift to get a better grip on concurrency in Swift.

Quick Guide to Async Await in Swift | Alex Dremov
Everything you need to know about new Swift asynchronous features. Async await, main actor, task, async get, and possible use cases — all covered.

Also, the iOS section of my blog has cool staff about iOS and Swift development. Check it out!

Alex Dremov | iOS
One of my favorites. Here I write about Swift and iOS development

Share

Subscribe to Alex Dremov

Get the email newsletter and receive valuable tips to bump up your professional skills