linux下多线程

linux下多线程

linux系统下的多线程遵循POSIX线程接口,连接时需要使用库libpghread.a。 这里选择一些常用的线程函数进行简单的分析和练习,以使自己对linux下多线程有一定的认识。 注意:随着时间的推移,很多东西可能不再适用,当前的版本信息如下。

1 多线程依赖头文件

#include <pthread.h>

2 线程id 线程id用来唯一标识每一个线程,在操作系统级别具有唯一性,类型是pthread_t,其值由创建线程函数pthread_create赋予。

pthread_t的定义在/usr/include/x86_64-linux-gnu/bits/pthreadtypes.h中,具体定义是 typedef unsigned long int pthread_t;

3 线程属性

对于每一个线程来说,都有线程属性,属性包括许多属性,且属性不能直接设置,需要通过相关的函数进行设置。 pthread_attr_init用来将每一个属性设置为默认值,函数中会申请一些其他资源,需要通过pthread_attr_destroy来释放。

3.1 作用域

作用域表示的是新创建的线程与其他线程竞争资源的范围,有两种取值,一种为PTHREAD_SCOPE_SYSTEM,表示新创建的线程于操作系统内所有其他线程竞争资源,另外一个取值是PTHREAD_SCOPE_PROCESS,表示的是新创建的线程与该进程创建的其他属性值为PTHREAD_SCOPE_PROCESS的线程竞争资源。

int pthread_attr_getscope(const pthread_attr_t *attr, int* scope); 用来获取属性结构体attr中作用域的值,执行成功,scope的值会被设置为作用域的值,返回0,否则返回非0值。

int pthread_attr_setscope(pthread_attr_t *attr, int scope); 用来将属性结构体attr中作用域的值设置为指定作用域,即参数scope的值。执行成功,attr中的作用域会被修改为scope的值,返回0,否则返回非0。以下两种情况pthread_attr_setscope会执行失败。

下面一个简单的demo用来介绍两个函数的使用方法:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <assert.h>
#include <stdbool.h>
#include <errno.h>

// 报错处理,将错误信息展示给用户
#define handle_error_eno(eno, msg)          \
        do { errno = eno; perror(msg); exit(EXIT_FAILURE); }while(0)                            

/**
 * @free_resource 释放资源,避免非正常退出时资源未正常释放。
 *
 * @param exitno exit执行时的退出号
 * @param resource 释放的资源,该用例中仅为线程属性attr
 */
void free_resource(int exitno, void* resource)
{
    if (exitno != EXIT_SUCCESS)
    {
        printf("非正常退出,准备释放attr...");
        if (pthread_attr_destroy((pthread_attr_t *)resource) != 0)
        {
            printf("attr释放失败,请检查原因!\n");
        }
        else
            printf("attr释放成功,退出!\n");
    }
}


int main(void)
{
    pthread_attr_t attr;
    int scope;
    int eno = 0;

    //POSIX中为pthread_attr_init指定了错误ENOMEM,但实际上在linux的实现中,不会失败,因为linux中采用数组保存各个属性的值。
    eno = pthread_attr_init(&attr);
    if (eno != 0)
        handle_error_eno(eno, "多线程属性值初始化失败\n");//这里退出时还没有注册退出函数,所以不会有问题。                                    

    //从这里开始attr已经有值,为了避免异常退出导致的资源非正常释放,这里注册退出时处理函数
    eno = on_exit(free_resource, (void*)&attr); 
    if (eno != 0)
        handle_error_eno(eno, "注册异常退出函数失败"); //这里退出时退出函数注册失败,所以也不会有问题。                                        

    //获取系统为该线程初始化的scope的默认值。
    eno = pthread_attr_getscope(&attr, &scope);
    if (eno != 0)
        handle_error_eno(eno, "多线程获取scope属性值失败");

    if (PTHREAD_SCOPE_SYSTEM == scope)
        printf("pthread_attr_init初始化的scope值是PTHREAD_SCOPE_SYSTEM!\n");
    else if (PTHREAD_SCOPE_PROCESS == scope)
        printf("pthread_attr_init初始化的scope值是PTHREAD_SCOPE_PROCESS!\n");
    else
        assert(false);//理论上来说scope应该只有两个取值。

    //将scope设置为无效的值99,看实际执行情况。
    eno = pthread_attr_setscope(&attr, 99);
    if (eno != 0)
    {
        errno = eno;
        perror("99不能设置为scope的值");
    }
    else
    {
        eno = pthread_attr_getscope(&attr, &scope);
        if (eno != 0)
            handle_error_eno(eno, "socpe的值设置为99后获取scope值失败");
        else
            printf("99也是scope的可用值!\n");
    }

    //为scope设置为PTHREAD_SCOPE_SYSTEM,看实际执行情况。
    eno = pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
    if (eno != 0)
    {
        errno = eno;
        perror("将scope设置为PTHREAD_SCOPE_SYSTEM失败");
    }
    else
    {
        eno = pthread_attr_getscope(&attr, &scope);
        if (eno != 0)
            handle_error_eno(eno, "多线程获取scope属性值失败");
        else
            printf("PTHREAD_SCOPE_SYSTEM是scope的可用值!\n");
    }

    //为scope设置为PTHREAD_SCOPE_PROCESS,看实际执行情况。
    eno = pthread_attr_setscope(&attr, PTHREAD_SCOPE_PROCESS);
    if(eno != 0)
    {
        errno = eno;
        perror("将scope设置为PTHREAD_SCOPE_PROCESS失败");
    }
    else
    {
        eno = pthread_attr_getscope(&attr, &scope);
        if (eno != 0)
            handle_error_eno(eno, "多线程获取scope属性值失败");
        else
            printf("PTHREAD_SCOPE_PROCESS是scope的可用值\n");
    }

    //POSIX中为pthread_attr_destroy指定了错误ENOMEM,但实际上在linux的实现中,不会失败,因为linux中采用数组保存各个属性的值。
    eno = pthread_attr_destroy(&attr);
    if (eno != 0)
        handle_error_eno(eno, "多线程属性值销毁失败");

    return 0;
}

检验方法: 将上面的代码保存到文件pthread_attr_scope.c中后执行命令 gcc -pthread pthread_attr_scope.c -o pthread_attr_scope.out编译链接。

执行./pthread_attr_scope.out后的输出为: pthread_attr_init初始化的scope值是PTHREAD_SCOPE_SYSTEM! 99不能设置为scope的值: Invalid argument PTHREAD_SCOPE_SYSTEM是scope的可用值! 将scope设置为PTHREAD_SCOPE_PROCESS失败: Operation not supported

属性总结: 1 该输出验证了linux下不支持将scope属性设置为PTHREAD_SCOPE_PROCESS。 2 在linux下,该属性不需要单独设置。 3 设置该属性前,最好写个小demo测试下该平台是否支持该属性的设置,避免无用的代码。

3.2 CPU亲缘性

对于早期的单核单线程操作系统来说,操作系统是通过cpu的轮转来实现所谓的并行,带来了响应速度上的提升,但同时相对来说也带来了实际执行时间的延长。 现代cpu大多具有多个线程,而且部分cpu还有超线程能力,但对于目前操作系统上运行的众多程序来说,cpu线程数依然不足,所以通常来说,操作系统依然是通过调度算法,选择某个cpu线程来完成你所需要完成的某个任务,也就是说,在很多情况下,你的程序可能会在不同的cpu线程之间切换。 在大多数情况下,这是完全没有问题的,甚至可以说这是效率更高的,因为操作系统更了解目前系统的运行情况,会比较智能的选择较为空闲的cpu线程来执行你的程序,避免某些cpu线程运行压力极大而其他cpu线程闲置的问题。 但某些特殊情况,人为的控制程序在某个或某些cpu线程上运行,会提供不错的性能提升。

这里为了避免我们创建的线程和cpu线程混淆,我们之后都成cpu线程为cpu,而我们要用代码创建的线程成为线程。 注:这里举一个简单的例子来介绍cpu数目/cpu内核数/cpu线程数,某款cpu具有4核8线程,这里就是说,一个cpu,有4个内核,而且由于cpu具有超线程能力,有8个可用线程。因此,理论上来说这样的机器我们就有8个cpu可用,但实际上在很多时候并不能达到8个物理cpu的效率,这里是因为线程与线程之间/内核与内核之间有些东西是共享的,因此很多时候某个线程/内核工作的时候,其他线程/内核并不能同时工作。例如:cpu缓存,对外连接的通道等等。

例如: 1 将某个线程绑定到某个cpu上,其他的线程绑定到其他cpu上,可以获取更快的响应速度和更多的执行时间。 2 将某些线程绑定到某个或某几个cpu上,可以通过共享cpu Cache的方式提升性能。(这里涉及到一些概念,一级数据缓存,一级指令缓存,二级缓存,三级缓存等,这里提供一些参考文档。) 七个例子帮你更好的理解cpu缓存 每个程序员都应该了解的CPU高速缓存 3 可以测试一些程序的瓶颈,比如某些程序在使用多个cpu时表现更好,当cpu个数达到一定程度时,反而会导致性能的下降。 4 未完待续…

综上所述,我们可以简单的认为cpu亲缘性是指该线程/进程与哪些cpu更为亲近,或者更干脆的认为cpu亲缘性就是指我们人为的将线程/进程绑定在某些/某个cpu上,从而让其避免切换到之外的cpu上执行,使cpu缓存失效。为了更方便的理解,我们从这里开始将cpu亲缘性称为绑定属性。

那么,一个线程如果绑定到所有 可用的 cpu上,我们称之为一个cpu集合(这个集合中包含了所有可用的cpu),那么理论上我们可以说,若线程执行时间足够长,该线程可以切换到任意一个可用的cpu上执行。我们假设这个集合是全集,那么也就是说,我们可以通过调整这个集合中的cpu,来实现将线程绑定在不同的cpu集合上,更极端的是,若集合内只有一个cpu时,我们就实现了将该线程绑定在某个cpu上的任务。 上面我们讲的cpu集合,已经有人为我们想到了,对应的数据结构是cpu_set_t,这个结构中存储着我们可用的cpu,对应的,有一系列的宏函数来让我们操作这个集合,比如: 1 CPU_ZERO来清空集合内所有的cpu。 2 CPU_SET将某个cpu添加到集合中。 3 CPU_ISSET判断某个cpu是否在集合中。 4 CPU_CLR将某个cpu从集合中剔除。 5 CPU_COUNT获取集合内cpu的个数。 6 CPU_ALLOC申请一个足够存放n个cpu的集合。 7 CPU_FREE释放对应的cpu集合。 等等,对应的还有更为安全的用法,以_S结尾,这些都可以去man手册中去查看。 注:如果有心试验一下的话,会发现当计算机只有8个线程时,9/10甚至1023都是可以添加到集合中的,因此并不能用CPU_ISSET来检查操作系统具体有多少个cpu,这里推荐两个函数来判断计算机有多少个cpu和有多少个可用的cpu。

#include <sys/sysinfo.h>
int get_nprocs(void);//多少个可用cpu
int get_nprocs_conf(void);//多少个cpu
//仅可在linux下执行,Windows请查看对应的API。

int pthread_attr_getaffinity_np(const pthread_attr_t *attr, size_t cpusetsize, cpu_set_t *cpuset); 用来获取线程属性cpu亲和性的值,从线程数性结构体attr中获取,结果存储在cpuset中,当执行成功返回0,否则返回非0。注意:cpuset必须有空间,例如:

    cpu_set_t *cpusetp = CPU_ALLOC(N);

或者

    cpu_set_t cpuset;
    &cpuset;

注:以上代码并不能执行,这里只是为了解释cpuset必须有实际空间,避免段错误。

int pthread_attr_setaffinity_np(pthread_attr_t *attr, size_t cpusetsize, const cpu_set_t *cpuset); 用来设置线程属性cpu亲和性的值,将线程属性设置为绑定cpuset中的cpu,成功返回0,失败返回非0。

下面一个简单的demo用来介绍两个函数的使用方法:


2 pthread_create

2.1 函数功能

创建线程

2.2 函数原型

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

2.3 参数说明

Table of Contents