【问题标题】:Only one thread ever acquiring semaphore只有一个线程获取信号量
【发布时间】:2021-09-30 01:18:23
【问题描述】:

我有一个程序,其中多个线程在一个循环中,它们获取一个二进制信号量,然后增加一个全局计数器。但是,通过打印线程 ID,我注意到只有一个线程获得了信号量。这是我的 MRE:

#include <stdbool.h>
#include <stdio.h>
#include <unistd.h>

#include <pthread.h>
#include <semaphore.h>

#define NUM_THREADS 10
#define MAX_COUNTER 100

struct threadCtx {
    sem_t sem;
    unsigned int counter;
};

static void *
threadFunc(void *args)
{
    struct threadCtx *ctx = args;
    pthread_t self;
    bool done = false;

    self = pthread_self();

    while (!done) {
        sem_wait(&ctx->sem);

        if ( ctx->counter == MAX_COUNTER ) {
            done = true;
        }
        else {
            sleep(1);
            printf("Thread %u increasing the counter to %u\n", (unsigned int)self, ++ctx->counter);
        }

        sem_post(&ctx->sem);
    }

    return NULL;
}

int main() {
    pthread_t threads[NUM_THREADS];
    struct threadCtx ctx = {.counter = 0};

    sem_init(&sem.ctx, 0, 1);

    for (int k=0; k<NUM_THREADS; k++) {
        pthread_create(threads+k, NULL, threadFunc, &ctx);
    }

    for (int k=0; k<NUM_THREADS; k++) {
        pthread_join(threads[k], NULL);
    }

    sem_destroy(&ctx.sem);

    return 0;
}

输出是

Thread 1004766976 increasing the counter to 1
Thread 1004766976 increasing the counter to 2
Thread 1004766976 increasing the counter to 3
...

如果我删除对sleep 的调用,则行为更接近我的预期(即,线程以看似不确定的方式被唤醒)。为什么会这样?

【问题讨论】:

  • 我认为不能保证任何“循环”意义上的调度都是“公平”的。另请注意,您使用获取的信号量调用 sleep(1),这将阻止所有其他调用 sem_wait 的线程。
  • "将阻止所有其他调用 sem_wait 的线程" - 这是故意的。
  • 好的,那么我认为它又回到了“公平”问题上。发布信号量后,您的while 循环立即调用sem_wait。不能保证这两个事件之间会发生上下文切换。
  • @DanielWalker 大多数操作系统会在持续获取资源的线程用完其时间片时切换上下文。时间片大小是为该确切目的而选择的。如果您从不让该线程用完它的时间片,那么该线程可能会继续获取资源。但是,您可以不编写执行您不想完成的工作的线程。
  • @DanielWalker 你说上下文永远不会切换似乎很奇怪。但为什么要这样做?不断获取信号量的线程总是可以向前推进并且永远不会用完它的时间片。什么会阻止它?

标签: c multithreading pthreads semaphore


【解决方案1】:

David Schwartz 的回答解释了低级别发生的事情。也就是说,他是站在操作系统开发者或者硬件设计者的角度来看的。这没什么错,但让我们从软件架构师的角度来看看你的程序:

你有多个线程都在执行同一个循环。循环锁定互斥体,* 它做了一些“工作”,然后释放互斥体。好的,但接下来会做什么?在释放互斥锁后,您的循环所做的几乎接下来的一件事就是再次锁定互斥锁。您的循环几乎 100% 的时间都花在了锁定互斥锁的“工作”上。

那么,当两个或多个线程从来没有有机会同时工作时,在多个线程中运行同一个循环有什么意义呢?

如果你想使用线程进行并行计算,你需要找到/发明安全的方法让线程在互斥锁解锁的情况下完成大部分工作。他们应该只锁定一个互斥锁足够长的时间来发布结果或接受另一项任务。

有时这意味着编写效率低于单线程代码的代码。但是假设程序 (A) 有一个线程,几乎 100% 使用 CPU,而程序 (B) 使用 8 个 CPU,但仅以 50% 的效率使用它们。哪个节目会赢?


* 我知道,您的示例使用 sem_t(信号量)对象。但是“信号量”是您正在使用的什么。 “Mutex”是您使用它的角色

【讨论】:

  • 我明白这一切。正如我在 cmets 中提到的,这个 MRE 就是这样。这不是试图用线程做任何有用或高效的事情。
  • @DanielWalker,好的,我没有阅读您与 John Bollinger 的评论对话。如果只是您阅读此内容,我会删除我的答案,因为它对您没有帮助。但就像我说的那样,您的问题(包括代码示例)与本网站上的许多其他问题非常相似,这些问题来自那些“完全理解”的人。我将把我的答案留在这里,以帮助其他可能正在寻找相同想法但对问题的了解比你少的人。
  • 我投了赞成票,因为它的答案很好,但它也没有提到“量子”、“时间片”或任何其他货物崇拜者。
  • @DanielWalker 好吧,首先,当就绪线程数不大于内核数时,操作系统不需要强制执行任何类型的 CPU 配额/共享。其次,对“quantum”和“timeslice”等术语的普遍且不加限定的使用强化了一种不幸的误解,即定时器中断是唯一导致调度/调度运行的中断,并且 I/O 完成和线程间信号具有在下一次计时器中断之前,对正在运行的线程集没有影响。如果没有线程等待 CPU,并且没有系统调用超时,则可以禁用计时器。
【解决方案2】:

为什么会这样?

上下文切换很昂贵,您的实施明智地减少了它们。您的线程都在争夺相同的资源,试图紧密调度它们会使性能变得更糟,可能对整个系统都是如此。

由于不断获取信号量的线程永远不会用完它的时间片,它将继续获取资源。您有责任编写代码来完成您想要完成的工作。尽可能高效地执行代码是实现的责任,这就是它正在做的事情。

很可能,幕后的情况是这样的:

  1. 不断获取信号量的线程总是可以向前推进,除非它处于休眠状态。但是当它处于休眠状态时,没有其他需要信号量的线程可以向前推进。

  2. 不断获取信号量的线程永远不会耗尽其时间片,因为它会在此之前休眠。

因此,除了在睡眠时,实现没有理由阻塞该线程,这意味着没有其他线程可以获取信号量。如果您不希望该线程继续与信号量一起休眠并阻塞其他线程,请编写不同的代码。

【讨论】:

  • “编写代码来完成你不想完成的工作是你的责任”听起来有点奇怪……那里有错字吗?
  • @JeremyFriesner 谢谢。固定。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-01-27
  • 1970-01-01
  • 2017-03-11
  • 1970-01-01
  • 2010-10-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多