Sunday, July 8, 2018

The Joys of Forking

Some programming situations require handing off data processing sub-tasks to different processes, rather than to background threads; the ways in which you can do that are discussed in this blog.

In many programming languages, on many types of CPUs, a single process (or thread) runs on a single CPU core. Threads created by a process, share time on the single CPU core with the main-line of the program. If you have a multi-processor CPU, your application is going to leave all the other cores in the CPU idle if your application is organised as a single process with threads. That might not matter on high powered machines, but it can become an issue on small, slower, devices (e.g. IOT devices, or mobile phones) that might have to do serious data crunching: perhaps in a signal processing, or AI machine learning environment.

Let’s look at some ways of making an application run on multiple CPU cores. For this blog we will use Python, although in a Linux environment most languages have inherited equivalent methods all deriving from the original C implementations.

In Python, there are basically two ways of starting another application from within an initial parent application. You can use the “Popen” method from the “subprocess” module, or the “fork” method from the “os” module. Either method results in a separate process running alongside the parent process, and hopefully, the OS will assign the new process to a CPU core of its own. Each process may need to use the “os” method, “nice”, to set its priority high enough to get a core, or the Linux shell command “taskset” might be required to set the affinity of a process to a specific CPU core.

Using “Popen”, or “fork”, will depend on the amount, and type, of data you need to pass to the child process from the parent process. “Popen” passes command line parameters to the child (which is launched from the file referenced in the command line parameters). On the other hand, “fork” causes the parent process to be duplicated and continue executing from the fork point in the code. This makes “fork” a very interesting mechanism. Let’s look at the code in a typical “fork” scenario:

. application sets up data, class instances, and so on, ready for use by both parent and child
some_data_to be passed_on = [5, 6, 7, 8]
# once the common code is done, we are ready to split into 2 processes
line 1 parent_pid = os.getpid()
line 2 fork_pid = os.fork()
line 3 if fork_pid == 0:
# this branch is running the child process
# the fork method returns 0 in the child process
# we can find out our pid as the child process
line 4 child_pid = os.getpid()
# now we add code which does the tasks required of the child
line 5 else:
# this branch is running in the parent process, the assert below will pass
line 6 assert os.getpid() == parent_pid
# now we add code which does the tasks required of the parent
line 7 # statements following the “if” “else”

The fork operation can be confusing to use, even though it has some extremely useful characteristics, so we need to go through the numbered lines above to make things clear.

At line 1, a variable is set to the process id (pid) of the parent process, which has run through its code down to line 1. At line 2, the fork operation is invoked. The fork copies the current application (which we are calling the parent here) including its working memory, its current stack, and the state of all its open files, and lets the copied application (which we are calling the child process) run from line 3. The original process is still alive, and it also moves on to line 3.

If both the parent and child are the same, and execute the same code, how will the child do something different to the parent? The answer is in the value returned by the fork operation, which is being examined by both the parent and child, at line 3. In the child process, the fork operation returns zero while in the parent, the value returned is non-zero. The outcome is that the child process will execute line 4, after line 3, while the parent process will follow the “else” at line 5 and execute line 6 after line 3.

When there is a need to pass the child process a great deal of information, forking provides the opportunity for any amount of data to be prepared. From the example above, both the child and parent inherit the local variable “some_data_to be passed_on” and its value among all the other variables local and global.

In summary, the fork operation is a very powerful way of passing lots of data to a child process, but it requires some care in its use. BOTH processes (unless they terminate early) will arrive at line 7. Depending on the application design, it may be perfectly fine for both processes to proceed through the code following line 7, but on the other hand it might be an unexpected problem. It’s an unusual mindset for a programmer to look at a single piece of code from the point of view of two (or even more) processes and keep track of what should happen in them at the same time.

nsquared solutions

No comments: