In the previous chapter, the default attributes of functions are used to assign threads, but pthreads allow us to fine-tune the behavior of threads and synchronized objects by setting different attributes associated with objects. The functions that manage these properties are basically of the same form.
Thread and thread attribute association, mutex and mutex attribute association, an attribute object can represent multiple attributes
There is an initialization function and properties can be set to default values
There is a destructor that destroys attribute objects and recycles resources
Each attribute has a function to get the value of the attribute from the attribute object.
Each property has a function to set the value of the property.
Remember that we used it in the previous chapter
pthread_createWhen a function passes in a null pointer, the function uses the default value to set the thread, but we can also use the null pointer.
pthread_attr_tStructures modify thread default properties.
int pthread_attr_init(pthread_attr_t *attr); int pthread_attr_destroy(pthread_attr_t *attr);
Just like the previous thread constructor destructor, it only produces thread attribute objects.
In the previous section, we talked about the concept of separating threads. If we are not interested in the termination status of the queue threads, we can use separating threads to let the operating system recycle threads. However, if we need to separate threads when creating them, we need to use them
pthread_attr_setdetachstateFunction sets its thread property to
PTHREAD_CREATE_JOINABLEThe two values are separation and waiting.
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate); int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
In addition to POSIX standard, SUS standard has some other attributes. Current Unix systems basically follow this standard, so thread stack is also an important attribute. Use in the compilation phase
_POSIX_THREAD_ATTR_STACKSIZESymbols are used to check whether these two thread stack attributes are supported, and of course, as mentioned earlier, we can also use the sysconf function to run-time check. Generally speaking, both compile-time and run-time checks are necessary because you don’t know if cross-platform compilation will occur.
On the Apple platform, there was no presence.
pthread_attr_setstackFunction, the Apple system splits it into two functions, that is, a total of four functions
int pthread_attr_getstackaddr(const pthread_attr_t *restrict attr, void **restrict stackaddr); int pthread_attr_getstacksize(const pthread_attr_t *restrict attr, size_t *restrict stacksize); int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr); int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
Processes know that there is a stack, and the virtual memory address space of the process is fixed, so the size does not matter at all, but all threads share the address space of the same process, so if the accumulated size of the thread stack exceeds the available space, it will lead to overflow.
If the virtual address space of the thread stack is exhausted, malloc or MMAP can be used to allocate space, and the function above can be used to change the stack location of the new thread.
stackaddrThe parameter specifies the minimum memory address of the stack.
guardsizeControls the extended memory size at the end of the thread stack to avoid stack overflow. The default value of this property is implemented in accordance with Unix. Can handle
guardsizeThread attributes are set to 0, and this characteristic behavior of attributes is not allowed to occur, which will lead to the absence of alert buffer. Similarly, if stackaddr is modified, the system will assume that we will manage the stack ourselves, which will invalidate the alert buffer. But Apple did not provide this function. It should have set a default value by default, or no alarm buffer at all.
In addition to thread attributes, thread synchronization objects also have attributes, such as various locks.
Use of mutex attributes
pthread_mutexattr_tStructures, in the previous chapters, are used
PTHREAD_MUTEX_INITIALIZERConstant or null pointer to mutex attribute structure as parameter call
pthread_mutex_initFunction. The system provides the corresponding interface for initialization.
int pthread_mutexattr_init(pthread_mutexattr_t *attr); int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
These two functions are initialization and anti-initialization functions, which need to pay attention to: process sharing properties, robust properties, and type properties.
In the process, we know that multiple threads can access the same resource, but the process needs to set up to access the same resource.
PTHREAD_PROCESS_PRIVATEOr process sharing mutex properties.
Unix environments actually have a mechanism that allows independent processes to map the same block of memory data into a common address space, and then multiple processes can access the shared data if the process sharing mutex is set to
PTHREAD_PROCESS_SHAREDThe memory block allocation mutex shared by multiple processes can be used for process synchronization.
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *restrict attr, int *restrict pshared); int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);
Mutex robustness attributes are related to mutexes shared between multiple processes. This means that when the address holding the mutex terminates, the mutex state recovery problem needs to be solved.
int pthread_mutexattr_getrobust(const pthread_mutexattr_t *restrict attr, int *restrict robust); int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr, int robust);
There are only two cases of robustness.
PTHREAD_MUTEX_STALLEDThis means that no action will be taken when the process terminates, which may cause other processes waiting for this mutex to be in a waiting state.
PHTREAD_MUTEX_ROBUSTThe mutex is unlocked.
Unfortunately, Apple doesn’t interface and support these two types, so we can only look at them.
Type mutex properties control the locking characteristics of mutexes. The POSIX standard specifies four types:
PTHREAD_MUTEX_NORMALStandard mutex type, no error checking or deadlock checking for it
PTHREAD_MUTEX_ERRORCHECKTypes of error checking are provided
PTHREAD_MUTEX_RECURSIVEThis type allows the same thread to lock the mutex multiple times before the mutex is unlocked, and maintains the count of locks.
PTHREAD_MUTEX_DEFAULTThis mutex type provides default behavior and characteristics. The operating system maps to other mutexes when it implements it
int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type); int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
The above two functions are to set and get functions.
Read-write lock property
Read-write locks are very similar to mutexes, so the functions of attributes are basically the same. Here’s a casual list.
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr); int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
The only support property for read-write locks is process sharing property, which has only two possible values.
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *restrict attr, int *restrict pshared); int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared); PTHREAD_PROCESS_SHARED Any thread of any process that has access to the memory where the read/write lock resides can manipulate the lock. PTHREAD_PROCESS_PRIVATE Only threads created within the same process as the thread that initialized the read/write lock can manipulate the lock. This is the default value.
Conditional variable attributes
Needless to say, let’s start with two initialization anti-initialization functions
int pthread_condattr_init(pthread_condattr_t *attr); int pthread_condattr_destroy(pthread_condattr_t *attr);
Conditional variables have only two attributes: process sharing and clock attributes, but it seems that Apple does not have these two attributes. So let’s not talk about it.
The barrier attributes in the original work are actually not explained under the Apple system, so they are skipped.
Since threads are executed in parallel, calling the same function at the same time can lead to conflicts. If a function can be called safely by multiple threads at the same time, it is said that the function is thread-safe. In Unix systems, if the function is thread-safe, it will be in
<unistd.h>Defined symbols in
_POSIX_THREAD_SAFE_FUNCTIONS, of course, you can also use the sysconf function to get restrictions.
For non-thread-safe functions, the system provides alternative thread-safe functions, which are simply appended after the name.
_rSurface is reentrant function. But reentrant does not mean asynchronous security, because signal processing functions may cause conflicts when invoked.
Thread specified data
It also becomes thread private data. Like literally, threads only allow certain specific data to be queried by themselves. Although the lightweight way of sharing data in thread model is very convenient, it also has many drawbacks, so thread private data is introduced to maintain thread-based data. For example, errno, a process’s errno can’t be shared with all threads, so the implementation of errno was reconstructed later.
But we know that threads can access all the address spaces of a process. Threads theoretically do not have real private thread data, except for registers and other storage provided by the kernel. But there is a mechanism that is more secure.
int pthread_key_create(pthread_key_t *key, void (*destructor)(void *));
Is it very similar to a key-value pair? Yes, that’s the key-value pair. The keys created are stored in the memory unit pointed to by keyp. The keys can be used by all threads, but each thread associates the key with a different thread-specific data address. There is a destructor besides the key. When the thread exits, if the data address is set to a non-null value, the destructor will be called. As we can see, it is an untyped pointer.
Threads usually use malloc to allocate memory for private data. The destructor is to release the allocated memory. If it is not destructed, it will lead to memory leak.
When a thread exits, the destructor for thread-specific data will be called sequentially. When all destructors are finished, the system checks whether there are any non-empty thread-specific data associated with keys, and if so, calls the destructor again. Until all keys are empty. Of course, there are also the largest number of attempts.
int pthread_key_delete(pthread_key_t key);
This function cancels the association of keys with thread private data.
In addition to the above attributes, there are actually two thread attributes: cancelable state and cancelable type, which affect the behavior of the pthread_cancel function.
Cancellable status attribute has two values.
PTHREAD_CANCEL_DISABLEThreads can call
int pthread_setcancelstate(int state, int *oldstate);
Very easy to understand functions that can be used to view the old state or to modify.
In the previous section, the pthread_cancel call does not wait for the thread to terminate, but for a cancel checkpoint to check the state uniformly. The default is when the thread starts.
PTHREAD_CANCEL_ENABLEThat is to say, receive cancel, and do
PTHREAD_CANCEL_DISABLEInstead of killing the thread, the request is blocked and processed uniformly when the state becomes received again and reaches the next checkpoint.
If the checkpoint has not been reached, the cancellation delay may be caused, so a function is provided to generate its own checkpoint.
By default, cancellation is delayed, but can be modified by calling pthread_setcanceltype
int pthread_setcanceltype(int type, int *oldtype);
There are only two types of parameters.
PTHREAD_CANCEL_ASYNCHRONOUSThat’s asynchrony and latency. When asynchronous cancellation occurs, threads can be undone at any time rather than at checkpoints.
Threads and signals
In the previous chapters on signals, signals are process-based, and when threads are introduced, signal processing becomes more complex. Each thread has its own thread shield word, but the signal receiving and processing is unified to the process. When the process registers the signal processing function, the signal processing of all threads will change, but the signal in the process is sent to a single thread, one thread can modify and cancel the signal selection of another thread. Choose,
int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
This is the pthread version of the sigprocmask function, also known as the multithreaded version. They are basically the same, but pthread_sigmask works under the thread.
To simplify signal processing, pthread provides another function
int sigwait(const sigset_t *restrict set, int *restrict sig);
The set parameter specifies the set of signals that the thread is waiting for. When returned, the memory pointed by the sig parameter will contain the number of signals sent.
The advantage of this function is that it simplifies signal processing and synchronizes asynchronous signals through blocking. Similarly, because of the thread’s signal reception, there is a new kill function.
int pthread_kill(pthread_t thread, int sig);
If sig is 0, error checking is performed, but no signal is actually sent.SIG can be specified as 0 to test the existence of threads.
Thread and fork
As we said, when fork, the child process basically inherits all the contents of the parent process, that is, all mutexes, read-write locks and conditional variables. However, due to the existence of multi-threading, after fork, the sub-process must clean up the lock state.
Perhaps your first reaction is whether the parent and child processes will be equally multithreaded. In fact, this is not the case. The child process will only contain copies of the thread whose parent calls fork. Because subprocesses inherit locks, but do not inherit threads that occupy locks, they need to clean locks, but they do not know which ones to clean.
In fact, POSIX.1 stipulates that between fork and the first exec, subprocesses can only call asynchronous signal-safe functions, which limits what subprocesses “do” rather than “how to clean up”.
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
To ensure lock cleanup, the process can register the cleanup function by calling the pthread_atfork function, which is called in the parent process after fork creates the child process and before it returns. This is to unlock all locks. The child function is called in the child process before fork returns. In fact, they unlock the same content, but only in different processes.
Threads and I/O
All threads share the same descriptor under multithreading, so new IO functions are needed, and the simplest way is to atomize them so that IO conflicts do not occur. That’s the pread and pwrite functions. I will not repeat it here.