Your resource for web content, online publishing
and the distribution of digital products.
S M T W T F S
 
 
 
1
 
2
 
3
 
4
 
5
 
6
 
7
 
8
 
9
 
 
 
 
 
 
 
 
 
 
19
 
20
 
21
 
22
 
23
 
24
 
25
 
26
 
27
 
28
 
29
 
30
 
31
 
 

Synchronization Challenges in Multithreading

DATE POSTED:January 17, 2025

I am Boris Dobretsov, and this is the sixth part of a series Understanding Parallel Programming: A Guide for Beginners.

\ If you haven’t read the first five parts, have a look at Understanding Parallel Programming: A Guide for Beginners, Understanding Parallel Programming: A Guide for Beginners, Part IIUnderstanding Threads to Better Manage Threading in iOSUnderstanding Parallel Programming: Thread Management for Beginners, How Grand Central Dispatch Library Helps Organise Threads.

\ Executing multiple code blocks in parallel may seem straightforward. However, writing concurrent code is often regarded as one of the most challenging aspects of programming. The complexity appears not only from running tasks in separate threads but also from ensuring they interact correctly with shared data. In this article, we’ll explore ==real-world scenarios and their solutions==.

Offloading Intensive UI Tasks to the Background Thread

A common problem in mobile development is offloading heavy computations from the main thread to prevent UI freezes. Typically, these tasks don't require synchronization with others but must reflect their results back in the main thread.

\ Consider this example: rendering a table view with images, applying a blur effect to each image.

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "SomeCell", for: indexPath) let inputImage = UIImage(named: "treeSmall")! let inputCIImage = CIImage(image: inputImage)! let blurFilter = CIFilter(name: "CIGaussianBlur", parameters: [kCIInputImageKey: inputCIImage])! let outputImage = blurFilter.outputImage! let context = CIContext() let cgiImage = context.createCGImage(outputImage, from: outputImage.extent) cell.imageView?.image = UIImage(cgImage: cgiImage!) return cell }

\ Running this will result in slow loading and laggy scrolling, even with just 10 rows and small images. Why? Applying a blur effect is computationally expensive. Let's refactor this to offload the task to a background thread:

func blur(image: UIImage, for imageView: UIImageView) { DispatchQueue.global(qos: .userInteractive).async { let inputCIImage = CIImage(image: image)! let blurFilter = CIFilter(name: "CIGaussianBlur", parameters: [kCIInputImageKey: inputCIImage])! let outputImage = blurFilter.outputImage! let context = CIContext() let cgiImage = context.createCGImage(outputImage, from: outputImage.extent) let blurredImage = UIImage(cgImage: cgiImage!) DispatchQueue.main.async { imageView.image = blurredImage } } }

This eliminates lag and ensures the UI updates are performed in the main thread, adhering to threading rules.

Waiting for Parallel Tasks to Complete

Sometimes, we need to perform an action only after several parallel tasks have finished. Using our earlier example, the blur operation could trigger repeatedly during scrolling. To fix this, we'll blur all images in parallel and update the table view once all tasks are complete.

var images = Array(repeating: UIImage(named: "treeSmall")!, count: 10) func blurImages() { let dispatchGroup = DispatchGroup() var blurredImages = images for (index, image) in images.enumerated() { dispatchGroup.enter() DispatchQueue.global().async { let inputCIImage = CIImage(image: image)! let blurFilter = CIFilter(name: "CIGaussianBlur", parameters: [kCIInputImageKey: inputCIImage])! let outputImage = blurFilter.outputImage! let context = CIContext() let cgiImage = context.createCGImage(outputImage, from: outputImage.extent) blurredImages[index] = UIImage(cgImage: cgiImage!) dispatchGroup.leave() } } dispatchGroup.notify(queue: .main) { self.images = blurredImages self.tableView.reloadData() } }

\

Race Conditions: The Subtle Bug Lurking in Parallel Code

Race conditions occur when multiple threads access shared data without proper synchronization, leading to unpredictable behavior. Let’s explore a simple example of assigning unique IDs to documents.

\ Here’s a straightforward implementation:

struct Document: CustomStringConvertible { let id: Int let name: String var description: String { "\(id) - \(name)" } } for charCode in UnicodeScalar("A").value...UnicodeScalar("Z").value { let docName = String(UnicodeScalar(charCode)!) let lastId = documents.last?.id ?? 0 documents.append(Document(id: lastId + 1, name: docName)) }

In a multithreaded setup, the above approach fails as multiple threads simultaneously read the same lastId. To fix this, we’ll use a thread-safe document store.

Implementing a Thread-Safe Data Structure

We’ll create a thread-safe document store using DispatchQueue.

class DocumentStore: CustomStringConvertible { private var documents = [Document]() private let syncQueue = DispatchQueue(label: "syncQueue", attributes: .concurrent) func append(document: Document) { syncQueue.async(flags: .barrier) { self.documents.append(document) } } var description: String { var result = "" syncQueue.sync { result = self.documents.map { $0.description }.joined(separator: ", ") } return result } }

The .barrier flag ensures write operations are isolated, while reads can occur in parallel. Using this, our document generation works reliably.

\ By combining these techniques, you can tackle synchronization challenges, optimize performance, and prevent vague bugs like race conditions.

\ Mastering concurrency takes time and effort, but practice and experimentation are your strongest allies on this journey!