From fdb11189474a2cb98ae2574e2c6c467b1cfad7ce Mon Sep 17 00:00:00 2001 From: Alexander Smarus Date: Fri, 27 Nov 2020 23:08:42 +0200 Subject: [PATCH] Fix concurrent operations execution in OperationQueue To be able to run operations concurrently OperationQueue needs concurrent underlying DispatchQueue. This matches with Darwin. --- Sources/Foundation/Operation.swift | 8 +-- .../Foundation/Tests/TestOperationQueue.swift | 54 +++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/Sources/Foundation/Operation.swift b/Sources/Foundation/Operation.swift index 682dba794d..b00549f616 100644 --- a/Sources/Foundation/Operation.swift +++ b/Sources/Foundation/Operation.swift @@ -938,15 +938,15 @@ open class OperationQueue : NSObject, ProgressReporting { let queue: DispatchQueue if let qos = _propertyQoS { if let name = __name { - queue = DispatchQueue(label: name, qos: qos.qosClass) + queue = DispatchQueue(label: name, qos: qos.qosClass, attributes: .concurrent) } else { - queue = DispatchQueue(label: "NSOperationQueue \(Unmanaged.passUnretained(self).toOpaque())", qos: qos.qosClass) + queue = DispatchQueue(label: "NSOperationQueue \(Unmanaged.passUnretained(self).toOpaque())", qos: qos.qosClass, attributes: .concurrent) } } else { if let name = __name { - queue = DispatchQueue(label: name) + queue = DispatchQueue(label: name, attributes: .concurrent) } else { - queue = DispatchQueue(label: "NSOperationQueue \(Unmanaged.passUnretained(self).toOpaque())") + queue = DispatchQueue(label: "NSOperationQueue \(Unmanaged.passUnretained(self).toOpaque())", attributes: .concurrent) } } __backingQueue = queue diff --git a/Tests/Foundation/Tests/TestOperationQueue.swift b/Tests/Foundation/Tests/TestOperationQueue.swift index e555347380..81361bbaa0 100644 --- a/Tests/Foundation/Tests/TestOperationQueue.swift +++ b/Tests/Foundation/Tests/TestOperationQueue.swift @@ -40,6 +40,8 @@ class TestOperationQueue : XCTestCase { ("test_CustomOperationReady", test_CustomOperationReady), ("test_DependencyCycleBreak", test_DependencyCycleBreak), ("test_Lifecycle", test_Lifecycle), + ("test_ConcurrentOperations", test_ConcurrentOperations), + ("test_ConcurrentOperationsWithDependenciesAndCompletions", test_ConcurrentOperationsWithDependenciesAndCompletions), ] } @@ -699,6 +701,58 @@ class TestOperationQueue : XCTestCase { Thread.sleep(forTimeInterval: 1) // Let queue to be deallocated XCTAssertNil(weakQueue, "Queue should be deallocated at this point") } + + func test_ConcurrentOperations() { + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 2 + + // Running several iterations helps to reveal use-after-dealloc crashes + for _ in 0..<3 { + let didRunOp1 = expectation(description: "Did run first operation") + let didRunOp2 = expectation(description: "Did run second operation") + + queue.addOperation { + self.wait(for: [didRunOp2], timeout: 0.2) + didRunOp1.fulfill() + } + queue.addOperation { + didRunOp2.fulfill() + } + + self.wait(for: [didRunOp1], timeout: 0.3) + } + } + + func test_ConcurrentOperationsWithDependenciesAndCompletions() { + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 2 + + // Running several iterations helps to reveal use-after-dealloc crashes + for _ in 0..<3 { + let didRunOp1 = expectation(description: "Did run first operation") + let didRunOp1Completion = expectation(description: "Did run first operation completion") + let didRunOp1Dependency = expectation(description: "Did run first operation dependency") + let didRunOp2 = expectation(description: "Did run second operation") + + let op1 = BlockOperation { + self.wait(for: [didRunOp1Dependency, didRunOp2], timeout: 0.2) + didRunOp1.fulfill() + } + op1.completionBlock = { + didRunOp1Completion.fulfill() + } + let op1Dependency = BlockOperation { + didRunOp1Dependency.fulfill() + } + queue.addOperations([op1, op1Dependency], waitUntilFinished: false) + queue.addOperation { + didRunOp2.fulfill() + } + + self.wait(for: [didRunOp1, didRunOp1Completion], timeout: 0.3) + } + } + } class AsyncOperation: Operation {