Как предотвратить доступ к критическому разделу с помощью atomics в C++

У меня есть программа, в которой 4 потока выполняют случайные транзакции между двумя банковскими счетами, при каждой 100-й транзакции на счет начисляются проценты в размере 5% на счет.

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

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

это часть кода, где я проверяю, является ли это сотой транзакцией

void transfer(Account& acc, int amount, string& thread)
{
    ++m_iCounter;

    withdraw(acc, amount);
    if (m_iCounter % 100 == 0)
    {
        addInterest(thread);
    }
}

Все потоки проходят через эту функцию и проверяют % 100, а иногда более чем один поток проскальзывает в addInterest. С помощью мьютексов я мог бы ограничить доступ здесь, но как мне это сделать с атомарным?

Может быть, я мог бы решить это как-то внутри addInterest, но если да, то как? Внутри addInterest находится следующий код (если несколько раз добавляется более 1 заинтересованного потока):

void addInterest(string& thread)
{
    float old = m_iBalance.load();
    const float interest = 0.05f;
    const float amount = old * interest;

    while (!m_iBalance.compare_exchange_weak(old, old + amount))
    {
        //
    }
    ++m_iInterest;
    cout << thread << " interest : Acc" << name << " " << m_iCounter << endl;
}

person Jeekim    schedule 09.10.2020    source источник


Ответы (2)


Одна проблема заключается в том, что вы увеличиваете и читаете счетчик отдельно, и из-за этого addInterest может вызываться несколько раз. Чтобы исправить это, вы должны писать и читать атомарно.

const int newCounter = ++m_iCounter;

Добавление интереса:

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

Если вы должны добавить проценты к балансу, который у вас был во время 100-й транзакции (даже если он уже изменился, когда этот поток достигает addInterest), вы должны каким-то образом сохранить старый баланс, прежде чем увеличивать счетчик. Единственный способ, который я могу придумать, - это синхронизировать весь transfer, используя атомарный флаг в качестве замены мьютекса:

// Member variable
std::atomic_bool flag;

// Before the critical section
bool expected;
do {
    expected = false;
} while (!flag.compare_exchange_weak(expected, true));

// Critical section here
    
// Unlock at the end
flag = false;
person vll    schedule 09.10.2020
comment
Благодарю вас! const int newCounter = ++m_iCounter; имеет большой смысл! Теперь я избавился от этой проблемы с несколькими записями. Я на самом деле думаю, что со мной все будет в порядке, если я не буду начислять проценты на точный баланс 100-й транзакции ... однако, если я хочу сделать addInterest с балансом, который был на 100-й транзакции, может ли эта идея работать: после проверки% 100 загрузить значение m_iBalance в переменную и передать его в качестве аргумента в addInterest? в конце начисление процентов это суммирование, остаток * 0,05 и потом сложение. Если я зафиксирую значение, которое я могу добавить немного позже, это сработает? - person Jeekim; 09.10.2020
comment
@Jeekim Это именно то, что вы делаете сейчас (нет разницы во времени, загружаете ли вы баланс перед вызовом addInterest или в начале). Это ненадежно, потому что после того, как вы загрузили значение счетчика, другой поток может выполнить другую транзакцию, прежде чем вы загрузите баланс. Таким образом, вам придется запретить другим потокам выполнять транзакции, пока вы не загрузите баланс. - person vll; 12.10.2020
comment
Кроме того, что, если вы находитесь в процессе добавления процентов, но другие потоки выполняют еще 100 транзакций, прежде чем они будут завершены? Тогда следующий вызов addInterest загрузит неправильный баланс. Это приводит к необходимости блокировки всего transfer. - person vll; 12.10.2020
comment
Для вашего упражнения, вероятно, достаточно объяснить недостатки в комментариях. - person vll; 12.10.2020

Хорошо 2 возможное решение:

  • первый - использовать шаблон atomic, который находится в заголовке <atomic>, в этом случае вы можете сделать хотя бы m_iCounter атомарной переменной. но для лучшей атомарности операции все переменные в критической секции должны быть атомарными. Подробнее читайте на Atomic и Атомарный шаблон.

  • второй вариант - использовать экспериментальную функцию С++, так называемую STM (Software Transactional Memory). В этом случае все содержимое функции transfer может быть транзакцией или, в любом случае, всеми вещами, которые вы ранее мьютексировали. Дополнительная литература Транзакционная память c++

person Zig Razor    schedule 09.10.2020
comment
m_iCounter — это atomic‹int›, но как его использовать для решения проблемы? Я пытался модифицировать передачу таким образом, чтобы m_iCounter проходил через CAS, но это не помогает, все еще делается несколько процентов. - person Jeekim; 09.10.2020
comment
также переменная Account и amount должна быть атомарной, возможно, также m_iBalance - person Zig Razor; 09.10.2020