Why can't *NIX PHP run in a threaded environment?

Back to Tech index

First, a generalized processor operating guide to set the context.
Intel (and AMD) processor models, like many other designs, use a segmented approach to system and processor RAM. A segment is, generally speaking, a block of RAM bounded by a segment register on the low address end and an address limit set when the program is loaded or granted a block of RAM on the other. A segment register (variable inside the processor) is set to an address, usually on a WORD (16 bit) PARAGRAPH (32 bit) or PAGE (64 bit) boundary. The segment register is used in calculating the exact location in memory for specific processor instructions. This guarantees that an errant data move won't overwrite the contents of an executing program in memory, making debugging much easier.

For example, when a program is running the processor uses the "IP" (Instruction Pointer) register in conjunction with the CS segment register to determine the exact location of the currently executing instruction. The IP is then incremented by the length of the instruction so it points to the next instruction, a fetch occurs and the next instruction is loaded into the execution cue. Since each executing program has a unique CS register, program code is generally well defined and protected. The processor will, by default, only use the CS segment register to determine the address of the IP register unless you use an override form of an instruction to execute code in another segment. Some processors do not have IP override prefixes. Segment registers DS, SS, ES and others operate similarly on data and stack blocks.

Of particular interest is the SS (Stack Segment) register used to point to a "pile" or "stack" in RAM. Since the processor itself only has a small amount of internal RAM at it's disposal, programs need a temporary place to keep things so the processor can be used for other things. The program Stack or Pile is just such a place. It's a convenient way to temporarily store the contents of the processor registers or other data while the processor is being used for something else. It's used mostly to keep return addresses during function calls or processor interrupt operations. It can also be used to pass data to functions, store error conditions, build an array of values in ram etc. The processor offers a special purpose register called SP that is always an offset into the Stack Segment, unless you use an override form of an instruction. Typically a program never alters it's segment registers unless there are good reasons for doing so.

Monolithic exectution
In an un-threaded (monolithic) processor context (such as PHP) each process is loaded as an initial code block plus a local data segment and a standard sized stack. It then allocates memory via system calls to increase or decrease it's data, stack and extra segments. Thus with Apache-mpm-prefork the code for apache is loaded as many times as there are processes running. If you need 50 processes to service 50 requests, you have 50 copies of apache2 in ram, plus data and stack segments for each process.

Threaded execution
In a threaded context the code segment is loaded only once per binary and the process then allocates and de-allocates data and a stack for each of it's threads. As long as the code is "re-entrant", meaning it does not store any data in the code segment and it maintains a separate stack for each thread, the same code can be called at any acceptable entry point without clobbering data from subsequent threads. Re-entrant code generally requires some forethought when defining data structures and much testing to assure stack and data segment pointers don't overrun their boundaries. Most modern compilers will generate re-entrant code and generalized warnings of possible segment overrun conditions (segfault or access violation).

The Skinny on Apache MPM worker
So, the benefit of Apache2-mpm-worker is that it does not duplicate code in RAM no matter how many threads it spawns or how many requests it services. The drawback is that threaded execution requires some additional overhead allocating and de-allocating RAM (Stack, Data segments and local Data) for each thread. But, it can also recycle threads faster than reloading and reallocating an entire MPM process making it much leaner and even a bit faster under some circumstances. My experience is that MPM-Worker can handle a heavy load much easier than MPM-Prefork.

Finally, PHP and threaded MPM-worker
The issue with PHP is that many older or contributed php modules that are included in current repositories don't contain re-entrant code so even if the binary was compiled as "thread safe" there is no guarantee it really will be once it loads all it's external modules. This means you run the risk of one PHP module tromping all over data from another copy of PHP if you are using a monolithic module. The condition usually causes a segfault (memory access violation) because one thread tried to access data that belongs to another thread causing a stack or data segment overrun or segfault (attempt to access memory not allocated to your segments). Generally speaking if you can successfully compile all your PHP modules into the PHP binary rather than loading them externally, you should be able to run the resulting libapache2-mod-php with apache2-mpm-worker successfully.

My tests indicate occasional segfaults in the GD module which hang the thread. Eventually it will clear but most scripts aren't awaren enough to recover from such an error so thumbnails disappear or are not correctly generated, among other issues.

PHP, Apache and Windows
While generally speaking, most binaries compiled under windows are "thread safe", some are not. The best way to be sure is to compile all your modules into the PHP binary. If there are non re-entrant code blocks, the compiler will toss a warning or an error.

PHP as CGI or FCGI gets around this issue by running multiple copies of PHP and other CGI binaries as monolithic processes. However, FCGID is an apache module that passes data to a monolithic copy of PHP (php-cgi). The process of passing the data/url in and of itself changes the way apache communicates with PHP and in many circumstances causes communication issues. I've not had good luck running PHP as a CGI.

That's all folks...