1.线程绑定的关键API:
关于linux下线程绑定的api在网上资料很多,关键是用到两个系统API:
int pthread_setaffinity_np(pthread_tthread,
size_t cpusetsize,const
cpu_set_t *cpuset);
int pthread_getaffinity_np(pthread_tthread,
size_t cpusetsize, cpu_set_t
*cpuset);
从函数名以及参数名都很明了,唯一需要点解释下的可能就是cpu_set_t这个结构体。这个结构体的理解类似于select中的fd_set,可以理解为cpu集,也是通过约定好的宏来进行清除、设置以及判断:
//初始化,设为空
void CPU_ZERO (cpu_set_t *set);
//将某个cpu加入cpu集中
void CPU_SET (int cpu, cpu_set_t *set);
//将某个cpu从cpu集中移出
void CPU_CLR (int cpu, cpu_set_t *set);
//判断某个cpu是否已在cpu集中设置了
int CPU_ISSET (int cpu, const cpu_set_t *set);
cpu集可以认为是一个掩码,每个设置的位都对应一个可以合法调度的 cpu,而未设置的位则对应一个不可调度的
CPU。
注意这个cpu集是一个集合,最终系统决定选定线程在哪个cpu上执行,选定标准我暂时没了解。最终选定一个cpu上执行
而不是在多个cpu上执行。
2.线程绑定的目的:
线程绑定的主要目的是提高线程访问cpu的cache(缓存)命中率,从而提高程序的并行性能。线程绑定的并行优化程度和服务器架构有密切关系。
传统的多核运算是使用SMP(Symmetric
Multi-Processor )模式:将多个处理器与一个集中的存储器和I/O总线相连。所有处理器只能访问同一个物理存储器,因此SMP系统有时也被称为一致存储器访问(UMA)结构体系,一致性意指无论在什么时候,处理器只能为内存的每个数据保持或共享唯一一个数值。很显然,SMP的缺点是可伸缩性有限,因为在存储器和I/O接口达到饱和的时候,增加处理器并不能获得更高的性能。
NUMA模式是一种分布式存储器访问方式,处理器可以同时访问不同的存储器地址,大幅度提高并行性。 NUMA模式下,处理器被划分成多个"节点"(node), 每个节点被分配有的本地存储器空间。 所有节点中的处理器都可以访问全部的系统物理存储器,但是访问本节点内的存储器所需要的时间,比访问某些远程节点内的存储器所花的时间要少得多。因此NUMA架构相比SMP架构上使用线程绑定的方式更能提高并行效率。
3.linux下线程绑定小例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
#include <sched.h>
#define MAX_PROCESSNUM 256
pthread_mutex_t mutex;
int nProcessNum = 0;
int GetProcessNum()
{
return sysconf(_SC_NPROCESSORS_CONF);
}
void *myfunc(void *arg)
{
int bindcpunum = *(int *)arg;
cpu_set_t mask;
cpu_set_t get;
int j = 0;
char buf[256];
CPU_ZERO(&mask);
CPU_SET(bindcpunum,&mask);
if (pthread_setaffinity_np(pthread_self(),sizeof(mask),&mask) < 0)
fprintf(stderr,"set thread affinity failed\n");
CPU_ZERO(&get);
if (pthread_getaffinity_np(pthread_self(),sizeof(get),&get) < 0 )
fprintf(stderr,"get thread affinity failed\n");
for (j = 0;j < nProcessNum;j++)
{
if (CPU_ISSET(j,&get))
printf("thread %d is running in processor%d\n",pthread_self(),j);
}
pthread_mutex_lock(&mutex);
printf("thread %d is working ...\n",bindcpunum);
pthread_mutex_unlock(&mutex);
for (j = 0; j < 100000;j++)
memset(buf,0,sizeof(buf));
pthread_mutex_lock(&mutex);
printf("thread %d is done\n",bindcpunum);
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
int main(int argc,char *argv[])
{
int cpubindset[MAX_PROCESSNUM] = {0};
pthread_mutex_init(&mutex,NULL);
pthread_t tid[MAX_PROCESSNUM];
nProcessNum = GetProcessNum();
for (int i = 0; i < nProcessNum; i++)
{
cpubindset[i] = i;
if (pthread_create(&tid[i],NULL,&myfunc,(void *)&cpubindset[i]) != 0)
{
fprintf(stderr,"thread create failed\n");
return -1;
}
}
for (int i = 0 ;i < nProcessNum; i++)
pthread_join(tid[i],NULL);
return 0;
}
实例分析:
例子用系统API获取当前系统的处理器核数,根据这个核数创建对应的n个线程,并把这n个线程绑定到对应的cpu上,
并循环100000次执行一个费时操作memset(buf,0,sizeof(buf));
例子中使用互斥量保证线程同步,我测试时把费时操作外面又加了一层循环,然后执行测试程序,用top发现程序cpu
所占比例为1600%(我的机器有16个cpu核),证明每个cpu都在跑那个费时操作。
4.疑问:
1.使用线程绑定机制和不使用线程绑定到底区别在哪?
我个人觉得在使用线程绑定机制时,当多个线程都需要访问相同的数据,可以把这些线程都绑定到一个cpu上,提高cache
的命中率。
不使用线程绑定时线程分配到哪个cpu是由操作系统负责的,操作系统可能根据当前的cpu状态来进行调度,把空闲的cpu分给新创建的
线程