Tackling Thread Safety in Python
This is a modal window.
The media could not be loaded, either because the server or network failed or because the format is not supported.
Formal Metadata
Title |
| |
Title of Series | ||
Number of Parts | 131 | |
Author | ||
License | CC Attribution - NonCommercial - ShareAlike 3.0 Unported: You are free to use, adapt and copy, distribute and transmit the work or content in adapted or unchanged form for any legal and non-commercial purpose as long as the work is attributed to the author in the manner specified by the author or licensor and the work or content is shared also in adapted form only under the conditions of this | |
Identifiers | 10.5446/69505 (DOI) | |
Publisher | ||
Release Date | ||
Language |
Content Metadata
Subject Area | ||
Genre | ||
Abstract |
|
EuroPython 202419 / 131
1
10
12
13
16
19
22
33
48
51
54
56
70
71
84
92
93
95
99
107
111
117
123
00:00
Design of experimentsComputer programChemical equationSet (mathematics)CodeCondition numberLibrary (computing)Heat transferDatabase transactionEndliche ModelltheorieProcess (computing)Semiconductor memoryOperator (mathematics)Mobile appElectronic visual displayDependent and independent variablesTupleOrder (biology)Computer programmingSynchronizationResultantTotal S.A.Functional (mathematics)Slide ruleCartesian coordinate systemProgrammschleifeIntrusion detection systemFunction (mathematics)Reverse engineeringElectronic mailing listProjective planeInterior (topology)Different (Kate Ryan album)Statement (computer science)Key (cryptography)Subject indexingNumberClique-widthRight angleSequenceMathematics1 (number)Table (information)Pole (complex analysis)Primitive (album)Multiplication signParameter (computer programming)Computer animation
05:56
Length of stayTotal S.A.Condition numberBefehlsprozessorParallel portTask (computing)CoroutineShared memoryComa BerenicesOperations researchInheritance (object-oriented programming)CodeCross-site scriptingRight angleCodeTask (computing)Slide ruleSpeicheradresseResultantChemical equation2 (number)DatabaseFlow separationComputer programmingOperator (mathematics)IterationString (computer science)Mobile appInterpreter (computing)Parameter (computer programming)Entropie <Informationstheorie>Context awarenessConcurrency (computer science)Default (computer science)Letterpress printingNumberInstance (computer science)Process (computing)Universe (mathematics)Functional (mathematics)MultiplicationRange (statistics)Loop (music)IdentifiabilityPhysical systemBefehlsprozessorBitInheritance (object-oriented programming)CausalityCondition numberGame controllerDatabase transactionSinc functionSocial classStatement (computer science)FreewareRandomizationShared memorySound effectSource codeMultiplication signLine (geometry)Meeting/InterviewComputer animation
13:26
GodEvent horizonGame controllerVapor barrierInstance (computer science)Exterior algebraQueue (abstract data type)Level (video gaming)Atomic numberContext awarenessBlock (periodic table)Physical lawSynchronizationSocial classPrimitive (album)Condition numberSheaf (mathematics)Concurrency (computer science)Data managementTask (computing)Object (grammar)Attribute grammarTimestampFunctional (mathematics)Deadlock2 (number)Computer programmingStatement (computer science)ResultantFerry CorstenSlide ruleCodeSymbol tableCASE <Informatik>Connected spaceSemaphore lineNumberLimit (category theory)Multiplication signMultiplicationRevision controlDatabaseCoroutineHeat transferDependent and independent variablesEvent horizonRight angleProcess (computing)Logic gateRouter (computing)WeightIntrusion detection systemFunction (mathematics)Chemical equationComputer animation
20:49
Set (mathematics)Single sign-onReading (process)Object-oriented analysis and designCASE <Informatik>Functional (mathematics)Context awarenessOperator (mathematics)Function (mathematics)Social classData managementCartesian coordinate systemMessage passingSingle-precision floating-point formatDeadlockObject (grammar)Letterpress printingStatement (computer science)Link (knot theory)Chemical equationWechselseitiger AusschlussInstance (computer science)Slide ruleLevel (video gaming)Multiplication signSynchronizationEndliche ModelltheorieIntegrated development environmentAtomic numberProduct (business)MultiplicationFlow separationSymbol tableBlock (periodic table)DatabaseSheaf (mathematics)Sound effectLibrary (computing)Primitive (album)Source codeStandard deviationCodeMachine codeSinc functionLine (geometry)Row (database)LoginMathematicsMobile appPrime idealHeat transferQuery languageReading (process)Exclusive orComputer animation
28:12
Computer animation
Transcript: English(auto-generated)
00:04
So let's go through what we are going to go and say in this talk, right? So basically, we're going to discuss about threading, race conditions, threat safety, synchronization primitives, and making your programs thread safe.
00:20
Yeah, so why use threading? To improve your application efficiency, we're using concurrent execution and improve your responsiveness. And to make you understand a little better, I'm going to show you an example of a simple banking app. So, here you go.
00:41
I just use SQL SQL model, which is a simple library in Python that you can use to work with SQL on your local projects and maybe even bigger ones. So you just have to look through the with statements in this slide. Basically, the first statement allows you to delete an account
01:00
and the second one allows you to add an account. And you can see a lot of sessions throughout this code to just make your code safe and transactions reversible and all that. And if we look deeper into this code, you can see that we have created three accounts with ID and a name and a balance to each of them.
01:23
So we have John, Jane, and Alice as examples for now and each of them has a $100 balance in their account. And if you look down below, you can see the account model made with the SQL model that has an ID, which is an int and a primary key and a name and a balance. Okay, let's go further.
01:42
So just to look at the initial table, we can see that John, Jane, and Alice with IDs and their balance. So the total balance money in the bank is basically $300 or so, right? And now we can take a look at the transfer code. So I have written this code to just, you know,
02:01
to be able to transfer from one account to another and a fixed amount of money, right? So basically we have two functions, one is transfer money and one is display balance and we're using sessions in both these places and the display balance just gets the account and then loops through the results to show the balance of that particular account.
02:22
And when we look into the transfer money function, you can see that we have a from user and a to user that is taken as arguments and then we deduct the balance from the user and add the balance to the to user, right? And we commit that particular session. So that's what it basically does.
02:42
Now, let's go a bit further. Okay, so now this is an operation which is completely sequential. We are not using any threats. It should go as easy as it is, right? So now let's look at this function. We initialize the DB, we display the balance first and then we do a couple of transfers. Now if you look at the variable to transfers
03:02
which is the list of tuple and it has the from ID in the first index, the to ID in the second index and we also have the amount in the third one, right? So we are performing a particular set of transactions on these accounts with these amounts, right? So what would be the balance after doing this process?
03:24
Let's look at the output of the display balance function. So basically what we did is transfer from John to Jane $10 then Jane to Alice $10 then John to Alice again another $10 which is 120, right? And then we transfer from Alice to John again.
03:42
So John should have 90, Jane should have 100 and Alice should have 110. Basic simple mathematics, right? And it works perfectly in the sequential order. So the money in the bank is still $300. Now let's look at another example. And so yeah, so the transfers are completely sequential
04:01
which is a bit slow as you can see because it has to wait for each and every single transaction to complete before it starts the next one. Now let's see an concurrent transaction with using multiple threads. So again, the only difference here that is the with statement is using a thread pool executor with a maximum number of 10 threads, right?
04:22
And this is also doing the same exact process but this process now has 10 different workers trying to do this particular transactions. Now it should yield the same results that we saw in the slide before but since the CPU chooses
04:40
when and where to execute these processes, these functions we'll get a weird result. Let's see that in the next slide. So yeah, the money in the bank has gone down by 10 which is not possible, right? Normally it's not possible. It's not right basically. So the money should be finite. It is being transferred among these accounts
05:01
which means that the money should be the same but let's see what happened here. It's 90, 90 and 110. Okay, let's see what happened. So debugging the current issue it is something that happens when you use concurrent reads and writes while using threading. Now this can lead to something called race conditions.
05:21
We'll look at that in the next few slides. Okay, so what are race conditions? Race conditions normally occur when we work with shared mutable data. So since the threads are sharing the same memory under the same process all of them has access to the same memory and we have not used any kind of technologies
05:43
to actually restrict access to one thread at a time. So this happens. One thread goes and mutates a particular data and another thread does the same and the condition does not match and we get completely different results.
06:04
Okay, give me a second here. All right, so if the operations are non-atomic then the context, just basically the threads get switched in between each other you get weird results. Let's see why that happens.
06:21
Okay, so race conditions. Let's get into a bit deeper example when it comes to race conditions. Plus we will just see what context switching is, right? So when you come to this example you can see that we are using a loop down below that is just, you know, iterating over a range of numbers
06:42
from zero to three, zero to two, right? So the threading.thread function is starting a new thread with the worker and the argument which is just zero, one and two, right? So that's what actually happens. So now we also get to see that the sys.getSwitchInterval returned 0.005 seconds.
07:03
That is basically saying that every 0.005 second if the CPU would try to switch which thread is executing a particular task. So every 0.005 seconds it'll just check okay, if the thread two is free let's hand over the control to thread two
07:21
instead of thread one. Okay, again, it'll check. And if the thread one is free now hand over the control to thread one, right? This is what basically is happening underneath. Now, if you look there the function worker function is sleeping for around 0.005 seconds which is much higher compared to 0.005.
07:41
So when the first thread comes into play it sleeps for 0.005 seconds but the system CPU is told to switch in just 0.005 which is much lesser, right? So it'll check for any other thread that is free on the block and it'll just switch to thread one and thread zero. If you can look at the string down below
08:01
you can see that thread two started iteration first so the zero iteration completes thread one, thread zero. That's probably not what you expected. You expected zero, one and two. But it happens in complete disorder since the threads are sleeping for 0.05 seconds, right?
08:20
Let's see a much simpler condensed example. So what are race conditions? So this is a simple example just to get it done completely. So yeah, consider two threads in our backing app. Now a user has an initial balance of 30 and amount of 100 is being transferred simultaneously
08:43
to the user by each one of these threads, right? So the thread one currently reads the balance as 30. Now the thread one updates the current balance to 130 but before it can save it to the database thread two also reads the balance as 30. Now thread two updates the balance to 130
09:01
just adds 100 to the amount, whatever it is. But imagine now thread one was able to complete the saving process to the database before thread two did. So the thread one is updated to 130. Now thread two writes 100 more to the database. Now you have a completely untrue amount in your database.
09:22
So in midway of the operation of thread one thread two was able to intervene and look at the resource. Now this will cause transaction issues if it's used in something like a banking app. Let's see what happens in the next few slides.
09:40
Okay, so how do you make this problem go away? Well, a program is said to be thread safe if it can be run using multiple threads without any unexpected side effects. Well, at least in the best of your knowledge, right? So if multiple threads are executing the same process working on the same resource, and they can get consistent results
10:02
without messing up each other's results, you have a thread safe program. Well, we'll see how we can achieve that in a couple instances, right? So this is a snapshot taken from Anthony Shaw in this talk called unlocking the parallel universe. So we have multiple ways of concurrency
10:22
which is threads, coroutines, multiprocessing, some interpreters which are really the cutting edge of Python and you can look into it on his talk. Okay, so the next slide. Right, so when should you worry about thread safety
10:40
in your program, right? If it has mutable data and it does non-atomic operations on that particular data, then you should be worried. You should look into that code, right? Since threads share memory, if the threads share the memory location of the parent process, every single thread has access to the same memory location
11:00
and it can do weird things like the example we saw before. Now, if the data is not shared, we probably don't have a problem. And if the code is executed with atomic processes, we still don't have a problem. We probably don't have a problem. But if it's not atomic, then we do. So keep in mind those points
11:20
and look at your code and if it resembles what we said, then yeah, go fix that. So now we have a non-thread safe example. This is pretty safe, but I just wanted to show you what is the expected result when it comes to this program. So what does this function do?
11:41
It's printing the getIdent function from the thread. What is the getIdent function? The getIdent returns the identifier of that particular thread instance. So we are printing from the thread 74112 if you look at the string down below, the result down below.
12:00
And now we have customized the end separator to print the same identifier, but we have mentioned it as a separator, right? So now the expected result should be, since the identifiers are the same, printing from thread one number, separator of the same number, and so on and so forth. But let's see what happens
12:22
when we introduce threading to this particular example. Okay, yeah. We can switch the slide, I guess. Yeah. So if you see here, since the default of the switching time, which is default 0.005, the threads get switched in between processes, right?
12:43
So if you see the print statement down below, you can actually see that in the third line, it's printing from the thread 79484, but then it switches over to the thread 72360, and it prints a separator of 72360
13:00
followed by the separator of the previous thread, which is 79480. Now you can see the context switching between these threads is causing the program to return results at random times, right? Not exactly random, the 0.005 seconds that I mentioned before. So let's see one more example.
13:23
Okay. So everybody knows what a singleton class is. You should be only able to create one instance of that particular class. So this is a proper example. You'll create a singleton class, and if you look down below, it prints the ID of both the objects and it has the same ID, right?
13:41
But imagine we introduce threading to this. What happens? Well, the classes will enter the dunder new method, and it'll look if the class has an instance, which it doesn't if the thread one looks at it, and imagine there is thread one and thread two, right, in this context. So the thread one looks at it, enters the dunder new function,
14:01
checks if it has an instance, it doesn't, so it creates new instance. At the same time, thread two also gets to the new method, checks if it has an instance already, which it doesn't, but both of these threads got the affirmative response that yes, this class does not have an instance. And both of them create different instances for the same class, and now you got two different IDs,
14:22
which completely goes against the law of making singleton classes. So this is another non-thread safe example. Okay, so what is the recommended method of doing this? So if you want to make concurrent executions of programs,
14:42
I mean, threads, just don't go use threads, right? Just use something, some other alternative method like Anthony Shaw mentioned in his talk. You have parallel process, I mean, sorry, you have coroutines and so on and so forth. You can use that and make your operations atomic. So that is the best method you can get consistent results
15:02
and not mess up your program and stop sharing mutable data across threads. So if two threads have the access to the same thing, it probably will mess it up unless you use some kind of synchronization primitives, which will be explained by others in a couple of slides later on.
15:22
So yeah, that's it for me, handing it over to you others. Yeah, so to solve the thread safety issues, we have synchronization primitives. So we will go over each of these one by one. This can be used to coordinate between multiple threads.
15:41
So let's take a look at the lock. A lock is a synchronization primitive that allows only one thread to access a resource. So we can use lock to mark critical sections and only one thread can execute the code in the critical section concurrently. Next, we have rlock.
16:01
Rlock is like an advanced version of the lock. It's a re-entrant lock, so it allows the same thread to acquire the lock multiple times without causing a deadlock. So we will see an example of rlock in the coming slides. Next, we have semaphore. So semaphore is like a counter.
16:20
It allows a certain number of threads to access the resource simultaneously, so we can limit the concurrency and the number of threads that can access a piece of code simultaneously. So it can be practically used in cases where we need to limit the number of concurrent connections in a database. For example, suppose we have,
16:41
in the case of connection pooling, suppose we have like limit on the number of concurrent connections, we can maintain that using semaphore. Next, we have event. So event is used for signaling, so it will help one thread to signal to some other threads that something has happened,
17:00
some particular condition has been met. Next, we have condition. So condition is a synchronization primitive that allows threads to wait for certain conditions to be met. For example, suppose we have some queues and we can use condition
17:22
to make a thread wait until the queue is filled. Next, we have barrier. So barrier is like a gate. It allows entry to that only after all of the threads have completed the previous execution. So this can be used to ensure that
17:43
all the worker threads complete their individual tasks before they proceed to the next level. Now, let's see a practical example of the lock. So here we can see we are creating a lock object by creating an instance of threading.Lock class.
18:03
Here we have a third function. Then we are creating two threads, thread one and thread two. Inside the third function we can see that we have a statement lock.Acquire. So this is the statement we can use to acquire a lock object. And at the last of the function you can see
18:21
there is a lock.Release statement. So this will release the lock. So it ensures that once a thread has acquired a lock, other threads have to wait until it's released by the first acquirer. So we can see the output, which is given below the code snippet. We can see we are creating two threads.
18:41
So we can see the first thread is initially waiting to acquire the lock. Then it actually acquires the lock. Inside between the acquire and release statement we can see we have marked a critical section of the code, which does nothing much. It just sleeps for five seconds.
19:00
So we can see initially the third one got the lock acquired. Next we can see that our second thread is waiting to acquire the lock. It's waiting there only after it's being released by the first thread we can see the lock is acquired by the second thread. So you can take a look at the timestamps also.
19:22
Between the two timestamps there is a delay of five seconds. So since our critical section is being executed, it sleeps for five seconds. So the thread two is waiting for five seconds to acquire the lock. So this is how the symbol lock works.
19:40
Next we have another way of acquiring and releasing the lock. It's using a context manager. Similar to the previous example, we can create a lock object and we can use the context manager statement with lock and we can add the critical section code inside the context manager block.
20:02
So when the control enters the context manager block, the acquire of the lock is automatically called and when it exits from the block, the release method is automatically called. So this is equivalent to what we have seen in the previous code. Next we have a deadlock scenario.
20:23
Deadlock means the threads get locked and none of the threads can proceed its execution and this leads to an infinite lock where the program execution cannot proceed further. So this is an example of a transfer between bank accounts.
20:42
So we have a bank account class. We can see there is a deposit function and an update balance function. We have a lock object at the self.lock attribute. We are using a symbol lock in that case. You can see inside the deposit function, we are entering the lock. We are acquiring the lock using the read statement.
21:02
Then inside that, we are calling the update balance function to update the balance. So what happens inside the update balance is that it's also trying to acquire the same lock. You can see the statement there. This will cause a deadlock. Currently, consider we have one thread.
21:21
So it's acquiring a lock from the deposit function and when it reaches the update balance function, it's being called from inside of the deposit function. It tries to again acquire the same lock. So a symbol lock is named. It will not allow re-entrancy. So this will be the output. We have two threads.
21:42
The first thread is waiting and it is acquiring the lock. Then we can see it acquits the lock of the first function like the deposit function. Sorry, we have three threads. So the first thread acquits the lock for the deposit function.
22:01
Then other two threads are waiting. Then we can see our first thread is trying to acquire the same lock for the update balance function, but since it's already acquired by the previous function, the deposit function, it cannot proceed further and this will lead to an infinitely waiting scenario.
22:22
So we can use an rLock to fix this case. So initially we can see that. In the account we have self-code balance equal to zero. We have initialized the account balance as zero. And we are trying to use three threads and transferring the amount of 100 using each thread.
22:43
So here the only change is that we are initializing the self-code lock with an rLock instance, threading.rLock instance. Then if we try to execute that, we can see that initially we have, similar to the previous example,
23:00
we have three threads. It allows for, we can see the first thread, 23128, the thread with the ID 23128. So it acquits the lock for the deposit function. And similarly we can see it again acquits the lock for update balance function also. Since it's a re-entrant lock, if it's currently holding the lock object,
23:23
it can re-enter to the critical section. So we can use rLock in scenarios where we are calling critical sections recursively. Like in our case, we have two functions which need to be locked. Next, let's look into our previous examples
23:42
and how we can make these threads safe. So we have our banking app. This is the transfer money code. Here we can see that the initial two lines are just building SQL queries. We can omit that from the critical section. They are just preparing the SQL statement and nothing is being executed.
24:03
Then we have an account lock which is a lock instance. And we wrap that into this context manager block. We have code for executing the SQL queries. We have code for updating the balance as well as saving the updated balance to DB. So using this lock, we have made the operation atomic.
24:21
So all these operations happen as a single piece and only one thread can execute these at the same time. So yeah, that will fix our problem. We have seen in the first case, in our banking application case. Next, let's look into the production consideration.
24:43
So this simple example here is not suitable for production. This is because there can be multiple instances of our Python application. Similarly, maybe we can have some other applications accessing the database simultaneously. So whenever we are working with some critical code,
25:02
we should enable lock at the source of truth level. Here we can use the locks but SQL provides us database level locks. So in such a scenario, we can use database level locks. And it's a recommended practice for production scenarios. Here we can see the statement using our SQL model library.
25:25
We have the statement and it has a read for update function appended, we can see. So that will acquire a row level lock for this session. And only the other threads can access the value only after our current thread completes this execution.
25:43
Next, we can look at the printer example. So as we have seen, the print function is not atomic. It has one operation of printing the message and another operation of printing the separator. So to make it atomic, we are just using a symbol lock.
26:01
Similar to one we used previously, we are defining a print lock object and we are using context manager to wrap our non-atomic operation. So we can see the problem is fixed now. Next, in case of Singleton also, we can solve that using a lock similarly.
26:21
So initially we can see if the class instance is not. We are trying to acquire a lock so that there is no interference from other threads. Then once we acquire the lock, we are again checking if the instance is already not. So suppose we are checking if another thread already got the lock and it has created an instance in the main thread.
26:43
So we are wrapping that with our lock to ensure that there is actually only a single instance is being created in a multi-threading run. Next, summarizing everything. So suppose we have a single-threaded or normal code
27:00
and we are moving to a multi-threading based model. So we should keep in mind that the code we are working with might not be designed for thread safety. It can include maybe third-party library codes or even standard library code. Like we have seen the example of print function. So when we are running code in a multi-threaded environment, maybe it can have unintended effects.
27:23
So before switching to multi-threading, we should check for shared mutable data. We should check if the data is shared across threads and these are mutable accidentally. And in that case, we need to add synchronization primitives. Also, we should check if the atomicity requirements are met.
27:43
Otherwise, we have to implement a primitive to ensure that. So as the C Python docs say, when in doubt, we should use a neutral exclusion primitive like logs to ensure that our code is thread safe.
28:01
So that's it. Thanks everyone. So you can visit our link to get the docs slides and to connect with us. Thank you. Thanks everyone.