LINUX.ORG.RU

Куда лучше помещать блокировки

 ,


2

1

Добрый день.

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

  1. В сам контекст и делать функции lock/unlock внутри модуля при вызове каждой функции. Примерно так
    struct mytype
    {
        mutex_t m;
        /* some data to protect */
    }
    
    void mytype_do_something(mytype *ctx)
    {
        mutex_lock(ctx->m);
        /* do something with data */
        mutex_unlock(ctx->m);
    }
    
  2. В сам контекст, но сделать API функции lock/unlock, которые должен вызывать пользователь модуля
    struct mytype
    {
        mutex_t m;
        /* some data to protect */
    }
    
    void mytype_lock(mytype *ctx)
    {
        mutex_lock(ctx->m);
    }
    
    void mytype_unlock(mytype *ctx)
    {
        mutex_unlock(ctx->m);
    }
    
    void mytype_do_something(mytype *ctx)
    {
        /* do something with data with the assumption that user called mytype_lock() */
    }
    
  3. Возложить ответственность за синхронизацию на пользователя модуля, т.е. мьютекс, как минимум будет определен на уровень выше
    void thread_func(void *user_ctx)
    {
        mytype *ctx = ((thread_ctx *)user_ctx)->ctx;
        mutex_t m = ((thread_ctx *)user_ctx)->m;
        /* ... */
        mutex_lock(m);
        mytype_do_something(ctx);
        mutex_unlock(m);
    }
    
    int main()
    {
        mytype *ctx;
        mutex_t m;
        /* init ctx and mutex */
        /* start threads */
        /* ... */
    }
    

Лично у самого предпочтения идут с конца списка в начало, т.е. сначала 3, потом 2 и меньше всего нравится скрытая блокировка.

Если есть возможность сделать вариант 3, стоит сделать вариант 3.

Не стоит принимать за пользователя решение, как и что он хочет блокировать, если в этом нет необходимости. Подобные ограничения в дизайне часто раздражают. Лучше - потратить время на «дырявую» абстракцию и пояснения в документации к чему и почему нельзя обращаться без синхронизации.

Исключением являются огороженные проприетарные api, где пользователю нужно дать минимум знаний о системе. Или api для пользователей, которым не нужно знать о потоках от слова совсем, как минимум до определённого момента.

pon4ik ★★★★★
()
Ответ на: комментарий от pon4ik

Если есть возможность сделать вариант 3, стоит сделать вариант 3.

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

V1KT0P ★★
()
Ответ на: комментарий от pon4ik

Это аргумент хорошо работает, когда разрабатывается именно библиотека. А если конечный результат это приложение, то отстаивать эту точку труднее, поскольку оппонента появляется аргумент, что используя метод 1 меньше шанс забыть поставить блокировку и стабильность приложения от этого выиграет. У меня в качестве хорошего контраргумента пока только то, что увеличивается шанс взаимоблокировки, поскольку все блокировки скрыты и не всегда легко отследить взаимоотношения между модулями.

Vovka-Korovka ★★★★★
() автор топика
Ответ на: комментарий от V1KT0P

Куда лучше помещать блокировки (комментарий)

+ допустим,потеря производительности при последовательном вызове мьютексов не имеет значения. Есть ли ещё какие-нибудь аргументы?

Vovka-Korovka ★★★★★
() автор топика
Ответ на: комментарий от Vovka-Korovka

+ допустим,потеря производительности при последовательном вызове мьютексов не имеет значения. Есть ли ещё какие-нибудь аргументы?

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

А если конечный результат это приложение, то отстаивать эту точку труднее

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

V1KT0P ★★
()
Ответ на: комментарий от xaizek

Не до конца, но рекурсивные мьютексы, действительно один из аргументов адептов подхода 1. У меня чувство прекрасного сильно протестует против такого применения рекурсивных мьютексов.

Vovka-Korovka ★★★★★
() автор топика
Ответ на: комментарий от Vovka-Korovka

Вариант 4: т.к. это не либа, а приложение, вынести мьютекс вообще наружу, даже в отдельный инклюд(?), так он будет доступен вообще в любом(?) контексте. Забывчивость про инклюд(?) и лок-анлок в каком-то целевом месте — личная ответственность того, кто полез в код, а тесты никто не отменял.

deep-purple ★★★★★
()
Ответ на: комментарий от V1KT0P

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

Такие ситуации в подходе 1 теоретически можно обойти, если использовать определенный порядок захвата блокировок(вызова функций), хотя выглядит это уже не очень, согласен.

Vovka-Korovka ★★★★★
() автор топика
Последнее исправление: Vovka-Korovka (всего исправлений: 1)

Сделать все поля приватными, обращаться к ним через методы, в методах делать блокировку. Глобальных/внешних мутексов избегать как черт ладана. Если класс будет расширяться и в нем будут нужны атомарные вызовы - сделать метод

synchronized_call(Action * action) { lock(); action->perform(); unlock(); }
и впоследствии синхронизированные вызовы реализовывать через него:
abstract class Action {
   void perform();
}

class TrackedCounter {
   int value;
   int call_count;
   Mutex * mutex;
public:
   Counter() {
      value = 0;
      call_count = 0;
      mutex = new Mutex();
   }
   int get_value() {
      mutex.acquire();
      int v = value;
      call_count++;
      mutex.release();
      return v;
   }
   void increase() {
      mutex.acquire();
      value++;
      call_count++;
      mutex.release();
   }
protected:
   void _value() { return value; }
   void _synchronized_call(Action * action) {
      mutex.acquire();
      action->perform();
      mutex.release();
   }
}

class BeepingCounter : TrackedCounter {
   ...
public:
   void beep_if_zero() {
      class BeepAction : public Action {
         if (_value()==0) beep();
      }
      BeepAction beeper = new BeepAction();
      _synchronized_call(beeper);
      delete beeper;
   }
}
Несинхронизированные вызовы сделать protected - тогда можно избежать рекурсивных мутексов.

Nastishka ★★★★★
()
Ответ на: комментарий от Nastishka

Да, много кода - но зато получаются очень точечные блокировки. Можно конечно огрести проблем в случае рекурсии A -> B -> A, но зато снаружи классов больше не надо заботиться о синхронизации.

Nastishka ★★★★★
()
Ответ на: комментарий от V1KT0P

На самом деле, безразлично. Принцип тот же самый остается. Блокировка внутри объекта, работа с блокировкой изолирована в соответствующие функции, вместо интерфейсов - указатели на функции.

Nastishka ★★★★★
()

1.

Но вообще, все варианты говно и ведут к ошибкам даже у внимательных прогеров. Единственный нормальный вариант структурирования многонитевого приложения - это CSP или очереди сообщений.

tailgunner ★★★★★
()
Последнее исправление: tailgunner (всего исправлений: 1)
Ответ на: комментарий от Vovka-Korovka

Так хорошие приложения так и пишутся - делается dsl для предметной области на нужных уровнях абстракции.

Хотя, я понимаю людей которые требуют вариант 1. Одно дело перфекционизм и другое - сделать поддерживаемый код под заданные требования.

В общем случае, и, особенно, для api, явно лучше использовать вариант 3. Если же ты точно знаешь, что те 400ns тебе погоды не сделают, блокировка всегда будет позиксовым мьютексом, когда первые пункты начнут будут иметь влияние, вы уже заработаете свой первый миллиард и у вас хватит денег переписать всё хоть на ассемблере, клиент твоего кода может быть неаккуратен и будет прав, этот список конкретных требований можно продолжить... Лучше выбрать такой вариант, который проще поддерживать.

pon4ik ★★★★★
()
Ответ на: комментарий от pon4ik

В общем случае, и, особенно, для api, явно лучше использовать вариант 3.

Такое впечатление, что тебе платят за консультации по отладке.

tailgunner ★★★★★
()

Первый вариант. Интерфейс взаимодействия с пользователем должен быть минимальным. Торчащие наружу мьютексы, а тем более перекладывание блокировок на пользователя, влечет повышение сложности твоего кода. Где-нибудь обязательно забудут разблокироваться/заблокироваться. Тем более у тебя C и нет RAII, где ты вызываешь гардлок и можешь не париться, т.к. в деструкторе все равно лок освободится.

xpahos ★★★★★
()
Ответ на: комментарий от xpahos

хм, сложность не в плане BigO notation, а читаемость и понимание самого кода.

xpahos ★★★★★
()
Ответ на: комментарий от tailgunner

Но вообще, все варианты говно и ведут к ошибкам даже у внимательных прогеров. Единственный нормальный вариант структурирования многонитевого приложения - это CSP или очереди сообщений.

тут +

десятки раз убеждался в некроспатформенности логики с мутексами(чистыми без либ), и внезапные ошибки всегда напрягают

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

либо пользоваться джаваой/дотнетом

missxu
()
Ответ на: комментарий от missxu

десятки раз убеждался в некроспатформенности логики с мутексами (чистыми без либ)

Без либ вообще всё некроссплатформенное. Мой пойнт в том, что грузить пользователя API проблемами локинга - гарантия ошибок.

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

Наверное, но, точно не больше, чем авторам libc(хотя, конечно, можно сказать «чувак, этож блин говно из 80»).

pon4ik ★★★★★
()
Ответ на: комментарий от pon4ik

точно не больше, чем авторам libc

Какие API в libc требуют самостоятельно захватывать блокировки?

этож блин говно из 80

Из 70-х по сути.

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

Ну, например, то же файловое api - mt-safe, с неявным локом на буфер. Но если ты будешь писать в файл без внешней синхронизации из разных потоков, блоками, то очевидно, ничего хорошего из этого не выйдет. Разве что - будет две блокировки вместо одной. Т.е. имеем реализованный 1, который всё равно требует работы в стиле 3 со своим интерфейсом, в ряде случае, о чём пользователю, ещё нужно сообразить.

И вот тут ты конечно прав - очереди рулят. Но, кмк, обычно на уровень повыше, чем api предоставляющее дескриптор, для работы с каким то объектом.

pon4ik ★★★★★
()
Ответ на: комментарий от pon4ik

если ты будешь писать в файл без внешней синхронизации из разных потоков, блоками, то очевидно, ничего хорошего из этого не выйдет.

Гарантии API libc ты не нарушишь.

Разве что - будет две блокировки вместо одной. Т.е. имеем реализованный 1, который всё равно требует работы в стиле 3 со своим интерфейсом

Точно так же я могу сказать, что, если у тебя под «своим интерфейсом» многонитевость, то ты всё равно вынужден будешь использовать API libc в стиле 1.

И вот тут ты конечно прав - очереди рулят. Но, кмк, обычно на уровень повыше, чем api предоставляющее дескриптор, для работы с каким то объектом.

Конечно. Но автор говорит вообще о приложении, там у него наверняка абстракции более высокого уровня, чем файловый дескриптор.

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

то ты всё равно вынужден будешь использовать API libc в стиле 1

Об этом и речь, что стиль 1 не даёт выбора, как ещё использовать api, что бы не платить лишнего. Это может быть и хорошо и плохо, зависит от цели.

Но автор говорит вообще о приложении, там у него наверняка абстракции более высокого уровня, чем файловый дескриптор.

Возможно. Но судя по интерфейсу, это обычное ООП, того же уровня, что и файловые дескрипторы. В общем, похоже, автор в очередной раз спросил: «что лучше агрегация или композиция?». И тут понятно, композицию проще тестировать, и строить на её основе новые примитивы, а агрегацию проще использовать как есть, давая по рукам, тем кто не согласен с реализацией. Дальше только конкретные требования к решению.

pon4ik ★★★★★
()
Ответ на: комментарий от pon4ik

то ты всё равно вынужден будешь использовать API libc в стиле 1

Об этом и речь, что стиль 1 не даёт выбора

У меня речь о том, что тебе всё равно придется использовать стиль 1.

судя по интерфейсу, это обычное ООП, того же уровня, что и файловые дескрипторы.

Файловые дескрипторы ниже уровнем, чем любое более-менее разумное ООП. Ну и CSP не противоречит ООП.

агрегация или композиция?

Большой разницы нет. Ну, если не сдавать экзамен на знание баззвордов.

tailgunner ★★★★★
()
Последнее исправление: tailgunner (всего исправлений: 1)
Ответ на: комментарий от tailgunner

Файловые дескрипторы ниже уровнем, чем любое более-менее разумное ООП.

У нас видимо сильно разные взгляды на ООП. Чем кроме сахара отличаются конструкции:

write(file, "data");
file.write("data");

?

pon4ik ★★★★★
()
Ответ на: комментарий от pon4ik

file.write(const void *) - это не пример разумного ООП.

И если для тебя типовая безопасность - «сахар» (а похоже на то), говорить дальше бесполезно.

tailgunner ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.