So here you see, we have our various print statements
and we found that we go taking first lock
taking first lock, done.
This is deadlocking. How do we fix it?
Well, the fundamental problem here is
that different threads could say
take first a lock on account A then B
or maybe they go B then A
and it's that order that is the problem.
If we could guarantee all threads first took a lock on A
and then they took a lock on account B
regardless of whether that was the from or to account
well then we could actually be great.
We could just take those locks
and we wouldn't have this deadlock problem.
So, how do we fix that?
Well, we can just decide some order for the accounts.
If accounts had ID's we could figure out
which one is lower and use that one first.
Our account just has a balance and a lock
but we can just ask for the memory address basically
the Python ID of the pointer and that'll be good enough.
So we can just say lock1, lock2.
This is going to be a little clumsy
and I can't think of a great way
in Python to write it better, so let's just do this.
We'll say from_account.lock
to_account.lock, and we're going to do a test.
If the id can get the id basically gives you back a number
based on the pointer from_account
is less than id of to_account.
We'll say else, and return these in a different order.
Then we just take this and say, you know what?
We're using lock1 and we're going to use lock2.
Now let's try it and see if it deadlocks.
The deadlocked one is still running again.
Kill it off there.
It's slow because it has all these print statements.
But it's not deadlocked, is it?
Boom, it finished. Let's get rid of these.
Alright, now we can run it without
any of those print statements, just let it run pure.
Whoo! So, do you see the huge performance benefit
we got from this?
We went and we said, look, we have six threads
I have 12 cores, we have a bunch of different accounts.
Let's actually let all of this happen in parallel
and we're going to do way better.
Our program is going to be so much faster
because now, as long as it's not
involving any of the two same accounts
we can actually do concurrent transfers.
Right, now we don't have that many here.
If we added more accounts, you might think this gets better.
But it turns out, this part gets faster
but this part down here, those parts
actually get equally slower.
So, adding more accounts, which would
add more concurrency actually just still
kind of makes it more computational.
And it turns out that it's just worse to remember
what did we get before.
Let me pin that, run that safe bank.
This is the global one. We got 1.1 seconds.
This new fancy high-performance version
gave us 1.3 seconds and a bunch of deadlocks.
So, I'm not saying you should never
use fine-grained locks, I'm just saying
you should think about whether it makes sense
to use fine-grained locks or coarse-grained locks.
And really the most important takeaway from this
whole section has to do with this.
If you're taking two locks from two different things
you have to make sure you're always taking them
in the same order, or there's a good chance
you're going to deadlock your application.
You don't want to do that, trust me.
That is a really, really bad place to be.
It's no fun to figure out what happened
or why this is happening.
Taking two locks, always take them the same order
you don't have to use my little ordering technique
figure out some way to order it
but this works for this particular situation.