需要一些关于如何使类“线程安全”的反馈 [英] Need some feedback on how to make a class "thread-safe"

查看:120
本文介绍了需要一些关于如何使类“线程安全”的反馈的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在学习如何在C ++中执行多线程。我的一个学习项目是俄罗斯方块游戏。在这个项目中,我有一个包含所有游戏状态数据的Game类。它有方法来移动块和其他一些东西。该对象将被用户访问(谁将使用箭头键从主线程移动块),同时线程定时器正在活动块上实施重力(定期降低它)。



起初我以为我可以通过添加互斥体成员变量并将其锁定在每个方法调用中来使Game类线程安全。但是问题是它只保护单个方法调用,而不是涉及多个方法调用的更改。例如:

  //这不是线程安全的。 
while(!game.isGameOver())
{
game.dropCurrentBlock();
}

我尝试的一个解决方案是为mutex变量添加一个访问器方法来锁定它也来自外部:

  //额外添加范围以限制scoped_lock的生命周期。 
{
// =>死锁,除非使用递归互斥体
boost :: mutex :: scoped_lock lock(game.getMutex());
while(!game.isGameOver())
{
game.dropCurrentBlock();
}
}

但是,除非使用递归互斥体,否则这将会死锁。现在,看看一些 信息,似乎有大多数人强烈反对使用递归互斥体。



但是,如果递归互斥是非选项,这是不是意味着不可能创建线程安全的类(支持协调的更改)?



唯一有效的解决方案似乎是永远不会在方法调用中锁定互斥体,而是总是依靠用户从外部执行锁定。


$ b $但是,如果是这样的话,那么简单的离开Game类就不是最好的了,并且创建一个将Game对象和互斥体相配合的包装类?



更新



我给了包装器的想法,并创建了一个名为 ThreadSafeGame cpp ),如下所示:

  class ThreadSafeGame 
{
public:
ThreadSafeGame(std :: auto_ptr< Game> inGame):mGame(inGame.release){}

const游戏* getGame()const
{return mGame.get(); }

Game * getGame()
{return mGame.get(); }

boost :: mutex& getMutex()const
{return mMutex; }

private:
boost :: scoped_ptr< Game> MGAME;
可变的boost :: mutex mMutex;
};

//假设threadSafeGame指向ThreadSafeGame对象的用法示例。
{
//首先锁定游戏对象。
boost :: mutex :: scoped_lock lock(threadSafeGame-> getMutex());

//然后访问它。
Game * game = threadSafeGame-> getGame();
game-> move(Direction_Down);
}

它具有相同的缺点,因为它取决于用户锁定互斥体从外部。但是除此之外,这似乎对我来说是一个可行的解决方案。



我做的是对吗?



更新



感谢@ FuleSnabel的建议,我消除了用户必须从外部执行锁定的缺点。 ThreadSafeGame现在仍然是持有该游戏的类。但是要访问游戏对象,您必须制作一个WritableGame或一个ReadOnlyGame对象。这些对象在其生命周期中保持Game对象被锁定。通过operator->可以访问实际的Game对象。以下是代码:

  class Game; 
struct GameAndMutex
{
GameAndMutex(std :: auto_ptr< Game> inGame):mGame(inGame.release()){}
boost :: scoped_ptr< Game> MGAME;
mutable boost :: mutex mMutex;
};


class ThreadSafeGame
{
public:
ThreadSafeGame(std :: auto_ptr< Game> inGame):
mGameAndMutex(new GameAndMutex inGame))
{
}
private:
friend class WritableGame;
朋友类ReadOnlyGame;
boost :: shared_ptr< GameAndMutex> mGameAndMutex;
};


class WritableGame
{
public:
WritableGame(ThreadSafeGame& inThreadSafeGame):
mLock(inThreadSafeGame.mGameAndMutex-> mMutex )
mGame(inThreadSafeGame.mGameAndMutex-> mGame.get())
{
}

Game * operator->(){return mGame; }
private:
boost :: mutex :: scoped_lock mLock;
游戏* mGame;
};


class ReadOnlyGame
{
public:
ReadOnlyGame(const ThreadSafeGame& inThreadSafeGame):
mLock(inThreadSafeGame.mGameAndMutex->
mGame(inThreadSafeGame.mGameAndMutex-> mGame.get())
{
}

const Game * operator->()const {返回mGame; }
private:
boost :: mutex :: scoped_lock mLock;
const游戏* mGame;
};

一些示例代码:

  void test()
{
ThreadSafeGame threadSafeGame(std :: auto_ptr< Game>(new Game));

//启用重力计时器
TimedGame timedGame(threadSafeGame);
timedGame.start();

//在块下降时反复旋转。
while(!ReadOnlyGame(threadSafeGame) - > isGameOver())
{
WritableGame游戏(threadSafeGame);
Block& block = game-> activeBlock();
block.rotate();
}
}


解决方案

你的案例你有一个需要同步的大型游戏。您已经注意到每个方法都是同步的问题,但是一个stil无法安全地执行操作。



如果我们看类ThreadSafeGame,我认为如果我们处于同步模式,我们只能访问游戏状态。有几种方法可以做到。一种方法是让getGame同时返回一个同时保存锁和实例的类。您在该类上定义operator->,以返回Game *。当这个类被销毁时,锁就被释放了。



我的例子使用一些C ++ 0x特性(lambdas,移动语义,auto和decltype),但是不可能它与C ++ 98兼容。



我将演示另一种方式,使用访问方式:

 模板< typename TValue> 
struct threadsafe_container:boost :: noncopyable
{
显式threadsafe_container(TValue&&&&&)值
:m_value(std :: move(value))
{
}

//访问时执行动作
template< typename TAction>
自动访问(TAction action) - > decltype(action(m_value))
{
boost :: mutex :: scope_lock lock(& m_mutex);

TValue&值(m_value);

return action(value);
}

private:
boost :: mutex m_mutex;
TValue m_value;
};

//额外的解释必要,否则c ++将其解释为函数声明
threadsafe_container< game> s_state((ConstructAGameSomehow()));

void EndTheGame()
{
s_state.visit([](game& state)
{
//在这里我们是同步的b $ b while(!state.is_game_over())
{
state.drop_current_block();
}
});


bool IsGameOver()
{
return s_state.visit([](game& state){return state.is_game_over();});
}

并且锁类方法:

 模板< typename TValue> 
struct threadsafe_container2:boost :: noncopyable
{
struct lock:boost :: noncopyable
{
lock(TValue * value,mutex * mtx)
:m_value(value)
,m_lock(mtx)
{
}

//支持移动语义
lock(lock&& l) ;

TValue * get()const
{
return m_value;
}

TValue * operator-> ()const
{
return get();
}
private:
TValue * m_value;
boost :: mutex :: scope_lock m_lock;
};

显式threadafe_container2(TValue&&&&&)值
:m_value(std :: move(value))
{
}

lock get()
{
return lock(& m_value,& m_mutex);
}

private:
boost :: mutex m_mutex;
TValue m_value;
};

//额外的解释必要,否则c ++将其解释为函数声明
threadsafe_container2< game> s_state((ConstructAGameSomehow()));

void EndTheGame()
{
auto lock = s_state2.get();
//在这里我们同步
while(!lock-> is_game_over())
{
lock-> drop_current_block();
}
}

bool IsGameOver()
{
auto lock = s_state2.get();
//在这里我们同步
reutrn lock-> is_game_over();
}

但基本思想是一样的。确保我们只能在锁定时访问游戏状态。当然这是C ++,所以我们总是可以找到打破规则的方法,但是引用Herb Sutter:保护Murphy不要反对Machiavelli ie。保护自己免受错误而不是来自打破规则的程序员(他们将永远找到一种方法)



现在到评论的第二部分: / p>

粗粒度锁定与细粒度锁定?
粗粒度很容易实现,但遇到性能问题,细粒度锁定是非常棘手的获得正确但可能有更好的性能。



我会说;尽量避免锁定。我不是这个意思交叉我的拇指,希望我没有得到比赛条件。我的意思是构造你的程序,所以只有一个线程管理可变状态并隔离这个可变状态,所以它不能被几个线程错误地变异。



在你的情况下有输入线程接受用户输入并更新状态。一个线程在计时器上更新游戏状态。



而是接受用户状态的输入线程向Game状态管理器线程发送消息,说
:这是用户做了什么然后,游戏状态线程消耗消息并适当地行动。这样,游戏状态只能被该线程访问,并且不会发生竞争条件和死锁。



这有时被称为活动对象模式。



提醒读者说:但是嘿,消息队列必须是线程安全的!这是真的,但是消息队列比较简单,使线程安全。



IMO这种模式是构建维护并发项目最重要的一种。


I'm currently learning how to do multithreading in C++. One of my learning projects is a Tetris game. In this project I have a Game class that contains all game state data. It has methods for moving the block around and a few other things. This object will be accessed by the user (who will use the arrow keys to move the block, from the main thread) and at the same time a threaded timer is implementing the gravity on the active block (periodically lowering it).

At first I thought that I could make the Game class thread safe by adding a mutex member variable and lock it inside each method call. But problem with this is that it only protects individual method calls, not changes that involve multiple method calls. For example:

// This is not thread-safe.
while (!game.isGameOver())
{
    game.dropCurrentBlock();
}

One solution that I tried is adding an accessor method for the mutex variable to lock it also from the outside:

// Extra scope added to limit the lifetime of the scoped_lock.    
{
    // => deadlock, unless a recursive mutex is used
    boost::mutex::scoped_lock lock(game.getMutex());
    while (!game.isGameOver())
    {
        game.dropCurrentBlock();
    }
}

However, this will deadlock unless a recursive mutex is used. Now, looking at some posts on StackOverflow, there seems to be a majority that strongly disapproves the use of recursive mutexes.

But if recursive mutexes are a non-option, doesn't that mean that it becomes impossible to create a thread-safe class (that supports coordinated changes)?

The only valid solution seems to be to never lock the mutex inside the method calls, and instead always rely on the user to do the locking from the outside.

However, if that is the case, then wouldn't it be better to simply leave the Game class as it is, and create a wrapper class that pairs a Game object with a mutex?

Update

I gave the wrapper idea a try and created a class called ThreadSafeGame (cpp) that looks like this:

class ThreadSafeGame
{
public:
    ThreadSafeGame(std::auto_ptr<Game> inGame) : mGame(inGame.release) {}

    const Game * getGame() const
    { return mGame.get(); }

    Game * getGame()
    { return mGame.get(); }

    boost::mutex & getMutex() const
    { return mMutex; }

private:
    boost::scoped_ptr<Game> mGame;
    mutable boost::mutex mMutex;
};

// Usage example, assuming "threadSafeGame" is pointer to a ThreadSafeGame object.    
{
    // First lock the game object.
    boost::mutex::scoped_lock lock(threadSafeGame->getMutex());

    // Then access it.
    Game * game = threadSafeGame->getGame();
    game->move(Direction_Down);
}

It has the same drawback in that it depends on the user to lock the mutex from the outside. But apart from that this seems like a workable solution to me.

Am I doing it right?

Update

Thanks to @FuleSnabel's suggestion, I eliminated the drawback that the user must do the locking from the outside. ThreadSafeGame is now still the class that holds the game. But to access the game object you must make a WritableGame or a ReadOnlyGame object. These objects keep the Game object locked during their lifetime. Access to the actual Game object is enabled through "operator->". Here's the code:

class Game;
struct GameAndMutex
{
    GameAndMutex(std::auto_ptr<Game> inGame) : mGame(inGame.release()) { }
    boost::scoped_ptr<Game> mGame;
    mutable boost::mutex mMutex;
};


class ThreadSafeGame
{
public:
    ThreadSafeGame(std::auto_ptr<Game> inGame) :
        mGameAndMutex(new GameAndMutex(inGame))
    {
    }
private:
    friend class WritableGame;
    friend class ReadOnlyGame;
    boost::shared_ptr<GameAndMutex> mGameAndMutex;
};


class WritableGame
{
public:
    WritableGame(ThreadSafeGame & inThreadSafeGame) :
        mLock(inThreadSafeGame.mGameAndMutex->mMutex),
        mGame(inThreadSafeGame.mGameAndMutex->mGame.get())
    {
    }

    Game * operator->() { return mGame; }
private:
    boost::mutex::scoped_lock mLock;
    Game * mGame;
};


class ReadOnlyGame
{
public:
    ReadOnlyGame(const ThreadSafeGame & inThreadSafeGame) :
        mLock(inThreadSafeGame.mGameAndMutex->mMutex),
        mGame(inThreadSafeGame.mGameAndMutex->mGame.get())
    {
    }

    const Game * operator->() const { return mGame; }
private:
    boost::mutex::scoped_lock mLock;
    const Game * mGame;
};

Some sample code:

void test()
{
    ThreadSafeGame threadSafeGame(std::auto_ptr<Game>(new Game));

    // Enable the gravity timer
    TimedGame timedGame(threadSafeGame);
    timedGame.start();

    // Rotate repeatedly while the block is falling.
    while (!ReadOnlyGame(threadSafeGame)->isGameOver())
    {
        WritableGame game(threadSafeGame);
        Block & block = game->activeBlock();
        block.rotate();
    }
}

解决方案

In your case you have a big class Game that requires synchronization. You've already noted the problem where each method is synchronized but one stil can't perform the operations safely.

If we look at the class ThreadSafeGame I think the interface for it could be improved so that we only can access the game state if we are in synchronized mode. There are several ways to do it. One way is to make getGame return a class that holds both a lock and the instance at the same time. You define operator-> on that class so that it returns Game*. When the class is destroyed the lock is released.

My examples uses some C++0x features (lambdas, move semantics, auto and decltype) but it's not impossible to make it C++98 compatible.

I will demonstrate another way to do it as well using a visit method:

template<typename TValue>
struct threadsafe_container : boost::noncopyable
{
   explicit threadsafe_container (TValue && value)
      :  m_value (std::move (value))
   {
   }

   // visit executes action when have the lock
   template<typename TAction>
   auto visit (TAction action) -> decltype (action (m_value))
   {
      boost::mutex::scope_lock lock (&m_mutex);

      TValue & value (m_value);

      return action (value);
   }

private:
   boost::mutex m_mutex;
   TValue m_value;
};

// Extra paranthesis necessary otherwise c++ interprets it as a function declaration
threadsafe_container<game> s_state ((ConstructAGameSomehow ())); 

void EndTheGame ()
{
   s_state.visit ([](game & state)
      {
         // In here we are synchronized
         while (!state.is_game_over ()) 
         { 
            state.drop_current_block (); 
         } 
      });
}

bool IsGameOver ()
{
   return s_state.visit ([](game & state) {return state.is_game_over ();});
}

And the lock class method:

template<typename TValue>
struct threadsafe_container2 : boost::noncopyable
{
   struct lock : boost::noncopyable
   {
      lock (TValue * value, mutex * mtx)
         :  m_value  (value)
         ,  m_lock   (mtx)
      {
      }

      // Support move semantics
      lock (lock && l);

      TValue * get () const 
      {
         return m_value;
      }

      TValue * operator-> () const
      {
         return get ();
      }
   private:
      TValue *                   m_value;
      boost::mutex::scope_lock   m_lock;
   };

   explicit threadsafe_container2 (TValue && value)
      :  m_value (std::move (value))
   {
   }

   lock get ()
   {
      return lock (&m_value, &m_mutex);
   }

private:
   boost::mutex   m_mutex;
   TValue         m_value;
};

// Extra paranthesis necessary otherwise c++ interprets it as a function declaration
threadsafe_container2<game> s_state ((ConstructAGameSomehow ())); 

void EndTheGame ()
{
   auto lock = s_state2.get ();
   // In here we are synchronized
   while (!lock->is_game_over ()) 
   { 
      lock->drop_current_block ();   
   } 
}

bool IsGameOver ()
{
   auto lock = s_state2.get ();
   // In here we are synchronized
   reutrn lock->is_game_over ();
}

But the basic idea is the same. Make sure we can only access the Game state when we have a lock. Of course this is C++ so we can always find ways to break the rules but to quote Herb Sutter: Protect against Murphy not against Machiavelli ie. protect yourself from mistake not from programmers that set out to break the rules (they will always find a way to do it)

Now to the second part of the comment:

Coarse grained locking versus fine grained locking? Coarse grained is rather easy to implement but suffers from performance issues, fine-grained locking is very tricky to get right but might have better performance.

I would say; do your best to avoid locking alltogether. With that I don't mean; cross my thumbs and hope I don't get race conditions. I mean structure your program so that only one thread manages mutable state and isolate this mutable state so it can't be mutated by mistake by several threads.

In your case you have an input thread accepting user inputs and updates the state. One thread updates the game state on timer.

Instead what about the input thread accepting user state posts a message to Game state manager thread saying : "This is what did user did". The game state thread then consumes messages and acts appropriately. That way the game state is only accessed by that thread and no race conditions and dead-locks can occurs.

This is sometimes called the "Active Object Pattern".

Alert readers say: But hey the message queue must be thread-safe! That's true but a message queue is comparativly trivial to make thread-safe.

IMO this pattern is one of the most important to build maintainble concurrent projects.

这篇关于需要一些关于如何使类“线程安全”的反馈的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆