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

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

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

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

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

线程函数 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;
}


上述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 的使用效率。

阅读更多:   C语言
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif