Fork and Run: The Definitive Guide to Getting Started with Multiprocessing

Since the early 2000s, the CPU industry has shifted from raw clock speed to core count. Pat Gelsinger took the stage in 2002 and gave the talk the industry needed, stating that processors needed specialized silicon or multiple cores to reduce power requirements and distribute heat. A few years later, the Core series was introduced with dual or quad core configurations to compete with the AMD Athlon 64 x2.

Nowadays we see heterogeneous chip designs with big and small cores, chips and other crazy manufacturing techniques that basically rely on the same concept: spreading the thermal load across multiple pieces of silicon. This writer is willing to invest big bucks betting that you'll see consumer desktop PCs with 32 physical cores in less than five years. It might be hard to believe, but a 2013 Intel Haswell i7 came with just four cores compared to the twenty you'll get in an i7 today. Even an ESP32 has two cores with support in FreeRTOS for pinning tasks to different cores. With so many cores, how do you even write software for that? What is the difference between processes and threads? How does this all work in straight vanilla C98?

The theory of multi-threading

Processes, threads, and lightweight threads are the most common types of multiprocessing. Processes have their own memory space and execution context and must coordinate via IPC (inter-process calls), pipes, sockets, FIFO files or explicitly shared memory. Threads are different execution contexts that make up a process but share the same memory space. For this reason, care must be taken to ensure that the various state elements are locked and unlocked to preserve data integrity and ensure correct behavior.

Lightweight threads are user-space threads. For most operating systems, threads and processes are constructs managed by the operating system, with context switching being handled by the kernel. This adds overhead. Some languages ​​implement threads inside the program itself, in user space. These types of lightweight yarns are often referred to as green yarns or fibers.

C98 Vanilla Fork

Because processes and threads are controlled by the operating system, Windows and Unix have different approaches to creating and managing them. For this section we will focus on the Unix/POSIX method.

For processes, there is one function that does all the heavy lifting: fork.

Calling fork will return two values, one for each process. The child will get a zero and the parent will get the new pid of the new child. If there is an error, a negative number will be returned. Parents can call waitpid to wait for the child to complete execution and get the status.

#to understand #to understand #to understand #to understand #to understand int main(int argc, car**argv) { pid_t child = fork(); if (child == -1) return EXIT_FAILURE; if (child) { /* I have a child! */ entire state; waitpid(child, &status,0); return EXIT_SUCCESS; } else { /* I'm the child */ // Other versions of exec pass arguments as arrays // Remember that the first argument is the name of the program // The last argument must be a char pointer to NULL execl("/bin/ls", "ls","-alh", (char *) NULL); // If we get to this line, something went wrong! error("exec failed!"); } }

The child picks up where the parent left off. And although they don't share memory address space, Unix makes a write-once. So they share until one of them changes something, then they get their own copy. Of course, this transparent copy can cause strange problems. For example, if you run:

#include /*fork declared here*/ #include /* printf declared here*/ int main() { whole answer = 24

Fork and Run: The Definitive Guide to Getting Started with Multiprocessing

Since the early 2000s, the CPU industry has shifted from raw clock speed to core count. Pat Gelsinger took the stage in 2002 and gave the talk the industry needed, stating that processors needed specialized silicon or multiple cores to reduce power requirements and distribute heat. A few years later, the Core series was introduced with dual or quad core configurations to compete with the AMD Athlon 64 x2.

Nowadays we see heterogeneous chip designs with big and small cores, chips and other crazy manufacturing techniques that basically rely on the same concept: spreading the thermal load across multiple pieces of silicon. This writer is willing to invest big bucks betting that you'll see consumer desktop PCs with 32 physical cores in less than five years. It might be hard to believe, but a 2013 Intel Haswell i7 came with just four cores compared to the twenty you'll get in an i7 today. Even an ESP32 has two cores with support in FreeRTOS for pinning tasks to different cores. With so many cores, how do you even write software for that? What is the difference between processes and threads? How does this all work in straight vanilla C98?

The theory of multi-threading

Processes, threads, and lightweight threads are the most common types of multiprocessing. Processes have their own memory space and execution context and must coordinate via IPC (inter-process calls), pipes, sockets, FIFO files or explicitly shared memory. Threads are different execution contexts that make up a process but share the same memory space. For this reason, care must be taken to ensure that the various state elements are locked and unlocked to preserve data integrity and ensure correct behavior.

Lightweight threads are user-space threads. For most operating systems, threads and processes are constructs managed by the operating system, with context switching being handled by the kernel. This adds overhead. Some languages ​​implement threads inside the program itself, in user space. These types of lightweight yarns are often referred to as green yarns or fibers.

C98 Vanilla Fork

Because processes and threads are controlled by the operating system, Windows and Unix have different approaches to creating and managing them. For this section we will focus on the Unix/POSIX method.

For processes, there is one function that does all the heavy lifting: fork.

Calling fork will return two values, one for each process. The child will get a zero and the parent will get the new pid of the new child. If there is an error, a negative number will be returned. Parents can call waitpid to wait for the child to complete execution and get the status.

#to understand #to understand #to understand #to understand #to understand int main(int argc, car**argv) { pid_t child = fork(); if (child == -1) return EXIT_FAILURE; if (child) { /* I have a child! */ entire state; waitpid(child, &status,0); return EXIT_SUCCESS; } else { /* I'm the child */ // Other versions of exec pass arguments as arrays // Remember that the first argument is the name of the program // The last argument must be a char pointer to NULL execl("/bin/ls", "ls","-alh", (char *) NULL); // If we get to this line, something went wrong! error("exec failed!"); } }

The child picks up where the parent left off. And although they don't share memory address space, Unix makes a write-once. So they share until one of them changes something, then they get their own copy. Of course, this transparent copy can cause strange problems. For example, if you run:

#include /*fork declared here*/ #include /* printf declared here*/ int main() { whole answer = 24

What's Your Reaction?

like

dislike

love

funny

angry

sad

wow