欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【Linux应用】pthread详解

程序员文章站 2024-02-20 20:20:47
...

前言

我并不假定你会使用Linux的线程,所以在这里就简单的介绍一下。如果你之前有过多线程方面的编程经验,完全可以忽略本文的内容,因为它非常的初级。
首先说明一下,在Linux编写多线程程序需要包含头文件pthread.h。当然,只包含一个头文件是不能搞定线程的,还需要连接libpthread.so这个库,因此在程序连接阶段应该有类似这样的指令:

gcc program.o -o program -lpthread

1. pthread_create

在Linux下创建的线程的API接口是pthread_create(),它的完整定义是:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*) void *arg);

pthread_create是类Unix操作系统(Unix、Linux、Mac OS X等)的创建线程的函数。它的功能是创建线程(实际上就是确定调用该线程函数的入口点),在线程创建以后,就开始运行相关的线程函数。pthread_create的返回值:若成功,返回0;若出错,返回出错编号。linux下用C语言开发多线程程序,Linux系统下的多线程遵循POSIX线程接口,称为pthread。

  • 第一个参数为指向线程标识符的指针。
  • 第二个参数用来设置线程属性。
  • 第三个参数是线程运行函数的起始地址。
  • 最后一个参数是运行函数的参数。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
 
#define    NUM_THREADS     8
 
void *PrintHello(void *args)
{
    int thread_arg;
    sleep(1);
    thread_arg = (int)(*((int*)args));
    printf("Hello from thread %d\n", thread_arg);
    return NULL;
}
 
int main(void)
{
    int rc,t;
    pthread_t thread[NUM_THREADS];
 
    for( t = 0; t < NUM_THREADS; t++)
    {
        printf("Creating thread %d\n", t);
        //此处t变量的用法是方便大家调测代码的写法,实际使用会有问题,因为这个t是局部变量,
        函数执行完后马上释放,大家传递参数时需要使用全局变量或malloc出来的变量。
        rc = pthread_create(&thread[t], NULL, PrintHello, &t);
        if (rc)
        {
            printf("ERROR; return code is %d\n", rc);
            return EXIT_FAILURE;
        }
    }
    sleep(5);
    for( t = 0; t < NUM_THREADS; t++)
        pthread_join(thread[t], NULL);
    return EXIT_SUCCESS;
}

结果为:

$ gcc thread_test.c -o thread_test -std=c99 -pthread
$ ./thread_test
Creating thread 0
Creating thread 1
Creating thread 2
Creating thread 3
Creating thread 4
Creating thread 5
Creating thread 6
Creating thread 7
Hello from thread 8
Hello from thread 8
Hello from thread 8
Hello from thread 8
Hello from thread 8
Hello from thread 8
Hello from thread 8
Hello from thread 8

2. 线程的合并与分离

我们首先要明确的一个问题就是什么是线程的合并。从前面的叙述中读者们已经了解到了,pthread_create()接口负责创建了一个线程。那么线程也属于系统的资源,这跟内存没什么两样,而且线程本身也要占据一定的内存空间。众所周知的一个问题就是C或C++编程中如果要通过malloc()或new分配了一块内存,就必须使用free()或delete来回收这块内存,否则就会产生著名的内存泄漏问题。既然线程和内存没什么两样,那么有创建就必须得有回收,否则就会产生另外一个著名的资源泄漏问题,这同样也是一个严重的问题。那么线程的合并就是回收线程资源了。
线程的合并是一种主动回收线程资源的方案。当一个进程或线程调用了针对其它线程的pthread_join()接口,就是线程合并了。这个接口会阻塞调用进程或线程,直到被合并的线程结束为止。当被合并线程结束,pthread_join()接口就会回收这个线程的资源,并将这个线程的返回值返回给合并者。
与线程合并相对应的另外一种线程资源回收机制是线程分离,调用接口是pthread_detach()。线程分离是将线程资源的回收工作交由系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。因为线程分离是启动系统的自动回收机制,那么程序也就无法获得被分离线程的返回值,这就使得pthread_detach()接口只要拥有一个参数就行了,那就是被分离线程句柄。
线程合并和线程分离都是用于回收线程资源的,可以根据不同的业务场景酌情使用。不管有什么理由,你都必须选择其中一种,否则就会引发资源泄漏的问题,这个问题与内存泄漏同样可怕。

3. 线程的属性

前面还说到过线程是有属性的,这个属性由一个线程属性对象来描述。线程属性对象由pthread_attr_init()接口初始化,并由pthread_attr_destory()来销毁,它们的完整定义是:

int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destory(pthread_attr_t *attr);

那么线程拥有哪些属性呢?一般地,Linux下的线程有:绑定属性、分离属性、调度属性、堆栈大小属性和满占警戒区大小属性。下面我们就分别来介绍这些属性。

3.1 绑定属性

说到这个绑定属性,就不得不提起另外一个概念:轻进程(Light Weight Process,简称LWP)。轻进程和Linux系统的内核线程拥有相同的概念,属于内核的调度实体。一个轻进程可以控制一个或多个线程。默认情况下,对于一个拥有n个线程的程序,启动多少轻进程,由哪些轻进程来控制哪些线程由操作系统来控制,这种状态被称为非绑定的。那么绑定的含义就很好理解了,只要指定了某个线程“绑”在某个轻进程上,就可以称之为绑定的了。被绑定的线程具有较高的相应速度,因为操作系统的调度主体是轻进程,绑定线程可以保证在需要的时候它总有一个轻进程可用。绑定属性就是干这个用的。
设置绑定属性的接口是pthread_attr_setscope(),它的完整定义是:

int pthread_attr_setscope(pthread_attr_t *attr, int scope);

它有两个参数,第一个就是线程属性对象的指针,第二个就是绑定类型,拥有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)。

#include <stdio.h>
#include <pthread.h>
……
int main( int argc, char *argv[] )
{
    pthread_attr_t attr;
    pthread_t th;
    ……
    pthread_attr_init( &attr );
    pthread_attr_setscope( &attr, PTHREAD_SCOPE_SYSTEM );
    pthread_create( &th, &attr, thread, NULL );
    ……
}

不知道你是否在这里发现了本文的矛盾之处。就是这个绑定属性跟我们之前说的NPTL有矛盾之处。在介绍NPTL的时候就说过业界有一种m:n的线程方案,就跟这个绑定属性有关。但是笔者还说过NPTL因为Linux的“蠢”没有采取这种方案,而是采用了“1:1”的方案。这也就是说,Linux的线程永远都是绑定。对,Linux的线程永远都是绑定的,所以PTHREAD_SCOPE_PROCESS在Linux中不管用,而且会返回ENOTSUP错误。
既然Linux并不支持线程的非绑定,为什么还要提供这个接口呢?答案就是兼容!因为Linux的NTPL是号称POSIX标准兼容的,而绑定属性正是POSIX标准所要求的,所以提供了这个接口。如果读者们只是在Linux下编写多线程程序,可以完全忽略这个属性。如果哪天你遇到了支持这种特性的系统,别忘了我曾经跟你说起过这玩意儿:)

3.2 分离属性

前面说过线程能够被合并和分离,分离属性就是让线程在创建之前就决定它应该是分离的。如果设置了这个属性,就没有必要调用pthread_join()或pthread_detach()来回收线程资源了。
设置分离属性的接口是pthread_attr_setdetachstate(),它的完整定义是:

pthread_attr_setdetachstat(pthread_attr_t *attr, int detachstate);

它的第二个参数有两个取值:PTHREAD_CREATE_DETACHED(分离的)和PTHREAD_CREATE_JOINABLE(可合并的,也是默认属性)。

3.3 调度属性

线程的调度属性有三个,分别是:算法、优先级和继承权。
Linux提供的线程调度算法有三个:轮询、先进先出和其它。其中轮询和先进先出调度算法是POSIX标准所规定,而其他则代表采用Linux自己认为更合适的调度算法,所以默认的调度算法也就是其它了。轮询和先进先出调度算法都属于实时调度算法。轮询指的是时间片轮转,当线程的时间片用完,系统将重新分配时间片,并将它放置在就绪队列尾部,这样可以保证具有相同优先级的轮询任务获得公平的CPU占用时间;先进先出就是先到先服务,一旦线程占用了CPU则一直运行,直到有更高优先级的线程出现或自己放弃。
设置线程调度算法的接口是pthread_attr_setschedpolicy(),它的完整定义是:

pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);

它的第二个参数有三个取值:SCHED_RR(轮询)、SCHED_FIFO(先进先出)和SCHED_OTHER(其它)。
Linux的线程优先级与进程的优先级不一样,进程优先级我们后面再说。Linux的线程优先级是从1到99的数值,数值越大代表优先级越高。而且要注意的是,只有采用SHCED_RR或SCHED_FIFO调度算法时,优先级才有效。对于采用SCHED_OTHER调度算法的线程,其优先级恒为0。
设置线程优先级的接口是pthread_attr_setschedparam(),它的完整定义是:

struct sched_param {
    int sched_priority;
}
int pthread_attr_setschedparam(pthread_attr_t *attr, struct sched_param *param);

sched_param结构体的sched_priority字段就是线程的优先级了。
此外,即便采用SCHED_RR或SCHED_FIFO调度算法,线程优先级也不是随便就能设置的。首先,进程必须是以root账号运行的;其次,还需要放弃线程的继承权。什么是继承权呢?就是当创建新的线程时,新线程要继承父线程(创建者线程)的调度属性。如果不希望新线程继承父线程的调度属性,就要放弃继承权。
设置线程继承权的接口是pthread_attr_setinheritsched(),它的完整定义是:

int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched);

它的第二个参数有两个取值:PTHREAD_INHERIT_SCHED(拥有继承权)和PTHREAD_EXPLICIT_SCHED(放弃继承权)。新线程在默认情况下是拥有继承权。
代码4能够演示不同调度算法和不同优先级下各线程的行为,同时也展示如何修改线程的调度属性。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define THREAD_COUNT 12
void show_thread_policy( int threadno )
{
    int policy;
    struct sched_param param;
    pthread_getschedparam( pthread_self(), &policy, ¶m );
    switch( policy ){
    case SCHED_OTHER:
        printf( "SCHED_OTHER %d\n", threadno );
        break;
    case SCHED_RR:
        printf( "SCHDE_RR %d\n", threadno );
        break;
    case SCHED_FIFO:
        printf( "SCHED_FIFO %d\n", threadno );
        break;
    default:
        printf( "UNKNOWN\n");
    }
}
void* thread( void *arg )
{
    int i, j;
    long threadno = (long)arg;
    printf( "thread %d start\n", threadno );
    sleep(1);
    show_thread_policy( threadno );
    for( i = 0; i < 10; ++i ) {
        for( j = 0; j < 100000000; ++j ){}
        printf( "thread %d\n", threadno );
    }
    printf( "thread %d exit\n", threadno );
    return NULL;
}
int main( int argc, char *argv[] )
{
    long i;
    pthread_attr_t attr[THREAD_COUNT];
    pthread_t pth[THREAD_COUNT];
    struct sched_param param;
    for( i = 0; i < THREAD_COUNT; ++i )
        pthread_attr_init( &attr[i] );
        for( i = 0; i < THREAD_COUNT / 2; ++i ) {
            param.sched_priority = 10;                  
            pthread_attr_setschedpolicy( &attr[i], SCHED_FIFO );
            pthread_attr_setschedparam( &attr[i], ¶m );
            pthread_attr_setinheritsched( &attr[i], PTHREAD_EXPLICIT_SCHED );
        }
        for( i = THREAD_COUNT / 2; i < THREAD_COUNT; ++i ) {
            param.sched_priority = 20;                  
            pthread_attr_setschedpolicy( &attr[i], SCHED_FIFO );
            pthread_attr_setschedparam( &attr[i], ¶m );
            pthread_attr_setinheritsched( &attr[i], PTHREAD_EXPLICIT_SCHED );
        }
        for( i = 0; i < THREAD_COUNT; ++i )                    
            pthread_create( &pth[i], &attr[i], thread, (void*)i );              
        for( i = 0; i < THREAD_COUNT; ++i )                    
            pthread_join( pth[i], NULL );                    
        for( i = 0; i < THREAD_COUNT; ++i )                    
            pthread_attr_destroy( &attr[i] );                   
    return 0;                           
}

3.4 堆栈大小属性

从前面的这些例子中可以了解到,线程的主函数与程序的主函数main()有一个很相似的特性,那就是可以拥有局部变量。虽然同一个进程的线程之间是共享内存空间的,但是它的局部变量确并不共享。原因就是局部变量存储在堆栈中,而不同的线程拥有不同的堆栈。Linux系统为每个线程默认分配了8MB的堆栈空间,如果觉得这个空间不够用,可以通过修改线程的堆栈大小属性进行扩容。
修改线程堆栈大小属性的接口是pthread_attr_setstacksize(),它的完整定义为:

int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);

它的第二个参数就是堆栈大小了,以字节为单位。需要注意的是,线程堆栈不能小于16KB,而且尽量按4KB(32位系统)或2MB(64位系统)的整数倍分配,也就是内存页面大小的整数倍。此外,修改线程堆栈大小是有风险的,如果你不清楚你在做什么,最好别动它(其实我很后悔把这么危险的东西告诉了你:)。

转载于:https://blog.csdn.net/jiajun2001/article/details/12624923

相关标签: linux应用