限制多对多类型的资源访问

时间:2018-12-18 19:55:27

标签: c multithreading thread-safety posix starvation

免责声明:此帖子包含对以下答案的修改,所有功劳归其各自所有者所有。

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

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

我还想考虑到,每当另一种类型想要使用资源时,它都必须等待轮换,其他类型也必须等待使用完资源。

编辑:我试图使用 @ Nominal-Animal 的解决方案,但似乎有时也会出现这种僵局。此外,我在结构中添加了缺少的转弯。现在,我还有其他问题:

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

现在,输入一些代码:

#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进行编译。

2 个答案:

答案 0 :(得分:2)

考虑以下对John Bollingers answer的扩展注释。

描述的规则OP不完整。例如,考虑以下情况:您有三个带有资源的黑线程,一个白线程正在等待资源,而另一个黑线程到达时希望抢占资源。应该怎么办?如果黑线程总是获得资源,那么黑(或白)线程可能会饿死其他类型的线程。如果在可能的情况下立即将所有权更改为另一种类型,那么我们将失去同类型线程间并发的大部分好处。如果传入线程类型的分布大致均匀,则可能一次只运行一种类型的线程,所有线程依次运行!

有几种可能的解决方案。似乎适合OP的问题说明的一个问题是,允许最多 N black 条黑线与资源一起运行,如果有等待,则切换到白条回合。在切换到黑人之前,最多使用 N white 白色线程来运行资源。 (一个时间限制,一个宽限期,当同一类型的其他线程也可能抢占资源时,可能是您实际在实践中使用的时间。)

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

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,但当没有参赛者也没有服务员时,转弯将发生变化,因此没有关系。如果初始的{{1 }}是1。)

让我们首先释放bwlock,因为它是更有趣的部分。它是控制转弯变化的因素。假设lock和release都有一个black参数(为0或1)。如果释放线程是其颜色的最后一个,而另一种颜色的线程正在等待,它将改变转弯,并在另一种颜色isblack的条件变量上广播以唤醒它们:

wait

抓住bwlock比较复杂。该限制仅在没有其他类型的线程正在等待时才适用(因为如果使用单一颜色的线程,则没有其他颜色的线程将死锁)。

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;
}

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(&(bwl->wait[black]), &(bwl->lock))处理以下情况:特定颜色的最后一个运行线程应将“ baton”处理到另一个线程集。 bwlock_unlock()确定线程是可以使用资源运行还是需要等待。仅当没有其他颜色的线正在运行或等待转弯时,才会更改转弯。

这是一个非常简单的方案,但是您可能需要考虑几种情况以了解其行为。

当转弯更改时,bwlock_lock()计数器将清除,并且在该转弯期间启动的每个线程都会递增。当它到达started时,将不再启动该类型的线程。他们将等待轮到他们。

比方说limitlimit,您有每种类型的四个线程,并且基本上所有线程都同时冲向bwlock。假设抓锁的第一个线程是黑色。前三个黑线程将与资源一起运行,一个黑线程和四个白线程将在条件变量上等待。当转弯改变时,三个白线开始运转;一个白色,一个随机的白色,将等到下一个白色回合。

而且,正如Craig Estey在对John Bollingers答复的评论中指出的那样,这不能保证相同类型线程之间的公平性。例如,如果A和B属于同一类型,则A在回合中多次访问受bwlock保护的资源,而B仅访问一次,则B可能必须无限期地等待转弯,因为A是“猪”所有插槽。

为了保证公平,我们需要某种票证锁定或有序的等待队列,以便唤醒特定颜色的{3, 3}等待时间最长的线程,而不是随机等待的线程。 / p>

答案 1 :(得分:1)

一般评论

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

一些细节

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

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

尽管实施这些更改需要进行一些重新设计,但结果在概念上更简单,并且实际上更可靠。例如,take_resource()中的问题主要围绕不确定性,即获取互斥锁的不确定性将消失,因为首先只有一个互斥锁涉及该部分。