Threads and PHP

Original author: Julien Pauli
  • Transfer


PHP and threads. A sentence of only four words, and on this topic you can write a book. As usual, I will not do this, but I will give you information so that you begin to understand the subject to a certain extent.


Let's start with the confusion that some programmers have in their heads. PHP is not a multi-threaded language. No threads are used inside PHP itself, and PHP does not allow user code to natively use them as a parallelization mechanism.


PHP is very far from other technologies. For example, in Java, threads are very actively used; they can also be found in user programs. There is no such thing in PHP. And there are reasons for this.


The PHP engine dispenses with threads of execution mainly for the sake of simplifying the structure. After reading the next section, you will learn that threading is not “magic technology to speed up any program.” Looks like the seller’s speech, right? But we are not traders - we are techies, and we know what we are talking about. There are currently no threads in the PHP engine. Perhaps in the future they will appear. But this will entail so many difficulties that the result may be far from expected. The main difficulty is cross-platform multi-threaded programming (thread programming). The second difficulty is shared resources and lock management. Third, threads are not suitable for every program. PHP architecture was born around 2000, and at that time streaming programming was not widespread and immature. Therefore, the authors of PHP (mainly the Zend engine) decided to make a whole engine without threads. And they did not have the necessary resources to create a stable cross-platform multi-threaded engine.


In addition, threads cannot be used in PHP user space. This language does not execute code correctly. PHP concept - "shot and forget." The request should be processed as quickly as possible to free up PHP for the next request. PHP was created as a connecting language: you do not handle complex tasks that require threads. Instead, you turn to fast-and-ready resources, bind everything together and send it back to the user. PHP is an action language, and if something requires processing “more time than usual”, then this should not be done in PHP. Therefore, for asynchronous processing of some difficult tasks, a queue-based system is used (Gearman, AMQP, ActiveMQ, etc.). In Unix, it’s customary to do this: “Develop small, self-contained tools and connect them to each other.” PHP is not designed for active parallelization, This is the lot of other technologies. Every problem is the right tool.


A few words about threads


Let’s refresh what the threads of execution are. We will not go into details, you will find them on the Internet and books.


The thread of execution is the “small" unit of work (light unit of work treatment) within the process. A process can create multiple threads of execution. A thread should be part of only one process. A process is a “large” processing unit within an operating system. On multi-core (multiprocessor) computers, several cores (processors) work in parallel and process part of the load of executable tasks. If processes A and B are ready for queuing and two cores (processors) are ready for operation, then A and B should be sent to processing at the same time. Then the computer efficiently processes several tasks per unit of time (time interval, timeframe). We call it "parallelism."


Process:


image


Thread of execution:


image


Together:


image


A and B used to be processes: fully independent handlers. But threads of execution are not processes. Streams are units living within processes. That is, the process can distribute the work among several smaller tasks performed simultaneously. For example, processes A and B can produce flows A1, A2, B1 and B2. If the computer is equipped with several processors, for example, eight, then all four threads can run in the same time interval (timeframe).


Execution threads are a way of dividing a process’s work into several small subtasks solved in parallel (in one time interval). Moreover, threads are executed in much the same way as processes: the kernel program thread manager (Kernel thread scheduler) manages threads using states.


image


Threads are lighter than processes; they only need a stack and several registers to work. And processes need a lot of things: a new virtual machine frame (VM frame) from the kernel, a bunch of different signal information, information about file descriptors, locks, etc.


The process memory is managed at the hardware level by the kernel and the MMU, and the thread memory is managed at the software level by the programmer and threading libraries.


So remember: execution threads are lighter than processes and easier to manage. With proper use, they also work faster than processes, since the OS kernel almost does not interfere with thread control and dispatching.


The thread memory scheme


Runtime threads have their own stack. Therefore, when accessing variables declared in functions, they get their own copy of this data.


The process heap is shared by threads, as are global variables and file descriptors. This is both an advantage and a disadvantage. If we are only reading from global memory, then we need to do this on time. For example, after stream X and before stream Y. If we write to the global memory, then it is worth making sure that there are no attempts to record several streams there too. Otherwise, this memory area will be in an unpredictable state - the so-called race condition . This is the main problem in streaming programming.


In case of simultaneous access, you need to implement some mechanisms into the code, such as reentrancy or synchronization routine. Re-entry violates concurrency. And synchronization allows you to manage consistency in a predictable way.


Processes do not share memory; they are ideally isolated at the OS level. And threads of execution within one process share a large amount of memory.


Therefore, they need tools to synchronize access to shared memory, such as semaphores and mutexes. The work of these tools is based on the principle of “blocking”: if a resource is locked and a thread tries to access it, then by default the thread will wait for the resource to unlock. Therefore, the threads of execution alone will not make your program faster. Without efficiently distributing tasks across threads and managing shared memory locks, your program will become even slower than using a single process without execution threads. It's just that the threads will constantly wait for each other (and I'm not talking about deadlocks, starvation, etc.).


If you do not have experience in streaming programming, then it will prove to be a difficult task for you. To gain experience with workflows, it will take many hours of practice and solving WTF moments. It is worth forgetting about some little things - and the whole program will go to pieces. It is more difficult to debug a program with threads than without them if we are talking about real projects with hundreds or thousands of threads in one process. You will go crazy and just drown in all this.


Stream programming is a difficult task. To become a master, you need to spend a lot of time and effort.


This thread-sharing scheme is not always convenient. Therefore, a thread local storage (Thread Local Storage, TLS) appeared. TLS can be described as "globals that belong to one flow and are not used by others." These are areas of memory reflecting the global state that are private to a particular thread of execution (as in the case of using processes alone). When creating a thread, part of the process heap — the storage — is allocated. The stream library is requested a key that is associated with this repository. It should be used by the thread of execution every time it accesses its repository. To destroy the allocated resources at the end of the life of the stream, a destructor is required.


A thread safe application, if every call to global resources is under full control and completely predictable. Otherwise, the scheduler will cross the road for you: some tasks will unexpectedly run, and performance will drop.


Stream libraries


Threads need help from the OS kernel. In operating systems, threads appeared in the mid-1990s, so the techniques for working with them were polished.


But there are cross-platform issues. There are especially many differences between Windows and Unix systems. These ecosystems have adopted different streaming execution models and use different streaming libraries.


On Linux, the kernel calls clone () to create a thread or process. But it is incredibly complex, therefore, to facilitate everyday streaming programming, system calls use C code. Libc still does not manage streaming operations (a standard library from C11 demonstrates a similar initiative), external libraries do this. Today, Unix systems typically use pthread (there are other libraries as well). Pthread is short for Posix threads. This POSIX-normalization of the use of threads and their behavior dates back to 1995. If you need threads, enable the libpthread library: pass -lpthread to GCC . It is written in C, its code is open , it has its own version control and version control mechanism.


So, on Unix systems, the pthread library is most often used . It provides concurrency, and concurrency depends on the specific OS and computer.


Consistency is when multiple threads randomly execute on the same processor. Concurrency is when several threads are simultaneously running on different processors.


Consistency:


image


Parallelism:


image


PHP and threading


To start, remember:


  • There are no execution threads in PHP: its engine and code dispense with threads to parallelize internal work.
  • PHP does not offer threads to users: you cannot natively use them in PHP. Joe Watkins ( by Joe Watkins ), one of the PHP developers, has created a good library that adds a thread of execution in user space: the ext / the pthread . But personally, I would not choose PHP for such tasks: it is not intended for this, it is better to take C or Java.

So what about execution threads in PHP?


How PHP handles requests


The thing is how PHP will handle HTTP requests. The web server needs to provide some kind of consistency (or concurrency) to serve multiple clients at the same time. After all, answering one client, you can not put all the others on pause.


Therefore, servers typically use multiple processes or multiple threads to respond to clients.


Historically, processes work under Unix. It’s just the basis of Unix, with its birth there are also processes that can create new processes ( fork()), kill them ( exit()) and synchronize ( wait(), waitpid()). In such an environment, multiple PHPs serve numerous client requests. But everyone works in their own process .


In this situation, PHP can do nothing: the processes are completely isolated. Process A processing the request A about the data of client A will not be able to interact (read or write) with process B processing the request B of client B. We need this.


In 98% of cases, two architectures are used: php-fpm and Apache with mpm_prefork .


Under Windows, everything is more complicated, as in Unix servers with threads.


Windows is a really great OS. It has only one drawback - closed source. But on the net or in books you can find information about the internal structure of many technical resources. Microsoft engineers talk a lot about how Windows works.


Windows has a different approach to consistency and concurrency. This OS uses thread threads very actively. In essence, creating a process on Windows is such a difficult task that it is usually avoided. Instead, threads always and everywhere apply. Streams in Windows are an order of magnitude more powerful than in Linux. Yes exactly.


When PHP runs on Windows, the web server (any) will process client requests in threads, not processes . That is, in such an environment, PHP runs in a thread. And so he should be especially careful about thread specifications: he should be thread safe .


PHP must be thread safe, that is, manage consistency that it did not create, but in which and with which it operates. That is, protect your access to your own global variables. And PHP has a lot of them.


The Zend Thread Safety level (ZTS, Zend Thread Safety ) is responsible for this protection .


Note that all the same is true under Unix, if you decide to use threads to parallelize client request processing. But for Unix-systems this is a very unusual situation, since classical processes are traditionally used here for such tasks. Although no one bothers with thread selection, it can improve performance. Threads are lighter than processes, so a system can execute many more threads. Also, if your PHP extension needs thread safety (like ext / pthread), then you will need thread safe PHP.


ZTS Implementation Details


ZTS is activated using --enable-maintainer-zts . Usually you do not need this switch if you do not run PHP under Windows or run PHP with an extension that requires the engine to be thread safe.


There are a number of ways to check the current mode of operation. CLI php –vwill tell you that NTS (Not Thread Safe) or ZTS (Zend Thread Safe) is now activated.


image


You can also use phpinfo():


image


You can read the constant PHP_ZTSfrom PHP in your code .


if (PHP_ZTS) {
    echo "You are running a thread safe version of the PHP engine";
}

When compiling with ZTS, the entire PHP framework becomes thread safe. But activated extensions may not be thread safe. All official extensions (distributed with PHP) are safe, but you cannot vouch for third-party ones. Below you will see that mastering the thread safety of PHP extensions requires a special use of the API. And, as this constantly happens with threads: one omission - and the whole server may fall apart.


When using execution threads, if you do not call reentrant functions (usually from libc) or blindly access a true global variable, this will lead to strange behavior in all sibling threads . For example, mess with threads in one extension - and this will affect every client served in all threads of the server! A terrible situation: one client can ruin all other client data.


When designing PHP extensions:


  • Extreme caution and good knowledge of streaming programming are required. Otherwise, you will completely unpredictably break the server, and you won’t be able to debug it fast enough.
  • If you make a mistake with the threads, then this will affect all clients served by all threads on the server. You may not even notice this, because erroneous streaming programming usually leads to terrible unpredictable behavior that is not easy to reproduce. *

Reentrant Functions


When designing a PHP extension, use reentrant functions : functions whose operation does not depend on the global state. Although this is too simplistic. In more detail, reentrant functions can be called until their previous call has completed. They are able to work in parallel in two or more threads. If they used the global state, they would not be reentrant. However, they can block their own global state, which means they can be thread safe;) Many of the traditional functions from libc are unrerantable, because they were created when threads were not yet invented.


So some libc (especially glibc) publish reentrant equivalent functions as functions with a suffix _r(). The new C11 standard provides more options for using threads. And the functions from C11 libc are redesigned and got a suffix _s()(for example, localtime_s()).


strtok() => strtok_r(); strerror(), strerror_r(); readdir() => readdir_r() - etc.


PHP itself provides some functions mainly for cross-platform use. Take a look at main / reentrancy.c .


Also do not forget about reentrantness when writing your own C-functions. A function will be reentrant if you can pass it everything you need in the form of arguments (on the stack or through registers) and if it does not use global / static variables or any non-reentrant functions.


Don't get attached to thread-safe libraries


Remember that in streaming programming, the whole process of sharing a memory image is important. This includes linked libraries.


If your extension is tied to a library that is precisely thread safe, then you will have to develop your own ways to ensure thread safety in order to protect the library from accessing global state. In streaming programming and C, this happens often, but is easily overlooked.


Using ZTS


ZTS is a code level that controls access to global streaming variables using TLS (Thread Local Storage) in PHP 7.


When developing the PHP language and its extensions, we have to distinguish between two types of globals in the code.


Есть истинные глобалы (true globals), представляющие собой просто традиционные глобальные переменные С. У них всё в порядке с архитектурой, но поскольку мы не защитили их от согласованности в потоках, то можем только считывать их, когда PHP обрабатывает запросы. Истинные глобалы создаются и записываются до того, как будет создан хотя бы один поток выполнения. По внутренней терминологии PHP этот шаг называется модульной инициализацией (module init). Это хорошо видно на примере расширений:


static int val; /* истинный глобал */
PHP_MINIT(wow_ext) /* модульная инициализация PHP */
{
    if (something()) {
        val = 3; /* запись в истинный глобал */
    }
}

This pseudo code shows what any PHP extension might look like. Extensions have several hooks initialized during the PHP life cycle. MINIT () interceptor refers to PHP initialization. With this procedure, PHP is launched and you can safely read the global variable or write to it, as in the above example.


The second important interceptor is RINIT (), the initialization of the request. This procedure is called for each extension when each new request is processed. That is, RINIT () can be called by the extension a thousand times. At this point, PHP is already flowing . The web server will split the original process into threads, so thread safety is required in RINIT () . This is completely logical in a situation where threads are created to simultaneously process multiple requests. Remember, you are not creating threads . Instead of PHP, the threads are created by the web server.


We also use streaming globals ( thread the globals ). These are global variables whose thread safety is ensured by the ZTS level:


PHP_RINIT(wow_ext) /* инициализация запроса в PHP */
{
    if (something()) {
        WOW_G(val) = 3; /* запись в потоковый глобал */
    }
}

To access the streaming global, we used a macro WOW_G(). Let's see how it works.


The need for macros


Remember: when PHP runs in threads, you need to protect access to all global states related to requests. If there are no threads, then this protection is not needed. After all, each process receives its own memory, which no one else uses.


So the way to access globals related to queries depends on the environment (a multitasking engine is used). Therefore, you need to make sure that access to the globals associated with the queries is performed equally regardless of the environment.


Macros are used for this.


The macro WOW_G()will be processed in different ways, in accordance with the work of the multitasking PHP engine (processes or threads). You can influence this by recompiling your extension. Therefore, PHP extensions are not compatible when switching between ZTS and nonZTS modes. Binary incompatible!


ZTS is not binary compatible with nonZTS. When switching from one mode to another, you need to recompile the exceptions.


When working in a process, a macro is WOW_G()usually processed like this:


#ifdef ZTS
#define WOW_G(v) wow_globals.v
#endif

When working in a stream:


#ifndef ZTS
#define WOW_G(v) wow_globals.v
#else
#define WOW_G(v) (((wow_globals *) (*((void ***) tsrm_get_ls_cache()))[((wow_globals_id)-1)])->v)
#endif

In ZTS mode it’s harder.


When working in the process - Non ZTS (Non Zend Thread Safe) mode - the true global, wow_globals , is used . This variable is a structure containing global variables, and with the help of a macro we refer to each of them. WOW_G(foo)leads to wow_globals.foo. Naturally, you need to declare this variable so that it is reset to zero at startup. This is also done using a macro (in the ZTS mode it is done differently):


ZEND_BEGIN_MODULE_GLOBALS(wow)
    int foo;
ZEND_END_MODULE_GLOBALS(wow)
ZEND_DECLARE_MODULE_GLOBALS(wow)

Then the macro is processed like this:


#define ZEND_BEGIN_MODULE_GLOBALS(module_name) typedef struct _zend_##module_name##_globals {
#define ZEND_END_MODULE_GLOBALS(module_name) } zend_##module_name##_globals;
#define ZEND_DECLARE_MODULE_GLOBALS(module_name) zend_##module_name##_globals module_name##_globals;

And that’s it. When working in the process - nothing complicated.


But when working in a thread - using ZTS - we no longer have true C globals. But the global declarations look the same:


#define ZEND_BEGIN_MODULE_GLOBALS(module_name) typedef struct _zend_##module_name##_globals {
#define ZEND_END_MODULE_GLOBALS(module_name) } zend_##module_name##_globals;
#define WOW_G(v) (((wow_globals *) (*((void ***) tsrm_get_ls_cache()))[((wow_globals_id)-1)])->v)

In ZTS and nonZTS, globals are declared the same way.


But access to them is different. In ZTS, a function is called tsrm_get_ls_cache(). This is a call to the TLS repository, which will return the memory allocated for the current specific thread of execution. Considering that we are first of all casting to void type, this code is not so simple.


TSRM Level


ZTS uses the so-called TSRM level - Thread Safe Resource Manager. This is just a piece of C code, nothing more!


It is thanks to the TSRM level that ZTS can work. For the most part, it is located in the / TSRM folder of the PHP source code.


TSRM is not an ideal level. In general, it was well designed and appeared back in the days of PHP 5 (around 2004). TSRM can work with several low-level stream libraries: Gnu Portable Thread, Posix Threads, State Threads, Win32 Threads and BeThreads. The desired level can be selected during configuration (./configure --with-tsrm-xxxxx).


In analyzing TSRM, we will only discuss pthreads-based implementations.


Download TSRM


When PHP loads during modular initialization, it quickly calls tsrm_startup(). PHP still does not know how many threads to create and how many resources are required to ensure thread safety. It prepares thread tables, each consisting of one element. Tables will grow later, but for now they are distributed using malloc().


This initial stage is also important because here we create the TLS key and TLS mutex, which will need to be synchronized.


static pthread_key_t tls_key;
TSRM_API int tsrm_startup(int expected_threads, int expected_resources, int debug_level, char *debug_filename)
{
    pthread_key_create( &tls_key, 0 ); /* Create the key */
    tsrm_error_file = stderr;
    tsrm_error_set(debug_level, debug_filename);
    tsrm_tls_table_size = expected_threads;
    tsrm_tls_table = (tsrm_tls_entry **) calloc(tsrm_tls_table_size, sizeof(tsrm_tls_entry *));
    if (!tsrm_tls_table) {
        TSRM_ERROR((TSRM_ERROR_LEVEL_ERROR, "Unable to allocate TLS table"));
        return 0;
    }
    id_count=0;
    resource_types_table_size = expected_resources;
    resource_types_table = (tsrm_resource_type *) calloc(resource_types_table_size, sizeof(tsrm_resource_type));
    if (!resource_types_table) {
        TSRM_ERROR((TSRM_ERROR_LEVEL_ERROR, "Unable to allocate resource types table"));
        free(tsrm_tls_table);
        tsrm_tls_table = NULL;
        return 0;
    }
    tsmm_mutex = tsrm_mutex_alloc(); /* распределяем мьютекс */
}
#define MUTEX_T pthread_mutex_t *
TSRM_API MUTEX_T tsrm_mutex_alloc(void)
{
    MUTEX_T mutexp;
    mutexp = (pthread_mutex_t *)malloc(sizeof(pthread_mutex_t));
    pthread_mutex_init(mutexp,NULL);
    return mutexp;
}

TSRM Resources


When the TSRM layer is loaded, you need to add new resources to it . A resource is a memory area containing a set of global variables, usually related to the PHP extension. The resource must belong to the current thread of execution or be protected for access.


This memory area has some size. She will need initialization (constructor) and deinitialization (destructor). Initialization is usually limited to zeroing the memory area, and nothing is done during deinitialization.


The TSRM layer gives the resource a unique ID. Then the caller must save this ID, since it will then be needed to return the protected memory area from TSRM.


TSRM function creating a new resource:


typedef struct {
    size_t size;
    ts_allocate_ctor ctor;
    ts_allocate_dtor dtor;
    int done;
} tsrm_resource_type;
TSRM_API ts_rsrc_id ts_allocate_id(ts_rsrc_id *rsrc_id, size_t size, ts_allocate_ctor ctor, ts_allocate_dtor dtor)
{
    int i;
    tsrm_mutex_lock(tsmm_mutex);
    /* получаем id ресурса */
    *rsrc_id = id_count++;
    /* сохраняем новый тип ресурса в таблице размеров ресурсов */
    if (resource_types_table_size < id_count) {
        resource_types_table = (tsrm_resource_type *) realloc(resource_types_table, sizeof(tsrm_resource_type)*id_count);
        if (!resource_types_table) {
            tsrm_mutex_unlock(tsmm_mutex);
            TSRM_ERROR((TSRM_ERROR_LEVEL_ERROR, "Unable to allocate storage for resource"));
            *rsrc_id = 0;
            return 0;
        }
        resource_types_table_size = id_count;
    }
    resource_types_table[(*rsrc_id)-1].size = size;
    resource_types_table[(*rsrc_id)-1].ctor = ctor;
    resource_types_table[(*rsrc_id)-1].dtor = dtor;
    resource_types_table[(*rsrc_id)-1].done = 0;
    /* увеличиваем массивы для уже активных потоков выполнения */
    for (i=0; icount < id_count) {
                int j;
                p->storage = (void *) realloc(p->storage, sizeof(void *)*id_count);
                for (j=p->count; jstorage[j] = (void *) malloc(resource_types_table[j].size);
                    if (resource_types_table[j].ctor) {
                        resource_types_table[j].ctor(p->storage[j]);
                    }
                }
                p->count = id_count;
            }
            p = p->next;
        }
    }
    tsrm_mutex_unlock(tsmm_mutex);
    return *rsrc_id;
}

As you can see, this function needs a mutually exclusive lock (mutex lock). If it is called in a child thread of execution (and it will be called in each of them), it will block other threads until it finishes manipulating the global thread storage state.


The new resource is added to the dynamic array resource_types_table[]and gets a unique identifier - rsrc_idwhich increments as resources are added.


Running request


Now we are ready to process requests. Remember that each request will be served in its own thread of execution. What happens when a request appears there? At the very beginning of each new request, a function is called ts_resource_ex(). It reads the ID of the current thread of execution and tries to extract the resources allocated for this thread, that is, the memory areas for the globals of the current thread. If resources are not detected (the stream is new), then resources are allocated for the current stream based on the model created when PHP was launched. This is done usingallocate_new_resource()


static void allocate_new_resource(tsrm_tls_entry **thread_resources_ptr, THREAD_T thread_id)
{
    int i;
    TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Creating data structures for thread %x", thread_id));
    (*thread_resources_ptr) = (tsrm_tls_entry *) malloc(sizeof(tsrm_tls_entry));
    (*thread_resources_ptr)->storage = NULL;
    if (id_count > 0) {
        (*thread_resources_ptr)->storage = (void **) malloc(sizeof(void *)*id_count);
    }
    (*thread_resources_ptr)->count = id_count;
    (*thread_resources_ptr)->thread_id = thread_id;
    (*thread_resources_ptr)->next = NULL;
    /* Настройка локального хранилища потока для структуры ресурсов этого нового потока */
    tsrm_tls_set(*thread_resources_ptr);
    if (tsrm_new_thread_begin_handler) {
        tsrm_new_thread_begin_handler(thread_id);
    }
    for (i=0; istorage[i] = NULL;
        } else
        {
            (*thread_resources_ptr)->storage[i] = (void *) malloc(resource_types_table[i].size);
            if (resource_types_table[i].ctor) {
                resource_types_table[i].ctor((*thread_resources_ptr)->storage[i]);
            }
        }
    }
    if (tsrm_new_thread_end_handler) {
        tsrm_new_thread_end_handler(thread_id);
    }
    tsrm_mutex_unlock(tsmm_mutex);
}

Extension Local Storage Cache


Each extension in PHP 7 can declare its cache in local storage. This means that at the start of each new thread of execution, each extension should read the local storage area of ​​its own thread of execution, and not iterate over the list of repositories each time the global access is accessed. There is no magic, for this you need to do a few things.


First you must compile PHP with cache support: at the compilation command line, enter -DZEND_ENABLE_STATIC_TSRMLS_CACHE = 1 . In any case, this should be done by default. Next, when declaring the globals of your extension, use the macro ZEND_TSRMLS_CACHE_DEFINE():


#define ZEND_TSRMLS_CACHE_DEFINE(); __thread void *_tsrm_ls_cache = ((void *)0);


Как видите, объявляется настоящий глобал C, только со специальным объявлением __thread. Это нужно для того, чтобы сказать компилятору, что это будет потоковая переменная (thread specific).


Затем нужно заполнить это хранилище void* данными из хранилища, зарезервированного для ваших глобалов уровнем TSRM. Для этого в конструкторе глобалов можете использовать ZEND_TSRMLS_CACHE_UPDATE():


PHP_GINIT_FUNCTION(my_ext)
{
#ifdef ZTS
    ZEND_TSRMLS_CACHE_UPDATE();
#endif
    /* Continue initialization here */
}
```cpp
Вот макрорасширение (macro expansion):
```#define ZEND_TSRMLS_CACHE_UPDATE() _tsrm_ls_cache = tsrm_get_ls_cache();```
И для реализации pthread:
```#define tsrm_get_ls_cache pthread_getspecific(tls_key)```
Наконец, вы должны лучше понять, как теперь происходит обращение к глобалам в расширениях — с помощью этого макроса:
```cpp
#ifdef ZTS
#define MY_G(v) (((my_globals *) (*((void ***) _tsrm_ls_cache))[((my_globals_id)-1)])->(v))

Как видите, если брать для обращения к глобалам макрос MY_G(), при использовании среды потоков он будет расширяться, чтобы проверить область _tsrm_ls_cache с помощью ID этого расширения: my_globals_id.


image


As we have seen, each extension is a resource and provides some space for its globals. An ID is used to return storage to a specific extension. TSRM will create this repository for the current thread of execution when a new request / thread appears.


Conclusion


Programming execution threads is no easy task. Here I just showed you how PHP works with global management: it isolates every global storage using TLS, created for each new thread of execution when the request is launched, with the engine or the selected level - TSRM. It locks the mutex, creates storage for the globals of the current thread, and then unlocks the mutex. Thus, each extension and each part of PHP can access its own repository without having to block the mutex with every access.


Everything becomes abstract beyond the TSRM level: the C-code level, which makes it easier to manage globals, especially for extension creators. You use a macro to access your global space, and if you work in ZTS, this macro will be converted to specific code to access only your small repository in the middle of each extension. Thanks to the TSRM cache, you don’t need to search every time you access the global: you are provided with a pointer to your specific storage, cache it and take it again when you need to access the global.


Obviously, all this is true for request-bound globals. You can still use real C-globals, but do not try to write to them by applying servinf to the request: you will encounter strange behavior that is difficult to debug, or even destroy the entire web server.


Also popular now: