Yang Sha Zhou
October 2001
This is a column about POSIX thread programming. On the basis of clarifying the concept, the author will detail the POSIX thread library API. This is the third article to introduce thread synchronization.
I. mutex lock
Although the IPC semaphore mechanism can also be used in POSIX thread to implement mutex functions, the semphore function is obviously too powerful, the POSIX thread defines another set of mutex functions for thread synchronization.
1. create and destroy
There are two ways to create mutex lock, static mode and dynamic mode. POSIX defines a macro pthread_mutex_initializer to statically initialize the mutex lock. The method is as follows:
Pthread_mutex_t mutex = pthread_mutex_initializer;
In linuxthreads implementation, pthread_mutex_t is a structure, while pthread_mutex_initializer is a structural constant.
The dynamic mode is to use the pthread_mutex_init () function to initialize the mutex lock. The API definition is as follows:
Int pthread_mutex_init (pthread_mutex_t * mutex, const pthread_mutexattr_t * mutexattr)
Here, mutexattr is used to specify the mutex lock attribute (see below). If it is null, the default attribute is used.
Pthread_mutex_destroy () is used to cancel a mutex lock. The API is defined as follows:
Int pthread_mutex_destroy (pthread_mutex_t * mutex)
Destroying a mutex lock means releasing the resources it occupies and requires the lock to be open. In Linux, mutex locks do not occupy any resources. Therefore, pthread_mutex_destroy () in linuxthreads has no action except checking the lock status (ebusy is returned when the lock status is locked.
2. mutex lock attributes
The mutex lock attribute is specified when the lock is created. There is only one lock type attribute in the linuxthreads implementation. Different lock types are different when trying to lock a locked mutex lock. Currently (glibc2.2.3, linuxthreads0.9) has four values available:
- Pthread_mutex_timed_np, which is the default value, that is, the common lock. When a thread locks, other threads requesting the lock form a waiting queue and obtain the lock by priority after unlocking. This lock policy ensures the fairness of resource allocation.
- Pthread_mutex_recursive_np: Nested lock, which allows the same thread to successfully obtain the same lock multiple times and unlock the lock multiple times. For different thread requests, re-compete when the lock thread is unlocked.
- Pthread_mutex_errorcheck_np: Check the error lock. If the same thread requests the same lock, edeadlk is returned. Otherwise, the action is the same as that of the pthread_mutex_timed_np type. This ensures that no deadlock will occur in the simplest case when many locks are not allowed.
- Pthread_mutex_adaptive_np: the most simple lock type to adapt to the lock. It only waits for the unlock before re-competition.
3. Lock Operation
The lock operation mainly includes locking pthread_mutex_lock (), unlocking pthread_mutex_unlock (), and testing locking pthread_mutex_trylock (). No matter which type of lock, it cannot be obtained by two different threads at the same time, you must wait for unlocking. For common locks and adaptive locks, the locker can be any thread in the same process, and the error locks must be unlocked by the locker; otherwise, the eperm is returned. For nested locks, the document and implementation requirements must be unlocked by the locker, but the experiment results show that there is no such restriction. This difference is not yet explained. If the lock is not unlocked for a thread in the same process, no other thread can obtain the lock again.
int pthread_mutex_lock(pthread_mutex_t *mutex)
int pthread_mutex_unlock(pthread_mutex_t *mutex)
int pthread_mutex_trylock(pthread_mutex_t *mutex)
The semantics of pthread_mutex_trylock () is similar to that of pthread_mutex_lock (). The difference is that ebusy is returned when the lock is occupied rather than waiting.
4. Miscellaneous
The implementation of POSIX thread lock mechanism in Linux is not a cancellation point. Therefore, threads with delayed cancellation do not leave the lock wait because they receive the cancellation signal. It is worth noting that if the thread is canceled before being unlocked after the lock is applied, the lock will always remain locked. Therefore, if there is a cancellation point in the key segment, or, if the asynchronous cancellation type is set, you must unlock it in the exit callback function.
This lock mechanism is not asynchronous signal security at the same time. That is to say, mutex locks should not be used during signal processing; otherwise, deadlock may occur.
Ii. Condition Variables
A condition variable is a mechanism for synchronizing global variables shared by threads. It mainly includes two actions: one thread waits for the condition variable to be established and suspends; another thread sets "condition to true" (a signal indicating condition to true ). To prevent competition, the use of condition variables is always combined with a mutex lock.
1. Create and log out
Like mutex lock, conditional variables have two static and dynamic creation methods. The static method uses the pthread_cond_initializer constant, as shown below:
Pthread_cond_t cond = pthread_cond_initializer
Call the pthread_cond_init () function dynamically. The API is defined as follows:
Int pthread_cond_init (pthread_cond_t * cond, pthread_condattr_t * cond_attr)
Although the POSIX standard defines attributes for condition variables, they are not implemented in linuxthreads. Therefore, the cond_attr value is usually null and ignored.
To cancel a condition variable, you must call pthread_cond_destroy (). This condition variable can be canceled only when no thread is waiting for the condition variable. Otherwise, ebusy is returned. Because the condition variables implemented in Linux do not allocate any resources, the logout action only includes checking whether there are waiting threads. The API is defined as follows:
Int pthread_cond_destroy (pthread_cond_t * Cond)
2. Waiting and stimulating
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime)
There are two waiting conditions: pthread_cond_wait () and pthread_cond_timedwait (). If the waiting time is not met before the given time, etimeout is returned and the waiting time ends, here, abstime appears in the absolute time format of the same meaning as the time () system call. 0 indicates GMT 00:00:00, January 1, January 1, 1970.
Either way, you must work with a mutex lock to prevent multiple threads from simultaneously requesting the Race Condition of pthread_cond_wait () (or pthread_cond_timedwait (), the same below ). Mutex locks must be common locks (pthread_mutex_timed_np) or adaptive locks (pthread_mutex_adaptive_np), and must be locked by this thread before pthread_cond_wait () is called ()), before the update condition is waiting for the queue, mutex remains locked and is unlocked before the thread is suspended and enters the waiting state. Before the condition is met and pthread_cond_wait () is left, mutex is re-locked to match the lock action before pthread_cond_wait.
There are two ways to activate the condition. pthread_cond_signal () activates a thread waiting for the condition. When there are multiple waiting threads, one of them is activated in the queue order; and pthread_cond_broadcast () activate all the waiting threads.
3. Miscellaneous
Pthread_cond_wait () and pthread_cond_timedwait () are both implemented as cancellation points. Therefore, the thread waiting for this point will immediately run again and leave pthread_cond_wait () after re-locking mutex (), then, cancel the operation. That is to say, if pthread_cond_wait () is canceled, mutex remains locked. Therefore, you need to define the exit callback function to unlock it.
The following example demonstrates the combination of mutex lock and condition variable, and the impact of cancellation on the conditional wait action. In this example, two threads are started and wait for the same condition variable. If you do not use the exit callback function (see the comments section in this example), tid2 will be in pthread_mutex_lock () wait permanently. If the callback function is used, the condition wait of tid2 and the condition excitation of the main thread can work normally.
#include <stdio.h>#include <pthread.h>#include <unistd.h>pthread_mutex_t mutex;pthread_cond_t cond;void * child1(void *arg){ pthread_cleanup_push(pthread_mutex_unlock,&mutex); /* comment 1 */ while(1){ printf("thread 1 get running /n"); printf("thread 1 pthread_mutex_lock returns %d/n",pthread_mutex_lock(&mutex)); pthread_cond_wait(&cond,&mutex); printf("thread 1 condition applied/n"); pthread_mutex_unlock(&mutex); sleep(5); } pthread_cleanup_pop(0); /* comment 2 */}void *child2(void *arg){ while(1){ sleep(3); /* comment 3 */ printf("thread 2 get running./n"); printf("thread 2 pthread_mutex_lock returns %d/n",pthread_mutex_lock(&mutex)); pthread_cond_wait(&cond,&mutex); printf("thread 2 condition applied/n"); pthread_mutex_unlock(&mutex); sleep(1); }}int main(void){ int tid1,tid2; printf("hello, condition variable test/n"); pthread_mutex_init(&mutex,NULL); pthread_cond_init(&cond,NULL); pthread_create(&tid1,NULL,child1,NULL); pthread_create(&tid2,NULL,child2,NULL); do{ sleep(2); /* comment 4 */ pthread_cancel(tid1); /* comment 5 */ sleep(2); /* comment 6 */ pthread_cond_signal(&cond); }while(1); sleep(100); pthread_exit(0);}
|
If you do not comment 5 on the pthread_cancel () Action, Child1 and child2 work normally even if there are no sleep () Delayed operations. Note 3 and note 4 delay makes Child1 have time to complete the cancellation action, so that child2 can enter the request Lock operation after Child1 exits. If the callback function definition of comment 1 and comment 2 is not defined, the system will suspend the child2 request lock. If the latency of comment 3 and comment 4 is not included at the same time, child2 can be controlled before Child1 completes the cancellation action, so that the lock application operation can be smoothly executed, but it may be suspended in pthread_cond_wait (), because there are also mutex application operations. The Child1 function provides the usage of standard condition variables: callback function protection, locking before the condition, and unlocking after pthread_cond_wait () is returned.
The Conditional Variable mechanism is not asynchronous signal security, that is, calling pthread_cond_signal () or pthread_cond_broadcast () in the signal processing function may cause a deadlock.
3. Traffic Signals
The main difference between traffic lights and mutex locks and condition variables lies in the concept of "Lights". When lights are on, resources are available, and when lights are off, they are unavailable. If the synchronization mode in the last two phases focuses on "Waiting" operations, that is, the resource is unavailable, the traffic signal mechanism focuses on lighting, that is, to inform the resource availability; it makes no sense to unlock a thread without waiting for it or to stimulate the conditions. The lighting operation of a thread without waiting for it is effective and can be kept on. Of course, such operation primitives also mean more overhead.
In addition to the binary lamp, the application of traffic signals can also use a lamp number greater than 1 to indicate that the number of resources is greater than 1.
1. Create and log out
POSIX signal lights are defined as well-known signal lights and unknown signal lights, but the implementation of linuxthreads only has unknown lights. Besides being well-known lights, they can always be used between multiple processes, there is no major difference between the use of lamps and lamps, so we will only discuss lamps.
Int sem_init (sem_t * SEM, int pshared, unsigned int value)
This is the API for creating traffic signals. value indicates the initial value of the traffic signal. pshared indicates whether to share the traffic signal with multiple processes rather than just one process. Linuxthreads does not implement multi-process shared traffic signals. Therefore, all non-0 pshared inputs will return-1 for sem_init () and set errno to enosys. The initialized signal lights are characterized by SEM variables and used for the following lighting and lamp removal operations.
Int sem_destroy (sem_t * SEM)
The canceled signal SEM requires that no thread is waiting for the signal. Otherwise,-1 is returned and errno is set to ebusy. In addition, threads's signal cancellation function does not perform any other action.
2. Lighting and lighting Removal
int sem_post(sem_t * sem)
The light-on operation adds 1 to the value of the signal lamp, indicating that an accessible resource is added.
int sem_wait(sem_t * sem)
int sem_trywait(sem_t * sem)
Sem_wait () is to wait for the light to shine, wait for the light to shine (the signal light value is greater than 0), then reduce the signal light by 1 and return. Sem_trywait () is a non-blocking version of sem_wait (). If the traffic signal count is greater than 0, 1 is reduced and 0 is returned. Otherwise,-1 is returned immediately, and errno is set to eagain.
3. Get the lamp Value
Int sem_getvalue (sem_t * SEM, int * sval)
Read the lamp count in SEM, save it in * sval, and return 0.
4. Miscellaneous
Sem_wait () is implemented as a cancellation point, and in the architecture that supports atomic "comparison and exchange" commands, sem_post () it is the only POSIX asynchronous signal security API that can be used for asynchronous signal processing functions.
Iv. asynchronous Signal
Because linuxthreads is implemented by a lightweight process outside the kernel, kernel-based Asynchronous signal operations are also effective for threads. But at the same time, because the asynchronous signal is always actually sent to a process, it is impossible to implement the POSIX standard requirement to "signal to a process, then, the process distributes the signal to all threads that do not block the signal. "The primitive affects only one thread.
POSIX asynchronous signal is also a function provided by the Standard C library, mainly including Signal Set Management (sigemptyset (), sigfillset (), sigaddset (), sigdelset (), sigismember (), etc), signal processing function installation (sigaction (), signal blocking control (sigprocmask (), blocked signal query (sigpending (), signal waiting (sigsuspend (), etc, they work with functions such as kill () for sending signals to Implement Asynchronous signal functions between processes. Linuxthreads encapsulates sigaction () He raise () around the thread. This section focuses on the extended asynchronous Signal Functions in linuxthreads, including pthread_sigmask (), pthread_kill (), and sigwait () three functions. Undoubtedly, all POSIX asynchronous signal functions are available to threads.
Int pthread_sigmask (INT how, const sigset_t * newmask, sigset_t * oldmask)
Sets the thread's signal shielding code. The semantics is the same as sigprocmask (), but the cancel signals that are not blocked and the restart signals that are not allowed to respond are protected. Blocked signals are stored in the signal queue and can be taken out by the sigpending () function.
Int pthread_kill (pthread_t thread, int signo)
Sends a signo signal to the thread. After the corresponding process number is located through the thread number, the kill () system call is used to complete sending.
Int sigwait (const sigset_t * Set, int * sig)
Suspends a thread, waits for one of the signals specified in the set to arrive, and stores the signals to * Sig. POSIX standard recommends that all threads in the Process Shield the signal before calling the sigwait () Wait signal to ensure that only the sigwait () caller obtains the signal, for asynchronous signals waiting for synchronization, pthread_sigmask () should always be called to shield the signal before any threads are created. In addition, the signal processing function attached to the signal will not be called when sigwait () is called.
If the cancel signal is received during the waiting period, exit the waiting immediately, that is, the sigwait () is implemented as the canceling point.
V. Other Synchronization Methods
In addition to the synchronization methods discussed above, many other inter-process communication methods are also available for linuxthreads, such as file system-based IPC (pipelines, Unix domain sockets, etc.) and Message Queue (sys. V or POSIX) and System V signal lights. Note that linuxthreads is viewed in the kernel as an independent process of shared storage area, shared file system attributes, shared signal processing, and shared file descriptor.