Подтвердить что ты не робот

Связать мьютекс с объектом

Учитывая следующий пример кода:

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) поддерживает их, и полученный код не должен быть элегантным или читаемым человеком.

Изменить, поскольку это, по-видимому, вызывает путаницу: вышеуказанное преобразование не является законным в С++. Описанная привязка мьютекса к переменной не существует. Вопрос заключается в том, как реализовать это или добиться того же эффекта.

4b9b3361

Ответ 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 и защищает только эту переменную.

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

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