https://kotlinlang.org logo
#coroutines
Title
# coroutines
n

Niklas

11/30/2023, 1:43 PM
Hi, I’m just learning about Coroutines and wanted to ask what effects the
<http://Dispatchers.IO|Dispatchers.IO>
has on iOS? Is it also backed by a global queue like
Dispatchers.Default
which is mentioned here: https://kotlinlang.org/docs/native-migration-guide.html#update-dependencies ? Unfortunately I can’t find any information about this and in the code I only see that the
DefaultIoScheduler
is being used for
<http://Dispatchers.IO|Dispatchers.IO>
. To be honest, I don’t know exactly what it does.
d

Daniel Pitts

11/30/2023, 5:21 PM
I don't have a concrete answer for you, but the intent is simply that any blocking IO operations you have should be dispatched with the
<http://Dispatchers.IO|Dispatchers.IO>
. If you don't, you run the risk for blocking threads that are expecting to run in a non-blocking manner.
u

uli

12/01/2023, 8:57 AM
Does Dispatchers.IO even exist on iOS? I have been painfully missing it, but it has also been a while since I checked.
n

Niklas

12/01/2023, 9:17 AM
I don't really know. When I use
<http://Dispatchers.IO|Dispatchers.IO>
and run the iOS app, everything seems to work fine and it doesn't seem to run on the main thread. But I can't find any information on how this works.
u

uli

12/01/2023, 10:20 AM
Do you know how it works on Android? I’d assume behaviour should be the same then.
n

Niklas

12/02/2023, 12:33 PM
Thanks for the links :) Do you know what that means that there is an extension? I’ve tried a bit and get the following output from this code snippet
Copy code
launch(Dispatchers.Main) {
    println("Main: $currentThread")
}
launch(Dispatchers.Default) {
    println("Default: $currentThread")
}
launch(<http://Dispatchers.IO|Dispatchers.IO>) {
    println("IO: $currentThread")
}
launch(newSingleThreadContext("MyOwnThread")) {
    println("MyOwnThread: $currentThread")
}
iOS:
Copy code
IO: <NSThread: 0x2800dd100>{number = 10, name = (null)}
Default: <NSThread: 0x2800d5280>{number = 11, name = (null)}
MyOwnThread: <NSThread: 0x2800b1280>{number = 12, name = (null)}
Main: <_NSMainThread: 0x2800b4040>{number = 1, name = main}
Android:
Copy code
IO: DefaultDispatcher-worker-5
Default: DefaultDispatcher-worker-5
MyOwnThread: MyOwnThread
Main: main
Seems so that on iOS different background thread are created and on Android these are reused for IO and Default.
u

uli

12/02/2023, 12:59 PM
I don't know about iOS. But yes, Android shares a thread pool. And has another important optimization. If a coroutine switches context from default and io or the other way around, it stays on the same native thread if the concurrency limit allows. Saving an expensive, native context switch. Seems that iOS is not there yet.
👍 1
n

Niklas

12/02/2023, 1:47 PM
I have also just tested how this works with
withContext
. Apparently it is always executed on the same background thread on iOS for the respective dispatcher and switched on Android if necessary.
Copy code
withContext(Dispatchers.Default) {
    println("Default 1: $currentThread")
}
withContext(Dispatchers.Default) {
    println("Default 2: $currentThread")
}
withContext(Dispatchers.IO) {
    println("IO 1: $currentThread")
}
withContext(Dispatchers.IO) {
    println("IO 2: $currentThread")
}
withContext(Dispatchers.IO) {
    println("IO 3: $currentThread")
}
iOS:
Copy code
Default 1: <NSThread: 0x282b05080>{number = 4, name = (null)}
Default 2: <NSThread: 0x282b05080>{number = 4, name = (null)}
IO 1: <NSThread: 0x282be8280>{number = 9, name = (null)}
IO 2: <NSThread: 0x282be8280>{number = 9, name = (null)}
IO 3: <NSThread: 0x282be8280>{number = 9, name = (null)}
Android:
Copy code
Default 1: DefaultDispatcher-worker-9
Default 2: DefaultDispatcher-worker-9
IO 1: DefaultDispatcher-worker-9
IO 2: DefaultDispatcher-worker-3
IO 3: DefaultDispatcher-worker-4
d

Daniel Pitts

12/02/2023, 6:48 PM
Interesting. What about if you launch several async and add a small delay? It could be that iOS is smart enough to complete the work immediately and then reuse the thread.
u

uli

12/02/2023, 6:51 PM
please wrap the whole block into
withContext{Disptachers.Default)
to see how switching between those Dispatchers works.
Copy code
withContext(Dispatchers.Default) {
  withContext(Dispatchers.Default) {    
    println("Default 1: $currentThread")
  }
  withContext(Dispatchers.Default) {
    println("Default 2: $currentThread")
  }
  withContext(Dispatchers.IO) {
    println("IO 1: $currentThread")
  }
  withContext(Dispatchers.IO) {
    println("IO 2: $currentThread")
  }
  withContext(Dispatchers.IO) {
    println("IO 3: $currentThread")
  }
}
Or nest them:
Copy code
withContext(Dispatchers.Default) {
  println("Default 1: $currentThread")
  withContext(Dispatchers.Default) {
    println("Default 2: $currentThread")
    withContext(Dispatchers.IO) {
      println("IO 1: $currentThread")
      withContext(Dispatchers.IO) {
        println("IO 2: $currentThread")
        withContext(Dispatchers.IO) {
          println("IO 3: $currentThread")
        }
      }
    }
  }
}
What’s happening with your test setup is probably, that you switch back to the surrounding dispatchers thread between each withContext.
When you switch context each time from, let’s say main or runBlocking-dispatcher to a background dispatcher, it does not really make a difference, which background thread you schedule to
On android, both variants of above code should run sequentially on a single thread without context switch.
n

Niklas

12/03/2023, 1:12 PM
@Daniel Pitts If I’m using async it behaves as follows, regardless of whether there are delays between them or not:
Copy code
async Default 1: <NSThread: 0x600001708800>{number = 7, name = (null)}
async Default 2: <NSThread: 0x600001708800>{number = 7, name = (null)}
async Default 3: <NSThread: 0x600001708800>{number = 7, name = (null)}
async IO 1: <NSThread: 0x6000017342c0>{number = 8, name = (null)}
async IO 2: <NSThread: 0x6000017342c0>{number = 8, name = (null)}
async IO 3: <NSThread: 0x6000017342c0>{number = 8, name = (null)}
@uli The same applies here. Different threads are created for Default and IO, but these are reused for the respective dispatcher. On android it behaves like you said. There are no context switches. And my tests are running inside runBlocking. iOS:
Copy code
withContext wrapped in Default: <NSThread: 0x600001703140>{number = 6, name = (null)}
withContext wrapped Default 1: <NSThread: 0x600001703140>{number = 6, name = (null)}
withContext wrapped Default 2: <NSThread: 0x600001703140>{number = 6, name = (null)}
withContext wrapped IO 1: <NSThread: 0x60000176d1c0>{number = 8, name = (null)}
withContext wrapped IO 2: <NSThread: 0x60000176d1c0>{number = 8, name = (null)}
withContext wrapped IO 3: <NSThread: 0x60000176d1c0>{number = 8, name = (null)}
--------
withContext nested Default 1: <NSThread: 0x600001703140>{number = 6, name = (null)}
withContext nested Default 2: <NSThread: 0x600001703140>{number = 6, name = (null)}
withContext nested IO 1: <NSThread: 0x60000176d1c0>{number = 8, name = (null)}
withContext nested IO 2: <NSThread: 0x60000176d1c0>{number = 8, name = (null)}
withContext nested IO 3: <NSThread: 0x60000176d1c0>{number = 8, name = (null)}
Android:
Copy code
withContext wrapped in Default: DefaultDispatcher-worker-2
withContext wrapped Default 1: DefaultDispatcher-worker-2
withContext wrapped Default 2: DefaultDispatcher-worker-2
withContext wrapped IO 1: DefaultDispatcher-worker-2
withContext wrapped IO 2: DefaultDispatcher-worker-2
withContext wrapped IO 3: DefaultDispatcher-worker-2
--------
withContext nested Default 1: DefaultDispatcher-worker-5
withContext nested Default 2: DefaultDispatcher-worker-5
withContext nested IO 1: DefaultDispatcher-worker-5
withContext nested IO 2: DefaultDispatcher-worker-5
withContext nested IO 3: DefaultDispatcher-worker-5
👍 1
u

uli

12/03/2023, 1:15 PM
So it seems, iOS is just using 2 thread pools, without any optimization
n

Niklas

12/03/2023, 1:22 PM
Yep, thanks for the help :)