一次脑残的记录:Linux 中实时任务调度与优先级

系统 Linux
在本文中,可以把线程约等于进程,有的地方也可能称为任务,在不同的语境下有一些不同的惯用说法。

[[405787]]

失败是成功之母,这篇文章就是一次真实的失败调试记录。

通过这篇文章,您能深刻体验到 Linux 系统中下面几个概念:

  1. 实时进程和普通进程的调度策略;
  2. Linux 中混乱的进程优先级是如何计算的;
  3. CPU亲和性的测试;
  4. 多处理器(SMP)遇到实时进程和普通进程的程序设计;
  5. 道哥的脑袋被门夹了一下的短路经历;

背景知识:Linux 调度策略

关于进程的调度策略,不同的操作系统有不同的整体目标,因此调度算法也就各不相同。

这需要根据进程的类型(计算密集型?IO密集型?)、优先级等因素来进行选择。

对于 Linux x86 平台来说,一般采用的是 CFS:完全公平调度算法。

之所以叫做完全公平,是因为操作系统以每个线程占用 CPU 的比率来进行动态的计算,操作系统希望每一个进程都能够平均的使用 CPU 这个资源,雨露均沾。

我们在创建一个线程的时候,默认就是这个调度算法 SCHED_OTHER,默认的优先级为 0。

  • PS: 在 Linux 操作系统中,线程的内核对象与进程的内核对象(其实就是一些结构体变量)是很类似的,所以线程可以说是轻量级的进程。
  • 在本文中,可以把线程约等于进程,有的地方也可能称为任务,在不同的语境下有一些不同的惯用说法。

可以这么理解:如果系统中一共有 N 个进程,那么每个进程会得到 1/N 的执行机会。每个进程执行一段时间之后,就被调出,换下一个进程执行。

如果这个 N 的数量太大了,导致每个进程刚开始执行时,分给它的时间就到了。如果这个时候就进行任务调度,那么系统的资源就耗费在进程上下文切换上去了。

因此,操作系统引入了最小粒度,也就是每个进程都有一个最小的执行时间保证,称作时间片。

除了 SCHED_OTHER 调度算法,Linux 系统还支持两种实时调度策略:

  • 1. SCHED_FIFO:根据进程的优先级进行调度,一旦抢占到 CPU 则一直运行,直达自己主动放弃或被被更高优先级的进程抢占;
  • 2. SCHED_RR:在 SCHED_FIFO 的基础上,加上了时间片的概念。当一个进程抢占到 CPU 之后,运行到一定的时间后,调度器会把这个进程放在 CPU 中,当前优先级进程队列的末尾,然后选择另一个相同优先级的进程来执行;

本文想测试的就是 SCHED_FIFO 与普通的 SCHED_OTHER 这两种调度策略混合的情况。

背景知识:Linux 线程优先级

在 Linux 系统中,优先级的管理显得比较混乱,先看下面这张图:

这张图表示的是内核中的优先级,分为两段。

前面的数值 0-99 是实时任务,后面的数值 100-139 是普通任务。

数值越低,代表这个任务的优先级越高。

数值越低,代表这个任务的优先级越高。

数值越低,代表这个任务的优先级越高。

再强调一下,以上是从内核角度来看的优先级。

好了,重点来了:

我们在应用层创建线程的时候,设置了一个优先级数值,这是从应用层角度来看的优先级数值。

但是内核并不会直接使用应用层设置的这个数值,而是经过了一定的运算,才得到内核中所使用的优先级数值(0 ~ 139)。

1. 对于实时任务

我们在创建线程的时候,可以通过下面这样的方式设置优先级数值(0 ~ 99):

  1. struct sched_param param; 
  2. param.__sched_priority = xxx; 

当创建线程函数进入内核层面的时候,内核通过下面这个公式来计算真正的优先级数值:

  1. kernel priority = 100 - 1 - param.__sched_priority 

如果应用层传入数值 0,那么在内核中优先级数值就是 99(100 - 1 - 0 = 99),在所有实时任务中,它的优先级是最低的。

如果应用层传输数值 99,那么在内核中优先级数值就是 0(100 - 1 - 99 = 0),在所有实时任务中,它的优先级是最高的。

因此,从应用层的角度看,传输人优先级数值越大,线程的优先级就越高;数值越小,优先级就越低。

与内核角度是完全相反的!

2. 对于普通任务

调整普通任务的优先级,是通过 nice 值来实现的,内核中也有一个公式来把应用层传入的 nice 值,转成内核角度的优先级数值:

  1. kernel prifoity = 100 + 20 + nice 

nice 的合法数值是:-20 ~ 19。

如果应用层设置线程 nice 数值为 -20,那么在内核中优先级数值就是 100(100 + 20 + (-20) = 100),在所有的普通任务中,它的优先级是最高的。

如果应用层设置线程 nice 数值为 19,那么在内核中优先级数值就是 139(100 +20 +19 = 139),在所有的普通任务中,它的优先级是最低的。

因此,从应用层的角度看,传输人优先级数值越小,线程的优先级就越高;数值越大,优先级就越低。

与内核角度是完全相同的!

背景知识交代清楚了,终于可以进行代码测试了!

[[405789]]

测试代码说明

注意点:

  1. #define _GNU_SOURCE 必须在 #include 之前定义;
  2. #include 必须在 #include 之前包含进来;
  3. 这个顺序能够保证在后面设置继承的 CPU 亲和性时,CPU_SET, CEPU_ZERO这两个函数能被顺利定位到。

  1. // filename: test.c 
  2. #define _GNU_SOURCE 
  3. #include <unistd.h>   
  4. #include <stdio.h> 
  5. #include <stdlib.h> 
  6. #include <sched.h> 
  7. #include <pthread.h> 
  8.  
  9. // 用来打印当前的线程信息:调度策略是什么?优先级是多少? 
  10. void get_thread_info(const int thread_index) 
  11.     int policy; 
  12.     struct sched_param param; 
  13.  
  14.     printf("\n====> thread_index = %d \n", thread_index); 
  15.  
  16.     pthread_getschedparam(pthread_self(), &policy, &param); 
  17.     if (SCHED_OTHER == policy) 
  18.         printf("thread_index %d: SCHED_OTHER \n", thread_index); 
  19.     else if (SCHED_FIFO == policy) 
  20.         printf("thread_index %d: SCHED_FIFO \n", thread_index); 
  21.     else if (SCHED_RR == policy) 
  22.         printf("thread_index %d: SCHED_RR \n", thread_index); 
  23.  
  24.     printf("thread_index %d: priority = %d \n", thread_index, param.sched_priority); 
  25.  
  26. // 线程函数, 
  27. void *thread_routine(void *args) 
  28.     // 参数是:线程索引号。四个线程,索引号从 1 到 4,打印信息中使用。 
  29.     int thread_index = *(int *)args; 
  30.      
  31.     // 为了确保所有的线程都创建完毕,让线程睡眠1秒。 
  32.     sleep(1); 
  33.  
  34.     // 打印一下线程相关信息:调度策略、优先级。 
  35.     get_thread_info(thread_index); 
  36.  
  37.     long num = 0; 
  38.     for (int i = 0; i < 10; i++) 
  39.     { 
  40.         for (int j = 0; j < 5000000; j++) 
  41.         { 
  42.             // 没什么意义,纯粹是模拟 CPU 密集计算。 
  43.             float f1 = ((i+1) * 345.45) * 12.3 * 45.6 / 78.9 / ((j+1) * 4567.89); 
  44.             float f2 = (i+1) * 12.3 * 45.6 / 78.9 * (j+1); 
  45.             float f3 = f1 / f2; 
  46.         } 
  47.          
  48.         // 打印计数信息,为了能看到某个线程正在执行 
  49.         printf("thread_index %d: num = %ld \n", thread_index, num++); 
  50.     } 
  51.      
  52.     // 线程执行结束 
  53.     printf("thread_index %d: exit \n", thread_index); 
  54.     return 0; 
  55.  
  56. void main(void) 
  57.     // 一共创建四个线程:0和1-实时线程,2和3-普通线程(非实时) 
  58.     int thread_num = 4; 
  59.      
  60.     // 分配的线程索引号,会传递给线程参数 
  61.     int index[4] = {1, 2, 3, 4}; 
  62.  
  63.     // 用来保存 4 个线程的 id 号 
  64.     pthread_t ppid[4]; 
  65.      
  66.     // 用来设置 2 个实时线程的属性:调度策略和优先级 
  67.     pthread_attr_t attr[2]; 
  68.     struct sched_param param[2]; 
  69.  
  70.     // 实时线程,必须由 root 用户才能创建 
  71.     if (0 != getuid()) 
  72.     { 
  73.         printf("Please run as root \n"); 
  74.         exit(0); 
  75.     } 
  76.  
  77.     // 创建 4 个线程 
  78.     for (int i = 0; i < thread_num; i++) 
  79.     { 
  80.         if (i <= 1)    // 前2个创建实时线程 
  81.         { 
  82.             // 初始化线程属性 
  83.             pthread_attr_init(&attr[i]); 
  84.              
  85.             // 设置调度策略为:SCHED_FIFO 
  86.             pthread_attr_setschedpolicy(&attr[i], SCHED_FIFO); 
  87.              
  88.             // 设置优先级为 51,52。 
  89.             param[i].__sched_priority = 51 + i; 
  90.             pthread_attr_setschedparam(&attr[i], &param[i]); 
  91.              
  92.             // 设置线程属性:不要继承 main 线程的调度策略和优先级。 
  93.             pthread_attr_setinheritsched(&attr[i], PTHREAD_EXPLICIT_SCHED); 
  94.              
  95.             // 创建线程 
  96.             pthread_create(&ppid[i], &attr[i],(void *)thread_routine, (void *)&index[i]); 
  97.         } 
  98.         else        // 后两个创建普通线程 
  99.         { 
  100.             pthread_create(&ppid[i], 0, (void *)thread_routine, (void *)&index[i]); 
  101.         } 
  102.          
  103.     } 
  104.  
  105.     // 等待 4 个线程执行结束 
  106.     for (int i = 0; i < 4; i++) 
  107.         pthread_join(ppid[i], 0); 
  108.  
  109.     for (int i = 0; i < 2; i++) 
  110.         pthread_attr_destroy(&attr[i]); 

编译成可执行程序的指令:

  1. gcc -o test test.c -lpthread 

脑残测试开始

首先说一下预期结果,如果没有预期结果,那其他任何问题都压根不用谈了。

一共有 4 个线程:

  1. 线程索引号 1和2:是实时线程(调度策略是 SCHED_FIFO,优先级是 51,52);
  2. 线程索引号 3和4:是普通线程(调度策略是 SCHED_OTHER, 优先级是 0);

我的测试环境是:Ubuntu16.04,是一台安装在 Windows10 上面的虚拟机。

我期望的结果是:

  1. 首先打印 1 号和 2 号这两个线程的信息,因为它俩是实时任务,需要优先被调度;
  2. 1 号线程的优先级是 51,小于 2 号线程的优先级 52,因此应该是 2 号线程结束之后,才轮到 1 号线程执行;
  3. 3 号和 4 号线程是普通进程,它俩需要等到 1 号和 2 号线程全部执行结束之后才开始执行,并且 3 号和 4 号线程应该是交替执行,因为它俩的调度策略和优先级都是一样的。

我满怀希望的在工作电脑中测试,打印结果如下:

  1. ====> thread_index = 4  
  2. thread_index 4: SCHED_OTHER  
  3. thread_index 4: priority = 0  
  4.  
  5. ====> thread_index = 1  
  6. thread_index 1: SCHED_FIFO  
  7. thread_index 1: priority = 51  
  8.  
  9. ====> thread_index = 2  
  10. thread_index 2: SCHED_FIFO  
  11. thread_index 2: priority = 52  
  12. thread_index 2: num = 0  
  13. thread_index 4: num = 0  
  14.  
  15. ====> thread_index = 3  
  16. thread_index 3: SCHED_OTHER  
  17. thread_index 3: priority = 0  
  18. thread_index 1: num = 0  
  19. thread_index 2: num = 1  
  20. thread_index 4: num = 1  
  21. thread_index 3: num = 0  
  22. thread_index 1: num = 1  
  23. thread_index 2: num = 2  
  24. thread_index 4: num = 2  
  25. thread_index 3: num = 1 
  26.  
  27. 后面打印内容不用输出了,因为前面已经出现了问题。 

 问题很明显:为什么 4 个线程为什么被同时执行了?

1 号和 2 号这两个线程应该被优先执行啊,因为它俩是实时任务!

怎么结果是这个样子?彻底凌乱了,一点都不符合预期!

想不出个所以然,只能求助网络!但是没有找到有价值的线索。

其中有一个信息涉及到 Linux 系统的调度策略,这里记录一下。

Linux 系统中,为了不让实时任务彻底占据 CPU 资源,会让普通任务有很小的一段时间缝隙来执行。

在目录 /proc/sys/kernel 下面,有 2 个文件,用来限制实时任务占用 CPU 的时间:

  • sched_rt_runtime_us: 默认值 950000 sched_rt_period_us: 默认值 1000000

意思是:在 1000000 微秒(1秒)的周期内,实时任务占用 950000 微秒(0.95秒),剩下的 0.05 秒留给普通任务。

如果没有这个限制的话,假如某个 SCHED_FIFO 任务的优先级特别高,恰巧出了 bug:一直占据 CPU 资源不放弃,那么我们压根就没有机会来 kill 掉这个实时任务,因为此时系统无法调度其他的任何进程来执行。

而有了这个限制呢,我们就可以利用这 0.05 秒的执行时间,来 kill 掉有 bug 的那个实时任务。

回到正题:资料上说,如果实时任务没有被优先调度,可以把这个时间限制删掉就可以了。方法是:

  1. sysctl -w kernel.sched_rt_runtime_us=-1 

我照做之后,依旧无效!

换一台虚拟机,继续测试

难道是电脑环境的问题吗?于是,把测试代码放到另一台笔记本里的虚拟机 Ubuntu14.04 里测试。

编译的时候,有一个小问题,提示错误:

  1. error: ‘for’ loop initial declarations are only allowed in C99 mode 

只要把编译指令中添加 C99 标准就可以了:

  1. gcc -o test test.c -lpthread -std=c99 

执行程序,打印信息如下:

  1. ====> thread_index = 2  
  2.  
  3. ====> thread_index = 1  
  4. thread_index 1: SCHED_FIFO  
  5. thread_index 1: priority = 51  
  6. thread_index 2: SCHED_FIFO  
  7. thread_index 2: priority = 52  
  8. thread_index 1: num = 0  
  9. thread_index 2: num = 0  
  10. thread_index 2: num = 1  
  11. thread_index 1: num = 1  
  12. thread_index 2: num = 2  
  13. thread_index 1: num = 2  
  14. thread_index 2: num = 3  
  15. thread_index 1: num = 3  
  16. thread_index 2: num = 4  
  17. thread_index 1: num = 4  
  18. thread_index 2: num = 5  
  19. thread_index 1: num = 5  
  20. thread_index 2: num = 6  
  21. thread_index 1: num = 6  
  22. thread_index 2: num = 7  
  23. thread_index 1: num = 7  
  24. thread_index 2: num = 8  
  25. thread_index 1: num = 8  
  26. thread_index 2: num = 9  
  27. thread_index 2: exit  
  28.  
  29. ====> thread_index = 4  
  30. thread_index 4: SCHED_OTHER  
  31. thread_index 4: priority = 0  
  32. thread_index 1: num = 9  
  33. thread_index 1: exit  
  34.  
  35. ====> thread_index = 3  
  36. thread_index 3: SCHED_OTHER  
  37. thread_index 3: priority = 0  
  38. thread_index 3: num = 0  
  39. thread_index 4: num = 0  
  40. thread_index 3: num = 1  
  41. thread_index 4: num = 1  
  42. thread_index 3: num = 2  
  43. thread_index 4: num = 2  
  44. thread_index 3: num = 3  
  45. thread_index 4: num = 3  
  46. thread_index 3: num = 4  
  47. thread_index 4: num = 4  
  48. thread_index 3: num = 5  
  49. thread_index 4: num = 5  
  50. thread_index 3: num = 6  
  51. thread_index 4: num = 6  
  52. thread_index 3: num = 7  
  53. thread_index 4: num = 7  
  54. thread_index 3: num = 8  
  55. thread_index 4: num = 8  
  56. thread_index 3: num = 9  
  57. thread_index 3: exit  
  58. thread_index 4: num = 9  
  59. thread_index 4: exit 

1 号和 2 号线程同时执行,完毕之后,再 3 号和 4 号线程同时执行。

但是这同样也不符合预期:2 号线程的优先级比 1 号线程高,应该优先执行才对!

不知道应该怎么查这个问题了,想不出思路,只好请教 Linux 内核的大神,建议检查一下内核版本。

这时,我才想起来在 Ubuntu16.04 这台虚拟机上因为某种原因,降过内核版本。

往这个方向去排查了一下,最后确认也不是内核版本的差异导致的问题。

比较结果,寻找差异

只好再回过头来看一下这两次次打印信息的差异:

  1. 工作电脑里的 Ubuntu16.04 中:4 个线程同时调度执行,调度策略和优先级都没有起作用;
  2. 笔记本里的 Ubuntu14.04 中:1 号和 2 号实时任务被优先执行了,说明调度策略起作用了,但是优先级没有起作用;

突然, CPU 的亲和性从脑袋里蹦了出来!

紧接着立马感觉到问题出在哪里了:这TMD大概率就是多核引起的问题!

[[405790]]

于是我把这 4 个线程都绑定到 CPU0 上去,也就是设置 CPU 亲和性。

在线程入口函数 thread_routine 的开头,增加下面的代码:

  1. cpu_set_t mask; 
  2. int cpus = sysconf(_SC_NPROCESSORS_CONF); 
  3. CPU_ZERO(&mask); 
  4. CPU_SET(0, &mask); 
  5. if (pthread_setaffinity_np(pthread_self(), sizeof(mask), &mask) < 0) 
  6.     printf("set thread affinity failed! \n"); 

然后继续在 Ubuntu16.04 虚拟机中验证,打印信息很完美,完全符合预期:

  1. ====> thread_index = 1  
  2.  
  3. ====> thread_index = 2  
  4. thread_index 2: SCHED_FIFO  
  5. thread_index 2: priority = 52  
  6. thread_index 2: num = 0  
  7. 。。。 
  8. thread_index 2: num = 9  
  9. thread_index 2: exit  
  10. thread_index 1: SCHED_FIFO  
  11. thread_index 1: priority = 51  
  12. thread_index 1: num = 0  
  13. 。。。 
  14. thread_index 1: num = 9  
  15. thread_index 1: exit  
  16.  
  17. ====> thread_index = 3  
  18. thread_index 3: SCHED_OTHER  
  19. thread_index 3: priority = 0  
  20.  
  21. ====> thread_index = 4  
  22. thread_index 4: SCHED_OTHER  
  23. thread_index 4: priority = 0  
  24. thread_index 3: num = 0  
  25. thread_index 4: num = 0  
  26. 。。。 
  27. thread_index 4: num = 8  
  28. thread_index 3: num = 8  
  29. thread_index 4: num = 9  
  30. thread_index 4: exit  
  31. thread_index 3: num = 9  
  32. thread_index 3: exit 

至此,问题真相大白:就是多核处理器导致的问题!

而且这两台测试的虚拟机,安装的时候分配的 CPU 核心是不同的,所以才导致打印结果的不同。

真相大白

最后,再确认一下这 2 个虚拟机中的 CPU 信息:

Ubuntu 16.04 中 cpuinfo 信息:

  1. $ cat /proc/cpuinfo  
  2. processor : 0 
  3. vendor_id : GenuineIntel 
  4. cpu family : 6 
  5. model  : 158 
  6. model name : Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz 
  7. stepping : 10 
  8. cpu MHz  : 2807.996 
  9. cache size : 9216 KB 
  10. physical id : 0 
  11. siblings : 4 
  12. core id  : 0 
  13. cpu cores : 4 
  14. 。。。其他信息 
  15.  
  16. processor : 1 
  17. vendor_id : GenuineIntel 
  18. cpu family : 6 
  19. model  : 158 
  20. model name : Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz 
  21. stepping : 10 
  22. cpu MHz  : 2807.996 
  23. cache size : 9216 KB 
  24. physical id : 0 
  25. siblings : 4 
  26. core id  : 1 
  27. cpu cores : 4 
  28. 。。。其他信息 
  29.  
  30. processor : 2 
  31. vendor_id : GenuineIntel 
  32. cpu family : 6 
  33. model  : 158 
  34. model name : Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz 
  35. stepping : 10 
  36. cpu MHz  : 2807.996 
  37. cache size : 9216 KB 
  38. physical id : 0 
  39. siblings : 4 
  40. core id  : 2 
  41. cpu cores : 4 
  42. 。。。其他信息 
  43.  
  44. processor : 3 
  45. vendor_id : GenuineIntel 
  46. cpu family : 6 
  47. model  : 158 
  48. model name : Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz 
  49. stepping : 10 
  50. cpu MHz  : 2807.996 
  51. cache size : 9216 KB 
  52. physical id : 0 
  53. siblings : 4 
  54. core id  : 3 
  55. cpu cores : 4 
  56. 。。。其他信息 

在这台虚拟机中,正好有 4 个核心,而我的测试代码正好也创建了 4 个线程,于是每个核心被分配一个线程,一个都不闲着,同时执行。

因此打印信息中显示 4 个线程是并行执行的。

这个时候,什么调度策略、什么优先级,都不起作用了!(准确的说:调度策略和优先级,在线程所在的那个 CPU 中是起作用的)

如果我在测试代码中,一开始就创建 10 个线程,很可能会更快发现问题!

再来看看笔记本电脑里虚拟机 Ubuntu14.04 的 CPU 信息:

  1. $ cat /proc/cpuinfo  
  2. processor   : 0 
  3. vendor_id   : GenuineIntel 
  4. cpu family  : 6 
  5. model       : 142 
  6. model name  : Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz 
  7. stepping    : 9 
  8. microcode   : 0x9a 
  9. cpu MHz     : 2304.000 
  10. cache size  : 4096 KB 
  11. physical id : 0 
  12. siblings    : 2 
  13. core id     : 0 
  14. cpu cores   : 2 
  15. 。。。其他信息 
  16.  
  17.  
  18. processor   : 1 
  19. vendor_id   : GenuineIntel 
  20. cpu family  : 6 
  21. model       : 142 
  22. model name  : Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz 
  23. stepping    : 9 
  24. microcode   : 0x9a 
  25. cpu MHz     : 2304.000 
  26. cache size  : 4096 KB 
  27. physical id : 0 
  28. siblings    : 2 
  29. core id     : 1 
  30. cpu cores   : 2 
  31. 。。。其他信息 

在这台虚拟机中,有 2 个核心,于是 2 个实时任务 1 号和 2 号被优先执行(因为是 2 个核心同时执行,所以这 2 个任务的优先级也就没什么意义了),结束之后,再执行 3 号和 4 号线程。

再思考一下

这一圈测试下来,真的想用键盘敲自己的脑袋,怎么就没有早点考虑到多核的因素呢?!

深层的原因:

  1. 之前的很多项目,都是 ARM、mips、STM32等单核情况,思维定式让我没有早点意识到多核这个屏体因素;
  2. 做过的一些 x86 平台项目,并没有涉及到实时任务这样的要求。一般都是使用系统默认的调度策略,这也是 Linux x86 作为通用电脑,在调度策略上所关注的重要指标:让每一个任务都公平的使用 CPU 资源。

随着 x86 平台在工控领域的逐渐应用,实时性问题就显得更突出、更重要了。

所以才有了 Windows 系统中的 intime,Linux 系统中的 preempt、xenomai 等实时补丁。

本文转载自微信公众号「 IOT物联网小镇」,可以通过以下二维码关注。转载本文请联系 IOT物联网小镇公众号。

 

责任编辑:姜华 来源: IOT物联网小镇
相关推荐

2020-06-04 08:36:55

Linux内核线程

2021-04-23 21:40:33

Python优先级调度器

2021-04-06 10:45:18

React前端优先级

2023-01-05 08:48:57

技术管理排优先级

2012-08-14 09:38:29

WAN优化

2023-11-03 08:22:09

Android系统算法

2021-02-02 14:55:48

React前端高优先

2019-07-31 15:14:33

2024-10-10 10:32:04

2010-09-01 14:10:36

CSS优先级

2023-11-16 09:30:27

系统任务

2022-12-23 09:41:14

优先级反转

2013-12-24 13:59:03

2019-11-01 12:36:14

人工智能机器学习技术

2020-03-10 07:51:35

面试讽刺标准

2023-03-29 09:36:32

2018-01-15 14:50:49

APP转让App账号

2020-09-30 09:07:37

DevOps

2010-03-18 14:09:20

Java线程同步

2024-03-11 07:46:40

React优先级队列二叉堆
点赞
收藏

51CTO技术栈公众号