Threading.Lock на Python: атомарное изменение общего словаря

Напишите решение на Python и проверьте его на странице.

Другие упражнения по этой теме: Multiprocessing и threading Python: задачи на Queue, Lock и остановку.

Атомарное списание из кассеты

В расчётном модуле банкомата сразу несколько рабочих потоков параллельно обрабатывают списания купюр разных номиналов. Управление остатками ведётся в общем словаре баланса кассет, где для каждой из них фиксируется текущее количество банкнот и ведётся список аудита выполненных операций.

Каждое списание состоит из нескольких шагов: сначала нужно проверить доступный остаток, затем уменьшить его на требуемую величину и занести идентификатор операции в аудиторский список. Если выполнять эти действия без взаимного исключения, конкурирующие потоки могут допустить овердрафт кассеты или перезаписать чужие транзакции. Чтобы сохранить целостность данных, обновление баланса защищают с помощью примитива threading.Lock.

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

Что уже дано

При выполнении кода доступны структура CashRegister, а также вспомогательные функции make_cash_register(initial, shared_lock=False) и observe_cash_debits(initial, operations, shared_lock=False) для локальной проверки списаний. Экземпляр CashRegister содержит:

  • balances — отображение идентификаторов кассет на их целочисленные балансы банкнот;
  • audit — отображение идентификаторов кассет на списки строк с идентификаторами успешных операций;
  • locks — отображение идентификатора кассеты на соответствующий объект блокировки со стандартным интерфейсом threading.Lock.

Вам предстоит реализовать функцию try_cash_debit.

Что нужно сделать

Напишите функцию try_cash_debit(register, cassette, quantity, operation_id) -> bool.

Параметры функции:

  • register — разделяемый реестр CashRegister, содержащий от 0 до 8 кассет;
  • cassette — непустая строка ASCII длиной до 30 символов, гарантированно присутствующая среди ключей реестра;
  • quantity — целое число запрашиваемых купюр от 1 до 100 000;
  • operation_id — уникальный непустой строковый идентификатор операции длиной до 64 символов.

Правила списания:

  • Все действия по проверке остатка, изменению баланса и регистрации операции должны выполняться строго под защитой блокировки, закреплённой за данной кассетой в register.locks[cassette].
  • Если текущий остаток в register.balances[cassette] больше или равен quantity, операция считается успешной: остаток уменьшается на quantity, строка operation_id добавляется методом append() в список аудита register.audit[cassette], а функция возвращает True.
  • Если доступных купюр меньше quantity, баланс кассеты и список её аудита остаются без изменений, а функция возвращает False.
  • Функция работает непосредственно с переданным общим объектом register. Не допускается возникновение отрицательного баланса (овердрафта) при одновременной работе до 16 конкурентных потоков.

Ввод и вывод

Стандартные потоки ввода stdin и вывода stdout в решении не используются: считывать ввод и вызывать print() не нужно. После выполнения кода учащегося проверяющая система считывает из пространства имён модуля функцию верхнего уровня try_cash_debit и вызывает её с проверочными аргументами.

Пример использования

register = make_cash_register({'R50': 3})
print(try_cash_debit(register, 'R50', 2, 'op1'))
print(try_cash_debit(register, 'R50', 2, 'op2'))
print(dict(register.balances))
print({key: list(items) for key, items in register.audit.items()})

Вывод:

True
False
{'R50': 1}
{'R50': ['op1']}
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Связанные задачи