Coverage Report

Created: 2026-10-13 06:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/swift-nio/Sources/NIOPosix/NIOThreadPool.swift
Line
Count
Source
1
//===----------------------------------------------------------------------===//
2
//
3
// This source file is part of the SwiftNIO open source project
4
//
5
// Copyright (c) 2017-2024 Apple Inc. and the SwiftNIO project authors
6
// Licensed under Apache License v2.0
7
//
8
// See LICENSE.txt for license information
9
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10
//
11
// SPDX-License-Identifier: Apache-2.0
12
//
13
//===----------------------------------------------------------------------===//
14
15
#if !os(WASI)
16
17
import Atomics
18
import DequeModule
19
import NIOConcurrencyHelpers
20
import NIOCore
21
22
#if canImport(Dispatch)
23
import Dispatch
24
#endif
25
26
/// Errors that may be thrown when executing work on a `NIOThreadPool`
27
public enum NIOThreadPoolError: Sendable {
28
29
    /// The `NIOThreadPool` was not active.
30
    public struct ThreadPoolInactive: Error {
31
0
        public init() {}
32
    }
33
34
    /// The `NIOThreadPool` operation is unsupported (e.g. shutdown of a perpetual pool).
35
    public struct UnsupportedOperation: Error {
36
0
        public init() {}
37
    }
38
}
39
40
/// A thread pool that should be used if some (kernel thread) blocking work
41
/// needs to be performed for which no non-blocking API exists.
42
///
43
/// When using NIO it is crucial not to block any of the `EventLoop`s as that
44
/// leads to slow downs or stalls of arbitrary other work. Unfortunately though
45
/// there are tasks that applications need to achieve for which no non-blocking
46
/// APIs exist. In those cases `NIOThreadPool` can be used but should be
47
/// treated as a last resort.
48
///
49
/// - Note: The prime example for missing non-blocking APIs is file IO on UNIX.
50
///   The OS does not provide a usable and truly non-blocking API but with
51
///   `NonBlockingFileIO` NIO provides a high-level API for file IO that should
52
///   be preferred to running blocking file IO system calls directly on
53
///   `NIOThreadPool`. Under the covers `NonBlockingFileIO` will use
54
///   `NIOThreadPool` on all currently supported platforms though.
55
public final class NIOThreadPool {
56
57
    /// The state of the `WorkItem`.
58
    public enum WorkItemState: Sendable {
59
        /// The `WorkItem` is active now and in process by the `NIOThreadPool`.
60
        case active
61
        /// The `WorkItem` was cancelled and will not be processed by the `NIOThreadPool`.
62
        case cancelled
63
    }
64
65
    /// The work that should be done by the `NIOThreadPool`.
66
    public typealias WorkItem = @Sendable (WorkItemState) -> Void
67
68
    @usableFromInline
69
    struct IdentifiableWorkItem: Sendable {
70
        @usableFromInline
71
        var workItem: WorkItem
72
73
        @usableFromInline
74
        var id: Int?
75
    }
76
77
    @usableFromInline
78
    internal enum State: Sendable {
79
        /// The `NIOThreadPool` is already stopped.
80
        case stopped
81
        /// The `NIOThreadPool` is shutting down, the array has one boolean entry for each thread indicating if it has shut down already.
82
        case shuttingDown([Bool])
83
        /// The `NIOThreadPool` is up and running, the `Deque` containing the yet unprocessed `IdentifiableWorkItem`s.
84
        case running(Deque<IdentifiableWorkItem>)
85
        /// Temporary state used when mutating the .running(items). Used to avoid CoW copies.
86
        /// It should never be "leaked" outside of the lock block.
87
        case modifying
88
    }
89
90
    @usableFromInline
91
    internal let _workAvailable: NIOThreadPoolWorkAvailable
92
0
    private var threads: [NIOThread]? = nil  // protected by `_workAvailable`
93
    @usableFromInline
94
0
    internal var _state: State = .stopped
95
96
    // WorkItems don't have a handle so they can't be cancelled directly. Instead an ID is assigned
97
    // to each cancellable work item and the IDs of each work item to cancel is stored in this set.
98
    // The set is checked when dequeuing work items prior to running them, the presence of an ID
99
    // indicates it should be cancelled. This approach makes cancellation cheap, but slow, as the
100
    // task isn't cancelled until it's dequeued.
101
    //
102
    // Possible alternatives:
103
    // - Removing items from the work queue on cancellation. This is linear and runs the risk of
104
    //   being expensive if a task tree with many enqueued work items is cancelled.
105
    // - Storing an atomic 'is cancelled' flag with each work item. This adds an allocation per
106
    //   work item.
107
    //
108
    // If a future version of this thread pool has work items which do have a handle this set should
109
    // be removed.
110
    //
111
    // Note: protected by 'lock'.
112
    @usableFromInline
113
0
    internal var _cancelledWorkIDs: Set<Int> = []
114
0
    private let nextWorkID = ManagedAtomic(0)
115
116
    public let numberOfThreads: Int
117
    private let canBeStopped: Bool
118
    private let pinnedCPUIDs: [Int]?
119
120
    /// Gracefully shutdown this `NIOThreadPool`. All tasks will be run before shutdown will take place.
121
    ///
122
    /// - Parameters:
123
    ///   - queue: The `DispatchQueue` used to executed the callback
124
    ///   - callback: The function to be executed once the shutdown is complete.
125
    @preconcurrency
126
0
    public func shutdownGracefully(queue: DispatchQueue, _ callback: @escaping @Sendable (Error?) -> Void) {
127
0
        self._shutdownGracefully(queue: queue, callback)
128
0
    }
129
130
0
    private func _shutdownGracefully(queue: DispatchQueue, _ callback: @escaping @Sendable (Error?) -> Void) {
131
0
        guard self.canBeStopped else {
132
0
            queue.async {
133
0
                callback(NIOThreadPoolError.UnsupportedOperation())
134
0
            }
135
0
            return
136
0
        }
137
0
138
0
        let threadsToJoin = self._workAvailable.withLockSettingWorkAvailable {
139
0
            switch self._state {
140
0
            case .running(let items):
141
0
                self._state = .modifying
142
0
                queue.async {
143
0
                    for item in items {
144
0
                        item.workItem(.cancelled)
145
0
                    }
146
0
                }
147
0
                self._state = .shuttingDown(Array(repeating: true, count: self.numberOfThreads))
148
0
149
0
                let threads = self.threads!
150
0
                self.threads = nil
151
0
152
0
                // Wake all threads so they can see .shuttingDown and exit.
153
0
                return (workAvailable: self.numberOfThreads, signal: .broadcastAll, result: threads)
154
0
155
0
            case .shuttingDown, .stopped:
156
0
                // Don't modify workAvailable - threads may still be waking up to see .shuttingDown
157
0
                return (workAvailable: nil, signal: .none, result: [])
158
0
159
0
            case .modifying:
160
0
                fatalError(".modifying state misuse")
161
0
            }
162
0
        }
163
0
164
0
        DispatchQueue(label: "io.swiftnio.NIOThreadPool.shutdownGracefully").async {
165
0
            for thread in threadsToJoin {
166
0
                thread.join()
167
0
            }
168
0
            queue.async {
169
0
                callback(nil)
170
0
            }
171
0
        }
172
0
    }
173
174
    /// Submit a `WorkItem` to process.
175
    ///
176
    /// - Note: This is a low-level method, in most cases the `runIfActive` method should be used.
177
    ///
178
    /// - Parameters:
179
    ///   - body: The `WorkItem` to process by the `NIOThreadPool`.
180
    @preconcurrency
181
0
    public func submit(_ body: @escaping WorkItem) {
182
0
        self._submit(id: nil, body)
183
0
    }
184
185
0
    private func _submit(id: Int?, _ body: @escaping WorkItem) {
186
0
        let submitted = self._workAvailable.withLock {
187
0
            let submitted: Bool
188
0
189
0
            switch self._state {
190
0
            case .running(var items):
191
0
                self._state = .modifying
192
0
                items.append(.init(workItem: body, id: id))
193
0
                self._state = .running(items)
194
0
                submitted = true
195
0
196
0
            case .shuttingDown, .stopped:
197
0
                submitted = false
198
0
199
0
            case .modifying:
200
0
                fatalError(".modifying state misuse")
201
0
            }
202
0
203
0
            return (workDelta: submitted ? 1 : 0, signal: submitted ? .signalOne : .none, result: submitted)
204
0
        }
205
0
206
0
        // if item couldn't be added run it immediately indicating that it couldn't be run
207
0
        if !submitted {
208
0
            body(.cancelled)
209
0
        }
210
0
    }
211
212
    /// Initialize a `NIOThreadPool` thread pool with `numberOfThreads` threads.
213
    ///
214
    /// - Parameters:
215
    ///   - numberOfThreads: The number of threads to use for the thread pool.
216
0
    public convenience init(numberOfThreads: Int) {
217
0
        self.init(
218
0
            numberOfThreads: numberOfThreads,
219
0
            pinnedCPUIDs: nil,
220
0
            canBeStopped: true
221
0
        )
222
0
    }
223
224
    #if !os(WASI)
225
    #if os(Linux) || os(Android)
226
    /// Initialize a `NIOThreadPool` thread pool with threads pinned to specific CPUs.
227
    ///
228
    /// - Parameters:
229
    ///   - pinnedCPUIDs: The IDs of the CPUs to use for the thread pool. One thread will be
230
    ///      created per entry in this array.
231
0
    public convenience init(pinnedCPUIDs: [Int]) {
232
0
        self.init(
233
0
            numberOfThreads: pinnedCPUIDs.count,
234
0
            pinnedCPUIDs: pinnedCPUIDs,
235
0
            canBeStopped: true
236
0
        )
237
0
    }
238
239
    #endif
240
    #endif
241
    /// Create a ``NIOThreadPool`` that is already started, cannot be shut down and must not be `deinit`ed.
242
    ///
243
    /// This is only useful for global singletons.
244
0
    public static func _makePerpetualStartedPool(numberOfThreads: Int, threadNamePrefix: String) -> NIOThreadPool {
245
0
        let pool = self.init(numberOfThreads: numberOfThreads, pinnedCPUIDs: nil, canBeStopped: false)
246
0
        pool._start(threadNamePrefix: threadNamePrefix)
247
0
        return pool
248
0
    }
249
250
0
    private init(numberOfThreads: Int, pinnedCPUIDs: [Int]?, canBeStopped: Bool) {
251
0
        self.numberOfThreads = numberOfThreads
252
0
        self.pinnedCPUIDs = pinnedCPUIDs
253
0
        self.canBeStopped = canBeStopped
254
0
        self._workAvailable = NIOThreadPoolWorkAvailable()
255
0
    }
256
257
    // Do not rename or remove this function.
258
    //
259
    // When doing on-/off-CPU analysis, for example with continuous profiling, it's
260
    // important to recognise certain functions that are purely there to wait.
261
    //
262
    // This function is one of those and giving it a consistent name makes it much easier to remove from the profiles
263
    // when only interested in on-CPU work.
264
    @inlinable
265
0
    internal func _blockingWaitForWork(identifier: Int) -> (item: WorkItem, state: WorkItemState)? {
266
0
        self._workAvailable.withLockWaitingForWork {
267
0
            let result: (WorkItem, WorkItemState)?
268
0
269
0
            switch self._state {
270
0
            case .running(var items):
271
0
                self._state = .modifying
272
0
                let itemAndID = items.removeFirst()
273
0
274
0
                let state: WorkItemState
275
0
                if let id = itemAndID.id, !self._cancelledWorkIDs.isEmpty {
276
0
                    state = self._cancelledWorkIDs.remove(id) == nil ? .active : .cancelled
277
0
                } else {
278
0
                    state = .active
279
0
                }
280
0
281
0
                self._state = .running(items)
282
0
283
0
                result = (itemAndID.workItem, state)
284
0
285
0
            case .shuttingDown(var aliveStates):
286
0
                self._state = .modifying
287
0
                assert(aliveStates[identifier])
288
0
                aliveStates[identifier] = false
289
0
                self._state = .shuttingDown(aliveStates)
290
0
291
0
                result = nil
292
0
293
0
            case .stopped:
294
0
                // Unreachable: 'stopped' is the initial state which is left when starting the
295
0
                // thread pool, and before any thread calls this function.
296
0
                fatalError("Invalid state")
297
0
298
0
            case .modifying:
299
0
                fatalError(".modifying state misuse")
300
0
            }
301
0
302
0
            return (workDelta: -1, signal: .none, result: result)
303
0
        }
304
0
    }
305
306
0
    private func process(identifier: Int) {
307
0
        repeat {
308
0
            let itemAndState = self._blockingWaitForWork(identifier: identifier)
309
0
310
0
            if let (item, state) = itemAndState {
311
0
                // if there was a work item popped, run it
312
0
                item(state)
313
0
            } else {
314
0
                break  // Otherwise, we're done
315
0
            }
316
0
        } while true
317
0
    }
318
319
    /// Start the `NIOThreadPool` if not already started.
320
0
    public func start() {
321
0
        self._start(threadNamePrefix: "TP-#")
322
0
    }
323
324
0
    public func _start(threadNamePrefix: String) {
325
0
        let alreadyRunning = self._workAvailable.withLock {
326
0
            switch self._state {
327
0
            case .running:
328
0
                // Already running, this has no effect on whether there is more work for the
329
0
                // threads to run.
330
0
                return (workDelta: 0, signal: .none, result: true)
331
0
332
0
            case .shuttingDown:
333
0
                // This should never happen
334
0
                fatalError("start() called while in shuttingDown")
335
0
336
0
            case .stopped:
337
0
                self._state = .running(Deque(minimumCapacity: 16))
338
0
                assert(self.threads == nil)
339
0
                self.threads = []
340
0
                self.threads!.reserveCapacity(self.numberOfThreads)
341
0
                return (workDelta: 0, signal: .none, result: false)
342
0
343
0
            case .modifying:
344
0
                fatalError(".modifying state misuse")
345
0
            }
346
0
        }
347
0
348
0
        if alreadyRunning {
349
0
            return
350
0
        }
351
0
352
0
        // We use this condition lock as a tricky kind of semaphore.
353
0
        // This is done to sidestep the thread performance checker warning
354
0
        // that would otherwise be emitted.
355
0
        let readyThreads = ConditionLock(value: 0)
356
0
        for id in 0..<self.numberOfThreads {
357
0
            let pinnedCPUID = self.pinnedCPUIDs?[id]
358
0
359
0
            // We should keep thread names under 16 characters because Linux doesn't allow more.
360
0
            NIOThread.spawnAndRun(name: "\(threadNamePrefix)\(id)") { thread in
361
                #if !os(WASI)
362
                #if os(Linux) || os(Android)
363
0
                if let pinnedCPUID {
364
0
                    precondition(thread.isCurrentSlow)
365
0
                    NIOThread.currentAffinity = LinuxCPUSet(pinnedCPUID)
366
0
                }
367
                #endif
368
                #endif
369
0
                readyThreads.withLock {
370
0
                    let threadCount = self._workAvailable.withLock {
371
0
                        self.threads!.append(thread)
372
0
                        let threadCount = self.threads!.count
373
0
                        return (workDelta: 0, signal: .none, result: threadCount)
374
0
                    }
375
0
376
0
                    return (unlockWith: threadCount, result: ())
377
0
                }
378
0
379
0
                self.process(identifier: id)
380
0
                return ()
381
0
            }
382
0
        }
383
0
384
0
        readyThreads.lock(whenValue: self.numberOfThreads)
385
0
        readyThreads.unlock()
386
0
387
0
        func threadCount() -> Int {
388
0
            self._workAvailable.withLock {
389
0
                (workDelta: 0, signal: .none, result: self.threads?.count ?? -1)
390
0
            }
391
0
        }
392
0
        assert(threadCount() == self.numberOfThreads)
393
0
    }
394
395
0
    deinit {
396
0
        assert(
397
0
            self.canBeStopped,
398
0
            "Perpetual NIOThreadPool has been deinited, you must make sure that perpetual pools don't deinit"
399
0
        )
400
0
        switch self._state {
401
0
        case .stopped, .shuttingDown:
402
0
            ()
403
0
        default:
404
0
            assertionFailure("wrong state \(self._state)")
405
0
        }
406
0
    }
407
}
408
409
extension NIOThreadPool: @unchecked Sendable {}
410
411
extension NIOThreadPool {
412
413
    /// Runs the submitted closure if the thread pool is still active, otherwise fails the promise.
414
    /// The closure will be run on the thread pool so can do blocking work.
415
    ///
416
    /// - Parameters:
417
    ///   - eventLoop: The `EventLoop` the returned `EventLoopFuture` will fire on.
418
    ///   - body: The closure which performs some blocking work to be done on the thread pool.
419
    /// - Returns: The `EventLoopFuture` of `promise` fulfilled with the result (or error) of the passed closure.
420
    @preconcurrency
421
    public func runIfActive<T: Sendable>(
422
        eventLoop: EventLoop,
423
        _ body: @escaping @Sendable () throws -> T
424
0
    ) -> EventLoopFuture<T> {
425
0
        self._runIfActive(eventLoop: eventLoop, body)
426
0
    }
427
428
    private func _runIfActive<T: Sendable>(
429
        eventLoop: EventLoop,
430
        _ body: @escaping @Sendable () throws -> T
431
0
    ) -> EventLoopFuture<T> {
432
0
        let promise = eventLoop.makePromise(of: T.self)
433
0
        self.submit { shouldRun in
434
0
            guard case shouldRun = NIOThreadPool.WorkItemState.active else {
435
0
                promise.fail(NIOThreadPoolError.ThreadPoolInactive())
436
0
                return
437
0
            }
438
0
            do {
439
0
                try promise.succeed(body())
440
0
            } catch {
441
0
                promise.fail(error)
442
0
            }
443
0
        }
444
0
        return promise.futureResult
445
0
    }
446
447
    /// Runs the submitted closure if the thread pool is still active, otherwise throw an error.
448
    /// The closure will be run on the thread pool, such that we can do blocking work.
449
    ///
450
    /// - Parameters:
451
    ///   - body: The closure which performs some blocking work to be done on the thread pool.
452
    /// - Returns: Result of the passed closure.
453
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
454
0
    public func runIfActive<T: Sendable>(_ body: @escaping @Sendable () throws -> T) async throws -> T {
455
0
        let workID = self.nextWorkID.loadThenWrappingIncrement(ordering: .relaxed)
456
0
457
0
        return try await withTaskCancellationHandler {
458
0
            try await withCheckedThrowingContinuation { (cont: CheckedContinuation<T, Error>) in
459
0
                self._submit(id: workID) { shouldRun in
460
0
                    switch shouldRun {
461
0
                    case .active:
462
0
                        let result = Result(catching: body)
463
0
                        cont.resume(with: result)
464
0
                    case .cancelled:
465
0
                        cont.resume(throwing: CancellationError())
466
0
                    }
467
0
                }
468
0
            }
469
0
        } onCancel: {
470
0
            self._workAvailable.withLock {
471
0
                self._cancelledWorkIDs.insert(workID)
472
0
                return (workDelta: 0, signal: .none, result: ())
473
0
            }
474
0
        }
475
0
    }
476
}
477
478
extension NIOThreadPool {
479
    @preconcurrency
480
0
    public func shutdownGracefully(_ callback: @escaping @Sendable (Error?) -> Void) {
481
0
        self.shutdownGracefully(queue: .global(), callback)
482
0
    }
483
484
    /// Shuts down the thread pool gracefully.
485
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
486
    @inlinable
487
0
    public func shutdownGracefully() async throws {
488
0
        try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
489
0
            self.shutdownGracefully { error in
490
0
                if let error = error {
491
0
                    cont.resume(throwing: error)
492
0
                } else {
493
0
                    cont.resume()
494
0
                }
495
0
            }
496
0
        }
497
0
    }
498
499
    @available(*, noasync, message: "this can end up blocking the calling thread", renamed: "shutdownGracefully()")
500
0
    public func syncShutdownGracefully() throws {
501
0
        try self._syncShutdownGracefully()
502
0
    }
503
504
0
    private func _syncShutdownGracefully() throws {
505
0
        let errorStorageLock = NIOLockedValueBox<Swift.Error?>(nil)
506
0
        let continuation = ConditionLock(value: 0)
507
0
        self.shutdownGracefully { error in
508
0
            if let error = error {
509
0
                errorStorageLock.withLockedValue {
510
0
                    $0 = error
511
0
                }
512
0
            }
513
0
            continuation.lock(whenValue: 0)
514
0
            continuation.unlock(withValue: 1)
515
0
        }
516
0
        continuation.lock(whenValue: 1)
517
0
        continuation.unlock()
518
0
        try errorStorageLock.withLockedValue {
519
0
            if let error = $0 {
520
0
                throw error
521
0
            }
522
0
        }
523
0
    }
524
}
525
526
extension ConditionLock {
527
    @inlinable
528
0
    func _lock(when value: T?) {
529
0
        if let value = value {
530
0
            self.lock(whenValue: value)
531
0
        } else {
532
0
            self.lock()
533
0
        }
534
0
    }
535
536
    @inlinable
537
0
    func _unlock(with value: T?) {
538
0
        if let value = value {
539
0
            self.unlock(withValue: value)
540
0
        } else {
541
0
            self.unlock()
542
0
        }
543
0
    }
544
545
    @inlinable
546
0
    func withLock<Result>(when value: T? = nil, _ body: () -> (unlockWith: T?, result: Result)) -> Result {
547
0
        self._lock(when: value)
548
0
        let (unlockValue, result) = body()
549
0
        self._unlock(with: unlockValue)
550
0
        return result
551
0
    }
552
}
553
#endif  // !os(WASI)