CS162 Spring 2002
Section 101: Tu 10-11am
Jeryl Soto Contemprato
Office Hours: 751 Soda, Tu 2:30pm-3:30pm, W 2-3pm
cs162-tc@cory.eecs.berkeley.edu
Discussion Notes
19 Feb 02
Admin
-
See newsgroup if you have questions; many can be answered there
-
Design meetings: schedule them, everyone must be there
Mutual Exclusion Implementation
-
Need higher-level synchronization methods that are easy for apps to use
-
Lock/mutual exclusion implementation techniques:
-
Atomic load/store: requires busy waiting, plus we can’t really generalize
it for all applications
-
Hardware locks & context switches: way too high-level for hardware;
would slow it down (too many stages)
-
Disable interrupts
-
Can we just disable interrupts on lock acquire and enable them on lock
release? NO.
-
User can’t operate with interrupts disabled
-
Real time systems must respond to interrupts within a specific amount of
time
-
Prevents threads who don’t even need the lock from running
-
So, only disable interrupts long enough to check/set an indicator and enqueue/dequeue
thread desiring access, not for the whole duration someone holds a lock:
Lock {
Int state = FREE
Threadqueue waiters = empty
acquire() {
disable interrupts
if (state is BUSY) {
add current thread to waiters
sleep current thread
// next thread dispatched will
// reenable ints
}
else
state = BUSY
enable interrupts
release() {
disable interrupts
if (waiters isn’t empty) {
take a thread off waiters
put that thread on ready queue
}
else
state = FREE
enable interrupts
}
}
-
When can we re-enable interrupts if a thread has to sleep?
-
Can’t enable before sleeping, because if we context switch right after
enabling, the lock holder might release the lock and take the would-be
acquirer off the waiters queue; context switch back to the would-be acquirer,
and it will sleep, without ever having the chance of waking up again because
no one knows about it
-
After we sleep, next thread to get control will re-enable interrupts.
-
Atomic read-modify-write (esp. test & set)
-
Test & set(address) atomically reads a value at the address
into a register, and sets the value at the address to 1.
-
Locks w/busy waiting:
Lock {
Int state = 0
acquire() { while (test&set(state) == 1) do noop }
release() { state = 0 }
}
-
Problems with busy waiting:
-
Busy waiting is inefficient! We eat up CPU time waiting for a condition
to happen without doing any useful operation
-
Busy waiting can cause starvation/deadlock! Waiting thread is still
runnable with this implementation. In a priority-based CPU scheduler,
the waiting thread might be higher priority while the lock holder might
be lower priority, so that lock holder never gets to run & release
the lock.
-
Minimal busy waiting
Lock {
Int state = free
Threadqueue waiters = empty
acquire() {
while (test&set(guard)) // Busy wait only for
noop // permission to check lock
if (state == BUSY) {
add current thread to waiters
let kernel know to set guard to 0
after we sleep
sleep
}
else {
state = BUSY
guard = 0
}
}
release() {
while (test&set(guard)) // Busy wait only for
noop // permission to release lock
if (waiters isn’t empty) {
take thread off waiters
put that thread on ready queue
}
else {
state = FREE
}
guard = 0
}
}
-
Keep busy waiting to a minimum; don’t busy wait for the entire time someone
else holds the lock, just busy wait for enough time to tell that someone
else holds the lock by checking a guard variable.
-
Can we get rid of busy waiting?
-
We can’t, because not busy waiting implies sleeping until a condition
happens. To sleep, we need to put ourselves on a queue of threads
to awaken before yielding to the kernel; this must happen atomically.
Atomicity is what we are trying to achieve in the first place, if we assume
all we have to work with is test&set (no disabling/enabling interrupts
is allowed).
Synch Primitive Issues
-
Semaphores
-
Interrupt handlers: see ElevatorTest.java. Interrupt handlers
are the only real-world use of semaphores (but very important!)
-
Multiple resource objects: if there are multiple objects of the same type
of resource, we might want to use a semaphore initialized to the number
of objects to allow something to proceed only if there is an object free.
We will see that this use is limited since in order to actually access
the object you’ll still need mutual exclusion amongst those threads that
do proceed.
-
Semaphores are the most general synchronization construct. Therefore,
it’s easy to misuse them.
-
Locks
-
Used to implement mutual exclusion
-
Why can’t we use them in interrupt handlers?
-
Typical interrupt handler interaction is this: Some thread has an
event loop that gets the next event and processes it, if there is an event
available. The interrupt handler lets that thread know there is an
event.
-
The specific interrupt handler itself is not a traditional thread.
There is an overall system interrupt handler that the CPU jumps to whenever
there is an interrupt. The system interrupt handler then dispatches
the interrupt based on what kind of interrupt it is; in the case of an
elevator or rider interrupt, it will dispatch to the specific interrupt
handler for each of those cases.
-
The interrupt handler isn’t a traditional thread because it does not have
its own context; its context is that of the thread that got interrupted.
So you can’t yield inside the interrupt handler without yielding the thread
that got interrupted.
-
We need to use some mechanism to prevent an event-loop thread from processing
events if there are no events to process. Synchronization primitives
such as locks, semaphores, and condition variables come to mind.
-
Locks are out of the question. One’s first vision of using locks
to do interrupt handling is this: in the event loop, have a Lock.Acquire()
that would block if someone else had the lock (which would be the case
if there were no events). In the interrupt handler, have Lock.Release()
to let the event loop go. But to release a lock you have to had acquired
it first! Interrupt handlers can execute in the context of any thread,
so the interrupt handler itself can’t own the lock.
-
Condition variables are similarly out of the question. Here’s the
misinformed plan to use condition variables:
Lock access;
Boolean eventAvailable;
Condition events(locked by access);
Thread eventloop() {
access.acquire()
while (no eventAvailable)
events.wait()
eventAvailable = FALSE
access.release()
…process events…
}
Procedure interrupthandler() {
access.acquire()
eventAvailable = TRUE
events.signal()
access.release()
}
-
What’s wrong with this? Well, we need a lock to use condition variables.
What happens if an interrupt happens while we are executing eventloop’s
critical section? In the interrupt handler, we are running in eventloop’s
context, and so we will try to acquire a lock we already have, which is
illegal. In addition, if someone else happens to hold the lock, this
would force the interrupt handler (and the thread the interrupt occurred
in) to sleep, which is bad because we haven’t handled the interrupt yet!
Interrupt handlers in general should have O(1) run time in real-time systems.
-
Condition variables
-
Used to implement scheduling constraints
-
We don’t want something to run until a certain condition has happened:
wait()
-
The thread that causes the condition to happen will call signal() so that
the person waiting doesn’t wait anymore
-
If applicable, a thread that causes a condition to happen might call broadcast()
to signal everyone that is waiting for the condition; this would be used
in the case that you want multiple threads to proceed after a condition
is satisfied
-
Monitors
-
Comprised of a lock and one or more condition variables
-
Lock used for mutual exclusion of code that accesses shared state
-
Condition variables themselves access shared state (thread queues) so they
themselves must be exclusively accessed
-
Condition variables are used to implement scheduling constraints
NACHOS Proj 1
-
Join: Make sure you use a ThreadQueue from the current Scheduler
to implement this, since priority donations are applicable to join() in
a priority scheduler. Semaphores won’t work because they don’t effect
priority donation.
-
Condition2: Use a ThreadQueue so that priority scheduling
is used (but not priority donation).
-
waitUntil: Make sure data structures shared by various calls
to waitUntil and the timerInterruptHandler don’t get clobbered in multiple
accesses. Easiest way to do this is to disable interrupts in waitUntil
for the required time (because timerInterruptHandler already has interrupts
disabled, and you don’t want to use locks in an interrupt handler).
-
Communicator: So, when it comes down to it, time ordering
doesn’t really matter, despite all the newsgroup traffic. Just make
sure that a speaker doesn’t leave until a listener has actually retrieved
the word the speaker gave it. And contrary to what I said in section
last week, you do need at least one flag (my bad!).
-
PriorityScheduler: Ensure your priority donations propagate.
For example, thread 1 owns lock A. Thread 2 which owns lock
B, tries to get lock A and waits. After a while, thread 3 tries to
get lock B and waits. Each thread has the inherent priority of its
number. So thread 3 should donate its priority of 3 to thread 2.
This donation should propagate to thread 1 since thread 2 is waiting on
it, so that both thread 2 and 1 should have an effective priority of 3.
Also, how to determine if you are doing it the fast way or the slow way:
if you are not storing the effectivePriority in some way or another, you
are doing it the slow way.
-
Elevators: Make sure you are actually using your multiple
elevator threads. Things you should be doing in those threads: opening
the doors of one elevator and doing waituntil() before closing them.
You might also want to move the elevator in those threads. In any
event, the mastermind ElevatorController thread should not stall just because
one elevator is in a waiting state. Make sure your each of your elevators
can never crash (i.e. if an elevator gets to a floor and there are no riders
there, it shouldn’t freak out).
-
Riders: In the same way, ensure a rider never dies because
of an unexpected event. It should recover and continue on with getting
to its stops.
Problem
-
Spring 2001 Midterm, Problem 1