Связать мьютекс с объектом
Учитывая следующий пример кода:
int var;
int mvar;
std::mutex mvar_mutex;
void f(){
mvar_mutex.lock();
mvar = var * var;
mvar_mutex.unlock();
}
Я хочу выразить, что mvar_mutex
привязан к переменной mvar
и защищает только эту переменную. mvar_mutex
не должен защищать var
, потому что он не связан с ним. Следовательно, компилятору будет разрешено преобразовать вышеуказанный код в следующий код:
int var;
int mvar;
std::mutex mvar_mutex;
void f(){
int r = var * var; //possible data race created if binding is not known
mvar_mutex.lock();
mvar = r;
mvar_mutex.unlock();
}
Это может привести к сокращению конкуренции на блокировке, поскольку при ее проведении выполняется меньше работы.
Для int
это можно сделать с помощью std::atomic<int> mvar;
и удаления mvar_mutex
, но для других типов, таких как std::vector<int>
, это невозможно.
Как выразить привязку переменной mutex в способе, которым компиляторы С++ понимают это и делают оптимизацию? Должно быть разрешено переупорядочивать любую переменную вверх или вниз по границам мьютекса для любой переменной, которая не связана с этим мьютексом
Поскольку код генерируется с использованием clang::ASTConsumer
и clang::RecursiveASTVisitor
, я готов использовать нестандартные расширения и манипуляции с АСТ, если clang (идеально clang 4.0) поддерживает их, и полученный код не должен быть элегантным или читаемым человеком.
Изменить, поскольку это, по-видимому, вызывает путаницу: вышеуказанное преобразование не является законным в С++. Описанная привязка мьютекса к переменной не существует. Вопрос заключается в том, как реализовать это или добиться того же эффекта.
Ответы
Ответ 1
Если вы хотите достичь того, что std::mutex
будет удерживаться только до тех пор, пока на защищенном объекте не будет выполнена операция, вы можете написать обертку class
следующим образом:
#include <cstdio>
#include <mutex>
template<typename T>
class LockAssignable {
public:
LockAssignable& operator=(const T& t) {
std::lock_guard<std::mutex> lk(m_mutex);
m_protected = t;
return *this;
}
operator T() const {
std::lock_guard<std::mutex> lk(m_mutex);
return m_protected;
}
/* other stuff */
private:
mutable std::mutex m_mutex;
T m_protected {};
};
inline int factorial(int n) {
return (n > 1 ? n * factorial(n - 1) : 1);
}
int main() {
int var = 5;
LockAssignable<int> mvar;
mvar = factorial(var);
printf("Result: %d\n", static_cast<int>(mvar));
return 0;
}
В приведенном выше примере factorial
будет вычисляться заранее, а m_mutex
будет получен только тогда, когда вызов или оператор неявного преобразования вызывается на mvar
.
Сборочный вывод
Ответ 2
Для примитивных типов данных вы можете использовать std::atomic
с std::memory_order_relaxed
.
В документации указано, что:
нет ограничений синхронизации или порядка, наложенных на другие читает или пишет, гарантируется только эта операция атомарности
В следующем примере атомарность присваивания гарантирована, но компилятор должен иметь возможность перемещать операции.
std::atomic<int> z = {0};
int a = 3;
z.store(a*a, std::memory_order_relaxed);
Для объектов я думал о нескольких решениях, но:
- Нет стандартного способа удаления требований к порядку из
std::mutex
.
- Невозможно создать
std::atomic<std::vector>
.
- Невозможно создать спин-блокировку с помощью
std::memory_order_relaxed
(см. пример).
Я нашел несколько ответов, в которых указано, что:
- Если функция не видна в блоке компиляции, компилятор генерирует барьер, потому что он не знает, какие переменные он использует.
- Если функция видна и есть мьютекс, компилятор создает барьер.
Например, см. this и .
Итак, чтобы выразить, что mvar_mutex привязан к переменной, вы можете использовать некоторые классы, как указано в других ответах, но я не думаю, что можно полностью разрешить переупорядочение кода.
Ответ 3
Как насчет заблокированного шаблона var?
template<typename Type, typename Mutex = std::mutex>
class Lockable
{
public:
Lockable(_Type t) : var_(std::move(t));
Lockable(_Type&&) = default;
// ... could need a bit more
T operator = (const T& x)
{
std::lock_guard<Lockable> lock(*this);
var_ = x;
return x;
}
T operator *() const
{
std::lock_guard<Lockable> lock(*this);
return var_;
}
void lock() const { const_cast<Lockable*>(this)->mutex_.lock(); }
void unlock() const { const_cast<Lockable*>(this)->mutex_.unlock().; }
private:
Mutex mutex_;
Type var_;
};
заблокирован оператором присваивания
Lockable<int>var;
var = mylongComputation();
Отлично работает с lock_guard
Lockable<int>var;
std::lock_guard<Lockable<int>> lock(var);
var = 3;
Практическое использование контейнеров
Lockable<std::vector<int>> vec;
и т.д...
Ответ 4
Я хочу выразить, что mvar_mutex привязан к переменной mvar и защищает только эту переменную.
Вы не можете этого сделать. Мьютекс фактически защищает критическую область инструкций машины между приобретением и выпуском. Только соглашение связано с конкретным экземпляром общих данных.
Чтобы избежать ненужных шагов внутри критической области, храните критические области как можно проще. В критической области только с локальными переменными, которые компилятор может "видеть", очевидно, не разделяются с другими потоками и с одним набором общих данных, относящихся к этому мьютексу. Не пытайтесь получить доступ к другим данным в критическом регионе, которые могут подозреваться в совместном использовании.
Если бы у вас была ваша предлагаемая языковая функция, она бы только представила возможность ошибки в программе. Все, что он делает, это взять код, который теперь правильный, и сделать некоторые из них неправильными (в обмен на обещание некоторой скорости: некоторый код остается верным и быстрее, потому что посторонние вычисления выведены из критической области).
Это похоже на выбор языка, который уже имеет хороший порядок оценки, в котором a[i] = i++
четко определен и прикручивает его неуказанным порядком оценки.
Ответ 5
Вы можете использовать folly:: Synchronized, чтобы убедиться, что переменная доступна только под блокировкой:
int var;
folly::Synchronized<int> vmar;
void f() {
*mvar.wlock() = var * var;
}
Ответ 6
Я хочу выразить, что mvar_mutex привязан к переменной mvar и защищает только эту переменную.
Это не то, как работает мьютекс. Он не "связывает" ни с чем, чтобы защитить его. Вы по-прежнему свободны напрямую обращаться к этому объекту, полностью игнорируя любую безопасность потоков.
Что вы должны сделать, так это скрыть "защищенную переменную", чтобы она не была напрямую доступна, и написать интерфейс, который управляет им, который проходит через мьютекс. Таким образом, вы гарантируете, что доступ к базовым данным защищен этим мьютексом. Это может быть один объект, он может быть функциональной группой объектов, он может быть набором многих объектов, мьютексов и атомитов, предназначенных для минимизации блокировки.