言之者无罪,闻之者足以戒。 ——《诗序》
三、Linux线程的高级控制
1、一次性初始化
有些事需要且只能执行一次(比如互斥量初始化)。通常当初始化应用程序时,可以比较容易地将其放在main函数中。但当你写一个库函数时,就不能在main里面初始化了,你可以用静态初始化,但使用一次初始(pthread_once_t)会比较容易些。
首先要定义一个pthread_once_t变量,这个变量要用宏PTHREAD_ONCE_INIT初始化。然后创建一个与控制变量相关的初始化函数。pthread_once_t once_control = PTHREAD_ONCE_INIT;
void init_routine()
{
//初始化互斥量
//初始化读写锁
......
}
接下来就可以在任何时刻调用pthread_once函数
(1)、pthread_once一次性初始化函数
int pthread_once(pthread_once_t* once_control, void (*init_routine)(void))
第一个参数:once_control(初始化函数调用情况的标志)
第二个参数:初始化执行函数
返回值:成功返回0,失败返回错误码
功能:此函数使用初值为PTHREAD_ONCE_INIT的once_control变量保证init_routine()函数在本进程执行序列中仅执行一次。在多线程编程环境下,尽管pthread_once()调用会出现在多个线程中,init_routine()函数仅执行一次,究竟在哪个线程中执行是不定的,是由内核调度来决定。
Linux Threads使用互斥锁和条件变量保证由pthread_once()指定的函数执行且仅执行一次。实际"一次性函数"的执行状态有三种:
NEVER(0)、IN_PROGRESS(1)、DONE (2),用once_control来表示pthread_once()的执行状态:
1)、如果once_control初值为0,那么 pthread_once从未执行过,init_routine()函数会执行。
2)、如果once_control初值设为1,则由于所有pthread_once()都必须等待其中一个激发"已执行一次"信号, 因此所有pthread_once ()都会陷入永久的等待中,init_routine()就无法执行
3)、如果once_control设为2,则表示pthread_once()函数已执行过一次,从而所有pthread_once()都会立即 返回,init_routine()就没有机会执行
当pthread_once函数成功返回,once_control就会被设置为2
直接给出代码:
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
pthread_t tid;
pthread_once_t once=PTHREAD_ONCE_INIT;
void thread_init()
{
printf("I am in thread 0x%x\n",tid);
}
void *thread_fun1(void *arg)
{
tid = pthread_self();
printf("I am thread 0x%x\n",tid);
printf("once is %d\n",once);
pthread_once(&once,thread_init);
printf("once is %d\n",once);
return NULL;
}
void *thread_fun2(void *arg)
{
sleep(2);
tid = pthread_self();
printf("I am thread 0x%x\n",tid);
// printf("once is %d\n",once);
pthread_once(&once,thread_init);
// printf("once is %d\n",once);
return NULL;
}
int main()
{
pthread_t tid1, tid2;
int err;
err = pthread_create(&tid1, NULL, thread_fun1, NULL);
if(err != 0)
{
printf("create new thread 1 failed\n");
return ;
}
err = pthread_create(&tid2, NULL, thread_fun2, NULL);
if(err != 0)
{
printf("create new thread 1 failed\n");
return ;
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
2、线程的属性
线程的属性用pthread_attr_t类型的结构表示,在创建线程的时候可以不用传入NULL,而是传入一个pthread_attr_t结构,有用户自己来配置线程的属性。pthread_attr_t类型对应用程序是不透明的,也就是说应用程序不需要了解有关属性对象内部结构的任何细节,因而可以增加程序的可移植性。
线程属性:
名称 | 描述 |
detachstate | 线程的分离状态 |
guardsize | 线程栈末尾的警戒区域大小(字节数) |
stackaddr | 线程栈的最低地址 |
stacksize | 线程栈的大小(字节数) |
并不是所有的系统都支持线程的这些属性,因此你需要检查当前系统是否支持你设置的属性。当然还有一些属性不包含在pthread_attr_t结构中,例如:线程的可取消状态、取消类型、并发度
(1)、pthread_attr_init线程属性初始化函数
int pthread_attr_init(pthread_attr_t *attr)
参数:要初始化的属性
返回值:成功返回0 ,失败返回错误码
(2)、pthread_attr_destory线程属性销毁函数
int pthread_attr_destroy(pthread_attr_t *attr)
参数:要初始化的属性
返回值:成功返回0 ,失败返回错误码
如果在调用pthread_attr_init初始化属性的时候分配了内存空间,那么pthread_attr_destroy将释放内存空间。除此之外,pthread_atty_destroy还会用无效的值初始化pthread_attr_t对象,因此如果该属性对象被误用,会导致创建线程失败。
注意:pthread_attr_t结构在使用之前需要初始化,使用完之后需要销毁
什么叫做分离?
分离一个正在运行的线程并不影响它,仅仅是通知当前系统该线程结束时,其所属的资源可以回收。一个没有被分离的线程在终止时会保留它的虚拟内存,包括他们的堆栈和其他系统资源,有时这种线程被称为“僵尸线程”。创建线程时默认是非分离的。如果线程具有分离属性,线程终止时会被立刻回收,回收将释放掉所有在线程终止时未释放的系统资源和进程资源,包括保存线程返回值的内存空间、堆栈、保存寄存器的内存空间等。
(3)、pthread_attr_setdetachstate设置线程的分离属性
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)
第一个参数:要设置的分离状态属性
第二个参数:状态设置(有两种值:PTHREAD_CREATE_DETACHED分离的、PTHREAD_CREATE_JOINABLE 非分离的,可连接的)
返回值:成功返回0 ,失败返回错误码
(4)、pthread_attr_getdetachstate分离状态属性获取函数
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate)
第一个参数:要设置的分离状态属性
第二个参数:已经设置的分离状态属性
返回值:成功返回0 ,失败返回错误码
设置线程分离属性的步骤
1、定义线程属性变量pthread_attr_t attr
2、初始化attr,pthread_attr_init(&attr)
3、设置线程为分离或非分离 pthread_attr_setdetachstate(&attr, detachstate)
4、创建线程pthread_create(&tid, &attr, thread_fun, NULL)
所有的系统都会支持线程的分离状态属性,
下面看一下程序框图:
程序代码:
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
void *thread_fun1(void *arg)
{
printf("I'm new thread 1\n");
return (void *)1;
}
void *thread_fun2(void *arg)
{
printf("I'm new thread 2\n");
return (void *)2;
}
int main()
{
pthread_t tid1, tid2;
int err;
int *flag;
//定义属性变量
pthread_attr_t attr;
//初始化属性
pthread_attr_init(&attr);
//设置分离状态属性,置为已分离
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
//获取分离状态的属性
pthread_attr_getdetachstate(&attr,flag );
printf("flag is %p\n",flag);
err = pthread_create(&tid1, &attr, thread_fun1, NULL);
if(err)
{
printf("create new thread 1 failed\n");
return;
}
err = pthread_create(&tid2, NULL, thread_fun2, NULL);
if(err)
{
printf("create new thread 2 failed\n");
return;
}
//连接线程1
err = pthread_join(tid1, NULL);
if(!err)
printf("join thread 1 success\n");
else
printf("join thread 1 failed\n");
//连接线程2
err = pthread_join(tid2, NULL);
if(!err)
printf("join thread 2 success\n");
else
printf("join thread 2 failed\n");
//销毁attr
pthread_attr_destroy(&attr);
return 0;
}
对于进程来说,虚拟地址空间的大小是固定的,进程中只有一个栈,因此它的大小通常不是问题。但对线程来说,同样的虚拟地址被所有的线程共享。如果应用程序使用了太多的线程,致使线程栈累计超过可用的虚拟地址空间,这个时候就需要减少线程默认的栈大小。另外,如果线程分配了大量的自动变量或者线程的栈帧太深,那么这个时候需要的栈要比默认的大。如果用完了虚拟地址空间,可以使用malloc或者mmap来为其他栈分配空间,并修改栈的位置。
(5)、pthread_attr_setstack修改栈的属性
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize)
第一个参数:栈属性
第二个参数:栈的内存单元最低地址
第三个参数:栈的大小
返回值:成功返回0,失败返回错误码
(6)、phread_attr_getstack获取栈属性
int pthread_attr_getstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize)
第一个参数:栈属性
第二个参数:栈的内存单元最低地址
第三个参数:栈的大小
返回值:成功返回0,失败返回错误码
参数stackaddr是栈的内存单元最低地址,参数stacksize是栈的大小。要注意stackaddr并不一定是栈的开始,对于一些处理器,栈的地址是从高往低的,那么这是stackaddr是栈的结尾。
当然也可以单独获取或者修改栈的大小,而不去修改栈的地址。对于栈大小设置,不能小于PTHREAD_STACK_MIN(需要头文件limit.h)
(7)、pthread_attr_setstacksize设置栈的大小
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize)
第一个参数:栈属性
第二个参数:要设置的栈大小
返回值:成功返回0,失败返回错误码
(8)、pthread_attr_getstacksize获取栈的大小
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize)
第一个参数:栈属性
第二个参数:要获取的栈大小
返回值:成功返回0,失败返回错误码
对于栈大小的设置,在创建线程之后,还可以修改。
对于遵循POSIX标准的系统来说,不一定要支持线程的栈属性,因此你需要检查
1)、在编译阶段使用
_POSIX_THREAD_ATTR_STACKADDR 和 _POSIX_THREAD_ATTR_STACKSIZE符号来检查系统
_POSIX_THREAD_ATTR_STACKSIZE符号来检查系统在/usr/include/bits/posix_opt.h(可以用#ifdef和#endif来实现)
2)、在运行阶段
把_SC_THREAD_ATTR_STACKADD和 _SC_THREAD_THREAD_ATTR_STACKSIZE传递给sysconf函数检查系统对线程栈属性的支持
线程属性guardsize控制着线程栈末尾以后用以避免栈溢出的扩展内存的大小,这个属性默认是PAGESIZE个字节。你可以把它设为0,这样就不会提供警戒缓冲区。同样的,如果你修改了stackaddr,系统会认为你自己要管理栈,警戒缓冲区会无效。
(9)、pthread_attr_setguardsize设置设置guardsize
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize)
第一个参数:栈属性
第二个参数:扩展内存的大小
返回值:成功返回0,失败返回错误码
(10)、pthread_attr_getguardsize获取guardsize
int pthread_attr_getguardsize(pthread_attr_t *attr, size_t *guardsize)
第一个参数:栈属性
第二个参数:扩展内存的大小
返回值:成功返回0,失败返回错误码
下面来看一下程序,熟悉一下上面说的函数:
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
pthread_attr_t attr;
void *thread_fun(void *arg)
{
size_t stacksize;
#ifdef _POSIX_THREAD_ATTR_STACKSIZE
pthread_attr_getstacksize(&attr,&stacksize);
printf("new thread stack size is %d\n",stacksize);
pthread_attr_setstacksize(&attr,17684);
pthread_attr_getstacksize(&attr,&stacksize);
printf("new thread stack size is %d\n",stacksize);
#endif
return (void *)1;
}
int main()
{
pthread_t tid;
int err;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
#ifdef _POSIX_THREAD_ATTR_STACKSIZE
pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN);
#endif
err = pthread_create(&tid, &attr, thread_fun, NULL);
if(err)
{
printf("create new thread failed\n");
return;
}
pthread_join(tid,NULL);
return 0;
}