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

Как я могу вернуть фиксированный замок?

Рассмотрим, скажем, набор балансов счетов. И тогда у вас есть сложная функция, которая должна проверять остатки нескольких разных учетных записей, а затем корректировать балансы нескольких разных учетных записей. Операции должны быть атомарными по отношению к другим пользователям коллекции. У вас есть класс коллекции, основная задача которого заключается в обеспечении такого рода атомарности. Какой "правильный" способ?

У меня есть класс, который имеет элемент boost:: mutex. Проблема в том, что вызывающим абонентам может потребоваться выполнить ряд вызовов в классе, удерживая мьютекс. Но я не хочу давать код вне класса, свободного от мьютекса.

То, что я хотел бы сделать, это нечто вроде этого (псевдокод):

class MyClass
{
 private:
  boost::mutex mLock;
 public:
  boost::scoped_lock& ScopedLock(return ScopedLock(mLock));
}

Таким образом, вызывающие могут это сделать:

MyClass f;
if(foo)
{
 boost::scoped_lock lock(f->GetScopedLock());
 f->LockedFunc1();
 f->LockedFunc2();
}

Идея состоит в том, что LockedFunc1 и LockedFunc2 будут вызваны с заблокированной блокировкой. Деструктор для lock разблокирует f->mLock.

У меня есть два основных вопроса:

1) Как я могу это сделать?

2) Является ли это разумным?

Примечание. Это полностью отличается от этого аналогично названного вопроса: вернуть boost:: scoped_lock.

4b9b3361

Ответ 1

Как я могу это сделать?

Альтернатива 1

Один подход заключается в создании типа с boost::scoped_lock:

class t_scope_lock {
public:
  t_scope_lock(MyClass& myClass);
  ...
private:
  boost::scoped_lock d_lock;
};

и MyClass, чтобы предоставить доступ к мьютексам для этого типа. Если этот класс написан специально для MyClass, я бы просто добавил его как внутренний класс MyClass::t_scoped_lock.

Альтернатива 2

Другим подходом было бы создание промежуточного типа для использования с блокировкой области видимости, которая могла бы быть конвертируемой в конструктор блокировки области (пользовательский). Затем типы могут выбирать, как они считают нужным. Многим людям может не нравиться блокировка пользовательской области, но это позволит вам легко указать доступ по вашему желанию и с хорошей степенью контроля.

Альтернатива 3

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

{
 boost::scoped_lock lock(f->GetScopedLock());
 f->LockedFunc1();
 f->LockedFunc2();
}

Альтернатива 4

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

Альтернатива 5

Как и в случае С# 4, в некоторых случаях вы можете использовать рекурсивную или readwrite-блокировку.

Альтернатива 6

Вы можете использовать заблокированный тип-обертку, чтобы выборочно предоставлять доступ к частям интерфейса типа.

class MyClassLockedMutator : StackOnly {
public:
    MyClassLockedMutator(MyClass& myClass);
// ...
    void LockedFunc1() { this->myClass.LockedFunc1(); }
    void LockedFunc2() { this->myClass.LockedFunc2(); }
private:
    MyClass& myClass;
    boost::scoped_lock d_lock; // << locks myClass
};

MyClass f;
MyClassLockedMutator a(f);

a.LockedFunc1();
a.LockedFunc2();

Является ли это разумным?

Имейте в виду, что я понятия не имею, каковы точные ограничения вашей программы (следовательно, несколько альтернатив).

Альтернативы # 1, # 2, # 3 и # 6 (практически) не имеют накладных расходов на производительность и во многих случаях имеют незначительную дополнительную сложность. Они, однако, синтаксически шумны для клиента. IMO, принудительная правильность, которую компилятор может проверить (по мере необходимости), более важен, чем минимизация синтаксического шума.

Альтернативы # 4 и # 5 - это хорошие способы введения дополнительных сбоев/конфликтов или блокировок/одновременных ошибок и ошибок. В некоторых случаях это простая замена, заслуживающая внимания.

Когда правильность, производительность и/или другие ограничения имеют решающее значение, я думаю, что имеет смысл абстрагироваться или инкапсулировать эти сложности, даже если это требует синтаксического шума или уровня абстракции. Я делаю это, потому что слишком легко вносить изменения, даже если я написал и поддерживал всю программу. Для меня это более сложный случай видимости и совершенно разумный, если он используется правильно.

Некоторые примеры

Прокрутите вниз до main - этот пример довольно неорганизован, потому что он демонстрирует несколько подходов в одном:

#include <iostream>
#include <boost/thread.hpp>

class MyClass;

class MyClassOperatorBase {
public:
    /* >> public interface */
    bool bazzie(bool foo);
protected:
    MyClassOperatorBase(MyClass& myClass) : d_myClass(myClass) {
    }

    virtual ~MyClassOperatorBase() {
    }

    operator boost::mutex & ();

    MyClass& getMyClass() {
        return this->d_myClass;
    }

    const MyClass& getMyClass() const {
        return this->d_myClass;
    }

protected:
    /* >> required overrides */
    virtual bool imp_bazzie(bool foo) = 0;
private:
    MyClass& d_myClass;
private:
    /* >> prohibited */
    MyClassOperatorBase(const MyClassOperatorBase&);
    MyClassOperatorBase& operator=(const MyClassOperatorBase&);
};

class MyClass {
public:
    MyClass() : mLock() {
    }

    virtual ~MyClass() {
    }

    void LockedFunc1() {
        std::cout << "hello ";
    }

    void LockedFunc2() {
        std::cout << "world\n";
    }

    bool bizzle(bool foo) {
        boost::mutex::scoped_lock lock(this->mLock);

        return this->imp_bizzle(foo);
    }

protected:
    virtual bool imp_bizzle(bool foo) {
        /* would be pure virtual if we did not need to create it for other tests. */
        return foo;
    }

private:
    class t_scope_lock {
    public:
        t_scope_lock(MyClass& myClass) : d_lock(myClass.mLock) {
        }

    private:
        boost::mutex::scoped_lock d_lock;
    };
protected:
    friend class MyClassOperatorBase;
private:
    boost::mutex mLock;
};

MyClassOperatorBase::operator boost::mutex & () {
    return this->getMyClass().mLock;
}

bool MyClassOperatorBase::bazzie(bool foo) {
    MyClass::t_scope_lock lock(this->getMyClass());

    return this->imp_bazzie(foo);
}

class TheirClassOperator : public MyClassOperatorBase {
public:
    TheirClassOperator(MyClass& myClass) : MyClassOperatorBase(myClass) {
    }

    virtual ~TheirClassOperator() {
    }

    bool baz(bool foo) {
        boost::mutex::scoped_lock lock(*this);

        return this->work(foo);
    }

    boost::mutex& evilClientMove() {
        return *this;
    }

protected:
    virtual bool imp_bazzie(bool foo) {
        return this->work(foo);
    }

private:
    bool work(bool foo) {
        MyClass& m(this->getMyClass());

        m.LockedFunc1();
        m.LockedFunc2();
        return foo;
    }
};

class TheirClass : public MyClass {
public:
    TheirClass() : MyClass() {
    }

    virtual ~TheirClass() {
    }

protected:
    virtual bool imp_bizzle(bool foo) {
        std::cout << "hallo, welt!\n";
        return foo;
    }
};

namespace {
/* attempt to restrict the lock visibility to MyClassOperatorBase types. no virtual required: */
void ExampleA() {
    MyClass my;
    TheirClassOperator their(my);

    their.baz(true);

// boost::mutex::scoped_lock lock(my); << error inaccessible
// boost::mutex::scoped_lock lock(my.mLock); << error inaccessible
// boost::mutex::scoped_lock lock(their); << error inaccessible

    boost::mutex::scoped_lock lock(their.evilClientMove());
}

/* restrict the lock visibility to MyClassOperatorBase and call through a virtual: */
void ExampleB() {
    MyClass my;
    TheirClassOperator their(my);

    their.bazzie(true);
}

/* if they derive from my class, then life is simple: */
void ExampleC() {
    TheirClass their;

    their.bizzle(true);
}
}

int main(int argc, const char* argv[]) {
    ExampleA();
    ExampleB();
    ExampleC();
    return 0;
}

Ответ 2

Предпочтительным решением будет такая атомная функция, как это:

void MyClass::Func_1_2( void )
{
   boost::lock_guard<boost::mutex> lock( m_mutex );
   LockedFunc1();
   LockedFunc2();
}

Возможно, вам придется предоставить несколько дополнительных методов. Принцип: лучше ли скрывать политики блокировки от пользователя. Если вы обнаружите, что нецелесообразно создавать специальные методы, вы можете захотеть переосмыслить свой дизайн, абстрактно на более высоком уровне.

Если у вас есть законные причины, чтобы поддерживать интерфейс так же, скройте данные блокировки за вспомогательными классами. Два примера.

Скрыть блокировку за классом маркера, которая передается методам, требующим блокировки.

MyClass my_class;

{
   LockMyClass locked( my_class );
   myclass.Func1( locked ); // assert or throw if locked is not locking my_class
   myclass.Func2( locked );
}

Создайте заблокированный класс интерфейса, который является другом MyClass:

MyClass my_class;

{
   LockedMyClass locked( my_class );
   locked.Func1(); 
   locked.Func2();
}

Является ли это разумным?

Если вы будете осторожны, это можно сделать, но обычно вы не хотите раскрывать свои данные синхронизации за пределами своего класса. Слишком много проблем, которые могут возникнуть. Sun пробовала аналогичную идею с java.util.Vector, но с тех пор перешла на лучшие методы.

Ответ 3

Вот как я сейчас планирую это сделать. Я сделаю класс ScopedLock, который можно вернуть. Чтобы использовать его, класс должен иметь boost::mutex и возвращать ScopedLock, созданный с помощью этого мьютекса. Вызывающий использует эту функцию для создания своего собственного ScopedLock, а вызывающий объект ScopedLock наследует блокировку, созданную функцией-членом класса.

Указатель безопасен, поскольку ScopedLock не может превышать срок жизни члена класса, функция члена которого вы вызывали для его получения. И вам гарантировано (по логике класса), что будет только одна разблокировка.

Единственная реальная проблема, которую я вижу, - это преднамеренное злоупотребление. Например, если кто-то создал новый ScopedLock с их ScopedLock, в результате чего другой ScopedLock (возможно, с более длительным сроком службы) наследует блокировку, которой он не должен обладать. (Больно, когда я это делаю. Поэтому не делай этого.)

class ScopedLock {
private:
    boost::mutex *mMutex;   // parent object has greater scope, so guaranteed valid
    mutable bool mValid;

    ScopedLock();           // no implementation

public:
    ScopedLock(boost::mutex &mutex) : mMutex(&mutex), mValid(true) {
        mMutex->lock();
    }

    ~ScopedLock() {
        if(mValid) mMutex->unlock();
    }

    ScopedLock(const ScopedLock &sl) {
        mMutex=sl.mMutex;
        if(sl.mValid)
        {
                mValid=true;
                sl.mValid=false;
        }
        else mValid=false;
    }

    ScopedLock &operator=(const ScopedLock &sl)
    { // we inherit any lock the other class member had
        if(mValid) mMutex->unlock();
        mMutex=sl.mMutex;
        if(sl.mValid) {
            mValid=true;
            sl.mValid=false;
        }
    }
};

Мне все еще кажется неправильным. Я думал, что весь смысл Boost заключается в том, чтобы обеспечить чистый интерфейс для всех вещей, которые вам, скорее всего, понадобятся. Это кажется мне очень рутинным. И тот факт, что нет чистого способа сделать это, пугает меня.

Обновить. "Правильный" способ повышения эффективности - использовать shared_ptr для объекта держателя замка. Объект исчезнет, ​​когда его последний указатель будет уничтожен, освободив блокировку.