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

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

问题描述

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



一开始我想通过添加一个互斥量成员变量并锁定每个方法调用,使Game类线程安全。但问题是,它只保护单个方法调用,而不是涉及多个方法调用的更改。例如:

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

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

  //添加额外范围以限制scoped_lock的生命周期。 
{
// =>死锁,除非使用递归互斥体
boost :: mutex :: scoped_lock lock(game.getMutex());
while(!game.isGameOver())
{
game.dropCurrentBlock();但是,除非使用递归互斥,否则这会死锁(除非使用了递归互斥体)。 。现在,查看一些 posts 在StackOverflow上,似乎有大多数人强烈反对使用递归互斥体。



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



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



然而,如果是这样的话,那么就不会更好地简单地离开Game类,并创建一个包含Game对象和Mutex对象的包装类。



更新



我尝试创建了一个名为 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;
};

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

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

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



我这样做吗?





感谢@ FuleSnabel的建议,我消除了用户必须从外部执行锁定的缺点。 ThreadSafeGame现在仍然是持有游戏的类。但是要访问游戏对象,你必须创建一个WritableGame或一个ReadOnlyGame对象。这些对象在其一生中保持游戏对象锁定。通过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;
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-> return mGame; }
private:
boost :: mutex :: scoped_lock mLock;
const Game * mGame;
};

一些示例代码:

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

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

//当程序段下降时反复旋转。
while(!ReadOnlyGame(threadSafeGame) - > isGameOver())
{
WritableGame game(threadSafeGame);
Block& block = game-> activeBlock();
block.rotate();
}
}


解决方案

你的情况下你有一个大类游戏,需要同步。你已经注意到每个方法同步但是一个stil不能安全地执行操作的问题。



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



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



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

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

//访问执行动作当有锁
模板< 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;
};

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

void EndTheGame()
{
s_state.visit([游戏和状态)
{
//在这里我们被同步
while(!state.is_game_over())
{
state.drop_current_block();
}
});
}

bool IsGameOver()
{
return s_state.visit(游戏和状态){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& ;

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))
{
}
$ b b 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:防止墨菲,而不是Machiavelli ie。保护自己免受错误,不是因为违反规则的程序员(他们总是会找到一种方法)



现在到注释的第二部分: / p>

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



说;尽最大努力避免锁定。这不是我的意思;横渡我的拇指,希望我不会得到种族条件。我的意思是结构你的程序,所以只有一个线程管理可变状态,并隔离这个可变状态,所以它不能被错误的几个线程的变异。



在你的情况下具有接受用户输入的输入线程并更新状态。



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



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



警告读者说:但是消息队列必须是线程安全的!这是真的,但是一个消息队列是比较微不足道的做线程安全的。



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


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天全站免登陆