需要一些关于如何使类"线程安全"的反馈

Sta*_*ked 17 c++ multithreading design-patterns

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

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

// This is not thread-safe.
while (!game.isGameOver())
{
    game.dropCurrentBlock();
}
Run Code Online (Sandbox Code Playgroud)

我尝试的一个解决方案是为互斥变量添加一个访问器方法,以便从外部锁定它:

// 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,除非使用递归互斥锁,否则这将会死锁.现在,看看StackOverflow 上的一些 帖子,似乎有多数人强烈反对使用递归互斥锁.

但是,如果递归互斥锁是非选项,那么这是否意味着创建一个线程安全类(支持协调更改)变得不可能?

唯一有效的解决方案似乎是永远不会在方法调用中锁定互斥锁,而是始终依赖用户从外部进行锁定.

但是,如果是这种情况,那么简单地将Game类保持原样不是更好,并创建一个将Game对象与互斥锁配对的包装类?

更新

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

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);
}
Run Code Online (Sandbox Code Playgroud)

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

我做得对吗?

Jus*_*mer 9

在你的情况下,你有一个需要同步的大型游戏.您已经注意到每个方法同步但一个stil无法安全执行操作的问题.

如果我们看一下ThreadSafeGame类,我认为可以改进它的接口,这样我们只有在同步模式下才能访问游戏状态.有几种方法可以做到这一点.一种方法是让getGame返回一个同时包含锁和实例的类.您在该类上定义operator->,以便它返回Game*.当类被销毁时,锁被释放.

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

我将使用visit方法演示另一种方法:

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 ();});
}
Run Code Online (Sandbox Code Playgroud)

而锁类方法:

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 ();
}
Run Code Online (Sandbox Code Playgroud)

但基本思路是一样的.确保我们只有在锁定时才能访问游戏状态.当然这是C++,所以我们总能找到打破规则的方法,但引用Herb Sutter:保护免受Murphy而不是对抗Machiavelli即.保护自己免于错误,而不是那些打算违反规则的程序员(他们总是会找到一种方法)

现在到评论的第二部分:

粗粒度锁定与细粒度锁定?粗粒度很容易实现,但是存在性能问题,细粒度锁定非常难以实现,但可能具有更好的性能.

我会说; 尽力避免全部锁定.我不是这个意思; 交叉我的拇指,希望我没有竞争条件.我的意思是构造你的程序,以便只有一个线程管理可变状态并隔离这个可变状态,因此它不会被多个线程错误地改变.

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

相反,接受用户状态的输入线程将消息发布到Game状态管理器线程说:"这就是用户所做的".游戏状态线程然后消费消息并适当地动作.这样,游戏状态只能由该线程访问,并且不会发生竞争条件和死锁.

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

警报读者说:但是,消息队列必须是线程安全的!这是真的,但是消息队列在线程安全方面相当微不足道.

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