8
$\begingroup$

It seems that the distinction between fibers and threads is that fibers are cooperatively scheduled, whereas threads are preemptively scheduled. The point of the scheduler seems like a way to make an otherwise serial processor resource act in a parallel way, by "time-sharing" the CPU. However, on a dual-core processor with each core running its own thread, I assume there's no need to pause the execution of one thread for the other to continue because they're not "time-sharing" a single processor.

So, if the difference between threads and fibers is the way they are interrupted by the scheduler, and interrupting is not necessary when running on physically separate cores, why can't fibers take advantage of multiple processor cores when threads can?

Sources of confusion:

..mainly wikipedia

  1. http://en.wikipedia.org/wiki/Fiber_%28computer_science%29

    A disadvantage is that fibers cannot utilize multiprocessor machines without also using preemptive threads

  2. http://en.wikipedia.org/wiki/Computer_multitasking#Multithreading

    ...[fibers] tend to lose some or all of the benefits of threads on machines with multiple processors.

$\endgroup$
9
$\begingroup$

The main distinction, as you point out in your question, is whether or not the scheduler will ever preempt a thread. The way a programmer thinks about sharing data structures or about synchronizing between "threads" is very different in preemptive and cooperative systems.

In a cooperative system (which goes by many names, cooperative multi-tasking, nonpreemptive multi-tasking, user-level threads, green threads, and fibers are five common ones currently) the programmer is guaranteed that their code will run atomically as long as they don't make any system calls or call yield(). This makes it particularly easy to deal with data structures shared between multiple fibers. Unless you need to make a system call as part of a critical section, critical sections don't need to be marked (with mutex lock and unlock calls, for example). So in code like:

x = x + y
y = 2 * x

the programmer needn't worry that some other fiber could be working with the x and y variables at the same time. x and y will be updated together atomically from the perspective of all the other fibers. Similarly, all the fibers could share some more complicated structure, like a tree and a call like tree.insert(key, value) would not need to be protected by any mutex or critical section.

In contrast, in a preemptive multithreading system, as with truly parallel/multicore threads, every possible interleaving of instructions between threads is possible unless there are explicit critical sections. An interrupt and preemption could become between any two instructions. In the above example:

 thread 0                thread 1
                         < thread 1 could read or modify x or y at this point
 read x
                         < thread 1 could read or modify x or y at this point
 read y
                         < thread 1 could read or modify x or y at this point
 add x and y
                         < thread 1 could read or modify x or y at this point
 write the result back into x
                         < thread 1 could read or modify x or y at this point
 read x
                         < thread 1 could read or modify x or y at this point
 multiply by 2
                         < thread 1 could read or modify x or y at this point
 write the result back into y
                         < thread 1 could read or modify x or y at this point

So to be correct on a preemptive system, or on a system with truly parallel threads, you need to surround every critical section with some kind of synchronization, like a mutex lock at the beginning and a mutex unlock at the end.

Fibers are thus more similar to asynchronous i/o libraries than they are to preemptive threads or truly parallel threads. The fiber scheduler is invoked and can switch fibers during long latency i/o operations. This can give the benefit of multiple simultaneous i/o operations without requiring synchronization operations around critical sections. Thus using fibers can, perhaps, have less programming complexity than preemptive or truly parallel threads, but the lack of synchronization around critical sections would lead to disastrous results if you tried to run the fibers truly simultaneously or preemptively.

$\endgroup$
  • $\begingroup$ I think some mention should probably be made of 1. hybrid systems where the user-level thread system takes charge of distributing (many) user-level threads across (few) CPU cores and 2. the fact that when programming on "bare metal", it's possible to get multiprocessing without preemption. $\endgroup$ – dfeuer Mar 14 '15 at 23:04
  • 1
    $\begingroup$ @dfeuer I don't think the question is asking for all the different possible ways to take advantage of multiprocessing. The question as I read it is "why can't fibers (also known as non-preemptive tasks) be treated just like preemptive threads?" If you are assuming real parallelism then you have to synchronize correctly, so you no longer have "fibers". $\endgroup$ – Wandering Logic Mar 14 '15 at 23:13
  • 1
    $\begingroup$ Beautiful answer. Fibers can't guarantee safety because the program would assume it has exclusive access to shared resources until it specified an interrupt point, where threads assume an access/mutation can be made at any point; obviously the safer assumption when multiple, truly parallel nodes are interacting with the same data. $\endgroup$ – James M. Lay Mar 15 '15 at 21:02
6
$\begingroup$

The answer is actually that they could, but there is a desire not to.

Fibers are used because they let you control how scheduling occurs. Accordingly, it is much simpler to design some algorithms using fibers because the programmer has say in which fiber is being executed at any one time. However, if you want two fibers to be executed on two different cores at the same time, you have to manually schedule them to do so.

Threads give control of which code is being executed to the OS. In exchange, the OS takes care of many ugly tasks for you. Some algorithms get more difficult, because the programmer has less say in which code is executed at a given time, so more unexpected cases can surface. Tools like mutexes and semaphores are added to an OS to give the programmer just enough control to make threads useful and beat down some of the uncertainty, without bogging the programmer down.

This leads to something which is even more important than cooperative vs. preemptive: fibers are controlled by the programmer, while threads are controlled by the OS.

You don't want to have to spawn a fiber on another processor. The assembly level commands to do so are atrociously complicated, and they are often processor specific. You don't want to have to write 15 different versions of your code to handle these processors, so you turn to the OS. The OS's job is to abstract these differences away. The result is "threads."

Fibers run on top of threads. They don't run on their own. Accordingly, if you want to run two fibers on different cores, you can simply spawn two threads, and run a fiber on each of them. In many implementations of fibers, you can do this easily. The multi-core support is not coming from the fibers, but the threads.

It becomes easy to show that, unless you want to write your own processor specific code, there is nothing you could do by assigning fibers to multiple cores that you couldn't do by creating threads and assigning fibers to each. One of my favorite rules for API design is "An API is not done when you have finished adding everything to it, but rather when you can no longer find anything else to take out." Given that multi-core is handled perfectly by hosting fibers on threads, there's no reason to complicate the fiber API by adding multi-core at that level.

$\endgroup$

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.