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.