2

I'v read a lot how dispatch work. But i Still a little confusing about it.

For example, what if I have

class ViewController: UIViewController {

    @IBAction func actionDoStuff(_ sender: UIButton) {

         DispatchQueue.global(qos: .userInitiated).async {

              Api.request { result in

                  //completes in main thread

                  //what if I need to dispatch again ?

                 DispatchQueue.global(qos: .userInitiated).async {

                     //Do other stuff here

                 }

              }

          }


     }
}

And

class Api {

    static func request(completion: @escaping (Result<String, NSError>) -> Void) {

        DispatchQueue.global(qos: .userInitiated).async {

        //url session configure
        let url = URL(fileURLWithPath: "test.com")
        URLSession.shared.dataTask(with: url) { data, response, error in

                DispatchQueue.main.async {

                     completion(.success("Request are success")) //without error handler for simplifier

                }

            }.resume()

        }


    }


}

So what we have is that we have ViewController action. When we start action we dispatch to global queue and make Api request.

1 At that point (let it be point 1) does queue gathered thread for the queue ?

Then we make Api.request call which make another dispatch to global queue.

2 Does it queued to same queue as at point 1 ? or it queue to the another queue with the same QoS ? or CPU make decision by itself ? And does it create new Thread ? Actually I know that GCD make decision to create thread by itself but I don't make sense

Then Api calls completes and by some circumstances we dispatch to the main queue

Then we dispatch again to do "//Do other stuff here" and does it creates new queue ? or new Thread ?

Also I know that we have GCD thread pool limit by 64. Thats why I'm afraid of. Also I've seen wwdc talks about Thread explosion but doesn't understand so if we often dispatch from queue to queue does it danger to get thread explosion ?

I understand that create new Thread for queue is expensive, and that we do not need often dispatch from queue to queue because we waste time to dispatch.

But what about my example is it wrong dispatch like this ?

Grigory
  • 53
  • 6

2 Answers2

5

Basically your understanding of queues are wrong, iOS provides by default several dispatch queues (The only difference between these queues are Quality Of Service they guarantee) and 1 serial queue (which is obviously main queue)to each app.

Of course you can create your own serial and dispatch queues, but because your code is using dispatch.global am gonna stick with only global queues here.

They are available all the time no matter whether your app access it or not. Thats why they are Called global queues :D

Quoting apple

The system provides each application with four concurrent dispatch queues. These queues are global to the application and are differentiated only by their priority level. Because they are global, you do not create them explicitly.

These are non reference counted objects available to all the apps, so your first question of "call which make another dispatch to global queue." is illogical. Queues are not created when you access DispatchQueue.global(qos: rather you only access one of the several Dispatch Queue already available in system and add your task to it based on the QoS you are opting for.

Lets answer your questions,

1 At that point (let it be point 1) does queue gathered thread for the queue ?

There is no way to guess whether Queue already has a thread or not, these are global queues, and threads are created, handled and managed by queues themselves. So if Queue already had a scheduled task to execute, or if it was already executing a task it might be having thread, else it might not. How many thread? Again we don't have control over it, Dispatch queue decides how many thread it needs to concurrently execute tasks.

2 Does it queued to same queue as at point 1 ? or it queue to the another queue with the same QoS ? or CPU make decision by itself ? And does it create new Thread ? Actually I know that GCD make decision to create thread by itself but I don't make sense

You are accessing the global dispatch queue with same QoS, userInitiated so obviously you added the task to same queue which you used in Point1. I hope by now you have figured out that you don't create queue when you access DispatchQueue.global(qos: rather you simply use one of the many dispatch queues provided by iOS.

Actually I know that GCD make decision to create thread by itself but I don't make sense

To be really honest, you don't have to that's the whole point of logic abstraction, they have written an interface called GCD api to hide the complications of low level api such as creating thread, managing and scheduling it

Issues in your code:

Clearly

static func request(completion: @escaping (Result<String, NSError>) -> Void) {

        DispatchQueue.global(qos: .userInitiated).async {

dispatches API call on .userInitiated queue, hence initial

 DispatchQueue.global(qos: .userInitiated).async {

              Api.request { result in

makes no sense. Thread context switching is costly and should be done only when it makes sense. Though it won't make switch again at the encounter of second statement, but initial switch was completely useless anyway

DispatchQueue.global(qos: .userInitiated).async {

As per Apple docs

User-initiated tasks are second only to user-interactive tasks in their priority on the system. Assign this class to tasks that provide immediate results for something the user is doing, or that would prevent the user from using your app. For example, you might use this quality-of-service class to load the content of an email that you want to display to the user.

Clearly your usage of .userInitiated queue to dispatch all your api call is, for the lack of better words I call abuse of userInitiated dispatch queue. User-initiated tasks are second only to user-interactive tasks in their priority on the system. do you really want all your long API call to have that priority. Thats very steep down the hill road you are taking if you ask me :)

What should I use?

Depends on your need, if its simple API call you might use default global queue, if you need to run your API on background configuration you might use background queue, clearly not .userInitiated to do all your laundry work.

Hope it helps

Sandeep Bhandari
  • 19,999
  • 5
  • 45
  • 78
  • I've figured out about global queues when we dispatch to that global queue we simple add task to queue. But what about private queue when we create it by writting let myQueue = DispatchQueue(label: "myQueue.concurent", qos: .userInitiated, attributes: .concurrent) – Grigory May 19 '20 at 20:58
  • @grigory: Now you are talking about the custom queue that you create on your own and not the global queues. Even in this case as rob mentioned you will end up in thread explosion only if you dispatch hell load of tasks on dispatch queue but if you are using serial queue, it will always have only one 1 thread because it has to execute only one task at a time. But modern CPU's are damn fast, small.operations like making API call , data parsing will be over in fraction of seconds to few seconds so to be really honest you might hardly ever face thread explosion scenario unless u do some long – Sandeep Bhandari May 19 '20 at 21:06
4

You're using dispatch too much here. Api.request doesn't have any reason to dispatch to .userInitiated before calling URLSession.shared.dataTask. That's an async call already. You definitely shouldn't be dispatching to .userInitiated before calling Api.request.

The dispatch in the completion handler could make sense if there's something you don't want to do on main, but it indicates that Api.request is dispatching when it shouldn't.

First, I would rewrite request this way:

// Accept a parameter for where you'd like to be called back; defaulting to .main.
// It is common for completion handlers to make no promise about where they're
// called (as in the case of URLSession), but it can be convenient if they do.
static func request(on queue: DispatchQueue = .main, completion: @escaping (Result<String, NSError>) -> Void) {
    let url = URL(fileURLWithPath: "test.com")
    URLSession.shared.dataTask(with: url) { data, response, error in
        queue.async {
            completion(.success("Request are success")) //without error handler for simplifier
        }

    }.resume()
}

And then call it this way (if you want to run your completion handler on a non-main queue):

@IBAction func actionDoStuff(_ sender: UIButton) {
    Api.request(on: .global(qos: .userInitiated)) { result in
        // ...
    }
}

Also I know that we have GCD thread pool limit by 64. Thats why I'm afraid of. Also I've seen wwdc talks about Thread explosion but doesn't understand so if we often dispatch from queue to queue does it danger to get thread explosion ?

This will most often occur if you dispatch many small things to concurrent queues (like the .global() queues). As long as you dispatch to serial queues (like .main) no additional threads will be created. You should be thoughtful about creating excessive concurrency, but for this kind of small-scale problem (network requests), you shouldn't run into problems. Network requests are incredibly slow; so slow that you should think of them as taking forever in terms of concurrency.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • So if I understand right if I don't dispatch before api.request calls then api calls on the main thread and block it untill task completes ? Why should I do not dispatch before Api.request ? – Grigory May 19 '20 at 20:52
  • 1
    That's backwards. `.dataTask` is an asynchronous call. It returns immediately. Anything with a parameter called "completion" or "completionHandler" is going to be an asynchronous call. – Rob Napier May 19 '20 at 20:53
  • 1
    1 for the insightful answer as usual :) – Sandeep Bhandari May 19 '20 at 21:31