【问题标题】:Restricting resource access by type for many-to-many按类型限制多对多的资源访问
【发布时间】:2018-12-18 19:55:26
【问题描述】:

免责声明:这篇文章包含对以下答案所做的修改,所有功劳归其各自所有者所有。

我正在尝试实现一个问题,即资源可能被两种类型的线程使用。每种类型可以有更多的线程。 (4 个白色类型的螺纹和 6 个黑色类型的螺纹)。任意数量的黑人可以同时使用该资源。白人也一样。我仍然无法解决这个问题......

我尝试使用互斥锁来实现这一点。我还想考虑此实现可能存在的饥饿问题,因此我决定检查是否已达到某种类型的服务线程数,从而允许其他类型工作。我似乎无法实施最新的。

我还想考虑到,每当其他类型想要使用资源时,它必须等待轮到它,以及其他类型完成使用资源。

编辑:我尝试使用 @Nominal-Animal 的解决方案,但似乎有时也会出现这种死锁。此外,我在结构中添加了缺少的转弯。现在,我有一些额外的问题:

  • 这似乎是正确的,但不起作用,为什么?
  • 为什么bwlock_lock()里面的isBLack参数需要双重否定

现在,一些代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <wait.h>
#include <pthread.h>

#define WHITES 31
#define BLACKS 33
#define TYPES 2
#define W_ID 0
#define B_ID 1

struct bwlock
{
    pthread_mutex_t lock;    /* Protects the rest of the fields */
    pthread_cond_t wait[2];  /* To wait for their turn */
    volatile int waiting[2]; /* Number of threads waiting */
    volatile int running[2]; /* Number of threads running */
    volatile int started[2]; /* Number of threads started in this turn */
    const int limit[2];      /* Maximum number of starts per turn */
    volatile int black;      /* Black threads' turn */
    volatile int turn;       /*The turn */
};

#define BWLOCK_INIT(whites, blacks, turn)                         \
    {                                                             \
        PTHREAD_MUTEX_INITIALIZER,                                \
            {PTHREAD_COND_INITIALIZER, PTHREAD_COND_INITIALIZER}, \
            {0, 0}, {0, 0}, {0, 0}, {whites, blacks}, 0, turn     \
    }

struct bwlock resource = BWLOCK_INIT(4, 5, W_ID);

void bwlock_unlock(struct bwlock *bwl, const int isblack)
{
    const int black = !!isblack; /* 0 if white, 1 if black */

    pthread_mutex_lock(&(bwl->lock));

    /* This thread is no longer using the resource. */
    bwl->running[black]--;

    /* Was this the last of this color, with others waiting? */
    if (bwl->running[black] <= 0 && bwl->waiting[!black])
    {
        /* Yes. It's their turn. */
        if (bwl->turn == black)
        {
            bwl->turn = !black;
            /* Clear their started counter. */
            bwl->started[!black] = 0;
        }
        /* Wake them all up. */
        pthread_cond_broadcast(&(bwl->wait[!black]));
    }

    pthread_mutex_unlock(&(bwl->lock));
}

void bwlock_lock(struct bwlock *bwl, const int isblack)
{
    const int black = !!isblack; /* 0 if white, 1 if black */

    pthread_mutex_lock(&(bwl->lock));
    while (1)
    {

        /* No runners or waiters of the other color? */
        if (!(bwl->waiting[!black] < 1) && bwl->running[!black] < 1)
        {
            /* No; we can run. Does this change the turn? */
            if (bwl->turn != black)
            {
                bwl->turn = black;
                /* Clear started counter. */
                bwl->started[black] = 0;
            }
            break;
        }

        /* Still our turn, and not too many started threads? */
        if (bwl->turn == black && bwl->started[black] < bwl->limit[black])
            break;

        /* We must wait. */
        bwl->waiting[black]++;
        pthread_cond_wait(&(bwl->wait[black]), &(bwl->lock));
        bwl->waiting[black]--;
    }

    bwl->started[black]++;
    bwl->running[black]++;

    pthread_mutex_unlock(&(bwl->lock));
}

typedef struct
{
    int thread_id;
    char *type;
    int type_id;
} data;

void use_resource(int thread_id, char *type)
{
    printf("U: Thread %d of type %s is using the resource!\n", thread_id, type);
}

void take_resource(int thread_id, char *type, int type_id)
{
    printf("W:Thread %d of type %s is trying to get the resource!\n", thread_id, type);
    bwlock_lock(&resource, type_id);
    printf("W:Thread %d of type %sB got resource!\n", thread_id, type);
}

void release_resource(int thread_id, char *type, int type_id)
{
    bwlock_unlock(&resource, type_id);
    printf("R:Thread %d of type %s has released the resource!\n", thread_id, type);
}

void *doWork(void *arg)
{
    data thread_data = *((data *)arg);

    int thread_id = thread_data.thread_id;
    char *type = thread_data.type;
    int type_id = thread_data.type_id;
    take_resource(thread_id, type, type_id);
    use_resource(thread_id, type);
    release_resource(thread_id, type, type_id);

    return NULL;
}

data *initialize(pthread_t threads[], int size, char *type, int type_id)
{
    data *args = malloc(sizeof(data) * size);
    for (int i = 0; i < size; i++)
    {
        args[i].type = type;
        args[i].thread_id = i;
        args[i].type_id = type_id;
        pthread_create(&threads[i], NULL, doWork, (void **)&args[i]);
    }
    return args;
}

void join(pthread_t threads[], int size)
{
    for (int i = 0; i < size; i++)
    {
        pthread_join(threads[i], NULL);
    }
}

int main()
{
    pthread_t whites[WHITES];
    pthread_t blacks[BLACKS];
    char *white = "WHITE";
    char *black = "BLACK";
    data *w_args = initialize(whites, WHITES, white, W_ID);
    data *b_args = initialize(blacks, BLACKS, black, B_ID);

    join(whites, WHITES);
    join(blacks, BLACKS);

    free(w_args);
    free(b_args);

    return 0;
}

这是使用gcc -g -o ba blacks_whites.c -Wall -Wextra -pthread 编译的。

【问题讨论】:

  • 让我确保我有这个直。您的几个线程分为两组,以一个属性为特征,我们将其两个可能的值标记为“黑色”和“白色”。对托管资源的访问权限是按组获取的,因此在任何给定时间,每个资源要么归黑线程所有,要么归白线程所有,或者可供获取。对吗?
  • 此外,除了当前的所有权信息之外,您还希望维护每个资源的使用计数,以防止任一线程组独占任何资源。
  • 你有一个错误。在take_resource 中,如果(例如)not_your_turn 返回 false,while 子句中的其他函数将被 not 调用(短路评估),release_locks 将尝试执行 pthread_mutex_unlock被获取的互斥体上(例如comparison_mutexstatus_mutex)。
  • 另外,您可能有竞争条件。在take_resource 中,您执行current[type_id]++; 没有 持有任何锁,因此您可能在另一个线程中与others_not_released 竞争。
  • 我感觉你有一个潜在的僵局,但没有时间去调查/证明它。遵循下面的一些示例,因为您的最新代码必须在与条件变量关联的互斥锁上执行pthread_mutex_lock使用该互斥锁调用pthread_cond_wait(即)pthread_cond_wait期望 要锁定的互斥体,可以 [在内部] 释放它,并返回锁定的互斥体

标签: c multithreading thread-safety posix starvation


【解决方案1】:

考虑以下对John Bollingers answer 的扩展评论。

OP 描述的规则不完整。例如,考虑这样一种情况,您有三个黑色线程与资源,一个白色线程等待资源,另一个黑色线程到达并希望获取资源。应该发生什么?如果黑色线程总是获取资源,那么黑色(或白色)线程可能会饿死其他类型的线程。如果所有权在可能的情况下立即更改为其他类型,我们将失去跨相同类型线程的并发性的大部分好处;如果传入线程类型的分布大致均匀,则可能一次只运行一种类型的一个线程,所有线程都是顺序的!

有几种可能的解决方案。似乎符合 OP 问题陈述的一个是允许最多 Nblack 个黑色线程与资源一起运行,然后再切换到轮到白人(如果有的话);最多有 Nwhite 个白色线程与资源一起运行,然后再切换到黑色。 (时间限制,即同一类型的其他线程也可能抢占资源的宽限期,可能是您在实践中实际使用的。)

我们可以用下面的结构来描述这种锁:

struct bwlock {
    pthread_mutex_t    lock;        /* Protects the rest of the fields */
    pthread_cond_t     wait[2];     /* To wait for their turn */
    volatile int       waiting[2];  /* Number of threads waiting */
    volatile int       running[2];  /* Number of threads running */
    volatile int       started[2];  /* Number of threads started in this turn */
    const int          limit[2];    /* Maximum number of starts per turn */
    volatile int       black;       /* Black threads' turn */
};
#define BWLOCK_INIT(whites, blacks) \
    { PTHREAD_MUTEX_INITIALIZER, \
      { PTHREAD_COND_INITIALIZER, PTHREAD_COND_INITIALIZER }, \
      { 0, 0 }, { 0, 0 }, { 0, 0 }, { whites, blacks }, 0 \
    }

lock 互斥锁仅在检查字段时保留,而不是在资源访问期间。

(还要注意虽然black初始为0,但当没有跑步者和服务员时,轮到会改变,所以没关系。如果初始black为1,代码将完全一样.)

我们先看释放bwlock,因为它是更有趣的部分;它是控制转弯变化的东西。假设 lock 和 release 都有一个 isblack 参数(0 或 1)。如果释放线程是它的最后一个颜色,而另一个颜色的线程正在等待,它会改变轮次,并在另一个颜色上广播wait条件变量来唤醒它们:

void bwlock_unlock(struct bwlock *bwl, const int isblack)
{
    const int black = !!isblack;  /* 0 if white, 1 if black */

    pthread_mutex_lock(&(bwl->lock));

    /* This thread is no longer using the resource. */
    bwl->running[black]--;

    /* Was this the last of this color, with others waiting? */
    if ((bwl->running[black] <= 0) && (bwl->waiting[!black] > 0)) {
        /* Yes. It's their turn. */
        if (bwl->black == black) {
            bwl->black = !black;
            /* Clear their started counter. */
            bwl->started[!black] = 0;
        }
        /* Wake them all up. */
        pthread_cond_broadcast(&(bwl->wait[!black]));
    }

    pthread_mutex_unlock(&(bwl->lock));
    return;
}

抓取 bwlock 更复杂。该限制仅适用于没有其他类型的线程等待的情况(因为如果我们这样做,单一颜色的线程会在没有其他颜色的情况下死锁)。

void bwlock_lock(struct bwlock *bwl, const int isblack)
{
    const int  black = !!isblack; /* 0 if white, 1 if black */

    pthread_mutex_lock(&(bwl->lock));
    while (1) {

        /* No runners or waiters of the other color? */
        if ((bwl->waiting[!black] < 1) && (bwl->running[!black] < 1)) {
            /* No; we can run. Does this change the turn? */
            if (bwl->black != black) {
                bwl->black = black;
                /* Clear started counter. */
                bwl->started[black] = 0;
            }
            break;
        }

        /* Still our turn, and not too many started threads? */
        if ((bwl->black == black) && (bwl->started[black] < bwl->limit[black]))
            break;

        /* We must wait. */
        bwl->waiting[black]++;
        pthread_cond_wait(&(bwl->wait[black]), &(bwl->lock));
        bwl->waiting[black]--;
    }

    bwl->started[black]++;
    bwl->running[black]++;

    pthread_mutex_unlock(&(bwl->lock));
}

pthread_cond_wait(&amp;(bwl-&gt;wait[black]), &amp;(bwl-&gt;lock)) 释放锁并等待条件变量上的信号或广播。收到信号后,它将在返回之前重新获取锁。 (在释放锁和等待条件变量时没有竞争窗口;它以原子方式有效地在同一时间点发生。)

如果您考虑上面的逻辑,bwlock_unlock() 会处理特定颜色的最后一个运行线程应该处理另一个线程集的“接力棒”的情况。 bwlock_lock() 确定线程是否可以使用资源运行,或者需要等待。只有当没有其他颜色的线程在运行或等待轮到它们时,它才会改变轮到。

这是一个相当简单的方案,但您可能需要考虑几种情况才能理解其行为。

started 计数器在轮次更改时被清除,并为在该轮次中启动的每个线程递增。当它到达limit 时,不会再启动该类型的线程;他们将等待轮到他们。

假设limit{3, 3},每种类型都有四个线程,它们基本上都是同时冲向bwlock。假设第一个获取锁的线程是黑色的。前三个黑色线程将与资源一起运行,一个黑色和四个白色线程将等待条件变量。当轮换时,三个白色线程开始运行;一个白色,一个随机的,将重新等待到下一个白色转弯。

此外,正如 Craig Estey 在对 John Bollingers 回答的评论中指出的那样,这并不能保证同一类型线程之间的公平性。例如,如果 A 和 B 属于同一类型,A 在其回合中多次访问受 bwlock 保护的资源,而 B 只访问一次,则 B 可能必须无限期地等待才能获得回合,因为 A “贪婪”它的所有插槽。

为了保证公平,我们需要某种票证锁或有序等待队列,这样我们就可以唤醒特定颜色的 limit 等待时间最长的线程,而不是随机等待的线程。

【讨论】:

  • 您说得对,先生。我知道一些可能的死锁,但似乎我未能正确发布它们。尽管如此,我更新了代码并在其他地方卡住了。我会从你的帖子中学到尽可能多的东西,但我不会偷东西。
  • 您似乎忘记了结构内部的转弯。
  • 您似乎也忘记了 bwl->waiting[!black] running[!black]
  • @Cyber​​Fox:是的;谢谢你的指点。现在应该修好了。
  • 我确实花了一些时间思考如何解决公平问题。这并不是微不足道的,而是使用waiting 计数器作为票证(这样那些获得小于limit 的计数器值的人就可以开始了,而其他人只需在唤醒时将其计数器值减少limit,并为下一次广播睡觉)应该可以工作。在实践中我没有尝试过,因为这是一个奇怪的用例,我不需要考虑这样的方案,所以我不能确定;但我相信这应该可以确保公平,假设limit 始终是常数。
【解决方案2】:

一般cmets

  1. sleep() 几乎不是同步问题的正确解决方案。
  2. 互斥体只提供互斥。如果您需要更多的线程间协调,那么您应该选择不同类型的同步对象(有时信号量更适合),或者添加另一种类型(条件变量通常是互斥锁的伴侣)。
  3. 将互斥锁和解锁拆分为不同的功能是有问题的。即使您必须这样做,至少更愿意设置一对平衡的函数来执行锁定和解锁。

一些细节

你有太多的互斥体,这给你带来了麻烦。我认为没有必要为您维护的不同类型的资源管理元数据维护单独的互斥锁。您最多需要两个互斥锁:一个用于保护组访问元数据(turnpendingcurrentserved),可能还有一个用于保护资源本身。任何线程都没有必要同时持有这两者,但您必须确保在没有适当互斥锁保护的情况下不会访问任何共享数据。

您应该使用条件变量来帮助调解对组访问元数据的访问。当轮到它们的组时,线程将在 CV 上等待,而不是休眠。这将与保护组访问元数据的互斥锁自然集成。

虽然实施这些更改需要重新设计,但结果将在概念上更简单,在实践上更强大。例如,take_resource() 中围绕已获取哪些互斥锁的不确定性的问题将消失,因为一开始该部分将只涉及一个互斥锁。

【讨论】:

  • CV 不保证公平[因为,IIRC,当一个 CV 被释放时,所有等待它的线程都被释放,并且他们争先恐后地获取它]。我发现对促进/保证公平有用的是“票证锁定”:en.wikipedia.org/wiki/Ticket_lock。使用非常轻量级的stdatomic.h 最容易实现这一点。但是,它可以使用 [单独的] 互斥体来实现。或者,只需获取票证锁来代替任何其他互斥体。
  • 同意,@CraigEstey,简历本身并不能确保公平。但是,OP 似乎已经在维护旨在提供公平性的元数据,我假设他们将继续在 mutex + CV 的上下文中使用这种方案。
猜你喜欢
  • 2013-08-28
  • 2011-10-14
  • 2011-05-03
  • 2018-01-04
  • 1970-01-01
  • 1970-01-01
  • 2020-04-27
  • 2012-11-03
  • 2016-10-13
相关资源
最近更新 更多