dave08
11/20/2023, 2:55 PMparMap
by default uses an EmptyCoroutineContext
doesn't it mean that it's not running in parallel at all? If so, it's a bit confusing, since in the docs, it uses the one w/o parameters... it should be parMap(<http://Dispatchers.IO|Dispatchers.IO>...) { .. }
or something?Javier
11/20/2023, 3:24 PMdave08
11/20/2023, 3:25 PM/**
* An empty coroutine context.
*/
@SinceKotlin("1.3")
public object EmptyCoroutineContext : CoroutineContext, Serializable {
private const val serialVersionUID: Long = 0
private fun readResolve(): Any = EmptyCoroutineContext
public override fun <E : Element> get(key: Key<E>): E? = null
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R = initial
public override fun plus(context: CoroutineContext): CoroutineContext = context
public override fun minusKey(key: Key<*>): CoroutineContext = this
public override fun hashCode(): Int = 0
public override fun toString(): String = "EmptyCoroutineContext"
}
dave08
11/20/2023, 3:26 PMdave08
11/20/2023, 3:26 PMdave08
11/20/2023, 3:27 PMJavier
11/20/2023, 3:29 PMdave08
11/20/2023, 3:32 PMdave08
11/20/2023, 3:32 PMdave08
11/20/2023, 3:36 PMJavier
11/20/2023, 3:41 PMJavier
11/20/2023, 3:42 PMdave08
11/20/2023, 3:43 PMdave08
11/20/2023, 3:44 PMJavier
11/20/2023, 3:46 PMlaunch
happens in the main thread, they are instantaneous, and the long task is done in the deeper dispatcher, which will be the one used by the library, so IO
, or a custom one like Room does for exampleJavier
11/20/2023, 3:47 PMIO
to do a request, if you wrap the request with a different dispatcher, your dispatcher will not be used and IO
will be used anywaydave08
11/20/2023, 3:49 PMDispatcher.Main
is a pool? Otherwise, it shouldn't run parallel as if you created a dispatcher with one thread...CLOVIS
11/20/2023, 3:49 PMEmptyCoroutineContext
means "use the exact same context as what this function is already running in". <http://Dispatcher.IO|Dispatcher.IO>
means "use the exact same context as what the function is already running in, but replace the dispatcher by Dispatcher.IO"
You can debug what the current context is at that point in the code with println(currentCoroutineContext())
dave08
11/20/2023, 3:49 PMJavier
11/20/2023, 3:50 PMdave08
11/20/2023, 3:51 PMYeah, that's what I understood from @Javier, but more explicit. I think something like that should be in the docs to avoid confusion...means "use the exact same context as what this function is already running in".EmptyCoroutineContext
means "use the exact same context as what the function is already running in, but replace the dispatcher by Dispatcher.IO"<http://Dispatcher.IO|Dispatcher.IO>
CLOVIS
11/20/2023, 3:52 PMlaunch
and async
in the coroutines lib), you can see they all have it as a default valueCLOVIS
11/20/2023, 3:54 PMasync
: https://github.com/arrow-kt/arrow//blob/main/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/ParMap.kt#L33dave08
11/20/2023, 3:55 PMdave08
11/20/2023, 3:56 PMdave08
11/20/2023, 3:57 PMCLOVIS
11/20/2023, 3:57 PMCLOVIS
11/20/2023, 3:58 PMEmptyCoroutineContext
, since in my experience that's the only place where it is used.dave08
11/20/2023, 3:59 PM*par*Map
on an unlimited pool, or limited by a concurrency limit, if specified... in async {} there isn't any concurrency parameter, you just limit the pool, so it could get confusing.stojan
11/20/2023, 3:59 PMparMap
, they will run in parallel as retrofit will run them on OkHttp's thread pool by defaultdave08
11/20/2023, 4:00 PMpublic suspend fun <A, B> Iterable<A>.parMap(
context: CoroutineContext = EmptyCoroutineContext,
concurrency: Int,
f: suspend CoroutineScope.(A) -> B
): List<B>
CLOVIS
11/20/2023, 4:00 PMdave08
11/20/2023, 4:06 PMJavier
11/20/2023, 4:08 PMlaunch/async
. Those milliseconds would be synchronous. After those milliseconds, the join/await
will be waiting until all requests are made. They would be happening in IO.CLOVIS
11/20/2023, 4:28 PM// network request here
.dave08
11/20/2023, 4:34 PMdave08
11/20/2023, 4:34 PMdave08
11/20/2023, 4:36 PMwithContext(...) { }
in a suspend function and then call that from parMap w/o paramsCLOVIS
11/20/2023, 4:37 PMwithContext(foo) {
parMap { … }
}
is exactly the same as
parMap(foo) { … }
(the second is probably very slightly faster, but I doubt it's even measurable)dave08
11/20/2023, 4:39 PMsuspend fun someBlockingthing() = withContext(foo) { ... }
parMap { someBlockingthing(); .... }
CLOVIS
11/20/2023, 4:40 PM....
in parMap
runs in whatever called parMap
dave08
11/20/2023, 4:42 PMCLOVIS
11/20/2023, 4:44 PMsimon.vergauwen
11/21/2023, 10:33 AMwithContext(foo) {
parMap(foo) { }
}
Doesn't result in a performance hit over parMap(foo)
, but you should only use withContext
if more than only parMap
needs to run on foo
.
The reason is that intercepting
or scheduling is what costs time (throughput/performance), and parMap
will schedule every element on a new coroutine.
Besides that the behavior just follows the same patterns as KotlinX Coroutines, so as everyone mentioned above EmptyCoroutineContext
is similar to null
. In the sense that ctx + EmptyCoroutineContext == ctx
and EmptyCoroutineContext + ctx == ctx
. So in this way CoroutineContext
has a Monoid
instance, similar to the one for (immutable) maps.
It runs concurrently, always. It only runs in parallel if the dispatcher it is running in supports parallel executionThis is a very important nuance, but applies to KotlinX Coroutines exactly the same.