kosikowski/swift-executors
A Swift library providing custom task executors for fine-grained control over concurrency and thread management in Swift applications.
Overview
Swift Executors provides three main executor types that allow you to control how Swift tasks are executed:
- QueueTaskExecutor: A task executor backed by
NSOperationQueuefor controlling concurrency and prioritizing work - DispatchQueueTaskExecutor: A task executor backed by Grand Central Dispatch (GCD) for efficient thread management and performance
- ThreadExecutor: A serial executor that pins every actor job to a dedicated thread for thread-affinity requirements
Features
- Concurrency Control: Limit the number of concurrent operations with
QueueTaskExecutor - GCD Integration: Leverage Grand Central Dispatch with
DispatchQueueTaskExecutorfor efficient thread management - Thread Affinity: Pin tasks to specific threads with
ThreadExecutorfor frameworks requiring thread-local storage - Quality of Service: Configure QoS levels for priority handling
- Swift Concurrency Integration: Seamlessly works with Swift's async/await and structured concurrency
- Cross-Platform: Supports iOS 18+ and macOS 15+
Installation
Swift Package Manager
Add the following dependency to your Package.swift:
dependencies: [
.package(url: "https://github.com/Kosikowski/swift-executors.git", from: "1.0.0")
]Or add it to your Xcode project:
- File → Add Package Dependencies
- Enter the repository URL
- Select the version you want to use
Usage
QueueTaskExecutor
Use QueueTaskExecutor when you need to control concurrency, prioritize work, or isolate blocking operations from Swift's cooperative thread pool.
import SwiftExecutors
// Create an executor for file I/O operations
let ioExecutor = QueueTaskExecutor(
label: "File-IO",
maxConcurrent: 4, // Limit to 4 concurrent operations
qos: .utility
)
// Use the executor for file operations
func loadFiles(urls: [URL]) async throws -> [Data] {
try await withTaskExecutorPreference(ioExecutor) {
try await withThrowingTaskGroup(of: Data.self) { group in
for url in urls {
group.addTask {
// This runs on the custom executor
return try Data(contentsOf: url)
}
}
return try await group.reduce(into: []) { $0.append($1) }
}
}
}DispatchQueueTaskExecutor
Use DispatchQueueTaskExecutor when you want to leverage GCD's efficient thread management, work with existing GCD-based code, or need the performance characteristics of dispatch queues.
import SwiftExecutors
// Create a concurrent executor for network operations
let networkExecutor = DispatchQueueTaskExecutor(
label: "Network",
qos: .userInitiated,
attributes: .concurrent
)
// Use it for network requests
func fetchData(urls: [URL]) async throws -> [Data] {
try await withTaskExecutorPreference(networkExecutor) {
try await withThrowingTaskGroup(of: Data.self) { group in
for url in urls {
group.addTask {
// This runs on the GCD queue
return try Data(contentsOf: url)
}
}
return try await group.reduce(into: []) { $0.append($1) }
}
}
}
// Create a serial executor for sequential operations
let serialExecutor = DispatchQueueTaskExecutor(
label: "SerialProcessor",
qos: .utility
)
// Use it for operations that must be sequential
func processSequentially(_ items: [String]) async -> [String] {
await withTaskExecutorPreference(serialExecutor) {
var results: [String] = []
for item in items {
// These run one after another on the same thread
let processed = await processItem(item)
results.append(processed)
}
return results
}
}ThreadExecutor
Use ThreadExecutor when you need thread affinity for:
- Legacy C/C++/Objective-C libraries that rely on thread-local storage
- Frameworks that demand single-thread affinity (Core MIDI, Core Audio)
- Real-time loops that must never hop between cores
import SwiftExecutors
// Create a dedicated thread executor
let audioExecutor = ThreadExecutor(name: "AudioThread")
// Use it for audio processing
func processAudio() async {
await withTaskExecutorPreference(audioExecutor) {
// This code runs on a dedicated thread
// Perfect for audio processing that requires thread affinity
await processAudioBuffer()
}
}API Reference
QueueTaskExecutor
public final class QueueTaskExecutor: TaskExecutor, @unchecked Sendable {
public init(
label: String = "TaskExec",
maxConcurrent: Int = OperationQueue.defaultMaxConcurrentOperationCount,
qos: QualityOfService = .default
)
}Parameters:
label: Human-readable name for debugging (shows up in Instruments/Xcode)maxConcurrent: Maximum number of simultaneous tasks (throttles throughput)qos: Quality of service for priority handling
DispatchQueueTaskExecutor
public final class DispatchQueueTaskExecutor: TaskExecutor, @unchecked Sendable {
public init(
label: String = "DispatchTaskExec",
qos: DispatchQoS = .default,
attributes: DispatchQueue.Attributes = [],
target: DispatchQueue? = nil
)
// Convenience initializers
public convenience init(concurrentLabel: String = "ConcurrentDispatchExec",
qos: DispatchQoS = .default,
target: DispatchQueue? = nil)
public convenience init(serialLabel: String = "SerialDispatchExec",
qos: DispatchQoS = .default,
target: DispatchQueue? = nil)
}Parameters:
label: Human-readable name for debugging (shows up in Instruments/Xcode)qos: Quality of service for priority handlingattributes: Queue attributes (e.g.,.concurrent,.initiallyInactive)target: Target queue for execution (nil for default)
ThreadExecutor
public final class ThreadExecutor: SerialExecutor, @unchecked Sendable {
public init(name: String = "ThreadExecutor")
}Parameters:
name: Human-readable thread name for debugging
Examples
Background Image Processing
let imageProcessor = QueueTaskExecutor(
label: "ImageProcessing",
maxConcurrent: 2,
qos: .background
)
func processImages(_ images: [UIImage]) async -> [UIImage] {
try await withTaskExecutorPreference(imageProcessor) {
try await withThrowingTaskGroup(of: UIImage.self) { group in
for image in images {
group.addTask {
// Heavy image processing on background thread
return await applyFilters(to: image)
}
}
return try await group.reduce(into: []) { $0.append($1) }
}
}
}Network Operations with GCD
let networkExecutor = DispatchQueueTaskExecutor(
label: "NetworkOperations",
qos: .userInitiated,
attributes: .concurrent
)
func downloadMultipleFiles(_ urls: [URL]) async throws -> [Data] {
try await withTaskExecutorPreference(networkExecutor) {
try await withThrowingTaskGroup(of: Data.self) { group in
for url in urls {
group.addTask {
// Concurrent downloads using GCD's efficient thread pool
return try await downloadFile(from: url)
}
}
return try await group.reduce(into: []) { $0.append($1) }
}
}
}Audio Processing with Thread Affinity
let audioThread = ThreadExecutor(name: "AudioProcessor")
func startAudioProcessing() async {
await withTaskExecutorPreference(audioThread) {
// All audio processing happens on the same thread
// This prevents audio glitches from thread hopping
while isProcessing {
await processAudioFrame()
await sleep(1.0 / sampleRate)
}
}
}Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Submit a pull request
License
This project is licensed under the Apache License 2.0. See the LICENSE file for details.
Acknowledgments
This project demonstrates advanced Swift concurrency patterns and provides practical solutions for real-world threading requirements in Swift applications.
Package Metadata
Repository: kosikowski/swift-executors
Default branch: main
README: README.md