1 00:00:00,729 --> 00:00:03,450 Let's take a moment to look at a different example. 2 00:00:03,450 --> 00:00:08,780 Automated teller machines allow bank customers to perform a variety of transactions: deposits, 3 00:00:08,780 --> 00:00:11,139 withdrawals, transfers, etc. 4 00:00:11,139 --> 00:00:16,560 Let's consider what happens when two customers try to withdraw $50 from the same account 5 00:00:16,560 --> 00:00:17,589 at the same time. 6 00:00:17,589 --> 00:00:23,220 A portion of the bank's code for a withdrawal transaction is shown in the upper right. 7 00:00:23,220 --> 00:00:28,529 This code is responsible for adjusting the account balance to reflect the amount of the 8 00:00:28,529 --> 00:00:29,529 withdrawal. 9 00:00:29,529 --> 00:00:33,050 Presumably the check to see if there is sufficient funds has already happened. 10 00:00:33,050 --> 00:00:35,200 What's supposed to happen? 11 00:00:35,200 --> 00:00:40,489 Let's assume that the bank is using a separate process to handle each transaction, so the 12 00:00:40,489 --> 00:00:46,370 two withdrawal transactions cause two different processes to be created, each of which will 13 00:00:46,370 --> 00:00:48,989 run the Debit code. 14 00:00:48,989 --> 00:00:55,050 If each of the calls to Debit run to completion without interruption, we get the desired outcome: 15 00:00:55,050 --> 00:00:59,890 the first transaction debits the account by $50, then the second transaction does the 16 00:00:59,890 --> 00:01:01,210 same. 17 00:01:01,210 --> 00:01:07,020 The net result is that you and your friend have $100 and the balance is $100 less. 18 00:01:07,020 --> 00:01:08,970 So far, so good. 19 00:01:08,970 --> 00:01:15,570 But what if the process for the first transaction is interrupted just after it's read the balance? 20 00:01:15,570 --> 00:01:20,400 The second process subtracts $50 from the balance, completing that transaction. 21 00:01:20,400 --> 00:01:26,000 Now the first process resumes, using the now out-of-date balance it loaded just before 22 00:01:26,000 --> 00:01:27,660 being interrupted. 23 00:01:27,660 --> 00:01:32,520 The net result is that you and your friend have $100, but the balance has only been debited 24 00:01:32,520 --> 00:01:35,050 by $50. 25 00:01:35,050 --> 00:01:38,690 The moral of the story is that we need to be careful when writing code that reads and 26 00:01:38,690 --> 00:01:44,580 writes shared data since other processes might modify the data in the middle of our execution. 27 00:01:44,580 --> 00:01:51,410 When, say, updating a shared memory location, we'll need to LD the current value, modify 28 00:01:51,410 --> 00:01:54,430 it, then ST the updated value. 29 00:01:54,430 --> 00:01:59,360 We would like to ensure that no other processes access the shared location between the start 30 00:01:59,360 --> 00:02:03,000 of the LD and the completion of the ST. 31 00:02:03,000 --> 00:02:08,470 The LD/modify/ST code sequence is what we call a "critical section". 32 00:02:08,470 --> 00:02:13,220 We need to arrange that other processes attempting to execute the same critical section are delayed 33 00:02:13,220 --> 00:02:16,379 until our execution is complete. 34 00:02:16,379 --> 00:02:22,519 This constraint is called "mutual exclusion", i.e., only one process at a time can be executing 35 00:02:22,519 --> 00:02:26,980 code in the same critical section. 36 00:02:26,980 --> 00:02:32,500 Once we've identified critical sections, we'll use semaphores to guarantee they execute atomically, 37 00:02:32,500 --> 00:02:38,140 i.e., that once execution of the critical section begins, no other process will be able 38 00:02:38,140 --> 00:02:42,650 to enter the critical section until the execution is complete. 39 00:02:42,650 --> 00:02:47,819 The combination of the semaphore to enforce the mutual exclusion constraint and the critical 40 00:02:47,819 --> 00:02:51,390 section of code implement what's called a "transaction". 41 00:02:51,390 --> 00:02:56,379 A transaction can perform multiple reads and writes of shared data with the guarantee that 42 00:02:56,379 --> 00:03:01,920 none of the data will be read or written by other processes while the transaction is in 43 00:03:01,920 --> 00:03:02,920 progress. 44 00:03:02,920 --> 00:03:07,200 Here's the original code to Debit, which we'll modify by adding a LOCK semaphore. 45 00:03:07,200 --> 00:03:11,650 In this case, the resource controlled by the semaphore is the right to run the code in 46 00:03:11,650 --> 00:03:13,569 the critical section. 47 00:03:13,569 --> 00:03:19,569 By initializing LOCK to 1, we're saying that at most one process can execute the critical 48 00:03:19,569 --> 00:03:21,060 section at a time. 49 00:03:21,060 --> 00:03:25,879 A process running the Debit code WAITs on the LOCK semaphore. 50 00:03:25,879 --> 00:03:32,950 If the value of LOCK is 1, the WAIT will decrement value of LOCK to 0 and let the process enter 51 00:03:32,950 --> 00:03:34,829 the critical section. 52 00:03:34,829 --> 00:03:37,599 This is called acquiring the lock. 53 00:03:37,599 --> 00:03:42,629 If the value of LOCK is 0, some other process has acquired the lock and is executing the 54 00:03:42,629 --> 00:03:48,779 critical section and our execution is suspended until the LOCK value is non-zero. 55 00:03:48,779 --> 00:03:53,489 When the process completes execution of the critical section, it releases the LOCK with 56 00:03:53,489 --> 00:03:58,790 a call to SIGNAL, which will allow other processes to enter the critical section. 57 00:03:58,790 --> 00:04:03,409 If there are multiple WAITing processes, only one will be able to acquire the lock, and 58 00:04:03,409 --> 00:04:06,419 the others will still have to wait their turn. 59 00:04:06,419 --> 00:04:11,730 Used in this manner, semaphores are implementing a mutual exclusion constraint, i.e., there's 60 00:04:11,730 --> 00:04:16,660 a guarantee that two executions of the critical section cannot overlap. 61 00:04:16,660 --> 00:04:21,500 Note that if multiple processes need to execute the critical section, they may run in any 62 00:04:21,500 --> 00:04:27,470 order and the only guarantee is that their executions will not overlap. 63 00:04:27,470 --> 00:04:31,390 There are some interesting engineering issues to consider. 64 00:04:31,390 --> 00:04:36,200 There's the question of the granularity of the lock, i.e., what shared data is controlled 65 00:04:36,200 --> 00:04:37,560 by the lock? 66 00:04:37,560 --> 00:04:42,800 In our bank example, should there be one lock controlling access to the balance for all 67 00:04:42,800 --> 00:04:43,890 accounts? 68 00:04:43,890 --> 00:04:48,920 That would mean that no one could access any balance while a transaction was in progress. 69 00:04:48,920 --> 00:04:53,140 That would mean that transactions accessing different accounts would have to run one after 70 00:04:53,140 --> 00:04:57,080 the other even though they're accessing different data. 71 00:04:57,080 --> 00:05:02,060 So one lock for all the balances would introduce unnecessary precedence constraints, greatly 72 00:05:02,060 --> 00:05:06,870 slowing the rate at which transactions could be processed. 73 00:05:06,870 --> 00:05:11,400 Since the guarantee we need is that we shouldn't permit multiple simultaneous transactions 74 00:05:11,400 --> 00:05:14,820 on the same account, it would make more sense to have a separate 75 00:05:14,820 --> 00:05:19,710 lock for each account, and change the Debit code to acquire the account's lock before 76 00:05:19,710 --> 00:05:21,790 proceeding. 77 00:05:21,790 --> 00:05:26,890 That will only delay transactions that truly overlap, an important efficiency consideration 78 00:05:26,890 --> 00:05:32,400 for a large system processing many thousands of mostly non-overlapping transactions each 79 00:05:32,400 --> 00:05:33,400 second. 80 00:05:33,400 --> 00:05:38,470 Of course, having per-account locks would mean a lot of locks! 81 00:05:38,470 --> 00:05:43,000 If that's a concern, we can adopt a compromise strategy of having locks that protect groups 82 00:05:43,000 --> 00:05:48,580 of accounts, e.g., accounts with same last three digits in the account number. 83 00:05:48,580 --> 00:05:53,220 That would mean we'd only need 1000 locks, which would allow up to 1000 transactions 84 00:05:53,220 --> 00:05:55,970 to happen simultaneously. 85 00:05:55,970 --> 00:06:00,110 The notion of transactions on shared data is so useful that we often use a separate 86 00:06:00,110 --> 00:06:04,080 system called a database that provides the desired functionality. 87 00:06:04,080 --> 00:06:09,640 Database systems are engineered to provide low-latency access to shared data, providing 88 00:06:09,640 --> 00:06:13,150 the appropriate transactional semantics. 89 00:06:13,150 --> 00:06:17,400 The design and implementation of databases and transactions is pretty interesting. 90 00:06:17,400 --> 00:06:22,360 To follow up, I recommend reading about databases on the web. 91 00:06:22,360 --> 00:06:27,380 Returning to our producer/consumer example, we see that if multiple producers are trying 92 00:06:27,380 --> 00:06:30,190 to insert characters into the buffer at the same time, 93 00:06:30,190 --> 00:06:35,570 it's possible that their execution may overlap in a way that causes characters to be overwritten 94 00:06:35,570 --> 00:06:40,490 and/or the index to be improperly incremented. 95 00:06:40,490 --> 00:06:45,691 We just saw this bug in the bank example: the producer code contains a critical section 96 00:06:45,691 --> 00:06:50,390 of code that accesses the FIFO buffer and we need to ensure that the critical section 97 00:06:50,390 --> 00:06:53,000 is executed atomically. 98 00:06:53,000 --> 00:06:58,080 Here we've added a third semaphore, called LOCK, to implement the necessary mutual exclusion 99 00:06:58,080 --> 00:07:04,400 constraint for the critical section of code that inserts characters into the FIFO buffer. 100 00:07:04,400 --> 00:07:09,580 With this modification, the system will now work correctly when there are multiple producer 101 00:07:09,580 --> 00:07:10,580 processes. 102 00:07:10,580 --> 00:07:14,990 There's a similar issue with multiple consumers, so we've used the same LOCK to protect the 103 00:07:14,990 --> 00:07:19,710 critical section for reading from the buffer in the RCV code. 104 00:07:19,710 --> 00:07:25,210 Using the same LOCK for producers and consumers will work, but does introduce unnecessary 105 00:07:25,210 --> 00:07:30,110 precedence constraints since producers and consumers use different indices, 106 00:07:30,110 --> 00:07:34,610 i.e., IN for producers and OUT for consumers. 107 00:07:34,610 --> 00:07:40,750 To solve this problem we could use two locks: one for producers and one for consumers. 108 00:07:40,750 --> 00:07:45,080 Semaphores are a pretty handy swiss army knife when it comes to dealing with synchronization 109 00:07:45,080 --> 00:07:46,400 issues. 110 00:07:46,400 --> 00:07:51,490 When WAIT and SIGNAL appear in different processes, the semaphore ensures the correct execution 111 00:07:51,490 --> 00:07:54,020 timing between processes. 112 00:07:54,020 --> 00:07:58,870 In our example, we used two semaphores to ensure that consumers can't read from an empty 113 00:07:58,870 --> 00:08:04,240 buffer and that producers can't write into a full buffer. 114 00:08:04,240 --> 00:08:09,930 We also used semaphores to ensure that execution of critical sections -- in our example, updates 115 00:08:09,930 --> 00:08:13,480 of the indices IN and OUT -- were guaranteed to be atomic. 116 00:08:13,480 --> 00:08:18,710 In other words, that the sequence of reads and writes needed to increment a shared index 117 00:08:18,710 --> 00:08:23,700 would not be interrupted by another process between the initial read of the index and 118 00:08:23,700 --> 00:08:24,490 the final write.