By default, do I use `default` or `io` dispatcher?...
# coroutines
u
By default, do I use
default
or
io
dispatcher? Or rather, when would I want to use io over default? I get that its in the name, but what exactly is io work? Networking? Retrofit has coroutines support, so the thread wont be blocked there. Database? Room has it as well Only thing that comes to mind is file writing/reading? Or would it be bad to just use the
io
? I mean it contracts eventually, right? Can I see how many actual threads are in the
io
at some point?
e
Dispatchers.Default
is expected to be used for computation - e.g., things which are CPU-bound, so by default uses a thread pool bounded by the number of CPU cores detected
(with the exception that on a single-core system, it still allows 2 threads)
u
I get all that but what is a computation in a regular android glorified crud app?
or rather, if networking is suspending, database as well, and I dont do any file writing, is everything else (model mapping, control flow, nothing really heavy duty) a “computation”?
e
the Android/Java APIs are mostly blocking - not quite sure what you mean by "networking is suspending, database as well"
u
I can think of json parsing, but that is streamed with moshi + okhttp, so basically a impl. detail of retrofit library
e
compression could be something to consider as computation - it's CPU-bound
u
well, on android I use retrofit, which is basically sugar around okhttp, which has its own threadpool,
which retrofit wraps with regular
suspendCancelableCoroutine
basically
okay when I think about it, im not sure where the json parsing is ran by retrofit, but I presume its the okhttp threadpool
e
in that case there isn't much practical difference in which dispatcher you use, since it doesn't block any of the dispatcher's threads
(if all threads in the dispatcher are already busy then I think there could be some difference in their rejected execution fallback, but I'd have to look at the code to check)
u
yea but even in this setup, lets say I have a Syncer objects, where its calls network and saves to database — even if net and db support coroutines, if I were to have 100 of these Syncers, and initialize them all at once, with io dispatcher, I do spin up 100 threads right?
e
no
if they are properly suspending, the thread is free to run other coroutines
u
no I mean like this
Copy code
Syncer {
	private val scope = CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>)

	fun sync() {
		scope.launch {
			val whatever = 1 + 1
			val apiData = apiClient.fetchData()
			val data = apiData.toData()
			database.save(data)
		}
	}
}
so apiclient and database have their own stuff and expose suspending api, so “my” thread basically only does the 1+1 and apiData.toData(), right?
e
assuming that's the case, yes
u
and if so, if I have 100 of these, and I call
sync
in a forloop, would I spin up 100 threads to do the 1+1 computation?
e
even a single-threaded dispatcher like
Dispatchers.Main
can run 100 parallel coroutines. it doess not require 100 threads
of course only one will be running at a time, but they can interleave at the suspension points
u
im aware, lets just stick to this example
e
IO is limited to 64 threads by default
u
i know but still, the 1+1 is technically a blocking code, right?
e
if you run 100 syncs, you may briefly use all 64 threads getting every coroutine to the first suspension point of fetchData
it will not use 100 threads
u
okay so maybe not 100 but yes a lot of those, lets say 50, I get that when it arrives at the fetchdata suspension point its free to do something else, presumably the 1+1 in other syncer
but I do spike memory way way too much, for basically nothing, right?
Copy code
for syncer in hunderSyncers {
	syncer.sync()
}
e
I don't understand what you mean by that
u
Copy code
for syncer in hunderSyncers {
	syncer.sync()
}
if the work is initiated like this, then you’d say for some short moment I do spin up 64 threads? Or whatever the number is, its 10s right? Way more that
default
would?
e
possibly, depending on how fast they launch
and whether that is more or less than
Default
depends on the system
note that
IO
and
Default
actually share the same underlying thread pool
actually maybe they don't, gotta check…
u
I know I’m just trying to figure out if I’m needlessly using
io
well lets assume a normal phone, so 8 cores for example, definetly not 100 cores
e
looks like they're separate thread pools instead of views into the same pool, but
IO.limitedParallelism
can also create new views that can go beyond `IO`'s original bounds
u
are you sure? when I print threads in default and io they look the same to me,
Copy code
DefaultDispatcher-worker-X
anyways, this exact scenario, should I go with
default
or
io
?
e
I'd go with IO.
u
why
e
you're going to use however many IO threads there are for networking and db anyway
u
thats true, but isnt less better? less memory?
n
Not true. The android libraries tend to use their own thread pools so using Dispatchers.IO for Room or Retrofit would be a waste. You’d start a thread just to dispatch to another thread.
e
I mean, it's a fixed amount of overhead. it's either that or bottleneck your usage of room/retrofit.
n
bottleneck your usage of room/retrofit how?
Just to clarify, room and retrofit have built in support kotlin coroutines. You can define the interfaces with suspend methods and then they are safe to call from any coroutine.
u
Hi @Nick Allen you are completely right. Oh Android i have only ever needed the IO dispatcher for disk based Io. And your example would spawn a lot of useless threads, because they would all be busy for a very brief amount. To launch 100 coroutines i would not dispatch at all and just stay on whatever dispatcher I am on. Changing threads is probably more expensive then the actual work
e
if you're starting from a non-suspending context, as the example is, you have to choose some dispatcher
but sure, if you change it to a
suspend fun sync()
then you can
launch(Dispatchers.Unconfined)
to avoid switching threads until necessary
j
Going back to the original use case of running this from a non-suspending function, since you just need to enqueue the coroutines, and most of the work is done on the libraries' own thread pool, I guess either dispatcher would do if you use
limitedParallelism()
to limit the thread spike. But honestly I think
Default
without
limitedParallelism
would be simple enough and allow to spawn all those coroutines and run them up to the first suspension point before they switch to the other thread pools.
👍 1
u
Would your opinion change if the database usage was blocking? Say using Sqldelight, not Room, which doesnt have suspending function wrapper on purpose because of the way transactions are handled
j
Definitely. As soon as you have actual blocking IO (blocks the thread without using the CPU) then it would be a waste to hold threads from the
Default
dispatcher (and could cause starvation)
u
In-process database is a corner case though, as it can be io&cpu heavy. So in best case, the database itself dispatches io to the io dispatcher and query logic to default