I used to work at a self-driving company in charge of HD map creation. The tile image loading pattern is exactly the same as yours. I recently abstracted the solution for this type of problem into a framework Monstra , which includes not only task scheduling but also on-demand task result caching.
Here are the details:
Your issue is a classic concurrency problem where Swift's task scheduler prioritizes task fairness over task completion. When you create multiple Task
s, the runtime interleaves their execution rather than completing them sequentially.
For simpler task management with batch execution capabilities:
import Monstra
actor Processor {
private let taskManager: KVLightTasksManager<Int, ProcessingResult>
init() {
// Using MonoProvider mode for simplicity
self.taskManager = KVLightTasksManager(
config: .init(
dataProvider: .asyncMonoprovide { value in
return try await self.performProcessing(of: value)
},
maxNumberOfRunningTasks: 3, // Match your CPU cores
maxNumberOfQueueingTasks: 1000
)
)
}
func enqueue(value: Int) {
taskManager.fetch(key: value) { key, result in
switch result {
case .success(let processingResult):
print("Finished processing", key)
self.postResult(processingResult)
case .failure(let error):
print("Processing failed for \(key):", error)
}
}
}
private func performProcessing(of value: Int) async throws -> ProcessingResult {
// Your CPU-intensive processing
async let resultA = performSubProcessing(of: value)
async let resultB = performSubProcessing(of: value)
async let resultC = performSubProcessing(of: value)
let results = await (resultA, resultB, resultC)
return ProcessingResult(a: results.0, b: results.1, c: results.2)
}
private func performSubProcessing(of number: Int) async -> Int {
await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
return number * 2
}
}
struct ProcessingResult {
let a: Int
let b: Int
let c: Int
}
Key advantages:
For your specific use case with controlled concurrency, use KVHeavyTasksManager
:
import Monstra
actor Processor {
private let taskManager: KVHeavyTasksManager<Int, ProcessingResult, Void, ProcessingProvider>
init() {
self.taskManager = KVHeavyTasksManager(
config: .init(
maxNumberOfRunningTasks: 3, // Match your CPU cores
maxNumberOfQueueingTasks: 1000, // Handle your 1000 requests
taskResultExpireTime: 300.0
)
)
}
func enqueue(value: Int) {
taskManager.fetch(
key: value,
customEventObserver: nil,
result: { [weak self] result in
switch result {
case .success(let processingResult):
print("Finished processing", value)
self?.postResult(processingResult)
case .failure(let error):
print("Processing failed:", error)
}
}
)
}
}
// Custom data provider
class ProcessingProvider: KVHeavyTaskDataProviderInterface {
typealias Key = Int
typealias FinalResult = ProcessingResult
typealias CustomEvent = Void
func asyncProvide(key: Int, customEventObserver: ((Void) -> Void)?) async throws -> ProcessingResult {
// Your CPU-intensive processing
async let resultA = performSubProcessing(of: key)
async let resultB = performSubProcessing(of: key)
async let resultC = performSubProcessing(of: key)
let results = await (resultA, resultB, resultC)
return ProcessingResult(a: results.0, b: results.1, c: results.2)
}
private func performSubProcessing(of number: Int) async -> Int {
// Simulate CPU work without blocking the thread
await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
return number * 2
}
}
KVHeavyTasksManager
limits concurrent tasks to match your CPU coresSwift Package Manager:
dependencies: [
.package(url: "https://github.com/yangchenlarkin/Monstra.git", from: "0.1.0")
]
CocoaPods:
pod 'Monstra', '~> 0.1.0'
If you prefer a pure Swift solution, you need to implement proper task coordination:
actor Processor {
private var currentProcessingCount = 0
private let maxConcurrent = 3
private var waitingTasks: [Int] = []
func enqueue(value: Int) async {
if currentProcessingCount < maxConcurrent {
await startProcessing(value: value)
} else {
waitingTasks.append(value)
}
}
private func startProcessing(value: Int) async {
currentProcessingCount += 1
await performProcessing(of: value)
currentProcessingCount -= 1
// Start next waiting task
if !waitingTasks.isEmpty {
let nextValue = waitingTasks.removeFirst()
await startProcessing(value: nextValue)
}
}
}
However, this requires significant error handling, edge case management, and testing - which Monstra handles for you.
Full disclosure: I'm the author of Monstra. Built it specifically to solve these kinds of concurrency and task management problems in iOS development. The framework includes comprehensive examples for similar use cases in the Examples folder.