We filled in the last details of the adaptive multilevel queue: - high-priority jobs should have short quanta (they are I/O bound and should not need long CPU allocations). Similarly, low-priority jobs should have long quanta
We also briefly discussed real time scheduling. Real time schedulers allow processes to request scheduling guarantees, such as a CPU burst of 10 ms sometimes within the next 100ms. In order to provide these guarantees, the scheduler must perform admission control: it needs the ability to deny requests for resources, and to kill or deschedule processes that attempt to use more resources than requested.
We will discuss other forms of admission control in more detail when we discuss deadlock avoidance and thrashing later in the course.
We spent the remainder of class working on the following problem. Suppose we wished to write code for two threads to ensure that after completing its code, a common resource will have been acquired once and only once.
For example, the threads may represent roommates who both wish to use some milk from a shared fridge. If the milk is gone, one of them should run to the store and purchase it, but we should avoid having both roommates purchase milk at the same time.
The tools at our disposal so far: threads share memory, so they can load and store values to shared variables. We can think of this as a shared notepad that the roommates can both read and write on.
Whenever solving synchronization problems, we must consider three criteria:
safety: the code does not violate the functional spec. In the milk example, the code would violate safety if one of the threads completes without milk having been bought, or if milk is bought twice. Safety is often summarized by saying "bad things don't happen."
liveness: the code does not prevent threads from making progress. In the milk example, both roommates must eventually be able to complete the milk acquisition and go on to make their omelettes. Liveness is often summarized by "good things do happen."
fairness: the code should not favor one participant over another. A solution to the milk problem would not be fair if, for example, only one roommate ever bought milk.
Shared state: (none) | |
Thread one code:
1: while true: 2: do nothing |
Thread two code:
3: while true: 4: do nothing |
Shared state: (none) | |
Thread one code:
1: do nothing |
Thread two code:
2: do nothing |
Shared state:
has_milk = False | |
Thread one code:
1: while not has_milk: 2: do nothing |
Thread two code:
3: buy_milk() 4: has_milk = True |
Perhaps the most obvious thing to try is the following:
Shared state:
has_milk = False | |
Thread one code:
1: if not has_milk: 2: buy_milk() 3: has_milk = True |
Thread two code: (same)
4: if not has_milk: 5: buy_milk() 6: has_milk = True |
The milk has been bought twice, violating safety.
One idea that was proposed is a "lock variable" that prevents one thread from going out if the other thread is working at all:
Shared state:
has_milk = False someone_busy = False | |
Thread one code:
1: while someone_busy: 2: do nothing 3: someone_busy = True 4: if not has_milk: 5: buy_milk() 6: has_milk = True 7: someone_busy = False |
Thread two code: (same)
11: while someone_busy: 12: do nothing 13: someone_busy = True 14: if not has_milk: 15: buy_milk() 16: has_milk = True 17: someone_busy = False |
The intent is that only one thread can be executing between lines 3 and 7 at a time, because the other threads will notice that there is already someone in the critical section and spin in the loop on lines 1 and 2.
Unfortunately this code is still not safe, because a context switch can occur after a thread finishes line 1 but before it executes line 3. Specifically:
Again, milk has been bought twice, violating safety.
A third proposal was to use an operating-system level lock to do the synchronization for us, perhaps by descheduling the other process:
Shared state:
has_milk = False | |
Thread one code:
1: system_call_to_force_thread_2_to_wait() 2: if not has_milk: 3: buy_milk() 4: has_milk = True 5: system_call_to_wake_up_thread_2() |
Thread two code: (symmetric)
11: system_call_to_force_thread_1_to_wait() 12: if not has_milk: 13: buy_milk() 14: has_milk = True 15: system_call_to_wake_up_thread_1() |
However, since this is 4410, we can't just assume that our operating system magically works. If we think about how we would implement this, the system call handler for the system_call_to_force_thread_to_wait must solve a similar synchronization problem: access to the shared ready and waiting queues and TCBs needs to be carefully coordinated. This can be done on a single processor machine by disabling interrupts or programming the ready and waiting cues very carefully, but to solve the problem on a multiprocessor machine will require us to solve an equivalent problem to the original problem.
The following solution is safe, live and fair. Note that it can be generalized to multiple threads, but it is not obvious how to do so.
Shared state:
has_milk = False working_1 = False working_2 = False turn = 0 | |
Thread one code:
1: working_1 = True 2: turn = 2 3: while working_2 and turn == 2: 4: do nothing 5: if not has_milk: 6: buy milk 7: has_milk = True 8: working_1 = False |
Thread two code: (symmetric)
11: working_2 = True 12: turn = 1 13: while working_1 and turn == 1: 14: do nothing 15: if not has_milk: 16: buy milk 17: has_milk = True 18: working_2 = False |
The idea behind this code is that neither can take control from the other, they can only yield control to the other.
This code is safe, live, and fair, although the argument is rather complicated:
safety: clearly, by the time either thread finishes, milk will have been bought at least once. However, we must show that it is bought at most once.
Suppose otherwise, that is, that both lines 6 and 16 are executed. This implies that thread one must have been on lines 5-7 at the same time that thread two was on lines 5-7. One of the two threads must have exited the while loop first. Without loss of generality, assume it was thread one. When it exited the loop on line 3, one of two things was true:liveness: the only place that the threads can get stuck is in the spin loops on lines 3 and 13. However, both threads cannot be stuck simultaneously, because turn cannot be both 1 and 2. Once one of the threads proceeds past the spin lock, it will eventually set its working variable to false, which will allow the other thread to exit from the spin loop
fairness: the code is completely symmetric, and thus fair.