Linux线程总结
一、线程的基本概念
在Ubuntu中使用pthread库的手册,
需要sudo apt install manpages-posix manpages-posix-dev
1.线程概念
- 概念
线程是允许应用程序并发执行多个任务的一种机制 。
线程是进程的一个执行流,更加 接近一个执行体的概念,是CPU调度和程序执行的最小单位,线程与同属于一个进行的其他线程共享进程所拥有的全部资源,
2.线程和进程的关系
- 线程与进程之间的关系:
- 一个进程可以有多个线程,但一个线程只能归属于一个进程。同一程序的所有线程均会独立执行相同的程序,且共享同一份全局的内存区域(包块初始化数据段,未初始化数据段,以及堆内存段)。
- 进程在创建的时候会自动生成一个主线程。一个进程有且只有一个主线程,其他线程都是该主线程的子线程。主线程退出,其他线程也会随之退出。
- 进程是资源分配的基本单位、是应用程序在内存中的一个实例,虚拟内存和实际内存都是以进程为单位进行分配的。线程大致位于系统的共享库和共享内存区域。
- 进程有独立的地址,一个进程崩溃后,在保护模式下不会对其他的进程产生影响。线程有自己的堆栈和局部变量,但是线程没有独立的地址空间。而一个线程的崩溃会导致进程的崩溃,从而造成一个进程内的其他线程崩溃。
- 线程和进程的优缺点对比
- 进程的优点
- 便于资源的管理和保护,因为进程间是独立的,每个进程都有自己的资源
- 进程的缺点
- 开销大,
- 速度慢,
- 进程间之间有数据需要传递的时候需要使用进程间通信,开销大不方便。
- 线程的优点:
- 开销小,
- 切换速度快,
- 线程之间共享同一进程的地址空间,通信很方便
- 线程的缺点
- 不利于资源的管理和保护
- 进程的优点
二、线程的创建和退出、等待
0.线程相关的基本数据类型
phread API通过以下几个数据类型对线程进程控制
以上数据类型并未有统一的定义,因此处于程序的可移植性考虑, 程序应该避免对这些数据类型产生依赖,尤其时不能使用C语言的==去比较这些变量
1. 线程的创建
编译Pthread API时,需要设置-pthread选项。
#include <pthread.h>
int pthread_create(pthread_t *thread, //线程ID
const pthread_attr_t *attr, //线程的属性
void *(*start_routine)(void *), //线程函数
void *arg); //线程函数的参数
正常执行返回0,异常返回错误代码
代码
关于线程属性的文章
https://blog.csdn.net/whatday/article/details/106470715
void* _func(void* parg){
int* times = (int*) parg;
for(int i = 0;i<*times;++i){
printf("hello,world");
}
}
int t = 10;
//1. 声明线程对象
pthread_t thid;
//2.创建线程
pthread_create(&thid,NULL,_func,&t);
2.线程的退出
#include <pthread.h>
void pthread_exit(void *retval);//线程的返回值
注意
同时使用pthread_exit和pthread_cleanup_push退出时,如果先调用的是pthread_exit(),那么在清理函数中不可以写pthread_exit(),否则会导致死循环。
因为如果没有pthread_cleanup_pop(0),那么pthread_exit()会在退出时自动调用清理函数,从而导致死循环
3.线程的等待
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);//传入需要等待的线程ID以及接收线程函数返回值的二级指针变量
pthread_join正常结束返回0,异常返回错误代码
三、线程的取消和资源清理
1.线程的取消
- 一个线程可以被其他线程杀死(取消) ,收到cancel信号的线程,可能直接结束,也可能忽略,可能运行到取消点(cancel-point),线程再结束(线程默认的行为)
+取消的方式时一个线程向指定线程发送cancel信号
#include <pthread.h>
用于向一个指定的线程发送一个取消请求
int pthread_cancel(pthread_t thread);
设置线程取消类型以及线程取消状态
int pthread_setcancelstate(int state, int *oldstate);//会将调用线程的取消属性设置为参数state所给定的值
int pthread_setcanceltype(int type, int *oldtype);
线程的取消状态:
PTHRAD_CANCEL_DISABLE 设置为线程不可取消
PTHRAD_CANCEL_ENABLE 设置为线程可以取消
PTHRAD_CANCEL_ASYNCHRONOUS 设置线程可以在任何时间点取消
PTHRAD_CANCEL_DEFERED 设置请求保持挂起状态,直到线程到达取消点,默认取消属性。
2.线程的资源清理
- 线程为了访问临界资源而为其加上所,但在访问过程中,该线程被外界取消或发生了中断,则该临界资源将永远处于锁定状态而得不到释放。外界取消操作时不可预见的,因此需要一个机制来简化用于资源释放的编程
#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *),
void *arg);
void pthread_cleanup_pop(int execute);
- void routine(void *arg)函数在调用pthread_cleanup_push()时压入清理函数栈,多次对pthread_cleanup_push()的调用将在清理函数栈中形成一个函数链,在执行该函数链时按照压栈的相反顺序弹出。execute参数表示执行到pthread_cleanup_pop()时是否在弹出清理函数的同时执行该函数,为0表示不执行,非0为执行;这个参数并不影响异常终止时清理函数的执行。
- pthread_cleanup_push()带有一个"{",而pthread_cleanup_pop()带有一个"}",因此这两个函数必须成对出现,且必须位于程序的同一级别的代码段中才能通过编译。
- pthread_cleanup_pop的参数execute如果为非0值,则按栈的顺序注销掉一个原来注册的清理函数,并执行该函数;当pthread_cleanup_pop()函数的参数为0时,仅仅在线程调用pthread_exit函数或者其它线程对本线程调用pthread_cancel函数时,才在弹出“清理函数”的同时执行该“清理函数”。
四、线程同步
线程有两个可以用来同步彼此行为的工具:1.互斥量(mutexe) 2.条件变量(condition variable)
1.互斥量 pthread_mutex_t
用以实现对共享资源的保护
- 创建和销毁锁
#include <pthread.h>
//动态方式
int pthread_mutex_init(pthread_mutex_t *restrict mutex,//锁的指针
const pthread_mutexattr_t *restrict attr);//锁的属性的指针,指定为NULL则使用默认属性
//静态方式
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//销毁锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);//销毁已经上锁的锁会导致异常
-
互斥锁的属性
互斥锁的属性在创建锁的时候指定,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同,也就是是否阻塞等待。有三个值可以选择
-
PTHREAD_MUTEX_TIMED_NP 默认值,普通互斥锁。当一个线程加锁以后,其余请求锁的线程会形成一个阻塞等待队列,并在解锁后按优先级获得锁,这种锁策略保证了资源分配的公平性
-
PTHREAD_MUTEX_RECURESIVE 嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加速线程解锁时重新竞争
-
PTHREAD_MUTEX_ERRORCHECK 检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED类型动作相同。这样就保证当不允许多次加锁时不会出现最简单的情况下的死锁。
pthread_mutex_t mutex; pthread_mutexattr_t mutexattr; pthread_mutexattr_init(&mutexattr); pthread_mutexattr_settype(&mutexattr,PTHREAD_MUTEX_ERRORCHECK);
-
-
锁的操作
锁的操作主要包括
#include <pthread.h> //1.加锁 int pthread_mutex_lock(pthread_mutex_t *mutex); 不论对哪一种类型的锁,都不可能被两个不同的线程同时得到,必须要等待解锁。 对于普通类型,解锁这可以是同进程内的任何线程,而检错锁必须由加锁者解锁才有效,否则返回EPERM 对于嵌套锁,文档和实现要求必须必须由加锁者解锁。在同一进程中,如果加锁后没有解锁,则任何其他线程都无法再获得锁 //2.测试加锁 int pthread_mutex_trylock(pthread_mutex_t *mutex); 与pthread_mutex类似,不同的是如果锁已经被占据时返回EBUSY而不是挂起等待 //3.解锁 int pthread_mutex_unlock(pthread_mutex_t *mutex); 对于快速锁,pthread_mutex_unlock解除锁定 对于递归锁,pthread_mutex_unlock使锁的计数引用减1 对于检错锁,如果锁时当前进程锁定的,则直接解除锁定,否则什么也不做
-
加锁注意事项
- 锁后,解锁前被取消,锁将永远保持锁定状态。
->解决方法:
1.使用pthread_cleanup_push/pop设置清理函数释放已经使用的锁
2.使用RAII类管理锁的声明周期
-
不要在信号处理函数中使用互斥锁,否则容器造成死锁
死锁以及处理机制
https://blog.csdn.net/wchajl/article/details/109021356
2. 条件变量
-
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作,1.一个线程等待条件变量的条件成立而挂起 2.另一个线程是条件成立(给出条件成立信号)。为了防止竞争,条件标量的使用总是和一个互斥锁结合在一期。
-
创建和注销
//动态创建 pthread_cond_t cond; int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); //静态创建 pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //注销 int pthread_cond_destroy(pthread_cond_t *cond); 只有在没有线程在该条件变量上等待的时候才能注销这个条件变量,否则会烦EBUSY。
-
等待和激发
等待的机制:
-
线程解开mutex指向的锁,并被条件变量cond阻塞。
-
计时等待方式表示经历abstime后,即使条件变量不满足,阻塞也被解除。无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()或pthread_cond_timewait()的竞争条件。
-
mutex互斥锁必须是普通锁PTHREAD_MUTEX_TIMED_NP。pthread_cond_wait之前,往往要用pthread_mutex_lock进行加锁,而调用pthread_cond_wait函数会将锁解开,然后将线程挂起阻塞。直到条件被pthread_cond_signal激发,再将锁状态恢复为锁定状态,最后再用pthread_mutex_unlock进行解锁
#include <pthread.h> //计时等待 int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime); //无条件等待 int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); //激活一个等待该跳线的线程,存在多个等待线程时按入队的顺序激活其中的一个 int pthread_cond_signal(pthread_cond_t *cond); //激活所有等待的线程 int pthread_cond_broadcast(pthread_cond_t *cond);
-
-
其他
pthread_cond_wait和pthread_cond_timewait都被实现为取消点,也就是说如果pthread_cond_wait被取消,则退出阻塞,然后将锁状态恢复,然而此时mutex时保持锁定状态的,而当前线程已经被取消掉,那么解锁的操作就会得不到执行,此时锁得不到释放,就会造成死锁,因此需要定义退出回调函数来为其解锁
-
五、线程的属性和线程安全
线程安全
-
如果一个函数能够安全的同时被多个线程调用而得到正确的结果,那么,我们就说这个函数时线程安全的
- 描述的是函数能否同时被多个线程安全的调用,并不要求调用函数的结果具有再现性。
-
线程安全问题产生的原因:大多数时由于对全局变量和静态变量的操作
-
可重入函数:访问时有可能因为重入而造成错乱,像这样的函数被称为不可重入函数。反之,如果一个函数只访问自己的局部变量或参数,则称为可重入函数。简而言之,可重入函数描述的是函数被多次调用但是结果具有可再现性。
- 可重入的概念之和函数访问的变量类型有关,和是否使用锁没有关
- 可重入函数时线程安全函数的一种,其特点在于他们被多个线程调用时,不会引用任何共享数据
- 线程安全是在多个线程情况下引发的,而可重入函数可以在只有一个线程的情况下来说
- 线程安全不一定是可重入的,而可重入的一定的是线程安全的
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
- 线程安全函数能够使不同的线程访问同一块地址空间,而可重入函数要求不同的执行流对数据的操作互不影响使结果是相同的。
-
一次性初始化
在多线程程序中,不管创建了多少线程,有些初始化的动作只能发生一次。
//1.定义用于初始化控制的变量 pthread_once_t once_control = PTHREAD_ONCE_INIT; //2.利用此变量作为标识符,作为参数的函数只能被调用一次 int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));
-
线程特有数据
实现函数形成安全最有效的方式就是使其可重入,应以这种方式来实现所有新的函数库。
- 该函数必须为每个调用者线程分配单独的存储,且只需在线程初次调用此函数时分配一次即可
- 在同一线程对此函数的后续所有调用中,该函数都需要获取初次调用时线程分配的存储块地址。由于函数调用结束时会释放自动变量,故而函数不应利用自动变量存放存储快地址,也不能将指针存放于静态变量中,因为静态变量在进程中只有一个实例
- 不同含的函数各自可能都需要使用线程特有数据。每个函数都需要方法来标识其自的线程特有数据(键),以便与其他函数锁使用的线程特有数据有所区分
- 当线程退出时,函数无法控制将要发生的情况。这时,线程可能会执行该函数意外的代码。不过,一定存在某些机制,在线程退出时会自动释放为该线程锁分配的存储。
-
线程特有数据API概述
要使用线程特有数据,库函数执行的一般步骤如下:
- 函数创建一个键(key),用以将不同函数使用的线程特有数据区分开来。
#include <pthread.h> //只需要在首个调用该函数的线程中创建一次。键在创建时并未分配任何线程特有数据块 //通过key将所指向的缓冲区返回给调用者, int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
-
调用pthread_create_key()还有另一个目的,即允许调用者指定一个自定义的解构函数,用于释放为该键所分配的各个存储块。当使用线程特有数据的线程终止时,Pthread API会自动调用此解构函数,同时将该线程的数据块指针作为参数传入。
//只要线程终止时与key相关联的值不为NULL,Pthread API会自动执行解构函数,并将与Key相关联的值作为参数传入解构函数 void destructor(void* value){ /*release storage pointed to by "value" */ }
-
函数会为每个调用者线程创建线程特有数据块。这一分配通过调用malloc()或类似函数完成,每个线程只分配一次,且只会在线程初次调用此函数时分配。
-
为了保存上一步所分配存储块的地址,函数会使用两个Pthread函数:pthread_setspecific()和pthread_getspecific()。
#include <pthread.h> //对Pthread发出请求,保存指针并记录其与特定键以及特定线程的关联关系 //value指向由调用者分配的一块内存。当线程终止时,会将该指针作为参数传递给与key向对应的解构函数(value也可以是任何可以通过强制转换为void*的标定量。在这种情况下,先前对pthread_key_create()函数的调用应将destructor设置为NULL) int pthread_setspecific(pthread_key_t key, const void *value); //执行的是互补操作,返回之前所保存的,与给定键以及调用线程相关联的指针,如果该没有与其相关联的指针,则返回NULL //函数可利用这一点来判断自身是否是初次为某个线程所调用,若为初次,则必须为该线程分配空间 void *pthread_getspecific(pthread_key_t key);
NPTL的实现方法:
1.全局(进程范围)数组,存放线程特有数据的键信息2.每个线程包含一个数组,存有为每个线程分配的线程特有数据库的指针(通过pthread_setspecific()来存储指针)。
pthread_key_create()返回的pthread_key_t只是对全局数组的索引,标记为pthread_keys。数组中的每个元素都是一个包含两个字段的结构,第一个字段标记该数组元素是否在用(由ptrhead_key_create()调用分配),第二个字段用于存放针对此键,线程特有数据块的结构函数指针(是函数指针destructor的一份拷贝)
-
线程的局部存储
优点:比线程特有数据的使用简单,值需要在全局或静态变量的声明中包含_thread说明符即可
注意点:
- 如果变量声明中使用了关键字static或extern,那么_thread必须紧随其后
- 与一般就的全局或静态变量声明一样,线程局部变量在声明时可设置一个初始值
- 可以使用C语言取地址符&来获取线程局部变量的地址
线程的属性
关于线程属性的文章
https://blog.csdn.net/whatday/article/details/106470715