1

Recently I've been learning about the different ways languages handle concurrency under the hood, and would like to know a bit more about the shortcomings of M:N threading. Please forgive me if I get some facts wrong in the following paragraphs.

I understand event loops (at least the style of event loop used in a language like node), and how the main disadvantages are you have to write code in an async style, lots of nested callbacks, it doesn't scale to more than one core (without creating more processes), CPU intensive tasks slow down the loop, and that complex logic ends up growing the call stack a lot. The advantage of course being that the issues with threading are, for the most part, abstracted away.

I've seen M:N be touted as the solution to the problems of the event loop, and in my personal experience with some Erlang and Go it's been mostly true.

Now my question is, regardless of the nuances of the existing different VMs and programming languages (I don't want this to be a Node vs Go vs Erlang discussion), are there any inherent disadvantages to an M:N model given that you have a perfectly written scheduler? Something similar to the disadvantages I listed above about event loops.

  • By "event loops" do you mean specifically single-threaded synchronous multiplexing I/O? There are lots of things you can use event loops for, and they don't all require complex logic, deep call stacks or the rest. – Useless Sep 30 '16 at 13:01
  • @Useless yeah that's what I mean. Sorry if I misconstrued them, I haven't come across them in other contexts, and don't intimately know what else they're used for. – m0meni Sep 30 '16 at 13:02
  • "you don't need to deal with shared memory concurrency" -- you don't need to explicitly synchronize access to shared state, but you very much need to deal with the bigger issues of staleness and update order. – kdgregory Sep 30 '16 at 15:15
  • @kdgregory agreed, sorry for not being clear about that. I expected to get some things not 100% accurate, which is why I put a disclaimer at the top about probably screwing up some facts. I'm just hoping to better understand M:N at the end of the day. – m0meni Sep 30 '16 at 15:17
5

One significant drawback with MxN threads is relative to blocking I/O.

If the system has blocking I/O, then the kernel threads (N's) can become blocked. So, assuming you have 4 processors and use N=4, your application might lose access to a processor for the duration of the blocking I/O operation.

This is not so much a property of the scheduler itself but of the interaction of the user code with the operating system.

In the following text, LWP is a light weight process equivalent to a kernel thread.

Scheduler Activation

Prior to Solaris 2.6 software, the kernel used a special signal, SIGWAITING, to inform the threads library that all LWPs were blocked in the kernel. This gave the library the opportunity to create another LWP so it could to continue to run other, nonblocking threads. In Solaris 2.6 software, this mechanism was augmented by the preferential use of a “door upcall.” Essentially, this involves the kernel being able to call into the user-level thread scheduler to adjust the number of LWPs in the process’ pool of LWPs. This door mechanism is more efficient than a signal, but if necessary, Solaris 2.6 software falls back to using the SIGWAITING mechanism.

In this text, there is no mention of how to perform the respective reduction of the N threads as would be appropriate when the blocking thread becomes unblocked. Suffice it to say that this is complicated.

Problems with blocking I/O can be avoided by using always using non-blocking I/O, and asking the scheduler to run a different M thread on the now-available N thread when doing the async I/O request. A library layer might translate blocking calls into such non-blocking calls that cooperate with the MxN scheduler and the async I/O completion mechanism. Apparently Erlang does this.


The are also potential issues are around thread priority with MxN as the kernel only sees the N threads, and the M threads are process-specific. So, no one sees all the M threads together across all processes, whereas with 1x1 threads, the kernel sees all threads together and thus has the widest view to make the most balanced decisions.

I believe this is mitigated if there is only one real process (e.g. one Erlang virtual machine) on the system.


A history of the trade-offs over the years of operating system development would be interesting to digest. It looks to me like Solaris implemented MxN threads fairly early but later switched to 1x1 by lowering the cost of kernel threads.

  • Thanks for taking the time to answer. I feel like these problems fall into the "perfectly written userspace scheduler" category though. They can be overcome, and as you mentioned Erlang has overcome them. Are there any unavoidable faults that are intrinsic to the M:N model that can't be avoided? For example, with event loops you have to write callbacks one way or another...they're unavoidable. – m0meni Sep 30 '16 at 17:43
  • This is all I can think of. User code must avoid using blocking I/O or else the application will suffer. I don't see that as being up to the scheduler alone, some yet other component is needed to translate user blocking I/O into asnyc I/O as the article mentions is being done for Erlang, but I guess that is just a matter of what you call the scheduler. – Erik Eidt Sep 30 '16 at 17:55
  • The only reason I think it's a scheduler problem is because of this question I asked a while back about the way go handles what you're describing stackoverflow.com/questions/36489498/…. – m0meni Sep 30 '16 at 18:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.