19

C语言陷阱与技巧第47节,有些工作线程比较重要,如何为其指定优先级?

 3 years ago
source link: https://blog.popkx.com/c%E8%AF%AD%E8%A8%80%E9%99%B7%E9%98%B1%E4%B8%8E%E6%8A%80%E5%B7%A7%E7%AC%AC47%E8%8A%82-%E6%9C%89%E4%BA%9B%E5%B7%A5%E4%BD%9C%E7%BA%BF%E7%A8%8B%E6%AF%94%E8%BE%83%E9%87%8D%E8%A6%81/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

经过前面几节的介绍,相信读者在C语言程序开发中,进行多线程编程已经不是什么难事了。以 Linux 下的开发来说,其实就是 pthread 库的应用而已。

多个线程是“同时运行”的吗?

在之前的文章中,我们提到在较大的C语言项目中,为了不阻塞主逻辑,比较耗时的任务一般都是以线程的形式运行在后台的。但是,不知道读者想过没有,“后台”常常会有不止一个任务线程运行,操作系统是如何协调这些后台任务线程的呢?

编写下面这段C语言代码,测试后台两个线程是如何运行的,同样的,为了尽量简洁的讨论主题,以下代码并没有做错误处理。

86846d7c550a254e01076c2658dd0d62.png

线程函数 thread_1() 和 thread_2() 的动作是一样的,都是向终端打印 10 次“thread_x is running...”信息。在 main() 函数中创建这两个线程后,后台就有两个线程运行了,程序会输出什么呢?

编译并执行这段C语言代码,得到如下输出,请看:

# gcc t.c -lpthread
# ./a.out 
thread_2 is running, cnt: 9
thread_2 is running, cnt: 8
thread_1 is running, cnt: 9
thread_1 is running, cnt: 8
thread_1 is running, cnt: 7
thread_2 is running, cnt: 7
thread_1 is running, cnt: 6
...

线程函数 thread_1() 和 thread_2() 的输出并没有什么规律性,看起来像是二者同时运行的,是不是呢?其实这要分情况,如果上面这段C语言程序运行在多核 CPU 系统中,两个线程的确是有机会同时运行的。

不过读者应该明白,一个 CPU 同时只能做一件事情,如果上述C语言程序运行在单核系统中,是不是两个线程就没有办法交替输出信息了呢?读者可自己做实验,应该会发现,即使是单核系统,只要 delay() 的延时恰当,thread_1() 和 thread_2() 仍然会交替输出信息。

这其实就是操作系统的功能之一了。大多操作系统都是可以管理和协调多个任务的,例如 Linux 通常会尽量保证每个线程都有机会运行,如果系统的核心数多于线程数,这当然没什么问题。

但是对于单核系统,同时只能有一个线程运行,Linux 只能让多个任务轮流使用 CPU,所以这种情况下,thread_1() 和 thread_2() 并不是严格意义上的“同时运行”,实际上它们是交替运行的,只不过这一“交替过程”比较快,人通常察觉不到,所以看起来就像是二者同时运行一样。

C语言程序中的线程优先级

在实际的C语言程序开发中,后台运行的线程常常并不是完全独立等价的,更多的情况是一些线程依赖另外一些线程,例如数据处理线程需要获得采集线程抓取到的数据,才能进行数据处理。再比如,存储线程通常比查询线程更重要,因为数据如果没有及时存储就有可能丢失,而查询则延迟一会也不会丢失数据。

这种情况下,我们更希望的是重要线程使用 CPU 的权利更大,甚至希望重要线程能够具备抢占不重要线程 CPU 的能力,而不是重要线程和普通线程同等共享 CPU,那该怎么做呢?

读者请看 pthread_create() 函数的C语言原型:

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

之前创建线程时,我们传递给第二个参数 attr 的是 NULL,其实该参数可以指定本次创建线程的类型,以及优先级等属性,线程的优先级越高,对 CPU 的使用权也就越大。

以 Linux 为例,操作系统协调任务进程轮流运行这一过程通常称作进程调度。Linux 的实时调度策略通常有三种:SCHED_FIFO,SCHED_RR 以及默认的 SCHED_NORMAL。

处于可运行状态的 SCHED_FIFO 进程会比任何 SCHED_NORMAL 的进程都先得到调度。一旦一个 SCHED_FIFO 级的进程处于可执行状态,就会一直运行,除非执行完毕或者它自己主动让出 cpu,否则就只有优先级更高的 SCHED_FIFO 和 SCHED_RR 级进程才能抢占它。

SCHED_RR 调度策略与 SCHED_FIFO 调度策略总体相同,只不过 SCHED_RR 调度策略也使用时间片,SCHED_RR级进程消耗完自己的时间片时,由同优先级的其他实时进程抢占。

SCHED_FIFO 和 SCHED_RR 调度策略,高优先级的进程总是立刻抢占低优先级的进程。低优先级进程不会抢占 SCHED_RR 进程,即使它的时间片已经使用完毕。

总而言之,SCHED_RR 与 SCHED_FIFO 通常比默认的 SCHED_NORMAL 的线程优先级高,现在我们编写下面的C语言代码测试之,请看:

int main()
{
    pthread_t pid1, pid2;

    pthread_attr_t attr = {};
    struct sched_param spm = {};

    pthread_attr_init(&attr);
    pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
    pthread_attr_setschedpolicy(&attr, SCHED_RR);

    spm.sched_priority = 20;
    pthread_attr_setschedparam(&attr, &spm);

    pthread_create(&pid1, NULL, thread_1, NULL);
    usleep(10000);
    pthread_create(&pid2, &attr, thread_2, NULL);

    pthread_join(pid1, NULL);
    pthread_join(pid2, NULL);
    return 0;
}
b6f0ebcd16ee4cf9b8479c0aa7ffbce2.png

上述C语言代码创建线程 thread_2() 的时候,指定调度类型为 SCHED_RR,并指定其优先级为 20。在创建 thread_1() 和 thread_2() 之间调用了 usleep(10000),这确保 thread_1() 在运行过程中,thread_2() 才被创建。那么这段C语言代码会输出什么呢?

编译并执行这段C语言代码,可以得到如下输出,请看:

# gcc t.c -lpthread
# ./a.out 
thread_1 is running, cnt: 9
thread_1 is running, cnt: 8
thread_1 is running, cnt: 7
thread_1 is running, cnt: 6
thread_1 is running, cnt: 5
thread_2 is running, cnt: 9
thread_2 is running, cnt: 8
thread_2 is running, cnt: 7
thread_2 is running, cnt: 6
thread_2 is running, cnt: 5
thread_2 is running, cnt: 4
thread_2 is running, cnt: 3
thread_2 is running, cnt: 2
thread_2 is running, cnt: 1
thread_2 is running, cnt: 0
thread_1 is running, cnt: 4
thread_1 is running, cnt: 3
thread_1 is running, cnt: 2
thread_1 is running, cnt: 1
thread_1 is running, cnt: 0

显然,从输出可以看出,由于 thread_2() 的优先级更高,所以在被激活后,它抢占了 thread_1() 的 CPU 使用权,并且在其运行完毕之前,thread_1() 没有机会运行。

另外需要说明的是,上述C语言代码中的 main() 函数为线程 thread_2() 指定了 PTHREAD_EXPLICIT_SCHED 标志:

pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);

因为如果不指定这个标志,thread_2() 会继承父线程的调度优先级,在有些平台中,后面为其指定的 SCHED_RR 策略以及优先级,都会被忽略。感兴趣的读者可以试试不指定这个标志,程序会输出什么。

此外,C语言程序设定 thread_2() 线程优先级时,传递给 sched_priority 是硬编码的 20,那么 sched_priority 的取值范围是多少呢?可以调用下面这两个函数获得,请看示例C语言代码:

printf("max priority: %d\n", sched_get_priority_max(SCHED_RR));
printf("min priority: %d\n", sched_get_priority_min(SCHED_RR));

编译上述C语言代码后执行,可得到如下输出:

max priority: 99
min priority: 1

可见,在我的机器上,SCHED_RR 调度策略的线程最低优先级是 1,最高优先级是 99,读者可以将 thread_1() 也改为 SCHED_RR,并指定高于 20 和低于 20 的优先级,对比最终C语言程序的输出。

因为资源是有限的,所以在C语言程序中,多个线程有时其实并不是严格意义上的“同时运行”,常常是在操作系统协调写“轮流运行”的。线程工作的重要程度往往也是不同的,因此在C语言程序开发中,应该根据重要程度为其指定优先级。

不过应该注意不能滥用线程优先级机制,否则可能会降低整个C语言程序的效率。请注意本节的例子,为 thread_2() 指定 SCHED_RR 后,即使在 thread_2() delay 阶段,thread_1() 也是没有运行的,这其实降低了 CPU 的使用效率。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK