目录
一、什么是多线程
线程是应用程序并发执行多个任务的机制,一个进程可以包含多个线程,且共享同一个全局内存区域,包括(未)初始化数据段、堆内存。多核处理器机器是可以支持多个线程真正意义上的多个线程并发执行。
二、线程操作
1、线程创建
用户创建的线程数量不能超过RLIMIT_NPROC软资源限制,所有用户创建的总线程数也不能超过/proc/sys/kernel/threads_max内核参数定义的值。
//from /usr/include/bits/pthread.h
int pthread_create(pthread_t* pid,pthread_attr_t* attr,
void*(fun)(void*),void* param);
参数:
pid: 保存新线程的标识符的地址。(typedef unsigned long int pthread_t)
attr: 该线程的属性, 为NULL则表示使用默认属性
func:新线程运行的函数
arg:新线程运行的函数传入的参数,多参数时,可以使用struct结构体传入
返回值:成功返回0, 失败返回错误码
2、线程退出
多线程编程中,线程结束执行的方式有 3 种,分别是:
- 线程将指定函数体中的代码执行完后自行结束
- 线程执行过程中,遇到 pthread_exit() 函数结束执行
- 线程执行过程中,被同一进程中的其它线程(包括主线程)强制终止
第一种就是普通函数的返回,下面先介绍第二种的pthread_exit()函数,并通过与第一种方式进行对比,以加深理解:
//from /usr/include/bits/pthread.h
void pthread_exit(void* status);
参数:
status: status 是 void* 类型的指针,可以指向任何类型的数据,它指向的数据将作为线程退出时的返回值。如果线程不需要返回任何数据,将 status 参数置为 NULL 即可。
注意:status 指针不能指向函数内部的局部数据(比如局部变量),即pthread_exit() 函数不能返回一个指向局部数据的指针,否则很可能使程序运行结果出错甚至崩溃。
代码示例:
#include <stdio.h>
#include <pthread.h>
//线程要执行的函数,arg 用来接收线程传递过来的数据
void *ThreadFun(void *arg)
{
//终止线程的执行,将“wozuishuai”返回
pthread_exit("wozuishuai"); //返回的字符串存储在常量区,并非当前线程的私有资源
printf("*****************");//此语句不会被线程执行
}
int main()
{
int res;
//创建一个空指针
void * thread_result;
//定义一个表示线程的变量
pthread_t myThread;
res = pthread_create(&myThread, NULL, ThreadFun, NULL);
if (res != 0) {
printf("线程创建失败");
return 0;
}
//等待 myThread 线程执行完成,并用 thread_result 指针接收该线程的返回值
res = pthread_join(myThread, &thread_result);
if (res != 0) {
printf("等待线程失败");
}
printf("%s", (char*)thread_result);
return 0;
}
假设程序存储在 thread.c 文件中,执行过程如下:
[root@localhost ~]# gcc thread.c -o thread.exe -lpthread
[root@localhost ~]# ./thread.exewozuishuai
通过执行结果不难看出,myThread 线程并没有执行 ThreadFun() 函数中最后一个 printf() 语句,从侧面验证了 pthread_exit() 函数的功能。
这里注意,如果将上述代码中的
pthread_exit("wozuishuai");
修改为线程函数体中主动结束:
return "wozuishuai";
此时,主线程中的pthread_join依然可以接收到,子线程返回的参数。那pthread_exit() 和 return 的到底有什么区别呢?
在主线程调用pthread_exit() 函数只会终止当前主线程,不会影响进程中其它线程的执行。例如:
#include <stdio.h>
#include <pthread.h>
void *ThreadFun(void *arg)
{
sleep(5);//等待一段时间
printf("wozuishuai\n");
}
int main()
{
int res;
pthread_t myThread;
res = pthread_create(&myThread, NULL, ThreadFun, NULL);
if (res != 0) {
printf("线程创建失败");
return 0;
}
printf("end\n");
return 0;
}
程序输出为:
~/wmm$ ./a.out
end
可以看出此时myThread 线程并没有任何打印输出,原因很简单,主线程执行速度很快,主线程最后执行的 return 语句不仅会终止主线程执行,还会终止其它子线程执行。也就是说,myThread 线程还没有执行输出语句就被终止了。
但是如果此时我们使用pthread_exit(NULL);替换main()函数中的return 0;时,myThread 线程将会有打印输出:
~/wmm$ ./a.out
end
wozuishuai
此外,pthread_exit() 可以自动调用线程清理程序(本质是一个由 pthread_cleanup_push() 指定的自定义函数),return 则不具备这个能力。
下面介绍第三种线程结束方式。多线程程序中,一个线程可以借助 pthread_cancel() 函数向另一个线程发送“终止执行”的信号(后续称“Cancel”信号),从而令目标线程结束执行。
//from /usr/include/bits/pthread.h
int pthread_cancel(pthread_t thread);
参数:thread:指定发送 Cancel 信号的目标线程。
返回值:成功返回数字 0;失败返回非零值
注意,pthread_cancel() 函数的功能仅仅是向目标线程发送 Cancel 信号,至于目标线程是否处理该信号以及何时结束执行,由目标线程决定。当目标线程因为 Cancel 信号而结束时,将返回PTHREAD_CANCELED宏。例如:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h> // sleep() 函数
//线程执行的函数
void * thread_Fun(void * arg) {
printf("新建线程开始执行\n");
sleep(10);
}
int main()
{
pthread_t myThread;
void * mess;
int value;
int res;
//创建 myThread 线程
res = pthread_create(&myThread, NULL, thread_Fun, NULL);
if (res != 0) {
printf("线程创建失败\n");
return 0;
}
sleep(1);
//向 myThread 线程发送 Cancel 信号
res = pthread_cancel(myThread);
if (res != 0) {
printf("终止 myThread 线程失败\n");
return 0;
}
//获取已终止线程的返回值
res = pthread_join(myThread, &mess);
if (res != 0) {
printf("等待线程失败\n");
return 0;
}
//如果线程被强制终止,其返回值为 PTHREAD_CANCELED
if (mess == PTHREAD_CANCELED) {
printf("myThread 线程被强制终止\n");
}
else {
printf("error\n");
}
return 0;
}
程序输出为:
~/wmm$ ./a.out
新建线程开始执行
myThread 线程被强制终止
3、设置线程状态是否可被取消
用以设置本线程对Cancel信号的反应。
int pthread_setcancelstate(int state,int* oldstate);
参数:
state: 为线程设置的新状态值,有以下两个选项:
- PTHREAD_CANCEL_DISABLE:线程的取消请求将处于未决状态,即现在不处理,等线程状态改变后,才会被取消。
- PTHREAD_CANCEL_ENABLE:创建线程的默认状态,线程的取消请求将被立即接受。
oldstate:用来存储老的状态值,以便后续恢复使用
返回值:成功返回0,否则返回-1
4、设置线程被取消时的处理函数
void pthread_cleanup_push((void*)(func)(void*),void* arg);
void pthread_cleanup_pop(int flag);
参数:
func: 线程被取消时,执行线程清理函数
arg:线程清理函数的参数
flag:线程清理函数执行标志位:
- 当flag != 0时,线程清理函数一定会执行,因为这是从栈中拿数据,所以处理函数的顺序会与pthread_cleanup_push注册的函数的顺序相反.
- 当flag == 0时,如果在pthread_cleanup_push和pthread_cleanup_pop之间没有调用pthread_exit()或收到Cancel信号,这时候pop函数仅仅是将函数指针从栈中取出来,而不会去执行
需要注意的是,在线程中两个函数必须成对出现。
当线程执行以下动作时,清理函数rtn是由pthread_cleanup_push函数调度的:
- 线程内主动调用pthread_exit()函数退出线程
- 收到外部发送的Cancel信号
- 用非零flag参数调用pthread_cleanup_pop时
例如:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <pthread.h>
#include <fcntl.h>
#include <unistd.h>
// 线程清理函数
void cleanup(void *arg) {
printf("cleanup: %s\n",(char *)arg);
}
void *thr_fn(void *arg) {
printf("thread 1 start\n");
// 将线程清理函数push入栈
pthread_cleanup_push(cleanup,"thread 1 first handlter");
pthread_cleanup_push(cleanup,"thread 2 second handlter");
printf("thread 1 begin sleep\n");
// 在此收到Cancel信号,执行清理函数后,直接退出
sleep(5);
printf("thread 1 end sleep\n");
// 参数为0,仅仅将函数弹出,并不执行
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
return ((void *)1);
}
int main() {
int err;
pthread_t tid1;
void *tret;
err = pthread_create(&tid1,NULL,thr_fn,NULL);
if(err != 0) {
printf("creat error:%s\n",strerror(err));
}
sleep(1);
pthread_cancel(tid1);
err = pthread_join(tid1,&tret);
if(err != 0) {
printf("get end error:%s\n",strerror(err));
}
printf("the thread code %ld\n",(long)tret);
return 0;
}
程序输出为:
~/wmm$ ./a.out
thread 1 start
thread 1 begin sleep
cleanup: thread 2 second handlter
cleanup: thread 1 first handlter
the thread code -1
5、设置线程收到Cancel信号的响应类型
int pthread_setcanceltype(int state,int* oldstate);
参数:
state: 设置线程Cancel相应的新类型,有三种选项,默认为PTHREAD_CANCEL_DEFERRED:
- PTHREAD_CANCEL_ASYNCHRONOUS(异步取消):一个线程可以在任何时刻被取消。
- PTHREAD_CANCEL_DEFERRED(同步取消):取消的请求被放在队列中,直到线程到达下个取消点,才会被取消。(取消点:很多系统函数都是取消点,详细的列表可以在Linux上用man 7 pthreads查看,另外pthread_testcancel()也是取消点)。
- PTHREAD_CANCEL_DISABLE(不可取消):取消的请求被忽略。
oldstate:原先的老类型,用以恢复使用
arg:线程清理函数的参数
返回值:成功返回0,失败返回非0值.
6、设置线程分离状态
我们知道如果一个线程结束运行但没有被主线程pthread_join(),则它的一部分资源将可能永远不会被回收,例如退出状态码。但如果我们设置子线程的状态设置为detached,则表示该线程运行结束后,会自动释放所有资源,不再需要主线程pthread_join(),而且如果此时我们仍然在主线程中调用pthread_join()函数等待子线程,将会报错。
int pthread_detach(pthread_t pid);
参数:
pid:分离的线程pid号
返回值:成功返回0,失败返回非0值.
7、等待线程结束
在很多情况下,在主线程创建并启动子线程,如果子线程里要进行大量的耗时的运算,此时主线程可能会先于子线程结束,这样将导致子线程的部分资源没有及时释放,或者如果主线程中事务依赖于子线程的处理结果,这个时候就要用到pthread_join()方法了。
pthread_join()的作用可以这样理解:主线程等待子线程的终止。也就是在子线程调用了pthread_join()方法后面的代码,只有等到子线程结束了才能执行。
注意,pthread_join()的线程必须是未分离的,即没有调用过pthread_detach()函数
int pthread_join(pthread_t pid,void** ret);
参数:
pid:等待的线程ID
ret:等待的线程返回的状态地址。
返回值:成功返回0,否则阻塞。
8、判断线程ID是否相等
比较两个线程标识符是否是同一个线程。
#include <pthread.h>
int pthread_equal(pthread_t t1 , pthread_t t2 );
参数:
t1:待比较多线程标识符
t2:待比较多线程标识符
返回值:
如果两个线程 ID 相等则返回非零值;否则返回 0
9、获取当前线程ID
获取该函数调用者所属线程ID。
#include <pthread.h>
pthread_t pthread_self(void);
返回值:
总是成功,返回调用线程的ID
10、发送信号给线程
发送特定信号给指定线程
int pthread_kill(pthread_t thread, int sig);
参数:
thread:发送信号的目标线程
sig:发送的信号,为0则不发送信号值
返回值:
成功返回0,失败返回非0值
11、线程私有数据
我们知道,在多线程编程中,所有线程共享程序中的全局变量,所有线程都可以使用它,改变它的值,但现在如果每个线程希望能单独拥有它,即该变量在不同的线程中,有不同的副本,此时就需要线程私有数据了。表面上看起来这是一个全局变量,所有线程都可以使用它,而它的值在每一个线程中又是单独存储的。
具体做法如下:
-
创建一个类型为
pthread_key_t
类型的变量。 -
调用
pthread_key_create()
来创建该变量。该函数有两个参数,第一个参数就是上面声明的pthread_key_t
变量,第二个参数是一个清理函数,用来在线程释放该线程存储的时候被调用。该函数指针可以设成NULL
,这样系统将调用默认的清理函数。该函数成功返回0.其他任何返回值都表示出现了错误。 -
当某线程中需要存储特殊值到该变量时,调用
pthread_setspcific()
。该函数有两个参数,第一个为前面声明的pthread_key_t
变量,第二个为void*
变量,这样你可以存储任何类型的值。 -
如果需要取出所存储的值,调用
pthread_getspecific()
。该函数的参数为前面提到的pthread_key_t
变量,该函数返回void *
类型的值。
#include <pthread.h>
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
void *pthread_getspecific(pthread_key_t key);
int pthread_setspecific(pthread_key_t key, const void *value);
参数:
key:pthread_key_t类型变量
destructor:一个清理函数,用来在线程释放该线程存储的时候被调用。该函数指针可以为
NULL
,这样系统将调用默认的清理函数value:设置的value值
返回值:
成功返回0,失败返回非0值
示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
pthread_key_t key; // 好吧,这个玩意究竟是干什么的呢?
struct test_struct { // 用于测试的结构
int i;
float k;
};
void *child1(void *arg)
{
struct test_struct struct_data; // 首先构建一个新的结构
struct_data.i = 10;
struct_data.k = 3.1415;
pthread_setspecific(key, &struct_data); // 设置对应的东西吗?
printf("child1--address of struct_data is --> 0x%p\n", &(struct_data));
printf("child1--from pthread_getspecific(key) get the pointer and it points to --> 0x%p\n", (struct test_struct *)pthread_getspecific(key));
printf("child1--from pthread_getspecific(key) get the pointer and print it's content:\nstruct_data.i:%d\nstruct_data.k: %f\n",
((struct test_struct *)pthread_getspecific(key))->i, ((struct test_struct *)pthread_getspecific(key))->k);
printf("------------------------------------------------------\n");
}
void *child2(void *arg)
{
int temp = 20;
sleep(2);
printf("child2--temp's address is 0x%p\n", &temp);
pthread_setspecific(key, &temp); // 好吧,原来这个函数这么简单
printf("child2--from pthread_getspecific(key) get the pointer and it points to --> 0x%p\n", (int *)pthread_getspecific(key));
printf("child2--from pthread_getspecific(key) get the pointer and print it's content --> temp:%d\n", *((int *)pthread_getspecific(key)));
}
int main(void)
{
pthread_t tid1, tid2;
pthread_key_create(&key, NULL); // 这里是构建一个pthread_key_t类型,确实是相当于一个key
pthread_create(&tid1, NULL, child1, NULL);
pthread_create(&tid2, NULL, child2, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_key_delete(key);
return (0);
}
输出为:
child1--address of struct_data is --> 0x0x7ffff77eff40 child1--from pthread_getspecific(key) get the pointer and it points to --> 0x0x7ffff77eff40 child1--from pthread_getspecific(key) get the pointer and print it's content: struct_data.i:10 struct_data.k: 3.141500 ------------------------------------------------------ child2--temp's address is 0x0x7ffff6feef44 child2--from pthread_getspecific(key) get the pointer and it points to --> 0x0x7ffff6feef44 child2--from pthread_getspecific(key) get the pointer and print it's content --> temp:20
三、线程属性
在创建线程时,我们可以直接指定其线程属性,也可以后续通过特定函数修改其某个属性值。线程属性的结构体声明如下:
typedef struct _pthread_attr_s
{
int __detachstate;
int __schedpolicy;
struct __sched_param __schedparm;
int __interitsched;
int __scope;
size_t __guardsize;
int __stackaddr_set;
void* __stackaddr;
size_t __stacksize;
}pthread_attr_t;
各参数含义如下:
- __detachstate:设置该线程是处于分离状态还是可连接状态,即是否可被等待,默认值为PTHREAD_CREATE_JOINABLE,即默认可被等待。
- __schedpolicy:线程使用的调度策略,例如FIFO,时间片轮转法以及优先级策略等,默认使用优先级策略
- __schedparm:调度策略的参数,例如当线程为优先级策略时,该值表示当前线程的优先级值默认为0
- __interitsched:设置该线程的属性值是继承父线程属性还是从属性对象中获取,默认从不继承
- __guardsize:创建的线程守护区大小,我也不知道有什么用
- __stackaddr:创建线程使用的栈空间起始地址,默认为NULL,即由系统分配
- __stacksize:创建线程的栈堆空间总大小,默认为0,即系统自行分配
1、初始化/销毁线程属性对象
/*初始化线程属性对象*/
int pthread_attr_init(pthread_attr_t* attr);
/*销毁线程属性对象*/
int pthread_attr_destory(pthread_attr_t* attr);
参数说明:
attr:指向一个线程属性结构的指针,调用pthread_attr_init之后,pthread_attr_t结构所包含的内容就是操作系统实现支持的线程所有属性的默认值。默认属性为非绑定、非分离、默认1MB堆栈、与父进程有相同优先级。
返回值:成功返回0,失败返回非0值
2、获取/设置线程的__detachstate属性
获取/修改线程的分离状态属性.
/*获取线程的__detachstate属性*/
int pthread_attr_getdetachstate(pthread_attr_t* attr,int* detachstate);
/*设置线程的__detachstate属性*/
int pthread_attr_setdetachstate(pthread_attr_t* attr,int detachstate);
参数说明:
attr: 线程属性变量
detachstate: 线程的分离状态属性
detachstate的选项有2个:
- PTHREAD_CREATE_DETACHED:使得使用属性对象attr所创建的所有线程处于处于分离状态,即在线程终止时,其资源由系统自行回收,且不能被其他线程等待。
- PTHREAD_CREATE_JOINABLE:使得使用属性对象attr所创建的所有线程处于处于连接状态,即在线程终止时,其资源不会被系统主动回收,必须调用pthread_join以回收资源。
返回值:成功返回0,失败返回非0值
3、获取/设置线程的__interitsched属性
获得/设置线程的继承性。继承性决定调度的参数是从创建的进程中继承还是使用在schedpolicy和schedparam属性中显式设置的调度信息。
int pthread_attr_getinheritsched(const pthread_attr_t*attr,int *inheritsched);
int pthread_attr_setinheritsched(pthread_attr_t *attr,int inheritsched);
参数说明:
attr: 线程属性变量
inheritsched: 线程的继承性,inheritsched的选项有2个:
- PTHREAD_INHERIT_SCHED:表示新线程将继承创建线程的调度策略和参数
- PTHREAD_EXPLICIT_SCHED:表示使用在schedpolicy和schedparam属性中显式设置的调度策略和参数
返回值:成功返回0,失败返回非0值
4、获取/设置线程的__schedpolicy属性
设置和得到线程的调度策略。
int pthread_attr_getschedpolicy(const pthread_attr_t*attr,int *policy);
int pthread_attr_setschedpolicy(pthread_attr_t *attr,int policy);
参数说明:
attr: 线程属性变量
policy: 线程的调度策略,policy的选项有2个:
- 先进先出(SCHED_FIFO):在SCHED_FIFO调度策略下,当有一个线程准备好时,除非有平等或更高优先级的线程已经在运行,否则它会很快开始执行。如果一个低优先级的SCHED_FIFO线程和一个高优先织的SCHED_FIFO线程都在等待相同的互斥锁,则当互斥锁被解锁时,高优先级线程将总是被首先解除阻塞。
- 轮转法(SCHED_RR):SCHED_RR(轮循)策略下,如果有一个SCHED_RR策略的线程执行了超过一个固定的时期(时间片间隔)没有阻塞,而另外的SCHED_RR或SCHBD_FIPO策略的相同优先级的线程准备好时,运行的线程将被抢占。
- 其它(SCHED_OTHER):
返回值:成功返回0,失败返回非0值
5、获取/设置线程的__schedparm属性
设置和得到线程的调度参数。
int pthread_attr_getschedparam(const pthread_attr_t*attr,struct sched_param *param);
int pthread_attr_setschedparam(pthread_attr_t *attr,const struct sched_param *param);
参数说明:
attr: 线程属性变量
param: 线程的调度参数,结构sched_param的子成员sched_priority控制一个优先权值,大的优先权值对应高的优先权。系统支持的最大和最小优先权值可以用sched_get_priority_max函数和sched_get_priority_min函数分别得到:
// include /usr/include/bits/sched.h struct sched_param { intsched_priority; };
返回值:成功返回0,失败返回非0值
注意:如果不是编写实时程序,不建议修改线程的优先级。因为,调度策略是一件非常复杂的事情,如果不正确使用会导致程序错误,从而导致死锁等问题。如:在多线程应用程序中为线程设置不同的优先级别,有可能因为共享资源而导致优先级倒置。
6、设置/获取线程的__stacksize属性
设置/获取线程属性对象中的栈大小属性 。
#include <pthread.h>
int pthread_attr_setstacksize(pthread_attr_t * attr , size_t stacksize );
int pthread_attr_getstacksize(const pthread_attr_t *attr,size_t * stacksize );
参数说明:
attr: 线程属性变量
stacksize: 用以存储获取到线程的栈大小或设置的栈大小
返回值:
成功返回0,失败返回非0值
7、设置/获取线程的栈信息属性
设置/获取线程的栈信息属性
#include <pthread.h>
int pthread_attr_setstack(pthread_attr_t* attr,void*stackaddr,size_t stacksize );
int pthread_attr_getstack(const pthread_attr_t*attr,void **stackaddr,size_t *stacksize );
参数说明:
attr: 线程属性变量
stackaddr: 栈的首地址
stacksize: 用以存储获取到线程的栈大小或设置的栈大小
返回值:
成功返回0,失败返回非0值