如何使应用程序线程安全?

时间:2011-02-26 05:22:50

标签: c++ c multithreading concurrency thread-safety

我认为线程安全,特别是,它意味着它必须满足多线程访问相同共享数据的需要。但是,似乎这个定义还不够。

任何人都可以列出要使应用程序线程安全的事情或需要处理的事情。如果可能的话,给出一个关于C / C ++语言的答案。

4 个答案:

答案 0 :(得分:54)

函数可以通过多种方式保证线程安全。

可以可重入。这意味着函数没有状态,并且不接触任何全局变量或静态变量,因此可以同时从多个线程调用它。该术语来自允许一个线程进入该函数而另一个线程已经在其中。

它可以有关键部分。这个术语被抛出很多,但坦率地说我更喜欢关键数据。每当您的代码触及跨多个线程共享的数据时,就会出现一个关键部分。所以我更愿意把重点放在关键数据上。

如果您正确使用mutex,则可以同步对关键数据的访问,从而正确保护线程不安全的修改。互斥锁和锁是非常有用的,但强大的功能带来了巨大的责任。您不能在同一个线程中两次锁定相同的互斥锁(这是一个自死锁)。如果您获得多个互斥锁,则必须小心,因为这会增加死锁的风险。您必须使用互斥锁始终保护数据。

如果您的所有函数都是线程安全的,并且所有共享数据都受到适当保护,那么您的应用程序应该是线程安全的。

正如Crazy Eddie所说,这是一个很大的主题。我建议阅读boost线程,并相应地使用它们。

低级警告:编译器可以重新排序语句,这可能会破坏线程安全性。对于多个内核,每个内核都有自己的缓存,您需要正确同步缓存以确保线程安全。此外,即使编译器没有重新排序语句,硬件也可能。因此,今天实际上不可能完全保证线程安全。你可以获得99.99%的方式,并且编译器供应商和cpu制造商正在努力解决这个挥之不去的警告。

无论如何,如果你正在寻找一个清单以使类线程安全:

  • 识别跨线程共享的任何数据(如果您错过了,则无法保护它)
  • 创建一个成员boost::mutex m_mutex并在您尝试访问该共享成员数据时使用它(理想情况下,共享数据对于该类是私有的,因此您可以更确定您是否正确保护它)。 / LI>
  • 清理全局变量。无论如何,Globals都很糟糕,并且试图用全局变量做任何线程安全的运气。
  • 注意static关键字。它实际上不是线程安全的。因此,如果您正在尝试单身,它将无法正常工作。
  • 小心双重锁定范例。大多数使用它的人都会以某种微妙的方式弄错,并且很容易被低级别的警告破坏。

这是一份不完整的清单。如果我想到它,我会添加更多,但希望它足以让你开始。

答案 1 :(得分:14)

两件事:

1。确保你没有使用全局变量。如果你当前有全局变量,那么让它们成为每线程状态结构的成员,然后让线程将结构传递给公共函数。

例如,如果我们从:

开始
// Globals
int x;
int y;

// Function that needs to be accessed by multiple threads
// currently relies on globals, and hence cannot work with
// multiple threads
int myFunc()
{
    return x+y;
}

一旦我们添加了一个状态结构,代码就会变成:

typedef struct myState
{
   int x;
   int y;
} myState;

// Function that needs to be accessed by multiple threads
// now takes state struct
int myFunc(struct myState *state)
{
   return (state->x + state->y);
}

现在您可能会问为什么不将x和y作为参数传递。原因是这个例子是一个简化。在现实生活中,你的状态结构可能有20个字段,并且通过大多数这些参数4-5个函数变得令人生畏。你宁愿传递一个参数而不是许多参数。

2。如果您的线程具有需要共享的共同数据,那么您需要查看关键部分和信号量。每当你的一个线程访问数据时,它就需要阻塞其他线程,然后在访问共享数据时取消阻塞它们。

答案 2 :(得分:3)

如果您想独家访问该课程'你必须在这些函数上使用锁的方法。

不同类型的锁:

使用 atomic_flg_lck:

class SLock
{
public:
  void lock()
  {
    while (lck.test_and_set(std::memory_order_acquire));
  }

  void unlock()
  {
    lck.clear(std::memory_order_release);
  }

  SLock(){
    //lck = ATOMIC_FLAG_INIT;
    lck.clear();
  }
private:
  std::atomic_flag lck;// = ATOMIC_FLAG_INIT;
};

使用原子:

class SLock
{
public:
  void lock()
  {
    while (lck.exchange(true));
  }

  void unlock()
  {
    lck = true;
  }

  SLock(){
    //lck = ATOMIC_FLAG_INIT;
    lck = false;
  }
private:
  std::atomic<bool> lck;
};

使用互斥:

class SLock
{
public:
  void lock()
  {
    lck.lock();
  }

  void unlock()
  {
    lck.unlock();
  }

private:
  std::mutex lck;
};

仅适用于 Windows

class SLock
{
public:
  void lock()
  {
    EnterCriticalSection(&g_crit_sec);
  }

  void unlock()
  {
    LeaveCriticalSection(&g_crit_sec);
  }

  SLock(){
    InitializeCriticalSectionAndSpinCount(&g_crit_sec, 0x80000400);
  }

private:
  CRITICAL_SECTION g_crit_sec;
};

原子以及atomic_flag 使线程保持旋转计数。 Mutex 只是睡觉了。如果等待时间太长也许最好睡眠线程。最后一个&#34; CRITICAL_SECTION &#34;将线程保持在旋转计数中直到消耗时间,然后线程进入休眠状态。

如何使用这些关键部分?

unique_ptr<SLock> raiilock(new SLock());

class Smartlock{
public:
  Smartlock(){ raiilock->lock(); }
  ~Smartlock(){ raiilock->unlock(); }
};

使用raii成语。用于锁定关键部分的构造函数和用于解锁它的析构函数。

实施例

class MyClass {

   void syncronithedFunction(){
      Smartlock lock;
      //.....
   }

}

此实现是线程安全且异常安全的,因为变量锁保存在堆栈中,因此当函数作用域结束时(函数结束或异常),将调用析构函数。

我希望你觉得这很有帮助。

谢谢!

答案 3 :(得分:0)

一个想法是将您的程序视为一系列通过队列进行换向的线程。每个线程都有一个队列,这些队列将与所有线程共享(以及共享数据同步方法(如互斥等))。

然后“解决”生产者/消费者问题,但是你想让队列保持下溢或溢出。 http://en.wikipedia.org/wiki/Producer-consumer_problem

只要你保持你的线程本地化,只是通过队列发送副本来共享数据,而不是在多线程中访问线程不安全的东西,如(大多数)gui库和静态变量,那么你应该没问题。