Это подходящий вариант использования для рекурсивного мьютекса? - PullRequest
4 голосов
/ 02 октября 2019

Я слышал из разных источников ( 1 , 2 ), что следует избегать использования рекурсивных мьютексов, поскольку это может быть признаком взлома или плохого дизайна. Однако иногда я полагаю, что они могут быть необходимы. В свете этого, является ли следующий подходящий вариант использования для рекурсивного мьютекса?

// main.c
// gcc -Wall -Wextra -Wpedantic main.c -pthread

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif /* _GNU_SOURCE */

#include <assert.h>
#include <pthread.h>
#include <stdlib.h>

typedef struct synchronized_counter
{
    int count;
    pthread_mutex_t mutex;
    pthread_mutexattr_t mutexattr;
} synchronized_counter;

synchronized_counter* create_synchronized_counter()
{
    synchronized_counter* sc_ptr = malloc(sizeof(synchronized_counter));
    assert(sc_ptr != NULL);

    sc_ptr->count = 0;

    assert(pthread_mutexattr_init(&sc_ptr->mutexattr) == 0);
    assert(pthread_mutexattr_settype(&sc_ptr->mutexattr, 
        PTHREAD_MUTEX_RECURSIVE) == 0);

    assert(pthread_mutex_init(&sc_ptr->mutex, &sc_ptr->mutexattr) == 0);

    return sc_ptr;
}

void synchronized_increment(synchronized_counter* sc_ptr)
{
    assert(pthread_mutex_lock(&sc_ptr->mutex) == 0);

    sc_ptr->count++;

    assert(pthread_mutex_unlock(&sc_ptr->mutex) == 0);
}

int main()
{
    synchronized_counter* sc_ptr = create_synchronized_counter();

    // I need to increment this counter three times in succesion without having
    // another thread increment it in between. Therefore, I acquire a lock
    // before beginning.

    assert(pthread_mutex_lock(&sc_ptr->mutex) == 0);

    synchronized_increment(sc_ptr);
    synchronized_increment(sc_ptr);
    synchronized_increment(sc_ptr);

    assert(pthread_mutex_unlock(&sc_ptr->mutex) == 0);

    return 0;
}

EDIT :

Я хотел задать вопрос на простом примере, но, возможно, это выглядело как слишком просто. Это то, что я предполагал, будет более реалистичным сценарием: у меня есть структура данных стека, к которой будут обращаться несколько потоков. В частности, иногда поток будет извлекать n элементов из стека, но он должен делать это все сразу (без того, чтобы другой поток выдвигал или выталкивал из стека между ними). Суть проблемы проектирования в том, чтобы клиент сам управлял блокировкой стека с помощью нерекурсивного мьютекса, или чтобы стек предоставлял синхронизированные, простые методы вместе с рекурсивным мьютексом, который клиент может использовать для выполнения нескольких атомарных транзакций, которыетакже синхронизируются.

Ответы [ 2 ]

4 голосов
/ 02 октября 2019

Оба ваших примера - исходный synchronized_counter и стек в вашем редактировании - являются правильными примерами использования рекурсивного мьютекса, но они будут считаться плохим дизайном API, если вы строите структуру данных. Я попытаюсь объяснить, почему.

  1. Предоставление внутренних данных - вызывающая сторона должна использовать такую ​​же блокировку, которая защищает внутренний доступ к членам структуры данных. Это открывает возможность неправомерного использования этой блокировки в целях, отличных от доступа к структуре данных. Это может привести к конфликту блокировок или, что еще хуже, к тупику.

  2. Эффективность - часто эффективнее выполнять специализированные массовые операции, такие как increment_by(n) или pop_many(n),

    • Во-первых, это позволяет структуре данных оптимизировать операции - возможно, счетчик может просто выполнить count += n или стек может удалить n элементов из связанного списка за одну операцию. [1]
    • Во-вторых, вы экономите время, не блокируя и не разблокируя мьютекс для каждой операции. [2]

Возможно,лучший пример использования рекурсивного мьютекса был бы следующим:

  • У меня есть класс с двумя методами Foo и Bar.
  • Класс был разработан, чтобы быть одно-
  • Иногда Foo вызывает Bar.

Я хочу сделать класс потокобезопасным, поэтому я добавляю мьютекс в класс и блокирую его внутри Foo и Bar. Теперь мне нужно убедиться, что Bar может заблокировать мьютекс при вызове из Foo.

Один из способов решить эту проблему без рекурсивного мьютекса - создать приватный unsynchronized_bar и иметь оба Fooи Bar вызовите его после блокировки мьютекса.

Это может быть сложно, если Foo является виртуальным методом, который может быть реализован подклассом и используется для вызова Bar, или если Foo вызывает какую-то другую часть программы, которая может перезвонить в Bar. Однако, если у вас есть код внутри критических секций (код, защищенный мьютексом), вызывающий другой произвольный код, поведение программы будет трудным для понимания, и легко вызвать взаимные блокировки между различными потоками, даже если вы используетерекурсивный мьютекс.

Лучший совет - решать проблемы параллелизма с помощью хорошего дизайна, а не причудливых примитивов синхронизации.


[1] Существуют некоторые шаблоны трикера, такие как «выскочить элемент, посмотритепри этом решите, если я вставлю еще один ", но это может быть реализовано путем предоставления предиката для массовой операции.

[2] Практически говоря, блокировка мьютекса, который у вас уже есть, должна быть довольно дешевой, но в вашемНапример, для этого требуется как минимум вызов внешней библиотечной функции, которая не может быть встроена.

0 голосов
/ 02 октября 2019

Логика, которую вы описываете, на самом деле не является рекурсивным мьютексом и не является подходящим случаем для одного.

И, если вам действительно нужно убедиться, что другой поток не увеличит ваш счетчикПрошу прощения за то, что ваша логика в том виде, в котором она написана, не гарантирует этого.

Поэтому я предлагаю вам отступить от этого, очистить голову и пересмотреть ваш реальный вариант использования. Я думаю, что замешательство в отношении рекурсивных мьютексов сбило вас с толку. Вполне может быть так, что логика, которую вы сейчас имеете в synchronized_increment ... фактически, необходимость во всем методе ... не нужна, и логика, которую вы показываете в main, это все, что вам действительно нужнои это всего лишь простая переменная.

...