Search code examples
swiftgrand-central-dispatch

How to sync start of two cyclic tasks (swift)


I have two loops that must run periodically.

One of the loop (A) has a constant duration and is scheduled with high accuracy, no problem.

The other loop (B) can have a variable duration. Much shorter or much longer than duration of A. But whatever B’s duration, I want to start next B cycle at the start of the next A cycle. If B is much shorter than A, then it must wait next A’start. If B is much longer than A, then, when it completes, whatever the number of A cycles that occurred, it shall wait and start at next A’s start.

I first thought of using a semaphore. A would signal the semaphore at its start, while B would wait for it at it start. This works if B is shorter than A.

But if B is longer, the semaphore gets increment (maybe over 1), and I don’t get B to wait, it restarts immediately at its end and starts-sync is lost.

Any solution (preferably with GCD, but any solution would do) ?


Solution

  • 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

    1. The most interesting data to analyse is up until Operation A10 done as after this A no longer runs

    2. 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

    1. 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

    1. 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.