Как я могу вернуть блокированный замок? - PullRequest
13 голосов
/ 11 ноября 2011

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

У меня есть класс, в котором есть член 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) Это разумно?

Примечание. Это полностью отличается от вопроса с таким же названием: возвращает повышение :: scoped_lock .

Ответы [ 3 ]

11 голосов
/ 11 ноября 2011

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

Альтернатива 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, в некоторых случаях вы можете использовать рекурсивную или перезапись блокировки.

Альтернатива 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's 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's 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;
}
0 голосов
/ 12 ноября 2011

Вот как я сейчас планирую это сделать.Я сделаю класс 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 в том, чтобы обеспечить чистый интерфейс для всех вещей, которые вам, скорее всего, придется делать.Это кажется мне 1011 * очень рутиной.И тот факт, что нет чистого способа сделать это, пугает меня.

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

0 голосов
/ 11 ноября 2011

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

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, но с тех пор перешла на лучшие методы.

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