Async Techniques and Examples in Python Transcripts
Chapter: Thread safety
Lecture: Demo: Make the bank safe (fine-grained)

Login or purchase this course to watch this video and the rest of the course contents.
0:00 We saw that our now safe bank uses a single global lock. And if we look and we think a little bit about that
0:06 what that means is even if we have 50 threads trying to do transfers between thousands of accounts every single transfer is going to have to slow down
0:16 and go through a single thread effectively a single processing pipeline because we're taking a lock for the entire process.
0:24 And you may think well what we could do is we could actually, instead of using the same lock for every transfer, we could somehow come up with
0:32 a lock that has to do with the various accounts. And if two accounts are being transferred by one thread and two other accounts are being transferred
0:40 by a separate thread those two could actually happen in parallel. There's no data problem there. The problem is if they're using the same account
0:49 you might think well we could gain a whole lot of parallelism, a whole lot of performance if we change from this coarse grain lock
0:56 to a very fine grain lock. Now I want to say this right up front this turns out to be probably a bad idea in Python you'll see for performance reasons
1:06 in this particular example and it turns out to make things a lot more complicated. We'll make it work. I'll show you how to do it
1:12 but mostly the point of this the takeaway is going to be, oh that added a whole lot of complexity and it didn't really add any benefit
1:19 or maybe it even made it worse or will see. Because of that, let's think about how much locking and how much complexity we're willing to do
1:27 and what the trade-offs are. Okay so this is almost a lesson in don't go too far with this because it might become worse and not better.
1:36 That said, let's set this up and see what we're going to do. So what we're going to do is have each account itself
1:42 have a lock. I'm going to take both locks of this and locks take both of those locks at the same time
1:49 before we can do transfers between any two accounts. Now that's easy. Just go over here and just say self.lock = RLock().
1:59 And we see how to do this here. So instead of doing with transfer lock we're going to say from_account.lock like so and to_account.lock.
2:12 And then we just indent this. It looks like everything is golden. It's going to be so wonderful. But remember, this is a lesson in
2:20 what you need to be aware of. So I'm going to do a little print statement here. Because it's going to look like it actually goes slow
2:26 but it's not going slow or there's something worse going on. Let's just run it so you can see the whole path here.
2:32 So, what we're going to do is do a bunch of transfers and we'll be done. Shouldn't take that long. It's running. See we got some inconsistent balance.
2:40 We'll talk about why that is in just a sec. It's just kind of sitting there. It is computing? Is it taking longer? No!
2:54 If we go over here and we search for Python in the process, zero CPU. It's doing nothing. It's just sitting there. What the heck is it doing?
3:04 Let's put some print statements. We could also do some debug statements but print statements will probably suffice here.
3:10 Taking first lock, ... Taking second lock. And then we should see let's reverse that here. Oh I'm not going to leave this code in.
3:25 But just so you see it we're taking the lock, we're releasing the lock. Things like that. All right again. Oh, we're taking... Doing some release.
3:34 Taking first, taking second taking first and then we're done. We never got to taking the second lock. And what's going on here?
3:42 So this is really hard to understand actually. We have one, two, three, four, five, six accounts and maybe six threads. If the accounts are separate
3:50 everything is fine. But what if we're you know let's say thread 1 is transferring from account A to B. Thread 2 is transferring from B to A.
4:01 They both get to line 64. They run that. Thread A takes a lock on. Sorry, thread 1 takes a lock on account A. Thread 2 takes a lock on account B.
4:13 Now for B to continue, A has to release the lock on the first account, right? Sorry, thread 1 has to release the lock
4:21 on the first account and for thread 1 to continue the lock on the other account has be to released. But they're both stuck here waiting
4:29 for that second lock. All right, they've sort of criss crossed over on the two accounts and neither of them can make progress
4:35 so they're in what's called a deadlock. So that's what's happened to our program. It's in a deadlock. You can see it's still trying to run here.
4:42 So let me try to get rid of it. Kill these off. So the problem is by doing this we've actually created a deadlock. Now the other thing we have to do
4:50 is this gets harder as well our validate_bank. Remember we're doing this safety check on the account. We're just taking one lock
4:57 and then summing up all the accounts. Well, now we can't do that. We have to do it more like this. Let's copy that.
5:05 We have to say account.lock.acquire for a in accounts. And we should spell that right. And then we should release it.
5:17 And of course we should put this in to try/finally just to be safe but I'm just going to put it here like this.
5:22 So we shouldn't see any of those errors anymore. Any of the inconsistencies cause now it's really safe. However, we're still deadlocked.
5:30 Taking first lock, taking first lock, you are done. And these are where the threads are just finished. They've all mangled themselves up
5:38 and they just stopped. So how do we fix this? Well, that's what we're going to do next. So we're going to come back and actually fix this
5:46 and see if this trick that we did is really better than what we had before.

Talk Python's Mastodon Michael Kennedy's Mastodon