Implementation of Haskell's forkIO

Viewed 968

Different OSes have different concurrency subsystems, there are OS processes, POSIX threads and today also "LWP" threads in Linux, Windows has processes, fibers, threads, etc. Each process is scheduling by OS scheduler and gets own quantum of CPU time. This is true for Linux "LWP"s because they are processes but sharing memory space, and it's not true for user-space threads, where all threads share one CPU time quantum.

Haskell has forkIO. I found in the Haskell sources next commentaries:

Scheduling of Haskell threads is done internally in the Haskell runtime system, and doesn't make use of any operating system-supplied thread packages.

also

In terms of performance, 'forkOS' (aka bound) threads are much more expensive than 'forkIO' (aka unbound) threads, because a 'forkOS' thread is tied to a particular OS thread, whereas a 'forkIO' thread can be run by any OS thread. Context-switching between a 'forkOS' thread and a 'forkIO' thread is many times more expensive than between two 'forkIO' threads.

which emphasizes that threads created with forkIO are not scheduling by the OS scheduler. They, as I understand, can be free from common blocking (with -thread option, sure), but however in the case there are 3 open questions for me:

  1. how do they ("threads" created with forkIO) share those CPU quantum?
  2. will they be guaranteed to be distributed to different cores or, since they are represented by one process, no? Or is this non-deterministic behavior?
  3. Am I right that to avoid interference effects is better to use forkOS than forkIO? I mean if I have 2 threads and one of them serves HTTP and another one makes heavy disk I/O operation, then better solution will be to use forkOS than forkIO?
2 Answers
  1. Haskell threads use cooperative multi-threading. Essentially, each time Haskell needs to allocate memory, it checks if enough time has passed, and if so it switches to the next thread. The exact mechanism is a bit more sophisticated (I think at some point it also involved POSIX signals e.g. 'alarm'), but this should be the main idea.

  2. The runtime system makes N Haskell threads run over K OS threads. K can be chosen by the user. It's the OS which then decides on which core(s) each OS thread is run -- this might always be the same core or not.

  3. IO heavy operations should not be a big issue. The Haskell runtime uses nonblocking IO and poll/select to multiplex IO over all threads. Also, if you have two running Haskell threads and you dedicate at least two OS threads to the runtime, these should be run over the OS threads, which the OS should allocate to both cores. Feel free to experiment withforkIO vs forkOS to see which provides the best performance for your case, but forkIO should be better in virtually all cases.

Little testing on Windows 10 with 4 core CPU shows results:

module Main where

import Control.Monad
import Control.Concurrent

t1 = forever $ print "1"
t2 = forever $ print "2"

main :: IO ()
main = do
  t1id <- forkOS t1 -- I tried forkOS and forkIO
  t2id <- forkOS t2
  getLine
  putStrLn "Done"

Both, forkIO and forkOS requires ghc-options: -threaded option to utilize mutli-thread execution. In both cases I see only one OS process, but multiple threads. In the case of forkIO threads number is 4, while in the case of forkOS I get only 3. More interesting is the number of context switches, it's different from mentioned suggestions: in the case of forkIO they are (for different threads): 480000 and 48... and in the case of forkOS they are about 2, 3, 16 for the same time period. This means that forkIO makes more context switches than forkOS (hundreds of thousands vs tens) and summary execution time of the same valuable effect should be greater, so forkOS looks more preferable on Windows box.

As I see in GHC source code, POSIX threads are using under the hood.

EDIT. Linux test (ps -eLf):

forkOS:

UID        PID  PPID   LWP  C NLWP STIME TTY          TIME CMD
...        ...   ...   ... ... ...   ... ...           ... ...
xyz     7432  4865  7432  0    7 10:03 pts/0    00:00:00 /home/xyz/prj/thr/.stack-work/install/x86_64-linux-tinfo6/lts-13.25/8.6.5/bin/yohoho
xyz     7432  4865  7448  0    7 10:03 pts/0    00:00:00 /home/xyz/prj/thr/.stack-work/install/x86_64-linux-tinfo6/lts-13.25/8.6.5/bin/yohoho
xyz     7432  4865  7449  0    7 10:03 pts/0    00:00:00 /home/xyz/prj/thr/.stack-work/install/x86_64-linux-tinfo6/lts-13.25/8.6.5/bin/yohoho
xyz     7432  4865  7450  0    7 10:03 pts/0    00:00:00 /home/xyz/prj/thr/.stack-work/install/x86_64-linux-tinfo6/lts-13.25/8.6.5/bin/yohoho
xyz     7432  4865  7451 66    7 10:03 pts/0    00:00:06 /home/xyz/prj/thr/.stack-work/install/x86_64-linux-tinfo6/lts-13.25/8.6.5/bin/yohoho
xyz     7432  4865  7452  0    7 10:03 pts/0    00:00:00 /home/xyz/prj/thr/.stack-work/install/x86_64-linux-tinfo6/lts-13.25/8.6.5/bin/yohoho
xyz     7432  4865  7453 67    7 10:03 pts/0    00:00:06 /home/xyz/prj/thr/.stack-work/install/x86_64-linux-tinfo6/lts-13.25/8.6.5/bin/yohoho

forkIO:

UID        PID  PPID   LWP  C NLWP STIME TTY          TIME CMD
...        ...   ...   ... ... ...   ... ...           ... ...
xyz     8209  4865  8209  0    6 10:08 pts/0    00:00:00 /home/xyz/prj/thr/.stack-work/install/x86_64-linux-tinfo6/lts-13.25/8.6.5/bin/yohoho
xyz     8209  4865  8225  0    6 10:08 pts/0    00:00:00 /home/xyz/prj/thr/.stack-work/install/x86_64-linux-tinfo6/lts-13.25/8.6.5/bin/yohoho
xyz     8209  4865  8226  0    6 10:08 pts/0    00:00:00 /home/xyz/prj/thr/.stack-work/install/x86_64-linux-tinfo6/lts-13.25/8.6.5/bin/yohoho
xyz     8209  4865  8227  0    6 10:08 pts/0    00:00:00 /home/xyz/prj/thr/.stack-work/install/x86_64-linux-tinfo6/lts-13.25/8.6.5/bin/yohoho
xyz     8209  4865  8228 99    6 10:08 pts/0    00:00:06 /home/xyz/prj/thr/.stack-work/install/x86_64-linux-tinfo6/lts-13.25/8.6.5/bin/yohoho
xyz     8209  4865  8229  0    6 10:08 pts/0    00:00:00 /home/xyz/prj/thr/.stack-work/install/x86_64-linux-tinfo6/lts-13.25/8.6.5/bin/yohoho

In Linux in forkIO case we have only 6 LWPs and one of them utilizes CPU on 99%. In the case of forkOS we have 7 LWPs, and 2 of them utilize CPU on about 66%, which from my point of view, looks better. So, seems that forkOS is more preferable in Linux too.

Related