Submind YouTube summaries
Thumbnail for Python & multiprocessing: Fork'ed ! by Teijo Holzer

Python & multiprocessing: Fork'ed ! by Teijo Holzer

Watch on YouTube

Video summary

Teijo Holzer presents an in-depth exploration of Python multiprocessing, specifically focusing on the complexities and pitfalls associated with forking processes on Unix-like systems such as Linux and macOS. He begins by distinguishing between threads, which are lightweight execution strands sharing memory within a process but requiring synchronization, and processes, which act as isolated containers utilizing separate resources to effectively leverage multiple CPU cores. While threading is efficient for many tasks, it often suffers from significant slowdowns in CPU-bound operations due to the Global Interpreter Lock (GIL), making multiprocessing a more robust solution for maximizing hardware performance when properly implemented. The central challenge addressed in the talk revolves around the use of `os.fork()` within multi-threaded Python programs, a practice that can lead to severe instability. When `fork()` is called, it duplicates the entire process memory at that exact moment, inadvertently killing any threads that are not actively executing the fork instruction in the child process. This leaves behind "zombie" locks and resources held by dead threads, frequently resulting in deadlocks, crashes, and undefined behavior. Holzer illustrates this danger with a live demonstration where a simple multi-threaded program using standard functions like `print` or `logging` hangs immediately after forking, as these functions are not async-signal-safe and cannot operate correctly in the child process's unstable state. To mitigate these risks, Holzer advises against using `fork()` in multi-threaded contexts entirely and instead recommends utilizing `posix_spawn`, which internally employs `vfork` to avoid duplicating memory and bypassing problematic thread states. He also notes that standard cleanup handlers registered via `atexit` often fail because they too are not async-signal-safe, further complicating error recovery. Furthermore, he highlights a development in Python 3.14 where an environment variable named `PYTHON_GIL` allows users to disable the GIL in threading scenarios to enhance performance, though the primary recommendation remains avoiding fork-related deadlocks by using multiprocessing or `posix_spawn`. The session concludes with practical demonstrations and audience interactions regarding Python's suitability for high-performance tasks. Holzer shows that disabling the GIL can significantly boost execution speed, allowing all CPU cores to operate at full capacity when using tools like NumPy or Xarray alongside correct resource management. He acknowledges that while intentional crashes, such as triggering a "SIGKILL" signal via the `requests` library on macOS due to forked threads, can occur, they serve as important lessons in system stability. Ultimately, he argues that Python remains extremely performant for critical applications when developers correctly manage resources and avoid the specific hazards of forking in threaded environments, ending with a casual suggestion for attendees to watch the film "Avatar" at the nearby Embassy Theater.
Read the full video transcript
[applause] Okay. Um, yes, welcome everybody. Thanks for joining my talk. Um, Python multi multi Python and multiprocessing forks. This is the official title there. My name is Teo Holtz. I've been working for W FX as a senior u uh software engineer for now almost 20 years and I've been working with uh the Unix operating system for more than 30 years, Linux as well 20 years and Python close to 20 years as well. So that's kind of my background and um today I'm going to take you through the through a ride of Python and multipprocessing and the problems that you might encounter. So this is the uh structure of my talk today that I'll be going through. So it's just a quick introduction. That's where we are now. And then I'm sort of going to um set the scene for the fundamentals. You know, what is the process? What what is a thread? You know, what are multi-threaded processes and so forth. And then I'm going to be explaining um different ways of how you can spawn processes in Python and why you want to do that, right? So there's also um you know, why are we doing this? You know, what's what's the motivation behind it? So I'm going to cover that. Then I'm going to explain the most of the fundamental um ways of spawning process on a Unix operating system. Linux belongs to that. Mac OS as well. Windows is a bit separate. I'm going to touch on that as well. Um, and then I'm going to talk about the forked part, you know, what are the problems when spawning subprocesses on a Unix operating system like Linux and Mac OS. And we're going to investigate or was going to have a live demo there as well. So I'm going to show what can happen if you spawn processes and I'm going to show how you can um diagnose and debug uh the underlying causes for those deadlocks, crashes, all those kind of things, right? And then um hopefully I'm going to leave you with some uh proposed fixes and workarounds so that when you go home um you have a better understanding of the um underlying issues that can happen when you are dealing with processes in general, you know. So that's the structure of the talk. So to start off with What is a process? Okay, so a process is an isolated container that runs on a computer and u this runs your code basically, right? And it's identified by an ID that's usually referred to as a process ID and it owns resources. Resources in that sense are memories, you know, the memory it can access the open files. It has sockets, pipes, you know, they're files as well. And so these are the resources that a process uses and interacts with, you know, and and each process can contain one or more threads of execution. And that's what what is a thread you know. So so you can have a single threaded process there's only one threaded for execution and it can stop and do do another task and then come back to the original task but that's just a single thread. So what is a thread then? Um so that's an exe execution strand within a process to run code and the the idea here is that you you can have multiple threads with inside a single process right and each of those threads is identified by a thread ID. You have the process ID for the whole process and then you have each individual thread thread and they identified by thread ID their unique thread ID and they can execute concurrently inside a single process right so you have in a multipprocessing a multi-core machine like I have here 12 cores I can have 12 things literally running in parallel within a single process right and that's a really effective usage of the resources available namely the 12 cores that I have and at one single process can max out all the 12 cores I can show I can show that in one of the live demos as well. So you can have one or more threads uh per process. So usually you refer to those entities that becomes important later on either as a single threaded process or a multi-threaded process. So it doesn't matter if it has two or more if it has more than one thread. It's a multi-threaded process and that's really important later on. So the um the key uh difference there is that processes are usually not allowed to share resources like memory or open files but threads are. So that's the main difference there. So the threads that are running inside the process, they have full access to the whole memory that the process can access and the all the resources like the files and the sockets and the pipes and all the graphics card and they can access that concurrently. Not always a good idea without proper synchronization mechanisms, but that's what threads allow you to do and it's really lightweight. Creating a thread is extremely lightweight for the operating system compared to creating a new process. We'll see that later on as well. And then yeah obviously the um threads should use synchronization mechanisms like mut mutxes or locks if they have content you know contentious access to the resource concurrently. There's lock algorithms as well that can help you there. So you don't need to use locks absolutely but they always help if you have these race conditions. So this is just like a visualization that you can see there. So on the left you can see the uh single threaded process and there's just the files and the socket and the pipes and the resources that you deal with in your memory and there's nothing special you have to do right but on the right hand side you can see there's multiple threads of execution that are running concurrently indicated by these um green loops uh and they access they have access to all these same files and the same memory but they would normally use locks or mutual access to access these resources so they don't trample over each other and go in there at the same time causing problems. So that's actually a a really important uh concept to remember that you have these different entities single threaded multi-threaded process. So that's the um the difference between those two. So now we're sort of coming to the APIs of dealing with processes you know how can you um spoon processes or create processes in Python. So this kind of um API stack that you see here, it's sort of top down, right? So the highest level API that the standard Python library brings with it, you know, like the batteries included type of thing is the um concurrent um uh standard library and specifically the futures inside that we're going to cover those a little bit more. So then underneath that is the multipprocessing library and that's where all the heavy lifting happens. Concurrent is just like a really uh simple abstraction layer of the multipprocessing library. All the heavy lifting happens in the multipprocessing library. We're going to talk a little bit about that in detail as well. Uh subprocess that's the original that's always been around. Um and that was just a simple way of spawning a single process and then capturing the output and the return code. That that is really what subprocess is used for. Not so much for managing a group of processes. This is more the domain of the multipprocessing module. And when you look at the code, you find that concurrent is there's virtually no code in there. All it does, it just delegates either to the threading library. Concurrent can do threading and as well as uh multipprocessing, multi- threading. It delegates all the work to multiprocessing. And in multipprocessing, you find there's tens of thousands of lines of code, both Python and C code that actually does all the heavy lifting of being able to spawn all these processes, managing them all, all the interprocess communication via sockets or pipes or shared memory or whatever it does, right? And collecting all the results and all that kind of stuff. So multipprocessing is really where all the complex work happens. And then on the lowest level, you have um the standard Python API. It's called the OS API. And that's where you find things like fork for example, right? Or exec. So those are the the actual underlying operating system primitives or like the C library primitives that you can call directly. There's also pix spawns and other ones that we could talk talk about a little bit later. Windows is the odd one out here. That's why I listed that one as well. Windows never really knew about fork and exec at all. That was always a Linux concept and uh also Unix concept I guess and then therefore Linux and Mac OS as well. But uh Windows always went down this separate path which uh is the create process path. And basically create process is very similar to project spawn. It spawns a completely new process. It's it's nothing like fork and exit. So what we're going to leave that aside a little bit. This is the main focus of this talk is mostly on the Unix based operating systems like Linux, Mac OS, Android and all those kind of things. So Windows is a little bit the odd one out. So we're just going to leave that aside for now. Okay. So concurrent futures the top level entry point a standard Python library module as I mentioned you can execute concurrent code right that's that's really what you want you've got code and you want to make it faster you make it run on multiple cores so it's usual will work that's CPU IO bound what does it mean so you have to compute something which is computationally intensive or you have to wait for something to happen like reading from the network reading from a file system something like that so and then you have multiple workers and they're all lined up and then they all run concurren concurrently, right? So, concurrent futures allows you to abstract away with a thread pool executor. So, that uses multiple threads within one single process. Remember the picture before one process, multiple threads or you can use the process pool executor that uses multiple processes, single thread each, right? That's the kind of idea behind it, right? And then they return you a future object. It's basically like a promise. So, the future object says, "Oh, are you finished?" No. Okay, not okay. Come back later. Oh, you finished. Okay. What's your result? Oh, no, you don't have a result. you have an exception because something went wrong. So that's the future object is just like a a promise of what's going to happen in the future. Either you get the result back or the exception from the worker basically, right? So that's concurrent futures. So let's look at some code. I mean this is some some of the stuff that we do at work quite often. We do image processing like a standard movie um you know has million frames in it you know million images in it. You know if it's a 48 frames and stereo yeah it's a million frames for three hours. So all this code does we have a a list of uh 50 images you can see in the line three there and then we do some processing. So obviously the function that does uh that is called process file in line number five. It loops through all the pixels. This is 512 x 512. So this is really a thumbnail. Normally we deal with 4K images but this is just 512 x 512 pixels. And then the line 8 really is a standin. So always do you random basically means give me 100 random numbers. And this is extremely expensive to do for an operating system to give you proper random numbers. So that's just simulates CPU work on the pixel. So that's that's all there is, right? So we're we're looping through 50 images and then for every image we loop through every pixel and then we perform an operation on it. And the the OS uran normally would do apply gamma or something like that. But the OS urandom is just a standin. So we can actually generate some work for the for the CPU, right? So then this is this runs sequentially and I I'm going to give a live demo in in a minute about that one and so every single file is processed in sequence on one single core and it just runs that program just runs now let's just make it faster you know how can we make it faster with concurrent futures I mean that's what we have this library for so first coming back to the slide with the multiple threads within a single process it's exactly the same code but you can see in line 10 now we do with concurrent futures and using the thread pool executor. We say like for every file that you have in this list of 50 file names in line three, simply call that process file but call it concurrently with as many threads as you can find or as many cores as you can find. Right? So that's what that library does for you. So the library goes out creates all these threads, calls these things in parallel and then gives you back the results, right? Yeah. I mean in this case it doesn't even return anything but it just processes the image in place. Yeah. But that that is the idea behind it. Now, how can you make this? How can you turn this from a multi-threaded single process execution into a multipprocess uh framework or solution? Trivial. See, like there's almost the same code. All you have to do in line 10 is replace one single keyword. First of all, it was a thread pool executor. Now, it's a process pool executor. Suddenly, you have now you have 12 processes each with a single thread and they're all running in parallel. Right? So what we're going to look at what we're going to have a look at now is we're going to compare the the way that these three programs run and how how uh how much time they take. Right? So let's see if I can um bring this up. Um so we got this concurrent here. So we got the three examples exactly like we saw them before, right? So we start with the first one and it's um it's really simple. We'll just um we'll just run it and see how long it takes. Um so remember this just runs them all in sequence. Oops. Python 3 of course. So as you can see these images are getting processed right one at a time. And now one single core is spinning at 100%. And that's it. That took us 8 seconds. So now let's go to the thread pool executor. Remember the uh the second slide that we had there. This is now using concurrent threads, you know. So, let's have a look at how that runs. You can instantly see it already started up 16 threads at once. But then what do you notice? It's just stuck. But if you look at the CPU usage now, you'll find multiple cores. They're both packing out. Oh, the next 16 coming in, right? So now the next 16 threads are running. And um but this is already taking much longer than the single threaded example beforehand. This is actually far worse, you know. So, we're just going to uh let this uh finish through there. I think this is now coming up to the end. But you can already tell that that this is u this much worse. So, even though we've paralyzed the work within a single process, this now runs much much slower. There you go. 39 seconds and the other one was eight. This is almost a 4x slowdown. But by us trying to make it faster is completely ridiculous, of course, you know. So now what we do we'll use the the exactly the same example that's so this was threads so pull up the thread pull executor here right so that was the extremely slow one and now we run it with a process pool executor same same code exactly the same code watch what happens bang finished so this is now less than two seconds so now you can see there's a real gain there moving from a single threaded you remember the first example just doing one process one thread one one file at a time we're doing it with threads four times slower. I'm going to explain to in the future the of this talk a little bit about how we can make this faster with threads as well. But multipprocessing gives us this kind of immense win, you know. So now suddenly it's four times faster. So that's actually what we want. So that's we're going to continue down that route now. And um the reason for that being four times slower that when you're using threads is the um Python interpreter the the global interpreter lock the gill. And we're going to talk a little bit about that um later on. Remember when I showed you the picture with the multiple threads and they all have to have this concurrent access to the memory and the files and and the locks that they use and that's exactly where that slowdown comes from. The contention comes from having to lock the global interpreter lock and I'll show a future example of how we can fix that. But for now, let's just concentrate it on the multipprocessing makes it extremely fast, four times faster. So that's good. We don't want it four times slower. We want it four times faster. So we use multiprocessing, right? So that's why we're doing it. So that's what I touched a little bit about before. So the multiprocessing mod module is um where all the heavy lifting is done, right? Standard Python library module. So the concurrent futures with a process pool executor goes into multiprocessing and it it tries to emulate the threading API. It's got the cues for the interprocess communication. The cues they use sockets, pipe, shape memory, whatever they have and then they they have support for fork or fork server spawn. These are different ways of spawning processes and we're going to talk a little bit about that now. So multipprocessing module obviously now as we've seen in in this example is responsible for spawning 16 processes right you know yeah however many it it deems fit based on the hardware that found there right so there's different ways that it can go about the spawning these process or creating these process for you and then collecting results and tearing down again at the end so we're going to look a little bit um into the start methods right so the original start method of again for Unix platforms there was fork right create process on Windows and it just forks a process when needed. So your Python code encounters this multipprocessing or concurrent statement says okay the the user now needs 10 processes let's just create them with fork one after the other. Um it can also use spawn right this is uh this using fork fork exit or spawn pix spawn which is a new implementation of creating a process. Uh and the third one that is relevant in this context is the fork server. So the idea of a fork server is actually quite simple. You throw the server out at the very beginning of your program and it just sits there and it's single threaded and all it does it waits for the process to say oh give me 10 processes I need five more okay and then it forks out these processes on behalf of the main application so that's the only difference the fork server is so fork server is basically a second process single threaded that just sits there waits for you to ask it to spawn processes and we we'll see a little bit later why that is useful but so you can see that the defaults have changed here as well across the different Python versions This is quite important. So, Windows had always was born, you know, there's no fork in Windows, right? You have it in Windows Subsystem for Linux if you want the Siguin as well and then MinGW or whatever they have some kind of implementation that emulates fork, you know, but natively Windows doesn't know anything about fork. So, that's why it's just great process and it's a spawn context. Now, Mac OS, we'll talk about that a little bit later. In 3.8, Python 3.8, They changed from fork to spawn and Linux has now followed suit as well. Um and we talk a bit why why fork is broken. I mean if this is really because fork is broken. So they changed from fork to fork server in the 314 uh because uh that was just released a few weeks ago um because of the deadlocks that we'll see now as well. Okay. So yeah sub process I briefly touched on that one. This is the original standard library. It's more like for in spawning individual programs collecting the output and with a timeout you know so that's that's what that is for. So just to for completeness sake I'll just mention it here subprocess you can say run me this this command on the you know on the shell collect standard out standard and then after 30 seconds if it hasn't finished just kill it you know and then give me back the results that's all there is right and the implementation again can use v fork instead of fork or pos spawn instead of fork exec is the same kind of concept there but that's that was sort of the original way of just spawning a single process manually so that was subprocess okay so what's this fork business all about right that goes back to the 70s. I mean, as I was said, I worked with Unix for 30 years. So then, um, this is much older than I am. Um, you know, probably as old as I am. And it duplicates the calling process. It's really important to understand what's actually happening here. So when you call fork in a process, OS.fork in Python, it makes a full copy of the whole process. That includes your memory, your open files, sockets, all that kind of stuff, mutx, conditions, everything gets duplicated. But every other thread but the one I called fork gets killed wherever they are. And that's that's the fundamental problem with fork because fork was there way before threads you know. So that was like there 50 years ago. That was the only way of creating a process on on Unix. But the threads came much later. They came only in the early 2000s for example in Linux with the the native posics thread library or threading library in Linux. And as I said all threads die in the child process except the one they called fork. So that's now obviously causing problems from other threaded application because if you think about it I think I have actually have a slide here. There you go. So this is now what happens in a fork. So now we'll look into it right we look what's actually happening. Okay so you can see we have a multiro a multi-threaded process on the left has three execution threads the green circles there but only the third one the third thread the the green uh the third green arrow there calls fork. Now what happens in the copy? So there's two processes now. One on the left, one on the right. The one on the left just continues running as if nothing had happened, right? The one on the right is a full copy of all the files and memories and the logs and everything except for the two threads at the beginning, thread one and two, and they just died wherever that red access, you know, and that's where the problem comes from. You can see thread number one happened to h happened to have a hold on a lock, you know, just because it was trying to access some shared resources. And then thread number three called fork and it was just killed off. Bang was dead. And then suddenly there's no time to clean up. Nothing to release the locks. That's the end of that thread. Same for thread two. It was not in a critical section where where it was. It was just killed. The only thread that's running after fork is that thread they called fork in the parent process. And it's a single threaded process now, right? And all the resources and everything was copied. So the parent process doesn't have to worry about anything because it just continues running as before. But the child process now has this problem. Right? So you can clearly see that these locks are being held and resources are not being cleaned up because that the thread set just got killed off. Okay. So this is now the fork part of the talk. So let's have a look at quickly look at this um simple program and we can have figure out what the uh bugs are in this program. So line number three again this is all standard libraries. Line number three uh just simply creates a thread and what are we saying here? Okay, we're saying we want to we want to run print and we just want to print the word thread. It's easy, right? Okay, and then we start it. That's it. So that's a multi-threaded program right from the start. We have created a second thread and it runs. It just prints thread and then exits. But that's fine. Then in line six, we call os.fork. So we're forking like in the in the in the diagram before. So now when pit returns, basically when you get back zero, you know that you're the child process and the parent gets back the pit of the child process. So you can see line eight, that's the child process, and line 11, that's the parent process. So the parent process in line 11 just simply says, I'll wait for the child process to finish and then print finish. That's it. Uh the child process says, oh, all I want to do is just say print process. Yeah, I'm the child process. And then exit. Okay, so um there's two bugs on the code. Um can anybody spot those bugs? Yes. Oh yes, you are you are correct. The the I should have put a a thread t. join. Yes, that's that is third bug. Very good spotting. That's a correct slide. That's actually good spotting. Um I have it in the in the and I'm going to run that code live. I think in the live code I have a t join but you are correct. That's that's um a third bug. But there's two two more. Exactly right. Yep. Yep. Exactly right. That is the correct answer. So the first bug is a race condition between line three and line eight on the print statement. That's correct. Like uh the audience member has correctly um pointed out the thread in line three that was spawned could still be running. We don't know. I mean, it's only printing something, but the the by the time the fork happens, it could still be running. And it was just trying to print the word thread, but it got halfway through and then it got killed off. So now in line eight, if if the child process now tries to print process, the thread in line three in the child process is dead. It's still running in the parent process, but not in the child process. And and thus the uh the print statement fails. That's correct. Um so this will actually deadlock. So I'm going to do a live demo now. Um there's there's a third bug. That's exactly right. Systems exit is suffers from exactly the same problem. Yeah. So what and don't take this for as a gospel. We're just going to run that now. So then you see what happens. So this is now what I call um so we'll just have a look at the code. And so we got the deadlock here. See if that I think I might actually have a join in here. Oh yeah, it has the join. So yeah, thanks for pointing that out. [laughter] It's made it in here, but otherwise it's exactly the same code. So we're printing and we're forking. So now the the interesting part about this one, this is now undefined behavior as Oscar pointed out very nicely. And undefined behavior is that I don't even know what's going to happen. So let's just run the program and see what happens. So that deadlock we'll just run that first. Okay, already there we have it. The deadlock has deadlocked, right? So a couple of things have happened now. So if you look at the output, you can see thread. Okay, so the thread has printed. Wow, but what happened now in line seven where we called fork the deprecation warning from the Python interpreter actually telling us this process is multi-threaded use of fork mal to deadlock and child and you can clearly see the weight p is being called as well and uh we're deadlock there's there's nothing happening anymore right so now what you what what we're going to do um we're just going to do the split terminal here and let's close this off a little bit so you can see that this process is just hung it should have just returned I did put the join in here. So I should have just returned and exited, right? So let's just um have a look inside that child process. Right? This is the one that's hung, right? So we just use GDB and um let's just have a look where some Python were hanging. There you have it. Look at that. That is it. The print is deadlocked. That's our deadlock. Right? So now you can clearly see line number nine in the stack trace is deadlocked. Right? I mean if you're interested in the uh in the back trace the Python back trace as well you can see where they call come from and if you're interested in the C++ or sorry the CPython side of things right you can look at the um the C back trace as well and here you can see the lock there right as as you pointed out there's a buffer writer text IO writer so that's pretty much what has happened right so the important thing is the print statement has deadlocked us right so that's what we wanted to So um and the message from the Python interpreter actually tells us about that. It tells us my deadlock and I go like yes we see that right now the print statement has deadlock. So now let's just run that a couple of times. Oh that did locks every single time. Like when I was testing it before it wasn't. So obviously something has changed but as like the race condition triggers every single time. I I thought I could get it to to to run through but now you can see that right. So that that is basically what we're saying here. So that is that print that print statement here that kills us. And yes, this exit suffers from exactly the same problem. So now that's really concerning of course because what do you do if you can't even do print? No, then we'll find out. So that we we know now that this happens and this is real. You know, this was just a this not a contrived example. It's really simple example. just a thread uh two processes and a couple of print statements. Right? So what we've seen we can use GDB like pi bt or pi back trace pi list shows you exactly where in the python code you're dead logged you can dive into the c python layer if you want the print function dead logs after fork we found that the logging library did logs oh that's actually the other one I wanted to show let's just have a look at there because we got let's say oh this is the problem with print I've got exactly the same code here so now instead of printing all I'm doing it's logging do warning right Let's run that. So there's no print statements in that code. It's just using login.warning. So it's got the basic config there and it just calls login. Otherwise the same code. Now watch this. See this has succeeded. Let's run it again. Succeeded again. Oh there it is. So this is the kind of intermittent behavior that I was talking about undefined behavior. I run the same code exactly the same code multiple times and I get different results. It's not insanity. No, this is experience. you run exactly the same do exactly the same thing over and over and over again and you get different results and that's exactly what's happening here and so now you can see this this is the same deal if you want to attach the debugger to it you can see the loginwarning call is stuck you know but it's exactly the same problem right so it's nothing to do with the print statement um so that was just basically showing that this this is not just limited to the print statement let's have a look okay cool exit as well that we pointed that out that that locks as well go to the exit handlers. Okay, so why is that happening? After a fork in a multi-threaded program, the child, what can the child actually do? You can't print. You can't call can't log. What can he actually do? He can't call exit. You can only call async signal safe functions. And that's the crux of this talk. Async signal safety is the key. Print is not async signal. Can't call it. Logging is not as signal safe. Can't call it. So exiting is not asyn signal safe. You can't call it. Is there anything we can do at all? That's a good question. Yes, there are. Okay. Okay, so as signal safety quick, you know, kind of degression here into the Unix world. Again, this comes originally from signal handlers because remember the original Unix concept didn't know about threads. It was just a single process. But sometimes you needed to interrupt a process and tell it, oh, there's something you need to do really quickly and then you can go back to what you were doing. So that's where the signals came in. You could send a signal to a process like, oh, there's an interrupt. Pick up these network packets otherwise they disappear. Okay, cool. I'll do that now quickly and then go back to what I was doing. So async signal safety means that these are functions you can call even in inside a signal handler because there's a lot of restrictions on what you can do inside that and there's only there's there's there's an absolute list you can look that up there's a man page for that on Linux and Unix there's 200 safe C functions only nothing else so definitely not melo or free you cannot allocate memory it's bad idea but you can call open read write really good you know so those kind of things you can do like open read write you and close you know receive send on sockets underscoreexit as compared to the exit which doesn't call any of the um exit handlers and doesn't tear down all the global destructors so underscore exit is safe to call and nothing else especially no print definitely not allocating memory that's a bad idea allocating Python objects on the heap not good not good so now okay so this is the the fix right so you can see it in line eight I've taken the print away and the logging you know and I I'm using o write why why am doing that because always write is the direct operating system in uh interface into the right call and that's asynch number two means standard error so zero is standard in one is standard out two standard standard error and I'm printing the word process just like I'm printing before and I'm not calling sisexit I'm calling oscoreexit because that's async signal safe doesn't run any of the um tear down code so again the um proof is in the pudding let's have a look if this actually works So this is the fix now. So we're just going to I think the uh here we go. We're just going to close this shell. So yeah, this this has killed it. So that's fine. So this is now the deadlock fix, right? Oops. So I'm just running the the fixed version. So you can see it prints threads, it prints process, and it finishes. It still gives us a warning. Let's run that again. Yeah, it still gives us a warning, but that's fine. But that's obviously that doesn't tell us anything because I just ran it three times, you know. Let's let's do this properly now, right? So remember this is now using async signal safe functions only, right? Uh and underscoreexit. Oops, it's not helpful. Um running it once doesn't tell us anything as as we've seen before. We want to run this forever like that. So this means just run it forever. And now now we'll see what happens. There you go. So that's running hundreds of times, thousands of times. I can run that overnight, million times, no problem. Right? So this works, right? So again, the proof is in the pudding. This is running. I can leave that running until the end of the talk. It'll still be running, right? So obviously this is completely impractical. [laughter] But yeah, what's going on here? What's What are you you can't use print, you can't use exit. What's obvious? It doesn't make any sense, right? I mean, nobody's going to write code like that. One in a thousand programmers write code like that. You know, I sometimes have to write code like that, but I tend to avoid it. It's not pleasant. It's not it's not very pleasant. Now, I can tell you it's not very pleasant. So, okay. So, we know that now. Okay. So, we we understand that that this is the problem and how we can fix it, but this is not practical. This is what's going on. So, people have thought, okay, let's clean up that mess. you know, we know that we got ourselves and you know, on the twist and it's all kind of broken and all these locks are hanging around from these threads that got killed off. Let's just clean them up. And they go like, "Oh, great. I always register at fork." There's a there's um a Python call that you can do. And it basically says, I want to register something that calls gets called when fork when anybody calls fork and then I can clean up clean up my mess, you know. So, you can give it three arguments. One gets called in the parent before the fork actually triggers. Then the other one gets called after the fork returns and the parent and the other one gets the third one gets called in the child. So the idea is in the child you can go a it's all this mess here let's just try to clean up that stuff and the problem is they tried doing that right and the standard login library is riddled with this to this day you know just all these registered fork and trying to clean up all these locks and but the problem is after child is not async single save either you know you cannot clean up a lock it's not async signal save they tried and they failed okay yeah there you go it's not asyn it's not async single save there's nothing you can do you can register something to be called in a child but you can't even what what good is that? Let alone trying to clean up any locks or freeing any memory or something like that. It doesn't work, right? So, this is garden path. I I recommend staying away from that. We just said fork. Um P3 fork is the C equivalent. And um so that's the underlying one. It's not fit for purpose and the Austin Pix group has finally given in and said [laughter] like no, just deprecate it. This was such a bad idea to even introduce this you know that like this kind of cleanup functions after fork and he goes like nope nope don't do that. So there there's a proposal in from the posit exhausting group to deprecate all that mess. So if a fork calls call in a multi thread process leads to a child fork handler calling any function that's not as single safe then the behavior is undefined. And these are the crashes and we'll show a crash later on as well. Deadlock logs that we've seen and Oscar pointed that out as well. Undefined behavior not good. Don't want that. It's basically unpredictable. You don't know what's happening. You run it once works. Run it again doesn't. So that's bad. So the this is kind of garden path territory, you know. Yes, they tried that for many years and trying to clean that stuff up. Didn't work and then they walked away from it. It's just still in the logging library. There's still stuff there that's trying to mitigate that problem. Okay. So what what can we actually do? So this is what I've used uh myself, you know, vfork. So it's similar to fork. It doesn't duplicate the RAM. So it's exactly the same RAM with a parent process. So you have to be extremely careful what you do there. Other resources are duplicated. But it doesn't call any at fork handlers. That's the main that's the key point there because the problem was especially if you have a really complex system like Maya with dozens of plugins and then another dozen of your third party plugins our own plugins and this all gets loaded in and people registering at forkanders left right center is such a mess. So you just really want to bypass that. Vfor does allow you to do that and but you can virtually do nothing in there. It's still the same restrictions apply and you can just only call exit or exit you know. So because you're sharing the same memory so it's even more restrictions apply but it doesn't call the add fork handler. So that's a that's a good thing you know I don't want those and then pix spawn came along that's actually comes from the pix group again and that's a replacement for fourth basically right so it's safe to call for multi thread applications at any time and it spawns a new process with a completely new executable so there's none of that duplication and killing off threads it runs a new executable and it uses vforoke internally they figured that out pretty quickly as well so as soon as the posics group have implemented that and they go like let's use fork they go like no this is broken let's use vog yes good idea So even they agree with that. So pix spawn with vfork is the solution, right? Okay. So uh it's a bit bit of a whirlwind there. Um so don't use at fork handlers. They're broken inherently because remember in the child after a fork there's nothing you can do if your program was multi-threaded. If it was single threaded nothing f there's no problem whatsoever. Remember all that stuff I've discussed all these problems only when you have multiple threads in a process when you call fork and then I show you later as well one when that's not inherently obvious immediately. Um do not assume your program is single thread simply because there's nobody creating a thread. Sometimes threads get created for you by third party libraries underneath the hood you don't even know about. Right? Use posics spawn for a clean alternative. And I wanted to briefly touch on that Python gill environment variable. Now this is a new addition to uh 314 now as well. So we can turn on and off the gill. That's amazing. You remember the at the very beginning of the talk I showed that it was four times slower was with with all these threads. You know the program run four times slower. Let's just go back and uh and turn off the guild the global interpreter lock and see what happens. So let's um let's just go back to that. Um okay. Oh cool. I [laughter] have to to go go have to go quickly. Uh what one thing I wanted do wanted to show so I'll just boot up um let's boot up my Mac in the meantime but um so um with that Python gill I just wanted to briefly show how quickly that can run but I also I do have some more bonus slides but um let's just yeah let's just do that. So, I've got a virtual environment here. Um, no, it's a way to have it. I think I've got this. Was it 314? I think, isn't it? And then no, I don't. But the idea is Z. I can now run this uh concurrent thing. Let's just have a look. Concurrent. So remember this one here that we ran before the threads one. So you can immediately see the threads getting created but then it gets stuck. So it's really slow. So let's just turn off um let's just turn off the gill. So how does it work? So let's just turn off the gill. Oh and the time as well. Oops. Oh, okay. Oh, sorry. I I know what I've I've done wrong. No gill. I want the nogill. Sorry. Does that work? Huh? Oh, there you go. Yeah. So, now you can see this is without the gill and we're actually a bit bit better. So, you say I don't need. Okay, cool. And then just Does that work? Yeah. See, that's much quicker. So, now I've turned on the global turned off the global interpreter lock. So, this is much quicker now. So, that's what I wanted to show you. Oh, and like last thing very quick. Um, this is now a Mac and I wanted to show you a quick crash because uh that's always good. Hard crash, you know. Yeah, of course. So, this was just deadlocks, you know. This is just for beginners. Now, we're on hard crash. So, you can see here again, this is using the request library quickly just to request something from the um um from the uh from a web server. So, let's just bring up a web server here as well. Python actually has a web server built in which is really good. So you can just run it like so. So then web server running and let's just see if we can crash this guy. Um what is it? Yeah. And normally come Oh yeah, there you go. Magic. Magic crash. There you go. And what does it tell us? It crashed hard. Sick kill. You know that's good. There was a sick kill that it got and the reason said look at that multif process forked. I've killed you. There you have it. Okay, that's me. Thank you very much. [applause] Thank you very much. Uh, I think we have time for one question. Oh, up the back. Making me work for it. I'm just wondering with all the kind of jumping through hopes to get Python more performance and all these kind of traps that you have there. At what point do you say well Python's maybe not the best language for performance anyway and is there a point where you go I'm giving up and I'm going to try something else? >> Um no I think Python can be extremely performant especially if you're using things like numpy or the xarray that was pointed out in one of the previous talks. um you just have to know how to use your resources appropriately. For example, with a the with the with a gill turned off or with a multiprocessing module used in a correct manner, all the cores were picked out at 100%. So you can use all the cores like 12 cores on that machine and they're all sitting at 100%. So you can use you can write performant code in Python. Um you know it's it is possible but you just have to be aware of the performance there. And yeah, go and see avatar that comes out very [laughter] FIRE AND ASH. AND I recommend watching that on the big screen. There's the embassy theater just around the corner and IMX lower hut. Of course, IMX lower hut. [laughter] Yeah. Cool. Cool. All good. >> Thank you very much. [applause]