42 comments

[ 15.9 ms ] story [ 90.8 ms ] thread
Well that was a frustrating read, largely because of the comment section
Please don't downvote me? I'm referring to the comment section, mostly
> Please don't comment about the voting on comments. It never does any good, and it makes boring reading.
Well, I can't delete comments, so I hope explaining them clarifies when they unintentionally are seen as mean. But, point taken.
Conclusion: A thread pool is the preferred solution and the go to that I have been using for years. The tricky part is optimizing the size of the pool.
I'm not sure a cached pool of OS threads is comparable in this way to coroutines. The author also isn't doing any I/O bound work... or... any work at all?

You can schedule coroutines over a threadpool. From the author's code, I'm not even sure this was accounted for in the benchmark. If they are using a threadpool and vanilla coroutines, without specifying where they are scheduled, yes, of course it won't start up a pool of threads for you and be as fast as a pool of threads. It would be terrible if it did that by default.

It does do this by default; Dispatchers.Default wraps a ThreadPoolExecutor with n threads, where n is the number of logical CPU cores. This is used for computation-bound work. The other built-in Dispatcher instances are Dispatchers.IO, with an unbounded thread pool for I/O-bound work; and Dispatchers.Main, which schedules on your platform's UI thread.
The IO dispatcher is a fixed size threadpool of 64 threads or the number of cpu cores (whichever is larger). You can use the kotlinx.coroutines.io.parallelism system property to override the default of 64.
That would require `withContext(Dispatchers.IO)`...
Is this the default on all platforms, Android, or JVM?
That's really not the conclusion, and not something you should seriously do.

The rule of thumb should be:

- Is your task CPU bound? Put it in a thread pool.

- Is your task spending most of its time waiting (e.g. network call)? Use a coroutine

Coroutines also run on a thread pool, and usually the pool size is optimized for the device‘s CPU.
You can schedule co-routines on a thread pool. Any Threadpool can be trivially turned into a co-routines dispatcher. Including project loom backed ones (not available on Android of course).

The comparison is nonsensical. It's not an either or kind of thing. If your co-routines are doing any blocking IO, you'd use the built in IO dispatcher, which is backed by a thread pool or you'd create your own dispatcher. Each co-routine gets its own thread if you use that; which can then do blocking IO and block the thread. Very common on the jvm if you are interacting with blocking IO things. I was working on some code that needed this yesterday.

The examples in this "benchmark" don't make any sense. It's not clear what the work is or why threads/co-routines are even needed. Handling UI events with threads is not a thing. But co-routines are very useful for that. Because they have very low overhead.

Deeply flawed methodology. This comment in the blog post summarizes it best

> Under the hood, [Kotlin] coroutines are runnables with an additional parameter representing the continuation, running on an executor. If you are not going to make any use of the continuation, you do not need the coroutines, you are just adding overhead over runnables and fixedthreadpools (which is what you measure).

Doesn't seem like there's anything wrong with the methodology in the quote. It's perfectly fine to compare two things, even if one is not meant for the task.
I don't understand: the methodology is flawed because it was empirical, and it could have just been argued from theory?

Sure, but, 15 years in a software career...I think the best parts have always come from checking what things did in theory versus practice

Lest someone new to Kotlin only reads the title and decides to spawn bare threads all over the place, don’t. At first pass, it's not clear to me what the actual work the author is doing in his benchmark. Instead, I’d point to the excellent work of Marcin Moskala’s Kt.Academy[0] and other resources on the topic.

[0] https://kt.academy/article/cc-best-practices

There aren’t numbers in your reference, just a set of recommendations. The author of the post tried to compare the performance overheads of using Coroutines over threads, that’s all.
The issue with OPs is that it's really a case where coroutines would have any advantage so it's kind of pointless.
I agree OP could have been more elaborate, but the main takeaway is that goroutines still cost at least 50% that of normal threads (though this needs to be verified in normal servers as well, not just smartphones). With goroutines, though usually supported by runtime systems (e.g., in Go), applications are responsible to manage tasks (e.g., HTTP requests) in order to harvest goroutines benefits, whereas this is easier with threads.

Edited for clarification.

What, you mean using threads doesn't automatically make your machine slow to a crawl because of le ""context switches""??

Heresy! But my async!

I went in expecting an odd explanation about how threads are inherently evil. Was surprised to see a speed comparison that largely confirmed that creating threads is the slowest option. And mostly delighted to see the admission that even thread creation is not hugely problematic.

The language affordances of coroutines are not to be ignored. But performance is a siren song.

Creating threads is about the fastest of all system calls. (Still a system call, though, and predicated on all the other runtime crap to support the obscure pthreads API features.)
From my limited understanding is that Coroutines can suspend themselves (unless you use some dispatcher I guess), while Threads you have to invoke suspend(). Right? Thus comparing the two of them is insane.
Since Kotlin threads appear to be Java threads (NB: Just following their documentation, I've never used Kotlin and have only started learning it because we may use it at work for some things in the near-ish future), you oughtn't use Thread::suspend. It's been deprecated for a while. Java threads are preemptible, though. You don't need to explicitly suspend them (even if that method weren't deprecated) since the scheduler will happily do it for you.
Coroutines are always scheduled on a Dispatcher; if you don't provide one in launch() it will use Dispatchers.Default.

But under the hood, Dispatchers schedule on Executors, usually ThreadPoolExecutor. So the more apt technology to compare to is VirtualThread with structured concurrency, and the takeaway should be that coroutines are an ergonomic way to manage concurrent work. Secondarily, they _can_ improve performance by reducing the number of OS threads, if the alternative were to naively spawn them and wait on I/O or something.

It’s a little more complicated than that, and Kotlin co-routine docs aren’t great at describing the internals of the coroutines infrastructure.

Coroutines by default are launched without an dispatcher, and simply create a single threaded event loop that schedules a single co-routine at a time to use the thread that created the coroutine scope. In this world the is no multithreading and coroutines can’t be moved between system threads, and no thread pools or equivalent are created.

However in most uses of coroutines (especially on Android), you’ll have a framework that already created a coroutine scope (using something like runBlocking) and provided the default dispatcher as the coroutine scopes dispatcher, which is then automatically inherited by any child coroutines or coroutines scope.

Personally I wished they had called Dispatchers.Default something else, it’s a confusing and slightly misleading name, given Dispatchers aren’t a hard requirement to use coroutines.

But I agree the big think to take away from Kotlin coroutines is structured concurrency, and the ergonomics to provides to manage concurrent operations in simple clean manner that avoids nasty resource leakages.

For everything else in coroutines, it’s pretty easy to reconfigure the coroutine runtime to use also most any underlying threading model, including ThreadPools, Singleton Threads, virtual threads, and my favourite, no additional threads whatsoever.

The above means that measuring coroutine startup and creation time and comparing it to threads kinda misses the point of using coroutines in the first place. And it’s possible to make coroutine creation cheaper or more expensive, depending on how exactly you’ve setup your coroutine scope, and which dispatchers (if any) you’re using.

I think the most powerful thing of coroutine is you can switch to other os thread without lost the scope and context as long as you implement the dispatcher of such os thread (it's likely it will be the os ui thread, because tons of system have such limit). You will usually rely on observer or publish/subscribe model to do such things before coroutine exists. Because you can't magically change the current thread. It's not how it works.

And unlike virtual thread. The underlying infrastructure is exposed directly to end user. So you can write your own dispatcher or continuation with ease as long as you know what are you doing. Make it more useful as a low level building block of a high level framework/library.

To get anyone from outside of Android ecosystem up to speed, blog author has always had controversial opinions. He's been trying to prove to the community that new Compose library is for what worse than decade old imperative UI system on Android for the past couple of years.
I agree with several of his opinions, many in the Android community cargo cult opinions from influencers in the Android team, sorry Developer Advocates, without any thoughts of how real they happen to be.
(I am aware of the author). The opinion not mentioned is not flawed. Some basic things were definitely missing when Compose was made 1.0. Now 2 years later, there are still features open that some consider basic.

My opinion as an android dev: he's definitely very knowledgeable, but you should also hear other opinions. It's not a writer you should listen to blindly.

will take advantage of your knowledge for a question and also a comment to your post.

No writer should be trusted blindly but i don’t thjnk you meant that literally ;) just for others to see. all information should have some verification even from trusted sources.

on the author though, can you share a bit about the subject and the author’s participation in android dev? i’ve never touched android anything dev wise so i’m not even sure if the topic being discussed is a new and unexplored territory, explored but still evolving, or if it’s something the community has settled on with plenty of data to back their conclusion. similarly im not sure if the author is really skilled enough to have controversial opinions or if it’s more they’re alright but excitable.

how would you position the two points just from your point of view?

i get the impression the author of the article is controversial and also opinionated i. “unusual” ways — for me, such persons are interesting to hear from even if a lot of their ideas are non-sense or fanciful. “out-there” ideas often are just out there for no reason, but sometimes there’s gold in such ideas, or good thoughts and starting points for actual golden ones.

wiuod you personally at least listen to this author just in case there’s some gold or are they very much so just in their own world?

Been using Compose for 3 years now professionally, while OP hasn't. I never had serious issues that other UI libraries do not have. OP arguments tend to be blatant and unreasonable, and I've been following him for last ~2 years on twitter.
(comment deleted)
I'm not sure I'd go with knowledgable.

While I agree with his opinions about cargo culting in the Android development world, many of his opinions lack substance and can be distilled to: "X sucks! it's hard to learn! don't use X! ... unless you pass my course". It's the classic "controversy > audience > course" funnel most of these twitter famous developers have.

Hate to be this guy, but If you look at his code, you'll find that this person wouldn't pass an average interview for a mid-level developer. From basics like naming and function scoping to weird architecture choices and abstractions. His "best practices" includes MVC that doesn't make sense as MVC. That is who is teaching people and selling courses.

It's sad that a lot of new developers get hooked onto what these people are saying as an avoidance tactic - "I don't understand X properly yet, the twitter man says X is complicated, ergo X = bad". Then they decline to learn best practices which can negatively impact their career - i.e. someone is going to use this as a "don't use coroutines" excuse, leading to them not using and understanding coroutines. This will mean that person will not learn about async/await, structured concurrency, how it works under the hood.

How will this person integrate with a team that uses the modern best practices? Even if they manage to pass the interviews, they'll be a lot behind the rest of the team in understanding some great computational concepts.

Unfortunately, people like these don't seem to care that they are having negative impact on their audience, as long as they get to sell their course.

I've been in the Android developer ecosystem professionally since '10 and I have no clue who this guy is.

Yet another Twitter/Reddit personality selling sketchy courses to the clueless crowd?

You are right, he is selling courses, but his opinions are contraversal primarily cause he just has to be different. Nothing to do with course selling if you ask me.
Showing results without any code is silly.
here is the talk by Roman Elizarov, the Kotlin project lead, at KotlinConf 2023 about Coroutines vs VThreads: https://www.youtube.com/watch?v=zluKcazgkV4 short version: both were optimized for different things, so there is a place for both of them.
Didn’t find it too convincing. He says Coroutines are for fine-granular concurrency, and Virtual Threads for coarse thread-per-request apps. In my experience fine granular concurrency is rare, and thread-per-request is 99% of the work. This might be different for Android development, but its true for server side code.

The cost you pay for this gain in fine grained control (which you rarely need) is unfortunately the colored function problem, and in my experience it is rather annoying. Also the complexity of having a second unit-of-computation abstraction besides threads, and Coroutines aren’t a small API surface (goes along with Flows).

tl;dr: the inferior developer experience (added complexity, colored functions) must be offset by a gain in usefulness, and I just don’t see it

one of the (main) benefits of coroutines is the luxury of offloading thread management and semantics to a lower layer so that we can deal in functions/methods (aka units of execution) as units of concurrency. obviously that comes at a cost, like any general purpose convenience.