Skip to content

Commit bb754d8

Browse files
authored
Use dedicated thread when invoking buildSystem.build() as it is a blocking operation (#9493)
Use Task.detachNewThread here because buildSystem.build() is a blocking operation. Running this on the Swift Concurrency thread pool can block a worker thread potentially causing thread pool starvation and deadlocks. By running it on a dedicated thread, we keep the Swift Concurrency pool available for other async work. ### Motivation: #9441
1 parent f43740c commit bb754d8

File tree

2 files changed

+24
-4
lines changed

2 files changed

+24
-4
lines changed

Sources/Basics/Concurrency/ConcurrencyHelpers.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import _Concurrency
1414
import Dispatch
1515
import class Foundation.NSLock
1616
import class Foundation.ProcessInfo
17+
import class Foundation.Thread
1718
import struct Foundation.URL
1819
import struct Foundation.UUID
1920
import func TSCBasic.tsc_await
@@ -39,6 +40,19 @@ public func unsafe_await<T: Sendable>(_ body: @Sendable @escaping () async -> T)
3940
return box.get()!
4041
}
4142

43+
extension Task where Failure == Never {
44+
/// Runs `block` in a new thread and suspends until it finishes execution.
45+
///
46+
/// - note: This function should be used sparingly, such as for long-running operations that may block and therefore should not be run on the Swift Concurrency thread pool. Do not use this for operations for which there may be many concurrent invocations as it could lead to thread explosion. It is meant to be a bridge to pre-existing blocking code which can't easily be converted to use Swift concurrency features.
47+
public static func detachNewThread(name: String? = nil, _ block: @Sendable @escaping () -> Success) async -> Success {
48+
return await withCheckedContinuation { continuation in
49+
Thread.detachNewThread {
50+
Thread.current.name = name
51+
return continuation.resume(returning: block())
52+
}
53+
}
54+
}
55+
}
4256

4357
extension DispatchQueue {
4458
// a shared concurrent queue for running concurrent asynchronous operations

Sources/Build/BuildOperation.swift

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
291291
if self.cacheBuildManifest {
292292
do {
293293
// if buildPackageStructure returns a valid description we use that, otherwise we perform full planning
294-
if try self.buildPackageStructure() {
294+
if try await self.buildPackageStructure() {
295295
// confirm the step above created the build description as expected
296296
// we trust it to update the build description when needed
297297
let buildDescriptionPath = self.config.buildDescriptionPath(for: .target)
@@ -829,15 +829,21 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
829829
}
830830

831831
/// Build the package structure target.
832-
private func buildPackageStructure() throws -> Bool {
832+
private func buildPackageStructure() async throws -> Bool {
833833
let (buildSystem, tracker) = try self.createBuildSystem(
834834
buildDescription: .none,
835835
config: self.config
836836
)
837837
self.current = (buildSystem, tracker)
838838

839-
// Build the package structure target which will re-generate the llbuild manifest, if necessary.
840-
let buildSuccess = buildSystem.build(target: "PackageStructure")
839+
// We use Task.detachNewThread here because buildSystem.build() is a blocking
840+
// operation. Running this on the Swift Concurrency thread pool can block a worker thread
841+
// potentially causing thread pool starvation and deadlocks. By running it on a dedicated
842+
// thread, we keep the Swift Concurrency pool available for other async work.
843+
let buildSuccess = await _Concurrency.Task.detachNewThread(name: "buildPackageStructure") {
844+
// Build the package structure target which will re-generate the llbuild manifest, if necessary.
845+
buildSystem.build(target: "PackageStructure")
846+
}
841847

842848
// If progress has been printed this will add a newline to separate it from what could be
843849
// the output of the command. For instance `swift test --skip-build` may print progress for

0 commit comments

Comments
 (0)