【问题标题】:How to use sched_yield() properly?如何正确使用 sched_yield()?
【发布时间】:2021-09-14 20:36:25
【问题描述】:

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

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

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

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

它需要运行的服务器有 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;
}

【问题讨论】:

  • 除非您为线程/进程使用实时调度程序算法,否则 sched_yield() 未指定,如手册页所述。从设置适当的调度程序开始,这样您就可以从屈服中获得可预测的行为。
  • @Shawn 会怎么做呢?我们被告知只需添加线程并同步它们,所以我不知道它会变得更加复杂
  • 假设您使用的是 Linux,请以 man7.org/linux/man-pages/man7/sched.7.html 开头
  • 如果您被告知使用 sched_yield() 而之前没有讨论调度算法及其适用的算法,那么这对您的班级或教师的质量来说并不好。
  • 顾名思义,sched_yield() 是关于线程调度,而不是同步。它不是同步对象(例如互斥锁)的合适替代品。

标签: c pthreads sched


【解决方案1】:

基本上我不确定如何仅使用 sched_yield() 考虑到我能找到的一切,同步这些线程 在线解决问题。

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

您可能从错误的角度看待问题。您需要每个线程等待执行,直到轮到它们执行为止,而为了让它们这样做,它们需要某种机制来在它们之间传达关于轮到谁的信息。有几种替代方案,但如果“仅sched_yield()”表示除sched_yield() 之外的任何库函数都不能用于该目的,那么共享变量似乎是预期的选择。因此,起点应该是如何使用共享变量使线程以适当的顺序轮流。

有缺陷的起点

这是一种可能会立即浮现在脑海中的幼稚方法:

/* 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() 函数以支持我建议的方法。为此需要进行更改,并将它们留作练习。

【讨论】:

  • 为了清晰起见,我更新了要求。 pthread_joinpthread_create 被明确允许,但据我了解,它们与我遇到的具体问题无关
  • @kdezra:所以你说你可以为a 创建一个线程并使用pthread_join() 等待它完成;然后为b创建一个线程并使用pthread_join()等待它完成;然后 ...?这将是最不疯狂的可能性(一种为完全不应该使用线程的完全顺序程序增加最少开销的方法)。
  • @Brendan 我们被允许创建顺序订单的唯一方法是通过sched_yield()。如果我们可以单独运行线程,分配将非常容易。
  • @kdezra:在那种情况下;我会检查您是否打算假设只有 1 个 CPU(和/或打算将您的进程固定到一个特定的 CPU)。做不到这一点;我会请您的主管/讲师在 Stackoverflow 上查看您的问题的 cmets(以防问题存在缺陷 - 讲师是人,有时会犯错误)。
【解决方案2】:

有4种情况:

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

b) 调度程序在理论上确实使用了多路复用,但是你的 CPU 比线程多,所以多路复用实际上并没有发生,sched_yield() 什么也不做。 注意:对于 16 个 CPU 和 2 个线程,这很可能是您在 Linux 等操作系统上获得“默认调度策略”的结果 - sched_yield() 只是执行“Hrm,没有其他线程我可以使用这个 CPU因为,所以我猜调用线程可以继续使用同一个 CPU!”)。

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

d) sched_yield() 确实会导致任务切换(将 CPU 交给其他任务),但这不足以单独进行任何类型的同步(例如,您需要一个原子变量或用于实际同步 - 可能像“while( atomic_variable_not_set_by_other_thread ) { sched_yield(); }”。请注意,使用原子变量(在 C11 中引入)它可以在没有 sched_yield() 的情况下工作 - sched_yield()(如果它有任何作用)只会让忙碌的等待变得不那么糟糕/浪费。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-09-15
    • 2020-12-04
    • 2012-07-12
    • 2013-01-21
    • 2021-10-24
    • 2011-12-22
    • 2020-10-14
    • 2015-12-10
    相关资源
    最近更新 更多