Since you have an idea of using GCD and semaphores as a solution, I feel something like this could also be achieved with OperationQueues
I like the use of OperationQueues for your use case because you can add dependencies like in the situation when B is longer and has to wait for the current A to finish.
Here is a small experiment I ran, please have a look if this suits your use case:
Logic with comments
// A duration is fixed at 5
let aDuration: UInt32 = 5
// Control B's duration between 1 and 15
let maxBDuration: UInt32 = 15
// Initialize an Operation queue
let operationQueue = OperationQueue()
// Operations A and B which will be initialized
// before being added to a queue
var operationA: BlockOperation!
var operationB: BlockOperation!
// Counters to identify which loop A and B currently
// are in, just for our identification
var aLoopCount = 0
var bLoopCount = 0
// When to stop the experiment
let experimentCount = 10
// Launch the A loop
runALoop()
// Launch the B loop simultaneously specifying
// it is first launch, you will see why soon
runBLoop(isFirstLaunch: true)
// B is identical to A with a few small differences
private func runBLoop(isFirstLaunch: Bool = false)
{
operationB = BlockOperation
{
self.bLoopCount += 1
// The duration of b varies as you mentioned
let duration = UInt32.random(in: 1...self.maxBDuration)
print("Operation B\(self.bLoopCount) started, Duration: \(duration)s")
sleep(duration)
print("Operation B\(self.bLoopCount) done")
if self.bLoopCount != self.experimentCount
{
self.runBLoop()
}
}
// Check if we are not in the first launch
if !isFirstLaunch
{
// Add a dependency of the newly initialize B loop
// To the currently running A so it starts only when
// the current A finishes
operationB.addDependency(operationA)
}
operationQueue.addOperation(operationB)
}
The Output
Operation A1 started, Duration: 5s
Operation B1 started, Duration: 14s
Operation A1 done
Operation A2 started, Duration: 5s
Operation A2 done
Operation A3 started, Duration: 5s
Operation B1 done
Operation A3 done
Operation A4 started, Duration: 5s
Operation B2 started, Duration: 13s
Operation A4 done
Operation A5 started, Duration: 5s
Operation A5 done
Operation A6 started, Duration: 5s
Operation B2 done
Operation A6 done
Operation A7 started, Duration: 5s
Operation B3 started, Duration: 8s
Operation A7 done
Operation A8 started, Duration: 5s
Operation B3 done
Operation A8 done
Operation B4 started, Duration: 4s
Operation A9 started, Duration: 5s
Operation B4 done
Operation A9 done
Operation B5 started, Duration: 3s
Operation A10 started, Duration: 5s
Operation B5 done
Operation A10 done
Operation B6 started, Duration: 7s
Operation B6 done
Operation B7 started, Duration: 5s
Operation B7 done
Operation B8 started, Duration: 13s
Operation B8 done
Operation B9 started, Duration: 3s
Operation B9 done
Operation B10 started, Duration: 8s
Operation B10 done
Conclusions
The most interesting data to analyse is up until Operation A10 done as after this A no longer runs
This output sequence shows you the case when B is longer:
Operation A1 started, Duration: 5s
Operation B1 started, Duration: 14s
Operation A1 done
Operation A2 started, Duration: 5s
Operation A2 done
Operation A3 started, Duration: 5s
Operation B1 done
Operation A3 done
Operation A4 started, Duration: 5s
Operation B2 started, Duration: 13s
As you see B1 starts when A1 starts and ends before A3 is done but it waits till A3 before it starts again with A4
- Later on in the sequence, it also shows you what happens when B is shorter:
Operation B4 started, Duration: 4s
Operation A9 started, Duration: 5s
Operation B4 done
Operation A9 done
Operation B5 started, Duration: 3s
Operation A10 started, Duration: 5s
Operation B5 done
Operation A10 done
Operation B6 started, Duration: 7s
In this situation, Both B4 and B5 end before the current A operation is finished but they wait
- I feel this could work for you because it does not matter if B is shorter or longer than A because the next B will always be dependant on the current A to finish executing before starting.