0

I'm using swift 3, to make delayed events, this is the code

public func delay(bySeconds seconds: Double, dispatchLevel: DispatchLevel = .main, closure: @escaping () -> Void)
{
    let dispatchTime = DispatchTime.now() + seconds
    dispatchLevel.dispatchQueue.asyncAfter(deadline: dispatchTime, execute: closure)
}

public enum DispatchLevel
{
    case main, userInteractive, userInitiated, utility, background
    var dispatchQueue: DispatchQueue
    {
        switch self
        {
            case .main:                 return DispatchQueue.main
            case .userInteractive:      return DispatchQueue.global(qos: .userInteractive)
            case .userInitiated:        return DispatchQueue.global(qos: .userInitiated)
            case .utility:              return DispatchQueue.global(qos: .utility)
            case .background:           return DispatchQueue.global(qos: .background)
        }
    }
}



override func viewDidAppear(_ animated: Bool)
{

}

override func viewDidLoad()
{
    super.viewDidLoad()

    for i in 0..<20
    {
        delay(bySeconds: 1.0+Double(i)*0.5, dispatchLevel: .background)
        {
            print("__ \(i)")
            // delayed code that will run on background thread
        }
    }

}

the actual output, notice the change of pattern after 10

__ 0
__ 1
__ 2
__ 3
__ 4
__ 5
__ 6
__ 7
__ 8
__ 9
__ 10
__ 12
__ 11
__ 14
__ 13
__ 16
__ 15
__ 17
__ 18
__ 19

the expected output

__ 0
__ 1
__ 2
__ 3
__ 4
__ 5
__ 6
__ 7
__ 8
__ 9
__ 10
__ 11
__ 12
__ 13
__ 14
__ 15
__ 16
__ 17
__ 18
__ 19

is there something wrong with the delay extension?

DeyaEldeen
  • 10,847
  • 10
  • 42
  • 75

2 Answers2

1

The key response is that you are using asynchronous function and concurent-queue(background) .

dispatchLevel.dispatchQueue.asyncAfter(deadline: dispatchTime, execute: closure)

The above code will retunrs immediately and it just set the task to be executed in a fututr time, as result your delay function will also return immediately. As result this does not block the loop from going to next (in contract with a sycn function).

Another side is the fact that background queue is a concurrent queue means no grantee that tasks will finish executing in the same order they are added the queue and this explain the resuts you get.

By contrast if you use main-queue as it is a serial-queue, then there is a garantee that it execute and finish one by one in the order they are added. but you are going to block the UI/ responsiveness of the app

Idali
  • 1,023
  • 7
  • 10
1

You made the assumption that when task is scheduled first on concurrent queue, it will be executed first. A concurrent queue distribute its tasks over multiple threads, and even though they are added to each thread in order, each task will have a random delay that may cause them to execute out of order.

To illustrate the point, let's strip down your code to the bare minimum, and measure when a task was scheduled vs. when it was actually executed:

let queue = DispatchQueue.global(qos: .userInteractive)

for i in 0..<20 {
    let scheduledTime = DispatchTime.now() + Double(i) * 0.5
    queue.asyncAfter(deadline: scheduledTime) {
        let threadID = pthread_mach_thread_np(pthread_self()) // The thread that the task is executed on
        let executionTime = DispatchTime.now()
        let delay = executionTime.uptimeNanoseconds - scheduledTime.uptimeNanoseconds

        print(i, scheduledTime.uptimeNanoseconds, executionTime.uptimeNanoseconds, delay, threadID, separator: "\t")
    }
}

// Wait for all the tasks to complete. This is not how you should wait
// but this is just sample code to illustrate a point.
sleep(15)

Result, in nanoseconds (some formatting added):

i     scheduledTime          executionTime          delay          threadID
0     142,803,882,452,582    142,803,883,273,138    820,556        3331
1     142,804,383,177,169    142,804,478,766,410    95,589,241     3331
2     142,804,883,221,388    142,804,958,658,498    75,437,110     3331
3     142,805,383,223,641    142,805,428,926,049    45,702,408     3331
4     142,805,883,224,792    142,806,066,279,866    183,055,074    3331
5     142,806,383,225,771    142,806,614,277,038    231,051,267    3331
6     142,806,883,229,494    142,807,145,347,839    262,118,345    3331
7     142,807,383,230,527    142,807,696,729,955    313,499,428    3331
8     142,807,883,231,420    142,808,249,459,465    366,228,045    3331
9     142,808,383,232,293    142,808,779,492,453    396,260,160    3331
10    142,808,883,233,183    142,809,374,609,495    491,376,312    3331
12    142,809,883,237,042    142,809,918,923,562    35,686,520     4355
11    142,809,383,234,072    142,809,918,923,592    535,689,520    3331
13    142,810,383,238,029    142,811,014,010,484    630,772,455    3331
14    142,810,883,238,910    142,811,014,040,582    130,801,672    4355
15    142,811,383,239,808    142,812,119,998,576    736,758,768    4355
16    142,811,883,240,686    142,812,120,019,559    236,778,873    3331
18    142,812,883,242,410    142,813,228,621,306    345,378,896    4355
17    142,812,383,241,550    142,813,228,646,734    845,405,184    3331
19    142,813,383,245,491    142,814,307,199,255    923,953,764    3331

The 20 tasks were distributed across 2 threads (3331 and 4355). Within each thread, the tasks were executed in order but the threads may be sent to a different CPU cores and hence causes out-of-order execution. Each task is also randomly delayed by up to 900ms. That's the trade-off in using queues: you have no control over the delay since who know what else is running on these global queues. You have 3 options here:

  • If timing is super-critical and you want to get a task executed with as little delay as possible, create and manage your own thread.
  • Use a serial instead of concurrent queue.
  • Scheduling dependent tasks by introducing a delay is usually a bad idea anyhow. Look into NSOperationQueue which allows you to specify the dependencies between tasks.
Code Different
  • 90,614
  • 16
  • 144
  • 163