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

时间:2010-08-14 06:53:34

标签: c++ multithreading design-patterns

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

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

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

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

// 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();
    }
}

但是,除非使用递归互斥锁,否则会死锁。现在,在StackOverflow上查看some posts,似乎有多数人强烈反对使用递归互斥锁。

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

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

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

更新

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

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);
}

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

我做得对吗?

5 个答案:

答案 0 :(得分:9)

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

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

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

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

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 ();});
}

并且锁类方法:

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 ();
}

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

现在评论的第二部分:

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

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

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

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

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

警报读者说:但是,消息队列必须是线程安全的!这是真的,但是消息队列在线程安全方面相当简单。

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

答案 1 :(得分:4)

从根本上来说,验证对象是“线程安全的”是没有意义的。你不能只获得任何旧对象并坚持使用互斥锁并声称拥有多线程代码。正确的设计是设计您的程序。没有人能告诉你你的程序应该如何设计,但你需要制定一个真正的线程设计,你采取了错误的引力方法,这无济于事。

你应该拥有的是这样的东西:

__int64 begin, end, frequency;
double elapsedtime = 0;
QueryPerformanceFrequency((LARGE_INTEGER*)&frequency);
while(true) {
    QueryPerformanceCounter((LARGE_INTEGER*)&begin);
    DoMessageLoop(); // grabs user input and moves the block, etc.
    QueryPerformanceCounter((LARGE_INTEGER*)&end);
    elapsedtime += (((double)end - (double)begin)/frequency) * 1000);
    if (elapsedtime > gravitytimeinMS) {
        MoveBlockDown();
        elapsedtime -= gravitytimeinMS;
    }
}

假设您的消息循环以合理的帧速率运行(在现代硬件上给定),您将拥有非常精确的重力并且不涉及线程。

现在,该代码非常适合Windows,并且它并不完美,因为我在其他平台上几乎没有经验。但是,基本概念是相同的 - 获取计时器,测量主循环的时间,如果时间足够长则移动。在这里引入线程没有任何必要或好处。当你真的需要保留线程时,真的需要在其他线程上完成大量的计算负载 - 要么是因为你当前的线程已经饱和,要么是因为你需要它来响应用户。使用它们作为计时机制是完全浪费。

答案 2 :(得分:3)

我个人只是从外面锁定。但这是基于我有限的经验 - 我并不是自称是线索大师,而且我很欣赏那些比我更了解的人的评论。

我经常发现,在很多情况下,让一个班级负责自己的线程安全是不可能的。即使你进入一个状态,你的类似乎不能违反其不变量,你也会遇到想要执行操作组合的问题,正如你现在所发现的那样。

我发现将线程安全的责任完全推卸到消费类上会使代码更易于理解,并且您的域类将更容易设计。

通过尝试默认情况下让你的课程线程安全,你将会在实践中推断出可能永远不会出现的情况(虽然这在教育上通常是一个很好的练习 - 我发现通过问自己两个问题在我短暂的职业生涯中,我改进了我的编码。一个是我如何进行单元测试,另一个是如果多个线程得到这个的话会发生什么。)

您的大多数并发操作似乎是将移动块的用户代理,以及将其拖动到楼层的计时器。这听起来像是两个互斥锁获得给我。你的俄罗斯方块块目前的样子是什么样的?听起来它可能比那复杂得多。

为了尽可能做到最简单的事情,我只是暴露互斥体并让你的消费系统在认为必要时锁定。

(顺便说一句,.NET开发人员(包括在BCL中)的默认MO是默认情况下使实例成员非线程安全,将责任推到消费类上。)

答案 3 :(得分:1)

isGameOver方法移至dropCurrentBlock方法是否存在问题?

void Game::dropCurrentBlock()
{
   boost::mutex::scoped_lock lock( getMutex() );
   if ( isGameOver() ) return; // game over

   // implement dropCurrentBlock
}

答案 4 :(得分:1)

我会在这里避免多线程 - 它会显着增加代码的复杂性,使调试/测试变得更加困难,实际上是不必要的。

继续定时触发计时器,但不是直接降低块,而是将新的LOWER_BLOCK事件发布到UI消息队列。然后通过降低活动块来处理UI线程上的LOWER_BLOCK。