Applicable occasions and commonly used programming models for muduo library learning 02-thread

Original link: https://blog.csdn.net/zhoucheng05_13/article/details/97612135

1 Thread creation

Create a thread, and print the process id and thread id in the main thread and sub-threads.

Example code

#include "apue.h"
#include<pthread.h>

pthread_t tdno;

//打印进程id、线程id
void printids(const char* s){
    
    
    pid_t pid;
    pthread_t tid;

    pid = getpid();
    tid = pthread_self();
    printf("%s pid:%lu tid:%lu (0x%lx)\n",s,(unsigned long)pid,(unsigned long)tid,(unsigned long)tid);
}


void *thr_fn(void *arg){
    
    
    printids("new thread: ");
    return ((void*)0);
}


int main(void){
    
    
    int err;

    //之所以不直接使用printids,是由于pthread_create对参数的要求
    err = pthread_create(&tdno,NULL,thr_fn,NULL);
    if(err != 0)
        exit(-1);
    printids("main thread: ");
    sleep(1);
    exit(0);
}
123456789101112131415161718192021222324252627282930313233

Test Results

[ ~/unix_practice]$ ./threadCreate 
main thread:  pid:13548 tid:139868882683680 (0x7f35c3114720)
new thread:  pid:13548 tid:139868865541888 (0x7f35c20bb700)
[ ~/unix_practice]$ ./threadCreate 
main thread:  pid:13551 tid:139870974031648 (0x7f363fb8b720)
new thread:  pid:13551 tid:139870956889856 (0x7f363eb32700)
[ ~/unix_practice]$ ./threadCreate 
new thread:  pid:13553 tid:140530159355648 (0x7fcfba3a8700)
main thread:  pid:13553 tid:140530176497440 (0x7fcfbb401720)
123456789

It can be seen from the running results that the running order of the main thread and the new thread is random. They have the same process id and different thread id, but they are in the same memory area.

2 thread termination

2.1 Process termination

If any thread in the process calls exit, _Exit, or _exit, the process will terminate.

2.2 Only thread termination

Without terminating the process, there are 3 ways to terminate the thread:

  1. return. The thread finishes running and returns from the startup routine.
  2. got canceled. Cancelled by other threads in the same process calling the pthread_cancel function.
  3. The pthread_exit function is called. Call the pthread_exit function inside the thread to terminate the thread.

2.3 Get thread termination status

The termination status of the thread can be obtained through the pthread_join function.

int pthread_join(pthread_t thread, void **rval_ptr);
1

When the thread terminates normally, rval_ptr contains the return code. If the thread is cancelled, the memory unit pointed to by rval_ptr is set to PTHREAD_CANCELED.

Note that pthread_join will block the current thread until the waiting thread terminates.

2.4 Thread cancellation

A thread in the same process can call the pthread_cancel function to cancel another thread, and its effect is like using the pthread_exit function with the PTHREAD_CANCELED parameter. pthread_cancel does not wait for the thread to terminate, it just makes a request, and the thread can call the cleanup routine calmly.

2.5 Thread cleaner

A thread can create multiple cleanup procedures, and the cleanup procedures are recorded in the stack, so their execution order is opposite to that of registration. pthread_cleanup_push is used to register the cleanup program, and pthread_cleanup_pop is used to delete the cleanup program. These two functions must be used in pairs (because they can be implemented as macros, the left and right braces must be paired).

The thread cleaner will be called in the following 3 situations:

  1. When pthread_exit is called;
  2. When responding to a cancellation request;
  3. When calling pthread_cleanup_pop with a non-zero execute parameter.

Note that if the thread is terminated by returning from its startup routine, its cleanup routine will not be called.

2.6 Thread detach pthread_detach

The default state of creating a thread is joinable. If a thread finishes running but is not joined, its state is similar to a zombie process, that is, some resources have not been recycled (exit status code). Therefore, the creating thread is obliged to call pthread_join to reclaim child thread resources (similar to waitpid).

But if the new thread does not end after calling pthread_join, the calling thread will be blocked. In some cases we don't want this, such as network services. In this case, the child thread can be set to a detached state (detach), in this state, all resources will be automatically released after the thread runs. There are two ways to set the detached state:

  1. The child thread calls pthread_detach(pthread_self()).
  2. The parent thread calls pthread_detach(thread_id), which is non-blocking and returns immediately.

3 Thread synchronization

3.1 Summary of thread synchronization methods

  1. Mutex: There are only two states of lock/unlock, and only one thread locks at a time.
  2. Read-write lock: There are three states: read lock, write lock, and no lock. The read lock can be occupied by multiple threads.
  3. Condition variable: A variable protected by a mutex, self-calculating conditions, more flexible.
  4. Spin lock: Spin waiting, eliminating scheduling.
  5. Barrier: Multi-threaded parallel synchronization.

3.2 Mutex

The mutex is represented by the pthread_mutex_t data type, which is essentially a lock, allowing only one thread to access at a time.

3.2.1 Mutex operations

Mutex mainly has the following 5 operations:

1pthread_mutex_init(pthread_mutex_t * mutex, const pthread_mutexattr_t *attr);
// 初始化锁变量mutex。
// attr为锁属性,NULL值为默认属性。

2pthread_mutex_lock(pthread_mutex_t *mutex);
// 加锁(阻塞操作)

3pthread_mutex_trylock(pthread_mutex_t *mutex);
// 试图加锁(不阻塞操作)
// 当互斥锁空闲时将占有该锁;否则立即返回
// 但是与2不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待。

4pthread_mutex_unlock(pthread_mutex_t *mutex);
释放锁

5pthread_mutex_destroy(pthread_mutex_t *mutex);
使用完后删除
1234567891011121314151617

3.2.2 Mutex usage examples

Use two threads to print 0~4 independently, and use mutual exclusion for thread synchronization.

#include <unistd.h>
  2 #include <pthread.h>
  3 #include <stdio.h>
  4 
  5 pthread_mutex_t mutex;
  6 void *print_msg(void *arg){
    
    
  7         int i=0;
  8         pthread_mutex_lock(&mutex);
  9         for(i=0;i<5;i++){
    
    
 10                 printf("output : %d\n",i);
 11                 usleep(100);
 12         }
 13         pthread_mutex_unlock(&mutex);
 14 }
 15 int main(int argc,char** argv){
    
    
 16         pthread_t id1;
 17         pthread_t id2;
 18         pthread_mutex_init(&mutex,NULL);
 19         pthread_create(&id1,NULL,print_msg,NULL);
 20         pthread_create(&id2,NULL,print_msg,NULL);
 21         pthread_join(id1,NULL);
 22         pthread_join(id2,NULL);
 23         pthread_mutex_destroy(&mutex);
 24         return 1;
 25 }
12345678910111213141516171819202122232425

Test Results

[~/unix_practice]$ g++ -l pthread -o mutexOpt mutexOpt.cpp 
[ ~/unix_practice]$ ./mutexOpt 
output : 0
output : 1
output : 2
output : 3
output : 4
output : 0
output : 1
output : 2
output : 3
output : 4
123456789101112

3.2.3 Avoid deadlock

The use of mutexes requires special care, otherwise it will easily cause deadlock. For example, if a thread tries to lock the same mutex twice, it will fall into a deadlock state; when the program uses multiple mutexes, it is possible for two threads to acquire the mutex in the reverse order. Will cause deadlock.

3.3 Read-write lock

The read-write lock is represented by the data type pthread_rwlock_t. It can have 3 states, read lock state, write lock state, and no lock state. The read lock can be occupied by multiple threads at the same time, and the read-write lock is very suitable for the situation where the number of reads to the data structure is much greater than the write. (Usually, when the read-write lock is in the read mode locked state, when a thread tries to acquire the lock in write mode, the read-write lock usually blocks subsequent read mode lock requests. This can avoid long-term occupation of the read mode lock, and The pending write mode lock request has not been satisfied.)

3.3.1 Read-write lock operation

#include<pthread.h>

//1. 读写锁初始化方法
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

//2. 读写锁销毁方法
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

//3. 读模式下加锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

//4.写模式下加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

//5. 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

//6. 尝试读锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

//7. 尝试写锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

//8. 带超时的读锁
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict tsptr);

//9. 带超时的写锁
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict tsptr);


//所有函数,成功返回0;否则,返回错误编号
12345678910111213141516171819202122232425262728293031

3.3.2 Application of read-write lock

The read-write lock can be applied in the job queue to control the read and write of the queue.

struct queue{
    
    
    struct job *q_head;
    struct job *q_tail;
    pthread_rwlock_t q_lock;
};
12345

3.4 Condition variables

The condition variable is represented by the pthread_cond_d data type, which provides a meeting place for multiple threads. The condition itself is protected by a mutex. When a condition variable is used with a mutex, it allows threads to wait for a specific condition to occur in a non-competitive manner.

3.4.1 Operation of condition variables

The operations related to condition variables are as follows:

#include<pthread.h>

//1.条件变量初始化方法
//静态分配的条件变量可以通过赋值PTHREAD_COND_INITIALIZER来初始化
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

//2.条件变量销毁方法
int pthread_cond_destroy(pthread_cond_t *cond);

//3.等待条件变量变为真。调用者把锁住的互斥量传给函数,函数自动把调用线程放到等待条件的线程列表上,并对互斥量解锁。该函数返回时,互斥量再次被锁住。
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

//4. 在给定时间内等待条件变量变为真
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict tsptr);

//5. 用于通知线程条件已满足。该函数至少能唤醒一个等待该条件的线程
int pthread_cond_signal(pthread_cond_t *cond);

//6.用于通知线程条件已满足。该函数将唤醒所有等待该条件的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
1234567891011121314151617181920

3.5 Spin lock

The spin lock is represented by the type pthread_spinlock_t, and is in a busy waiting (spin) blocking state until the lock is acquired. The situation where the spin lock is applicable is that the lock is held for a short time, and the thread does not want to spend too much cost on rescheduling.

Spin locks also lead to waste of CPU resources to a certain extent: when the thread is spinning, the CPU cannot do anything else. Many mutexes are implemented very efficiently, and their performance is basically the same as that of using spin locks. Spin locks are only useful in certain situations.

3.5.1 Spin lock operation

//1. 初始化自旋锁
// pshared为PTHREAD_PROCESS_SHARED时,其他进程的线程也可以访问该锁
// pshared为PTHREAD_PROCESS_PRIVATE时,只有本进程中的线程才可以访问该锁
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

//2. 销毁自旋锁
int pthread_spin_destroy(pthread_spinlock_t *lock);

//3. 加锁。如果不能获取锁,则一直自旋
int pthread_spin_lock(pthread_spinlock_t *lock);

//4. 尝试加锁。如果不能获取锁,则立即返回EBUSY错误,不会自旋
int pthread_spin_trylock(pthread_spinlock_t *lock);

//5. 自旋锁解锁
int pthread_spin_unlock(pthread_spinlock_t *lock);
12345678910111213141516

3.6 Barrier

Barrier is a synchronization mechanism for users to coordinate multiple threads to work in parallel, and is represented by the type pthread_barrier_t. It allows each thread to wait until all cooperating threads have reached a certain point, and then continue execution from that point. The pthread_join function is a special barrier that allows one thread to wait until another thread exits.

3.6.1 Operation of the barrier

//1. 屏障初始化。count表示继续运行前到达屏障的线程数目,attr用于配置屏障的属性。
int pthread_barrier_init(pthread_barrier_t *restrict barrier, const pthread_barrierattr_t *restrict attr, nsigned int count);

//2. 销毁屏障
int pthread_barrier_destroy(pthread_barrier_t *barrier);

//3. 表明调用线程已经完成工作,进入休眠状态,等count数量的其他线程赶来
int pthread_barrier_wait(pthread_barrier_t *barrier);
12345678

3.6.2 Barrier application examples

Using multi-thread + barrier to sort millions of data, the efficiency is more than 6 times that of single thread.

4 Thread control

At present, mainstream operating systems, including Linux, have no definite limit on the maximum number of threads that a process can create. But this does not mean that there are no restrictions, just that you cannot use sysconf to find out.

4.1 Thread attributes

The attributes of the thread can be set through the attribute parameter pthread_attr_t, such as the separation state, the lowest address of the thread stack, and the size of the thread stack. For specific information, please refer to Chapter 12, p341, of Advanced Programming in Unix Environment 3rd Edition.

4.2 Synchronization properties

Thread synchronization objects also have attributes, such as mutex attributes, read-write lock attributes, condition variable attributes, and barrier attributes.

  1. Mutex attributes : pthread_mutexattr_t indicates that there are process shared attributes, robust attributes, and type attributes.
  2. Read-write lock attribute : pthread_rwlockattr_t indicates that the only attribute supported by the read-write lock is the process shared attribute.
  3. Condition variable attributes : pthread_condattr_t indicates that condition variables have two attributes: process shared attributes and clock attributes.
  4. Barrier attributes : pthread_barrierattr_t indicates that only the process shares attributes.

4.3 Thread and fork

When a thread calls fork, it creates a copy of the entire process address space for the child process.

POSIX.1 states that between the fork return and the child process calling one of the exec functions, the child process can only call functions that are safe for asynchronous signals.

The reason for this limitation is that the thread in the parent process calls the fork function to generate the child process, and there is only one thread inside the child process (the copy of the fork thread is called). The generated child process has a copy of the entire address space of the parent process (including the locks held by the parent process), but because it does not inherit all threads, the child process has no way to know which locks it holds and which locks need to be released. Therefore, after the parent process calls fork and the child process calls exec to clean up the memory space, the child process is not safe. During this period, only functions that are safe for asynchronous signals can be called.

To clear the lock state, you can create a fork handler by calling the pthread_atfork function (see p367 for function prototypes and examples).

4.4 Threads and I/O

In a multithreaded environment, common read and write operations (such as lseek, read, and write) on a file at the same time may cause unexpected results such as mutual overwriting. This is because threads in the same process share the same file description Caused by the character. To avoid this situation, you can use thread-safe read and write functions, such as pread, pwrite, etc.

The pwrite() function is basically equivalent to write(), but it does not change the file offset (regardless of whether o_append is set or not), so it is thread-safe.

Guess you like

Origin blog.csdn.net/qq_22473333/article/details/113506147