如何正确使用sched_yield()?

How to use sched_yield() properly?

对于一个作业,我需要使用sched_yield()来同步线程。我知道互斥 lock/conditional 变量会更有效,但我不允许使用它们。

我们唯一可以使用的函数是 sched_yield()pthread_create()pthread_join()。我们不能使用互斥量、锁、信号量或任何类型的共享变量。

我知道 sched_yield() 应该放弃对线程的访问,以便另一个线程可以 运行。所以它应该将它执行的线程移到 运行ning 队列的后面。

下面的代码应该按顺序打印 'abc',然后在所有三个线程都执行完后换行。我在函数 b()c() 中循环了 sched_yield() 因为它没有像我预期的那样工作,但我很确定所做的一切都是延迟打印,因为函数是 运行宁了这么多次,不是因为sched_yield()在工作。

它需要 运行 的服务器有 16 个 CPU。我在某处看到 sched_yield() 可能会立即将线程分配给新的 CPU.

基本上我不确定如何仅使用 sched_yield() 来同步这些线程,因为我可以在网上找到所有内容并进行故障排除。

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <sched.h>

void* a(void*);
void* b(void*);
void* c(void*);

int main( void ){
    pthread_t a_id, b_id, c_id;
    pthread_create(&a_id, NULL, a, NULL);
    pthread_create(&b_id, NULL, b, NULL);
    pthread_create(&c_id, NULL, c, NULL);

    pthread_join(a_id, NULL);
    pthread_join(b_id, NULL);
    pthread_join(c_id, NULL);

    printf("\n");

    return 0;
}

void* a(void* ret){
    printf("a");
    return ret;
}

void* b(void* ret){
    for(int i = 0; i < 10; i++){
        sched_yield();
    }
    printf("b");
    return ret;
}

void* c(void* ret){
    for(int i = 0; i < 100; i++){
        sched_yield();
    }
    printf("c");
    return ret;
}

有4个案例:

a) 调度程序不使用多路复用(例如,不使用“循环”但使用“可以 运行 执行 运行 的最高优先级线程”,或“最早截止日期优先” ", 或 ...) 并且 sched_yield() 什么都不做。

b) 调度程序在理论上确实使用了多路复用,但是您的 CPU 多于线程,因此多路复用实际上并没有发生,并且 sched_yield() 什么都不做。 注意:使用 16 个 CPU 和 2 个线程,这可能是您在 OS 上获得的“默认调度策略”,例如 Linux - sched_yield() 只是做了一个“Hrm,没有其他线程我可以使用这个 CPU,所以我猜调用线程可以继续使用相同的 CPU!”)。

c) 调度程序确实使用了多路复用,并且线程数超过了 CPUs,但是为了提高性能(避免任务切换),调度程序设计者决定 sched_yield() 什么也不做。

d) sched_yield() 确实会导致任务切换(将 CPU 交给其他任务),但这不足以自行进行任何类型的同步(例如,你会需要一个原子变量或其他用于实际同步的东西 - 可能像“while( atomic_variable_not_set_by_other_thread ) { sched_yield(); }”。请注意,使用原子变量(在 C11 中引入)它可以在没有 sched_yield() 的情况下工作 - sched_yield()(如果它做了什么)只会减少忙碌等待 awful/wasteful.

Essentially I'm unsure of how, using only sched_yield(), to synchronize these threads given everything I could find and troubleshoot with online.

那是因为 sched_yield() 不太适合这项任务。正如我在评论中所写,sched_yield() 是关于调度,而不是同步。两者之间存在某种关系,从某种意义上说,同步事件会影响哪些线程有资格使用 运行,但这与您的需求方向不符。

您可能从错误的角度看待问题。您需要您的每个线程等待执行,直到轮到它们执行,并且为了让它们执行此操作,它们需要某种机制来在它们之间传递有关轮到谁的信息。有几种选择,但是如果“仅 sched_yield()”被认为意味着除了 sched_yield() 之外没有库函数可以用于该目的,那么共享变量似乎是预期的选择。因此,起点应该是如何使用共享变量使线程以适当的顺序轮流。

错误的起点

这是一种可能 spring 立即想到的天真的方法:

/* FLAWED */
void *b(void *data){
    char *whose_turn = data;

    while (*whose_turn != 'b') {
        // nothing?
    }

    printf("b");
    *whose_turn = 'c';

    return NULL;
}

也就是说,线程执行一个繁忙的循环,监视共享变量等待它取一个表示线程应该继续的值。当它完成它的工作时,线程修改变量以指示下一个线程可以继续。但这有几个问题,其中包括:

  1. 假设至少有一个其他线程写入由 *whose_turn 指定的对象,程序包含数据竞争,因此其行为是未定义的。实际上,一旦进入该函数的循环体的线程可能会无限循环,而不管其他线程的任何操作。

  2. 在不对线程调度做出额外假设(例如公平策略)的情况下,假设将对共享变量进行所需修改的线程将在有限时间内进行调度是不安全的。

  3. 当一个线程正在执行该函数中的循环时,它会阻止任何其他线程在同一核心上执行,但在其他线程采取行动之前它无法取得进展。就我们可以假设抢占式线程调度而言,这是一个效率问题并且有助于 (2)。但是,如果我们假设既没有抢占式线程调度也没有在单独的核心上调度线程,那么这就是死锁的邀请。

可能的改进

在 pthreads 程序中执行此操作的常规且最合适的方法是使用互斥锁和条件变量。正确实施,解决了数据竞争(问题 1),并确保其他线程有机会 运行(问题 3)。如果除了将修改共享变量的线程之外没有其他线程符合 运行 的条件,那么它也解决了问题 2,在某种程度上假定调度程序完全授予进程任何 CPU .

但是你不能这样做,那还有什么办法呢?那么,您可以使共享变量 _Atomic。这将解决数据竞争问题,并且在实践中它可能足以满足所需的线程排序。然而,原则上,它并没有解决问题 3,实际上,它不使用 sched_yield()。此外,所有这些忙循环都是浪费。

但是等等!你有一个线索,告诉你使用 sched_yield()。那能为你做什么?假设您在繁忙循环的主体中插入对 sched_yield() 的调用:

/* (A bit) better */
void* b(void *data){
    char *whose_turn = data;

    while (*whose_turn != 'b') {
        sched_yield();
    }

    printf("b");
    *whose_turn = 'c';

    return NULL;
}

这解决了问题 2 和 3,明确为其他线程提供了 运行 的可能性,并将调用线程放在调度程序线程列表的尾部。形式上,它没有解决问题 1,因为 sched_yield() 没有记录对内存排序的影响,但在实践中,我不认为它可以在没有(完整的)内存屏障的情况下实现。如果您被允许使用原子对象,那么将原子共享变量与 sched_yield() 组合在一起将勾选所有三个框。然而,即便如此,仍然会有一堆浪费的忙循环。

最后的评论

请注意 pthread_join() 是一个同步函数,因此,据我了解该任务,您可能不会使用它来确保最后打印主线程的输出。

另请注意,我还没有谈到需要如何修改 main() 函数以支持我建议的方法。为此需要进行更改,并将它们留作练习。