linux下多线程
linux下多线程
linux系统下的多线程遵循POSIX线程接口,连接时需要使用库libpghread.a。 这里选择一些常用的线程函数进行简单的分析和练习,以使自己对linux下多线程有一定的认识。 注意:随着时间的推移,很多东西可能不再适用,当前的版本信息如下。
-
linux内核版本:
uname -r 4.4.0-59-generic
-
gcc版本
gcc –version gcc (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609
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
会执行失败。
- EINVAL 指定了错误的scope值。
- ENOTSUP 指定了PTHREAD_SCOPE_PROCESS,目前linux系统不支持。
下面一个简单的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 参数说明
-
pthread_t *thread 输出参数,线程id的地址,函数pthread_create会为新创建的线程指定对应的线程id。
-
const pthread_attr_t *attr pthread_attr_t的定义在/usr/include/x86_64-linux-gnu/bits/pthreadtypes.h中,具体定义是
union pthread_attr_t { char __size[__SIZEOF_PTHREAD_ATTR_T]; long int __align; };
__SIZEOF_PTHREAD_ATTR_T的值也在/usr/include/x86_64-linux-gnu/bits/pthreadtypes.h中定义,具体值由一些宏控制,可能为56/32/36。 线程属性包括许多属性,属性不能直接设置,需要通过相关的函数进行设置,pthread_attr_init
用来完成对属性的初始化,初始化后所有属性为操作系统实现支持的所有属性的默认值。pthread_attr_init
需要在pthread_create
之前调用。 对于某些值如果不想使用默认值,可以调用该属性的设置方法来为该重性重新赋值,一般为pthread_attr_xxx
。pthread_attr_destroy
用来在属性不再需要的时候释放属性内所有资源。 -
void (start_routine) (void *) 线程起始函数,即新创建的函数从该函数开始执行。
-
void *arg 线程函数所需参数。