Swift
Thread Safety
Arrays
Concurrency
Multithreading

Create thread safe array in Swift

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

A plain Swift Array is not thread-safe for concurrent mutation. If multiple threads append, remove, or iterate while another thread writes, you can get data races and undefined behavior. The right fix is not to sprinkle random locking around call sites. It is to put ownership of the array behind one concurrency boundary and make all access go through that boundary.

Why Arrays Are Not Automatically Safe

Swift collections use value semantics, but that does not mean shared mutable access is magically safe. If multiple tasks can reach the same mutable storage, you still need synchronization.

The risk appears in patterns such as:

  • one queue appends while another reads
  • one thread removes while another iterates
  • multiple background tasks update shared state in a cache or store

Once shared mutable state exists, you need a serialization strategy.

The Modern Swift Approach: Use an actor

In modern Swift concurrency, an actor is usually the cleanest answer. An actor protects its internal mutable state so only one task accesses it at a time.

swift
1actor ThreadSafeArray<Element> {
2    private var storage: [Element] = []
3
4    func append(_ element: Element) {
5        storage.append(element)
6    }
7
8    func remove(at index: Int) -> Element {
9        storage.remove(at: index)
10    }
11
12    func getAll() -> [Element] {
13        storage
14    }
15
16    var count: Int {
17        storage.count
18    }
19}

Usage:

swift
1let numbers = ThreadSafeArray<Int>()
2
3Task {
4    await numbers.append(10)
5    await numbers.append(20)
6    print(await numbers.count)
7    print(await numbers.getAll())
8}

This is easier to reason about than manual locking because the isolation boundary is explicit in the type itself.

Why an Actor Is Better Than Ad Hoc Locks

Actors give you a clear ownership model. Callers do not need to know which lock to take or whether a read is safe during a write. They simply await access.

That prevents a class of bugs where one method forgets to lock or where different locks protect related state inconsistently.

The tradeoff is that actor access is asynchronous. If your codebase is not using Swift concurrency yet, you may need a lower-level approach.

A GCD-Based Alternative

If you need compatibility with older patterns, use a private dispatch queue. A concurrent queue with barrier writes is a common design.

swift
1final class LockedArray<Element> {
2    private var storage: [Element] = []
3    private let queue = DispatchQueue(label: "locked-array", attributes: .concurrent)
4
5    func append(_ element: Element) {
6        queue.async(flags: .barrier) {
7            self.storage.append(element)
8        }
9    }
10
11    func snapshot() -> [Element] {
12        queue.sync {
13            storage
14        }
15    }
16
17    var count: Int {
18        queue.sync {
19            storage.count
20        }
21    }
22}

Reads run concurrently through sync, while writes use a barrier so they execute exclusively.

This pattern works, but it is easier to misuse than an actor because call ordering and async writes can surprise you.

Synchronous Writes Versus Async Writes

Notice that the append method above uses queue.async. That means the append is scheduled, not completed immediately when the method returns.

If the caller expects the value to exist right away, use queue.sync(flags: .barrier) instead:

swift
1func appendNow(_ element: Element) {
2    queue.sync(flags: .barrier) {
3        storage.append(element)
4    }
5}

This is an important design choice. A thread-safe API must also be semantically clear about when changes become visible.

Avoid Returning Internal Mutable Storage

Even if access is synchronized, returning a direct mutable reference to internal storage breaks the abstraction. Prefer returning a snapshot copy.

That is why snapshot() or getAll() style APIs are safer than exposing the raw array itself. The moment callers can mutate the internal collection outside the protection boundary, thread safety is gone.

Sometimes You Do Not Need a Thread-Safe Array

A lot of code reaches for a thread-safe wrapper when the better design is message passing or task ownership. If one actor, queue, or service owns the data completely, you may not need a shared thread-safe collection at all.

That is often the better architecture:

  • one owner of mutable state
  • other components send requests
  • no arbitrary shared writes

The wrapper should solve a real sharing problem, not hide a design problem.

Common Pitfalls

The most common mistake is thinking that reads are always safe while writes are synchronized. They are not. Unsynchronized reads during mutation are still races.

Another issue is mixing direct array access with wrapped access. One unlocked path is enough to invalidate the whole design.

Be careful with asynchronous barrier writes if callers assume immediate consistency.

Finally, do not expose iteration over live mutable state without defining how concurrent mutation is handled. Snapshot semantics are usually safer.

Summary

  • Swift Array is not safe for concurrent mutation by default.
  • In modern Swift, an actor is usually the cleanest thread-safe wrapper.
  • A private DispatchQueue with barrier writes is a workable lower-level alternative.
  • Define whether writes are synchronous or asynchronous so visibility is clear.
  • Return snapshots rather than exposing internal mutable storage.
  • Prefer clear ownership of state over shared mutable collections when possible.

Course illustration
Course illustration

All Rights Reserved.