mutex's to
protect critical sections in a simple concurrent
program (multithreaded, in our case). It's important to look
at how something like pthread_mutex's mutual
exclusion can be implemented. How can we guarantee:
lock. They might try to use it to implement
mutual exclusion following this protocol:
int lock = 0; // Shared variable!
| P0 | P1 |
1 while(1)
{
2 while(lock == 1)
3 ;
4 lock = 1;
5 critical section here
6 lock = 0;
}
|
7 while(1)
{
8 while(lock == 1)
9 ;
10 lock = 1;
11 critical section here
12 lock = 0;
}
|
This doesn't work:
Mutual exlusion fails! Why? What if both P0 and P1
execute lines 2 / 8, which test lock == 1,
before either gets to lines 4 / 10, which set
lock = 1? Then both are in their critical
section. This approach fails because
testing lock and setting lock are not
atomic, i.e. they do not happen together as a
single step.
turn. They might try to use it to implement
mutual exclusion following this protocol:
int turn = 0; // Shared variable!
| P0 | P1 |
1 while(1)
{
2 while(turn != 0)
3 ;
4 critical section here
5 turn = 1;
6 remainder (non-critical section code)
}
|
7 while(1)
{
8 while(turn != 1)
9 ;
10 critical section here
11 turn = 0
12 remainder (non-critical section code)
}
|
Mutual exlusion holds for this, though it takes walking through several scenarios to convince yourself. However, progress is not satisfied. Why? Suppose P0 has a very long remainder, and P1 has a very short remainder. Say P0 goes through its critical section and starts its remainder. P1 goes through its critical section, sets turn to 0, goes through its short remainder section, and is back waiting to get into its critical section, while P0 is still going through its long remainder section. So, P1 is kept from getting into its critical section by P0, even though P0 isn't in its critical section. The progress requirement isn't satisfied. (Think of what a performance problem this would be!)
int flag[2]. They might try to use it to implement
mutual exclusion following this protocol:
flag[i] = 1flag[i] = 0
int flag[2] = {0,0}; // Shared variable!
| P0 | P1 |
1 while(1)
{
2 flag[0] == 1;
3 while(flag[1] == 1)
4 ;
5 critical section here
6 flag[0] = 0;
7 remainder (non-critical section code)
}
|
8 while(1)
{
9 flag[1] == 1;
10 while(flag[0] == 1)
11 ;
12 critical section here
13 flag[1] = 0;
14 remainder (non-critical section code)
}
|
Mutual exlusion holds for this, though it takes walking through several scenarios to convince yourself. However, both progress and bounded wait are not satisfied. Why? Suppose P0 executes line 2 and P1 executes line 9 before either P0's line 3 or P1's line 10 get executed? Then both flag[0] and flag[1] are set to 1, and both processes will sit in their inner while loops forever. This is known as deadlock. No process/thread can move forward.
int flag[2] = {0,0}; // Shared variable!
int turn = 1; // Shared variable!
| P0 | P1 |
while(1)
{
flag[0] = 1; // show I want to enter
turn = 1; // but yield to other
while(flag[1] && turn == 1) // wait for other to leave
;
critical section here
flag[0] = 0; // indicate I've left
remainder (non-critical section code)
}
|
while(1)
{
flag[1] = 1; // show I want to enter
turn = 0; // but yield to other
while(flag[0] && turn == 0) // wait for other to leave
;
critical section here
flag[1] = 0; // indicate I've left
remainder (non-critical section code)
}
|
The above algorithm can be proven to solve the mutual exclusion problem for two threads/processes. moreover, it can be generalized to an arbitrary number of processes.
lock == 1 and setting lock
= 1 are separate operations: not atomic. Well
... what if the underlying hardware provided a single
test-and-set machine instruction? Then our approach would
work. It might look like this:
1 while(1)
{
2 while(test_and_set(lock))
3 ;
4 critical section here
5 lock = 0;
}
Alternatively, since (on a single machine) mutual exclusion
problems occur from threads/processes getting interrupted,
what if we could disable interrupts? Then the following
would work:
while(1)
{
START:
disable_interrupts();
if (lock == 0)
{
lock == 1;
enable_interrupts();
}
else
{
enable_interrupts();
goto START:
}
critical section here
lock = 0;
}
Now, hopefully you see that allowing user-processes to disable interrupts is asking for real trouble!\ I'll say nothing further about it!
So, the moral of the story is: the kernel / threads-run-time-system implements mutex's for us, so that processes/threads awaiting a lock are suspended until the lock is available, rather that using up CPU cycles in spin locks.