美文网首页
(七)QT专题-多线程编程-POSIX多线程库

(七)QT专题-多线程编程-POSIX多线程库

作者: GoodTekken | 来源:发表于2023-02-13 08:53 被阅读0次
  • 线程的属性
    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

相关文章

  • Android NDK开发之旅33--NDK-Linux入门之P

    POSIX POSIX是一种标准,例如有多线程编程标准、网络编程标准等。 POSIX多线程 Linux下,一般多线...

  • POSIX线程 (一)

    POSIX POSIX是一种标准,例如有多线程编程标准、网络编程标准等。Android NDK的线程是通过POSI...

  • iOS POSIX多线程编程

    关于多线程的介绍、多线程的创建、使用场景和Runloop可以参考《iOS多线程编程指南》。已上传到GitHub仓库...

  • NDK POSIX 多线程编程

    该文章首发于微信公众号“字节流动” 本博客 NDK 开发系列文章: NDK 编译的三种方式 NDK 开发中引入第三...

  • Qt多线程编程爬坑笔记

    最近在工作中用到了Qt中的多线程,踩了不少坑,故作下笔记,警示后人 - -! Overview 使用多线程编程可以...

  • C++ & Python 多线程笔记

    C++ Posix多线程 boost多线程 全局函数作为线程函数 #include #include #i...

  • 多线程

    多线程 多线程:一个进程中开辟多条线程,同时完成不同的任务,就是多线程。 创建方式:pthread:POSIX线程...

  • 多线程编程

    多线程编程之Linux环境下的多线程(一)多线程编程之Linux环境下的多线程(二)多线程编程之Linux环境下的...

  • 2018-03-20

    多线程的学习记录 1.pthread学习(pthread 属于POSIX 多线程开发框架) NSString* s...

  • POSIX多线程—异步编程举例

    Content 0.序 1.基本的同步版本 2.多进程版本 3.多线程版本 4.小结 0.序 本节通过一个简单的闹...

网友评论

      本文标题:(七)QT专题-多线程编程-POSIX多线程库

      本文链接:https://www.haomeiwen.com/subject/comzhdtx.html