- 线程的属性
POSIX标准规定线程具有多个属性,那么具体有哪些属性呢? 线程的主要属性包括分离状态(Detached State)、调度策略和参数(Scheduling Policy and Parameters)、作用域(Scope)、堆栈尺寸(Stack Address)、优先级(Priority)等。Linux为线程属性定义了一个联合体pthread_attr_t,注意是联合体而不是结构体,定义的地方在usr/include/bits/pthreadtype.h
中,定义如下:
union pthread_attr_t
{
char __size[__SIZEOF_PTHREAD_ATTR_T];
long int __align;
};
从这个定义中可以看出,属性值都存在数组__size中,很不方便存取。别急,Linux已经准备了一组专门用于存取属性值的函数,在后面具体讲解某个属性的时候会看到。获取线程的属性时,首先调用函数pthread_getattr_np()来获取属性结构体的值,再调用相应的函数来具体获得某个属性值。函数pthread_getattr_np()的原型声明如下:
int pthread_getattr_np(pthread_t thread,pthread,pthread_attr_t *attr);
其中,参数thread是线程id,attr返回线程属性结构体的内容。如果函数运行成功,就返回0,否则返回错误码。注意,使用该函数需要定义宏 _GNU_SOURCE,而且要在pthread.h前定义,例如:
#define _GNU_SOURCE
#include <pthread.h>
并且,当函数pthread_getattr_np()获得的属性结构体变量不再需要时,应该调用函数pthread_attr_destory()进行销毁。
我们前面调用pthread_create()创建线程时,属性结构体指针参数用了NULL,此时创建的线程具有默认属性,即为非分离、大小为1MB的堆栈、与父进程同样级别的优先级。如果要创建非默认属性的线程,则可以在创建线程之前调用函数pthread_attr_init()来初始化一个线程属性的结构体,再调用相应API函数来设置相应的属性,接着把属性结构体作为指针参数传入pthread_create()函数。函数pthread_attr_init()的原型声明如下:
int pthread_attr_init(pthread_attr_t *attr);
其中,参数attr为指向线程属性结构体的指针。如果函数执行成功就返回0,否则返回一个错误码。
需要注意的一点是:调用函数pthread_attr_init()初始化线程属性,线程运行完毕(传入pthread_create)之后需要调用pthread_attr_destroy()进行销毁,从而释放相关的资源。函数pthread_attr_destroy()的原型声明如下:
int pthread_attr_destroy(pthread_attr_t *attr)
其中,参数attr为指向线程属性结构体的指针。如果函数运行成功就返回0,否则返回一个错误码。
除了创建时指定属性外,我们也可以通过一些API函数来改变已经创建了线程的默认属性,后面讲具体属性的时候再详述。至此,线程属性的设置方法我们基本了解,那获取线程属性的方法呢:答案是通过pthread_getattr_np(),该函数可以获取某个正在正在运行的线程的属性,该函数的原型声明如下:
int pthread_getattr_np(pthread_t thread,pthread_attr_t *attr);
其中,参数thread是要获取属性的线程ID,attr用于返回得到的属性。如果函数执行成功就返回0,否则为错误码。
下面我们通过例子来演示一下该函数的作用。
1.分离状态
分离状态(Detached State)是线程很重要的一个属性。POSIX线程的分离状态决定一个线程以什么样的方式来终止自己。要注意和前面线程的状态相区别,前面所说的线程的状态是不同操作系统上的线程都有的状态(线程当前活动状态的说明),而这里所说的分离状态是POSIX标准下的属性所特有的,用于表明该线程以何种方式终止自己。默认的分离状态是可连接的,即创建线程时如果使用默认属性,则分离状态属性就是可连接的,因此默认属性下创建的线程时可连接的线程。
POSIX下的线程要么是分离的,要么是非分离的(也称可连接的,joinable)。前者用宏PTHREAD_CREATE_DETACHED表示,后者由宏PTHREAD_CREATE_JOINABLE表示。默认情况下创建的线程是可连接的,一个可连接的线程可以被其他线程收回资源和杀死(或撤销),并且不会主动释放资源(比如堆栈空间),必须等待其他线程来回收它占用的资源,因此我们要在主线程中调用pthread_join()函数(阻塞函数,当它返回时所等待的线程线程的资源就被释放了)。再次强调,如果是可连接的线程,那么线程函数自己返回结束时或调用pthread_exit()结束时都不会释放线程所占用的堆栈和线程描述符(总计8KB多),必须调用pthread_join()且返回后才会释放这些资源。这对于父进程长时间运行的进程来说会是灾难性的。因为父进程不退出并且没有调用pthread_join(),则这些可连接线程的资源就一直不会释放,相当于编程僵尸进程,僵尸进程越来越多,再想创建新进程时将没有资源可用!如果不调用pthread_join(),并且父进程先于可连接子线程退出,那会不会资源泄露呢?答案是不会。如果父进程先于子线程退出,那么它将被init进程所收养,这时init进程就是它的父进程,将会调用wait()系列函数为其回收资源。因此不会造成资源泄露。重要的事情再说一遍,一个可连接的线程所占用的内存仅当有线程对其执行pthread_join()后才会释放,因此调用pthread_join()函数来回收资源。另外,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join()的线程将得到错误代码ESRCH。
了解了可连接的线程,再来看一下可分离的线程。这种线程运行结束时,它的资源将会立刻被系统回收。可以这样理解,这个线程是能独立(分离)出去的,可以自生自灭,父进程不用管它。将一个线程设置为可分离状态有两种方法:一种方法是调用函数pthread_detach(),它可以将线程转换为可分离线程;另一种方法是在创建线程时就将它设置为可分离状态,基本过程是首先初始化一个线程属性的结构体变量(通过函数pthread_attr_init()),然后将它设置为可分离状态(通过函数pthread_attr_setdetachstate()),最后将该结构体变量的地址作为参数传入线程创建函数pthread_create(),这样所创建出来的线程就直接处于可分离状态了。
函数pthread_attr_setdetachstate()用来设置线程的分离状态属性,声明如下,
int pthread_attr_setdetachstate(pthread_attr_t * attr,int detachstate);
其中,参数attr是要设置的属性结构体;detachstate是要设置的分离状态值,可以取值为PTHREAD_CREATE_DETACHED或PTHREAD_CREATE_JOINABLE。如果函数执行成功就返回0,反则返回非零错误码。
#include "MainWindow.h"
#include <QApplication>
#include <unistd.h> //for sleep
#include <iostream>
using namespace std;
void *thfunc(void *arg)
{
cout<<("sub thread is running\n");
return NULL;
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
pthread_t thread_id;
pthread_attr_t thread_attr;
struct sched_param thread_param;
size_t stack_size;
int res;
res = pthread_attr_init(&thread_attr);
if(res)
{
cout<<"pthread_attr_init failed:"<<res<<endl;
}
res = pthread_attr_setdetachstate(&thread_attr,PTHREAD_CREATE_DETACHED);
if(res)
{
cout<<"pthread_attr_setdetachstate failed:"<<res<<endl;
}
res = pthread_create(&thread_id,&thread_attr,thfunc,NULL);
if(res)
{
printf("pthread_create failed:%d\n",res);
}
printf("pthread_create tidp:%ld\n",thread_id);
printf("main thread will exit\n");
sleep(1);
return a.exec();
}
在上面的代码中,我们首先初始化了一个线程属性结构体,然后设置其分离状态为PTHREAD_CREATE_DETACHED,并用这个属性结构体作为参数传入线程创建函数中。这样创建出来的线程就是可分离的线程。这意味着,该线程结束时,它所占用的任何资源都可以立刻被系统回收。在程序的最后让main线程挂起1秒,让子线程有机会执行。因为如果main线程很早就退出,则会导致整个进程很早退出,子进程就没有机会执行了。
如果子线程执行的时间长,那么sleep()函数到底应该睡眠多少秒呢?有没有一种机制不用sleep()函数,让子线程完整执行完呢?答案是肯定的。对于可连接的线程,主线程可以调用pthread_join()函数等待子线程结束。对于可分离线程,并没有这样的函数,但是可以先让主线程退出而进程不退出,一直等到子线程退出了才退出进程。也就是说,在主线中调用函数pthread_exit(),如果在main线程中调用了pthread_exit(),那么此时终止的只是main线程,而进程的资源会为由main线程创建的其他线程保持打开的状态,直到其他线程都终止。值得注意的是,如果在非main线程(其他子进程)中调用pthread_exit(),则不会有这样的效果,只会退出当前子线程。重写改写上例,不调用sleep(),显得更专业一些。
#include "MainWindow.h"
#include <QApplication>
#include <unistd.h> //for sleep
#include <iostream>
#include <pthread.h>
using namespace std;
void *thfunc(void *arg)
{
cout<<("sub thread is running\n");
return NULL;
}
int main(int argc, char *argv[])
{
// QApplication a(argc, argv);
// MainWindow w;
// w.show();
pthread_t thread_id;
pthread_attr_t thread_attr;
struct sched_param thread_param;
size_t stack_size;
int res;
res = pthread_attr_init(&thread_attr);
if(res)
{
cout<<"pthread_attr_init failed:"<<res<<endl;
}
res = pthread_attr_setdetachstate(&thread_attr,PTHREAD_CREATE_DETACHED);
if(res)
{
cout<<"pthread_attr_setdetachstate failed:"<<res<<endl;
}
res = pthread_create(&thread_id,&thread_attr,thfunc,NULL);
if(res)
{
printf("pthread_create failed:%d\n",res);
}
printf("pthread_create tidp:%ld\n",thread_id);
printf("main thread will exit\n");
pthread_exit(NULL); // the main thread will exit, but main process will not
cout<<"main thread has exited,this line will not run\n"<<endl;
// return a.exec();
}
正如我们所料的那样,在main()线程中调用了函数pthread_exit()后将退出main线程,但进程并不会在此刻退出,而是等到子线程结束后才退出。因为是分离的线程,所以它结束的时候所占用的资源会立刻被系统回收。如果是一个可连接(joinable)线程,则必须在创建它的线程中调用pthread_join()函数来等待可连接线程的结束并释放该线程所占用的资源。因此,在上面的代码中如果创建的是可连接的线程,则main()函数不能调用pthread_exit()函数先退出。在此,我们再总结一下可连接的线程和可分离的线程的重要区别:在任何一个时间点上,线程是可连接的(Joinable),或者是分离的(Detached)。一个可连接的线程在自己退出或pthread_exit()时都不会释放线程所占用堆栈和线程描述符(总计8K多),需要通过其他线程调用pthread_join()之后才释放这些资源;一个分离的线程时不能被其他线程回收或杀死的,所占的资源在它终止时由系统自动释放。
除了直接创建可分离的线程外,还能把一个可连接的线程转换为可分离的线程。这样做有一个好处,就是把线程的分离状态转为可分离后,它就可以自己退出或调用pthread_exit()函数后由系统回收资源。转换方法是调用函数pthread_detach()。该函数可以把一个可连接的线程转变为一个可分离的线程,这个函数的原型声明如下:
int pthread_detach(pthread_t thread)
其中,参数thread是要设置为分离状态的线程ID。如果函数调用成功就返回0,否则返回一个错误码(比如返回EINVAL,表示目标线程不是一个可连接的线程;或者返回ESRCH,表示该ID的线程没有找到)。需要注意的是,如果一个线程已经被其他线程连接了,则phread_detach()函数不会产生作用,并且该线程继续处于可连接的状态。同时,一个线程成功地进行了pthread_detach后,再想去连接时一定会失败。
下面我们来看一个例子。首先创建一个可连接的线程,然后获取其分离状态,把它转换为可分离的线程,再获取其分离状态的属性。获取分离状态的函数是pthread_attr_getdetachstate(),该函数的原型声明如下:
int pthread_attr_getdetachstate(pthread_attr_t *attr,int *detachstate);
其中,参数attr为属性结构体指针,detachstate用于返回分离状态。如果函数调用成功就返回0,否则返回错误码。
#include "MainWindow.h"
#include <QApplication>
#include <unistd.h> //for sleep
#include <iostream>
#include <pthread.h>
using namespace std;
#define handle_error_en(en,msg) do {errno = en;perror(msg);exit(EXIT_FAILURE);}while (0);
static void *thread_start(void *arg)
{
int i,s;
pthread_attr_t gattr;
s=pthread_getattr_np(pthread_self(),&gattr);
if(s!=0)
{
handle_error_en(s,"pthread_getattr_np");
}
printf("Thread's detach attributes:\n");
s = pthread_attr_getdetachstate(&gattr,&i);
if(s)
{
handle_error_en(s,"pthread_attr_getdetachstate");
}
printf("Detach state = %s\n",
(i==PTHREAD_CREATE_DETACHED)?"PTHREAD_CREATE_DETACHED":
(i==PTHREAD_CREATE_JOINABLE)?"PTHREAD_CREATE_JOINABLE":
"???");
pthread_detach(pthread_self()); //change the joinable to the detach
s = pthread_getattr_np(pthread_self(),&gattr);
if(s != 0)
{
printf("pthread_getattr_np failed\n");
}
s = pthread_attr_getdetachstate(&gattr,&i);
if(s)
{
printf("pthread_attr_getdetachstate failed");
}
printf("after pthread_detach,\n"
"Detach state = %s\n",
(i==PTHREAD_CREATE_DETACHED)?"PTHREAD_CREATE_DETACHED":
(i==PTHREAD_CREATE_JOINABLE)?"PTHREAD_CREATE_JOINABLE":
"???");
pthread_attr_destroy(&gattr);
}
int main(int argc, char *argv[])
{
// QApplication a(argc, argv);
// MainWindow w;
// w.show();
pthread_t thread_id;
pthread_attr_t thread_attr;
struct sched_param thread_param;
size_t stack_size;
int res;
res = pthread_attr_init(&thread_attr);
if(res)
{
cout<<"pthread_attr_init failed:"<<res<<endl;
}
// res = pthread_attr_setdetachstate(&thread_attr,PTHREAD_CREATE_DETACHED);
// if(res)
// {
// cout<<"pthread_attr_setdetachstate failed:"<<res<<endl;
// }
res = pthread_create(&thread_id,&thread_attr,&thread_start,NULL);
if(res)
{
printf("pthread_create failed:%d\n",res);
}
printf("pthread_create tidp:%ld\n",thread_id);
printf("main thread will exit\n");
pthread_exit(NULL); // the main thread will exit, but main process will not
cout<<"main thread has exited,this line will not run\n"<<endl;
// return a.exec();
}
2.堆栈尺寸
除了分离状态的属性外,线程的另外一个重要属性是堆栈尺寸。这对于我们在线程函数中开设堆栈上的内存空间非常重要。像局部变量、函数参数、返回地址等都存放在堆栈空间里,而动态分配的内存(比如用mallloc)或全局变量等都属于堆栈空间。我们学了堆栈尺寸属性后,要注意,在线程函数中开始局部变量(尤其是数组)不要超过默认堆栈空间的大小。获取线程堆栈尺寸属性的函数是pthread_attr_getstacksize,该函数的原型声明如下:
int pthread_attr_getstacksize(pthread_attr_t *attr,size_t *stacksize);
其中,参数attr指向属性结构体;stacksize用于获得堆栈尺寸(单位是字节),指向size_t类型得变量。如果函数调用成功就返回0,否则返回错误码。
int i,s;
pthread_attr_t gattr;
size_t stack_size;
s=pthread_getattr_np(pthread_self(),&gattr);
if(s!=0)
{
handle_error_en(s,"pthread_getattr_np");
}
s = pthread_attr_getstacksize(&gattr,&stack_size);
if(s!=0)
{
printf("pthread_attr_getstacksize failed\n");
}
printf("Default stack size is %u byte; \n"
"minimum is %u byte\n",stack_size,PTHREAD_STACK_MIN);
Output:
Default stack size is 8388608 byte;
minimum is 16384 byte
3.调度策略
线程的调度策略是线程的另一个重要属性。某个线程肯定有一种策略来调度。进程中有了多个线程后,就要管理这些线程如何去占用CPU,这就是线程调度。线程调度通常由操作系统来安排,不同操作系统的调度方法(或称调度策略)不同,比如有的操作系统采用轮询法来调度。在理解线程调度之前,先要了解一下实时与非实时。实时就是指操作系统对一些中断等的响应时效性非常高。非实时正好相反。目前VxWorks属于实时操作系统,而Windows和Linux则属于非实时操作系统,也叫分时操作系统。响应实时的表现主要是抢占,抢占是通过优先级来控制的,优先级高的任务优先占用CPU。
Linux虽然是一个非实时操作系统,但是它的线程也有实时和分时之分,具体的调度策略可以分为3种:SCHED_OTHER(分时调度策略)、SCHED_FIFO(先来先服务调度策略)、SCHED_RR(实时的分时调度策略)。我们创建线程的时候可以指定其调度策略。默认的调度策略是SCHED_OTHER。SCHED_FIFO和SCHED_RR只用于实时线程。
(1)SCHED_OTHER
SCHED_OTHER表示分时调度策略(也可称轮转策略),是一种非实时调度策略,系统会为每个线程分配一段运行时间,称为时间片。该调度策略不支持线程优先级,无论获取该调度策略下最高、最低优先级都是0。该调度策略有点像排队买票,前面的人占用了位置,后一个人是轮不上的,而且也不能强行占用(不支持优先级,没有VIP特权之说)。
(2)SCHED_FIFO
SCHED_FIFO表示先来先服务调度策略,是一种实时调度策略,支持优先级抢占,可以算是一种实时调度策略。在SCHED_FIFO策略下,CPU按照创建线程的先后顺序让先来的线程执行完再调度下一个线程。线程一旦占用CPU就会一直运行,直到有更高优先级的任务到达或原线程放弃。如果有和正在运行的线程具有同样优先级的线程就绪,则必须等待正在运行的线程主动放弃后才可以占用CPU投入运行。在SCHED_FIFO策略下,可设置的优先级范围是1到99。
(3)SCHED_RR
SCHED_RR表示时间片轮转(轮询)调度策略,但支持优先级抢占,因此也是一种实时调度策略。在SCHED_RR策略下,CPU会分配给每个线程一个特定的时间片,当线程的时间片用完时,系统将重新分配时间片,并将线程置于实时线程就绪队列的尾部,这样即可保证所有具有相同优先级的线程能够被公平的调度。
#include "MainWindow.h"
#include <QApplication>
#include <unistd.h> //for sleep
#include <iostream>
#include <pthread.h>
using namespace std;
#define handle_error_en(en,msg) do {errno = en;perror(msg);exit(EXIT_FAILURE);}while (0);
int main(int argc, char *argv[])
{
// QApplication a(argc, argv);
// MainWindow w;
// w.show();
printf("Valid priority range for SCHED_OTHER: %d - %d\n",
sched_get_priority_min(SCHED_OTHER),
sched_get_priority_max(SCHED_OTHER));
printf("Valid priority range for SCHED_FIFO: %d - %d\n",
sched_get_priority_min(SCHED_FIFO),
sched_get_priority_max(SCHED_FIFO));
printf("Valid priority range for SCHED_RR: %d - %d\n",
sched_get_priority_min(SCHED_RR),
sched_get_priority_max(SCHED_RR));
pthread_exit(NULL); // the main thread will exit, but main process will not
cout<<"main thread has exited,this line will not run\n"<<endl;
// return a.exec();
}
Output:
Valid priority range for SCHED_OTHER: 0 - 0
Valid priority range for SCHED_FIFO: 1 - 99
Valid priority range for SCHED_RR: 1 - 99
网友评论