• 【linux C】绑定任务到特定CPU(CPU亲和性)


    1 相关概念及工具

    1.1 CPU亲和性

    CPU亲和性(affinity)就是进程要在某个给定的CPU上尽量长时间的运行而不被迁移到其他处理器的倾向性。
    linux内核进程调度器天生具有软CPU亲和性(affinity)的特性,着意味着进程通常不会在处理器之间频繁迁移。这种状态正是我们希望的,因为进程迁移的频率小就意味着产生的负载小。
    Linux内核还包含一些机制,它让开发人员可以编程实现硬CPU亲和性(affinity)。着意味着应用程序可以显示的指定进程在那个(或那些)处理器上运行。
    引用自CPU亲和性

    注:本文中所有的术语CPU,都要狭义的理解为一个单核CPU(更严格的说,本文所说的一CPU,都是最小执行单元,比如cpu0是一个执行单元,cpu1又是一个执行单元,每个执行单元上,都可以跑一个线程),不要考虑一个CPU又包含多少多少个核心的情况。
    注:本文中的术语,在本文中,同CPU是一个概念

    1.2 查看cpu有几个核

    1.2.1 命令查看

    $ cat /proc/cpuinfo |grep processor | wc -l
    # 或
    $ nproc
    
    # 使用cat /proc/cpuinfo可以更详细的查看哪些可以用,哪些不可以用
    # 如可以先禁用一个cpu1
    $ echo 0 > /sys/devices/system/cpu/cpu1/online
    $ cat /proc/cpuinfo
    # 再解除禁用,再对比cat /proc/cpuinfo的输出
    $ echo 1 > /sys/devices/system/cpu/cpu1/online
    $ cat /proc/cpuinfo
    # 输出已省略,可自行实验。可以看到,禁用的时候,是没有processor1的
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    1.2.2 代码查看

    #include 
    int sysconf(_SC_NPROCESSORS_CONF);/* 返回系统可以使用的核数,但是其值会包括系统中禁用的核的数目,因此该值并不代表当前系统中可用的核数 */
    int sysconf(_SC_NPROCESSORS_ONLN);/* 返回值真正的代表了系统当前可用的核数 */
    
    /* 以下两个函数与上述类似 */
    #include 
    int get_nprocs_conf (void);/* 可用核数 */
    int get_nprocs (void);/* 真正的反映了当前可用核数 */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    1.3 taskset命令

    #查看一个已存在的任务的亲和性
    #注意,该命令显示的结果,是一个十六进制数。如显示32,实际上为0x32,表示亲和性为cpu1,cpu4,cpu5(从cpu0开始计数)
    taskset -p <pid>
    #执行一个shell命令并设置其亲和性
    #注意,这里的21表示的是掩码,十六进制,即0x21,意为cpu0和cpu5
    taskset 21 <cmd>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    更多用法参见manpage

    2 代码讲解

    2.1 cpu集(cpu_set_t)

    2.1.1 cpu_set_t的使用

    cpu_set_t用来描述CPU的集合,被sched_setaffinity等类似的函数使用。

    2.1.1.1 非动态分配cpu_set_t

    常用接口声明(在glibc中用本质为宏定义):

           void CPU_ZERO(cpu_set_t *set);
           void CPU_SET(int cpu, cpu_set_t *set);
           void CPU_CLR(int cpu, cpu_set_t *set);
           int  CPU_ISSET(int cpu, cpu_set_t *set);
           int  CPU_COUNT(cpu_set_t *set);
           void CPU_AND(cpu_set_t *destset,
                        cpu_set_t *srcset1, cpu_set_t *srcset2);
           void CPU_OR(cpu_set_t *destset,
                        cpu_set_t *srcset1, cpu_set_t *srcset2);
           void CPU_XOR(cpu_set_t *destset,
                        cpu_set_t *srcset1, cpu_set_t *srcset2);
           int  CPU_EQUAL(cpu_set_t *set1, cpu_set_t *set2);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    用法举例:

    cpu_set_t set1, set2;
    CPU_ZERO(&set1); //清空集合,即set1里不包含任何CPU,本质为所有bit清零
    CPU_ZERO(&set2); //清空集合,即set2里不包含任何CPU,本质为所有bit清零
    CPU_SET(0, &set1); //将cpu0添加到集合set1中,本质为对应bit置1
    CPU_SET(1, &set2); //将cpu1添加到集合set2中,本质为对应bit置1
    CPU_CLR(0, &set1); //将cpu0从集合set1中移除,本质为对应bit清零
    int ret = CPU_ISSET(1, &set2); //判断cpu1是否在集合set2中,在返回非零,不在返回0
    int cnt = CPU_COUNT(&set2); //返回集合set2中的CPU的个数
    cpu_set_t result;
    CPU_AND(&result, &set1, &set2); //set1和set2的所有bit按位与,结果存入result
    CPU_OR(); //按位或
    CPU_XOR(); //按位异或
    ret = CPU_EQUAL(&set1, &set2); //集合set1和集合set2相等的话,ret为非零,不相等,ret为0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    2.1.1.2 动态分配cpu_set_t

    刚开始接触cpu_set_t时,对_S系列接口有疑问,不明白它存在的意义,明明自己malloc一个cpu_set_t就可以,然后使用各种非_S对其操作,为什么非要有_S系列接口呢?具体原因见glibc源码粗讲解
    常用接口声明(在glibc中用本质为宏定义):

           cpu_set_t *CPU_ALLOC(int num_cpus);
           void CPU_FREE(cpu_set_t *set);
           size_t CPU_ALLOC_SIZE(int num_cpus);
           void CPU_ZERO_S(size_t setsize, cpu_set_t *set);
           void CPU_SET_S(int cpu, size_t setsize, cpu_set_t *set);
           void CPU_CLR_S(int cpu, size_t setsize, cpu_set_t *set);
           int  CPU_ISSET_S(int cpu, size_t setsize, cpu_set_t *set);
           int  CPU_COUNT_S(size_t setsize, cpu_set_t *set);
           void CPU_AND_S(size_t setsize, cpu_set_t *destset,
                        cpu_set_t *srcset1, cpu_set_t *srcset2);
           void CPU_OR_S(size_t setsize, cpu_set_t *destset,
                        cpu_set_t *srcset1, cpu_set_t *srcset2);
           void CPU_XOR_S(size_t setsize, cpu_set_t *destset,
                        cpu_set_t *srcset1, cpu_set_t *srcset2);
           int  CPU_EQUAL_S(size_t setsize, cpu_set_t *set1, cpu_set_t *set2);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    用法举例:

    #define _GNU_SOURCE
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int
    main(int argc, char *argv[])
    {
        cpu_set_t *cpusetp;
        size_t size;
        int num_cpus, cpu;
    
        if (argc < 2) {
            fprintf(stderr, "Usage: %s \n", argv[0]);
            exit(EXIT_FAILURE);
        }   
    
        num_cpus = atoi(argv[1]);
        
        /* 申请一个能够存放num_cpus个CPU的集合 */
        cpusetp = CPU_ALLOC(num_cpus);
        if (cpusetp == NULL) {
            perror("CPU_ALLOC");
            exit(EXIT_FAILURE);
        }   
    
        /* 获取能存放num_cpus个CPU的集合的大小(单位字节) */
        size = CPU_ALLOC_SIZE(num_cpus);
    
        /* 清零cpusetp */
        CPU_ZERO_S(size, cpusetp);
        for (cpu = 0; cpu < num_cpus; cpu += 2)
            CPU_SET_S(cpu, size, cpusetp); //将cpu0,cpu2,... 添加到cpusetp集合
    
        printf("CPU_COUNT() of set:    %d\n", CPU_COUNT_S(size, cpusetp));
    
        /* 释放cpusetp空间 */
        CPU_FREE(cpusetp);
        exit(EXIT_SUCCESS);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    2.1.2 glibc源码粗讲解(无兴趣可跳过)

    /* Data structure to describe CPU mask.  */
    typedef struct
    {
      /* 不同架构下,__cpu_mask的类型不同,但粗略查看源码,均为unsigned long */
      __cpu_mask __bits[__CPU_SETSIZE / __NCPUBITS];
    } cpu_set_t;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    __NCPUBITS:一个宏,定义为__cpu_mask类型的位宽。在64位操作系统下,unsigned long占8字节,即位宽为64bit。
    __CPU_SETSIZE:固定数值,1024。该宏定义的逻辑意义是,定义了cpu_set_t这个类型,可以包含多少个CPU。即定义一个cpu_set_t cpus;变量的话,最多可以表示1024个CPU。
    cpu_set_t实际就是一个__cpu_mask(无符号long类型)数组,并且数组所占的总位宽为1024。

    1024个bit为128Byte,在x86_64架构下,unsigned long占8字节,所以,当定义一个cpu_set_t变量时,实际上是定义了一个包含16个unsigned long的数组,共占128字节。所有的操作(如CPU_ZEROCPU_SET等)都是通过与、或、移位等操作,对每个bit进行清零或置一。如,要把cpu0添加到集合,则把bit0置一;要把cpu70添加到集合,就把bit70置一(显然,bit70在数组的第2个元素中,即bit0~bit63是第一个unsigned long,bit64~bit127是第二个unsigned long,以此类推)。

    当想使用动态分配内存的方式定义cpu_set_t时,是可以直接malloc(sizeof(cpu_set_t)),然后直接使用非_S系列的接口对其操作即可(使用_S系列也可,只是需要将sizeof(cpu_set_t)作为参数传入_S系列函数。非_S系列接口,实现的时候也是调用的_S系列接口,只是size参数统一传入sizeof(cpu_set_t))。

    cpu_set_t *CPU_ALLOC(int num_cpus)函数说明:该函数本质也是调用malloc,只是其申请的内存大小,足够包含num_cpus个CPU就好了,不会一股脑直接申请128B(1024bit)空间。该函数申请空间的最小单位是sizeof(__cpu_mask),在x86_64下,该值为sizeof(unsigned long),即8B。

    #define __CPU_ALLOC_SIZE(count) \
      ((((count) + __NCPUBITS - 1) / __NCPUBITS) * sizeof (__cpu_mask))
    #define __CPU_ALLOC(count) __sched_cpualloc (count)
    
    cpu_set_t *
    __sched_cpualloc (size_t count)
    {
      return malloc (CPU_ALLOC_SIZE (count));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    一般来说,用非_S系列函数就够了,即便是想动态分配内存,非_S也是可以的。
    总结_S应用场景:

    • 想省空间。一般来说,CPU个数不会很大(远小于1024个),那么使用非_S就意味着,cpu_set_t要占用128B内存空间。如果想省空间,可以使用CPU_ALLOC来申请更精准大小的空间。
    • 你的代码要操控的CPU个数超过1024。

    2.2 绑定任务到指定CPU

    CPU亲和性只是一种倾向性,当绑定的CPU不存在或者存在但是被禁用了,任务会在其他的CPU上执行
    设置任务亲和性的接口有:

    • sched_setaffinity:修改指定pid_t的任务的亲和性
    • pthread_setaffinity_np:gnu接口,修改指定pthrad_t的任务的亲和性。
    • pthread_attr_setaffinity_np:gnu接口。创建线程前,通过线程属性结构体控制新线程的亲和性。

    注:np的意思为不可移植,即在非gnu的系统上,应该是没有这个接口的。

    2.2.1 sched_setaffinity

    如果考虑可移植性的话,推荐使用sched_setaffinity()函数将任务绑定到特定CPU执行。

    test.c:

    /* 该代码只是为了验证亲和性,故多数函数均没有检查返回值 */
    #define _GNU_SOURCE
    #include 
    #include 
    #include 
    #include 
    #include 
    
    void *func(void *arg)
    {
      pthread_setname_np(pthread_self(), (char *)arg);
      while(1)
      {
        char name[128] = {};
        printf("%s\n", (char *)arg);
        /* 死循环耗时,避免打印过快,不同的机器可能需要调整循环次数 */
        for(int i=0; i<1000000000; i++);
      }
      return NULL;
    }
    
    int main(void)
    {
      pthread_t th1, th2;
      cpu_set_t set;
    
      /* 初始化一个CPU集合,并向其中添加cpu0和cpu3 */
      CPU_ZERO(&set);
      CPU_SET(0, &set);
      CPU_SET(3, &set);
    
      /* 设置主线程的亲和性,子线程会继承父线程的亲和性设置(见man pthread_create) */
      int ret = sched_setaffinity(getpid(), sizeof(cpu_set_t), &set);
      if (ret)
      {
        perror("sched_setaffinity");
        return -1;
      }
    
      pthread_create(&th1, NULL, func, "th1");
      pthread_create(&th2, NULL, func, "th2");
    
      pthread_join(th1, NULL);
      pthread_join(th2, NULL);
    
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    该代码运行后会死循环打印,我们重点要关注的是亲和性:

    $ gcc test.c -pthread
    $ ./a.out
    
    • 1
    • 2

    a.out运行后,再打开一个新的终端,查询a.out的进程号,然后用taskset命令查看其亲和性,可以看到,亲和性为9(十六进制),转换为二进制为1001,即a.out运行在cpu0和cpu3上。
    亲和性展示
    或者使用htop来查看亲和性,进入htop后,搜索a.out,查看其亲和性:
    htop查看亲和性
    可以看到,cpu0和cpu3占用率已经100%了,a.out处于阻塞态,th1和th2分别在cpu3和cpu0上运行。

    但是,sched_setaffinity函数的不方便之处在于,无法给线程指定亲和性,要用sched_setaffinity给线程指定亲和性,比较麻烦,需要使用到不可移植的函数gettid()。修改test.c如下:

    test.c:使用sched_setaffinity修改线程的CPU亲和性。主要逻辑为,主线程设置亲和性为cpu0,启动两个新线程th1和th2,此时从htop可以看到,主线程阻塞,th1和th2交替运行在cpu0上。然后过一段时间后,线程函数自己修改自己的亲和性(th1将自己的亲和性设置为cpu1,th2将自己的亲和性设置为cpu2),此时从htop可以看到,th1到cpu1上运行,th2到cpu2上运行。

    /* 该代码只是为了验证亲和性,故多数函数均没有检查返回值 */
    #define _GNU_SOURCE
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include    /* For SYS_xxx definitions */
    
    void *func(void *arg)
    {
      /* 用来计时 */
      int count = 0;
      pthread_setname_np(pthread_self(), (char *)arg);
      while(1)
      {
        char name[128] = {};
        printf("%s\n", (char *)arg);
        /* 死循环耗时,避免打印过快,不同的机器可能需要调整循环次数 */
        for(int i=0; i<1000000000; i++);
        count++;
        /* count小于15时,线程都运行在cpu0上;等于15时,线程会设置自身的亲和性,导致后续时间,亲和性就不再是cpu0了 */
        if(count == 15)
        {
          pid_t tid;
          cpu_set_t set;
    
          CPU_ZERO(&set);
          /* 获取本线程的pid_t类型的ID号,因为sched_setaffinity的参数类型为pid_t */
          tid = syscall(SYS_gettid);
          if(!strncmp((char *)arg, "th1", strlen("th1")+1))
          {
            CPU_SET(1, &set);
            sched_setaffinity(tid, sizeof(cpu_set_t), &set);
          }
          else
          {
            CPU_SET(2, &set);
            sched_setaffinity(tid, sizeof(cpu_set_t), &set);
          }
        }
      }
      return NULL;
    }
    
    int main(void)
    {
      pthread_t th1, th2;
      cpu_set_t set;
    
      CPU_ZERO(&set);
      CPU_SET(0, &set);
    
      /* 设置主线程亲和性为cpu0,这样的话,默认新线程亲和性也是cpu0 */
      int ret = sched_setaffinity(getpid(), sizeof(cpu_set_t), &set);
      if (ret)
      {
        perror("sched_setaffinity");
        return -1;
      }
    
      pthread_create(&th1, NULL, func, "th1");
      pthread_create(&th2, NULL, func, "th2");
    
      pthread_join(th1, NULL);
      pthread_join(th2, NULL);
    
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    开始时,都在cpu0上运行:
    在cpu0运行

    过一段时间,在各自的cpu上运行:
    在各自cpu运行

    2.2.2 pthread_setaffinity_np

    如果要设置已存在的线程的亲和性,就可以考虑使用pthread_setaffinity_np,尽管pthread_setaffinity_np是不可移植的,是gnu独有的。因为就算是考虑到移植性,想要使用sched_setaffinity,也避免不了要使用gettid函数(该函数也是gnu独有)。与使用sched_setaffinity设置线程亲和性相比,使用pthread_setaffinity_np的另一个好处就是,可以在线程外设置任一线程的亲和性(只要知道pthread_t即可),而sched_setaffinity的gettid,需要在线程函数内调用。

    test.c:使用pthread_setaffinity_np,在main函数中,对两个子线程设置其亲和性。前10s亲和性都追随主函数(cpu0),10s后使用pthread_setaffinity_np设置th1亲和性为cpu1,th2亲和性为cpu2,结果见htop截图。

    /* 该代码只是为了验证亲和性,故多数函数均没有检查返回值 */
    #define _GNU_SOURCE
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include    /* For SYS_xxx definitions */
    
    void *func(void *arg)
    {
      pthread_setname_np(pthread_self(), (char *)arg);
      while(1)
      {
        char name[128] = {};
        printf("%s\n", (char *)arg);
        /* 死循环耗时,避免打印过快,不同的机器可能需要调整循环次数 */
        for(int i=0; i<1000000000; i++);
      }
      return NULL;
    }
    
    int main(void)
    {
      pthread_t th1, th2;
      cpu_set_t set;
    
      CPU_ZERO(&set);
      CPU_SET(0, &set);
    
      /* 设置主线程亲和性为cpu0,这样的话,默认新线程亲和性也是cpu0 */
      int ret = sched_setaffinity(getpid(), sizeof(cpu_set_t), &set);
      if (ret)
      {
        perror("sched_setaffinity");
        return -1;
      }
    
      pthread_create(&th1, NULL, func, "th1");
      pthread_create(&th2, NULL, func, "th2");
    
      /* 前10s,亲和性都是cpu0 */
      sleep(10);
      /* 10s过后,亲和性被修改 */
    
      /* 移除CPU集合中的cpu0,此时集合中没有任何CPU */
      CPU_CLR(0, &set);
      /* 增加cpu1,此时集合中只有cpu1 */
      CPU_SET(1, &set);
      /* 设置th1的亲和性为cpu1 */
      pthread_setaffinity_np(th1, sizeof(cpu_set_t), &set);
    
      /* 移除集合中的cpu1,此时集合中没有任何CPU */
      CPU_CLR(1, &set);
      /* 增加cpu2,此时集合中只有cpu2 */
      CPU_SET(2, &set);
      /* 设置th2的亲和性为cpu2 */
      pthread_setaffinity_np(th2, sizeof(cpu_set_t), &set);
    
      pthread_join(th1, NULL);
      pthread_join(th2, NULL);
    
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    前10s亲和性:cpu0
    前10s亲和性为cpu0

    10s后亲和性:th1在cpu1上,th2在cpu2上。
    10秒后亲和性发生改变

    2.2.3 pthread_attr_setaffinity_np

    创建新线程时,通过属性结构体,控制新线程的亲和性。

    test.c:

    /* 该代码只是为了验证亲和性,故多数函数均没有检查返回值 */
    #define _GNU_SOURCE
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include    /* For SYS_xxx definitions */
    
    void *func(void *arg)
    {
      pthread_setname_np(pthread_self(), (char *)arg);
      while(1)
      {
        char name[128] = {};
        printf("%s\n", (char *)arg);
        /* 死循环耗时,避免打印过快,不同的机器可能需要调整循环次数 */
        for(int i=0; i<1000000000; i++);
      }
      return NULL;
    }
    
    int main(void)
    {
      pthread_t th1;
      cpu_set_t set;
      pthread_attr_t attr;
    
      CPU_ZERO(&set);
      CPU_SET(0, &set);
    
      /* 设置主线程亲和性为cpu0,这样的话,默认新线程亲和性也应该是cpu0 */
      /* 但是由于新线程创建的时候,使用属性指定了亲和性,就会发现新线程启动之后直接
       * 就在指定的cpu运行了 */
      int ret = sched_setaffinity(getpid(), sizeof(cpu_set_t), &set);
      if (ret)
      {
        perror("sched_setaffinity");
        return -1;
      }
    
      pthread_attr_init(&attr);
      CPU_ZERO(&set);
      CPU_SET(1, &set);
      pthread_attr_setaffinity_np(&attr, sizeof(cpu_set_t), &set);
    
      pthread_create(&th1, &attr, func, "th1");
    
      pthread_join(th1, NULL);
    
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    运行结果:通过属性控制新线程亲和性为cpu1
    属性控制线程亲和性

  • 相关阅读:
    Spring Cloud教程 第十二弹 Spring Cloud Config整合Spring Cloud Bus实现配置动态刷新
    AI 绘画 - 如何 0 成本在线体验 AI 绘画的魅力
    leetcode:575. 分糖果(python3解法)
    Notepad++正则查询替换操作
    使用SpringBoot优雅的实现文件上传
    Django之ORM
    C语言模拟类的宏
    基于设计模式的绘图程序设计(大二下学期课程设计)
    迪士尼FAMA认证作用及取得FAMA注意事项
    从autojs到冰狐智能辅助的心里历程
  • 原文地址:https://blog.csdn.net/u013511885/article/details/126427521