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

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

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

4个解决方案
57 votes

有几种方法可以使函数成为线程安全的。

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

它可以有一个关键部分。 这个术语经常被提及,但坦率地说,我更喜欢关键数据。 每当您的代码接触跨多个线程共享的数据时,就会发生关键部分。 因此,我宁愿将重点放在那些关键数据上。

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

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

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

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

无论如何,如果您正在寻找使类线程安全的清单:

  • 确定跨线程共享的任何数据(如果丢失,则无法保护它)
  • 创建一个成员static,并在每次尝试访问该共享成员数据时使用它(理想情况下,共享数据是该类的私有数据,因此可以肯定地保护了它)。
  • 清理全局变量。 无论如何,全局变量都是不好的,而尝试对全局变量执行线程安全的任何事情也很幸运。
  • 当心static关键字。 它实际上不是线程安全的。 因此,如果您尝试做一个单例,它将无法正常工作。
  • 当心双重检查的锁范例。 大多数使用它的人会以一些微妙的方式弄错它,并且低级警告很容易使其损坏。

那是不完整的清单。 如果我考虑的话,我会添加更多,但希望足以让您入门。

Tim answered 2019-11-14T12:54:56Z
14 votes

两件事情:

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

一旦添加了状态struct,代码就会变成:

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.如果您的线程具有需要共享的共同数据,那么您需要查看关键部分和信号量。 每次您的一个线程访问数据时,它都需要阻塞其他线程,然后在完成对共享数据的访问后取消阻塞它们。

Theo answered 2019-11-14T12:55:56Z
3 votes

如果要对类的方法进行独占访问,则必须在这些函数上使用锁。

不同类型的锁:

使用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和atomic_flag将线程保持在旋转计数中。 互斥体只是使线程休眠。 如果等待时间太长,则最好休眠线程。 最后一个“ CRITICAL_SECTION”将线程保持在旋转计数中,直到消耗了时间为止,然后线程进入睡眠状态。

如何使用这些关键部分?

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

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

使用raii成语。 构造器锁定关键部分,析构函数将其解锁。

class MyClass {

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

}

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

希望对您有所帮助。

谢谢!!

GutiMac answered 2019-11-14T12:57:57Z
0 votes

一种想法是将程序视为一堆通过队列交换的线程。 每个线程都有一个队列,并且这些队列将与所有线程共享(以及共享的数据同步方法(例如互斥锁等))。

然后“解决”生产者/消费者问题,但是您希望避免队列下溢或溢出。 [http://en.wikipedia.org/wiki/Producer-consumer_problem]

只要您使线程保持本地化,通过在队列上发送副本来共享数据,并且不访问线程不安全的东西(例如(大多数)gui库和多个线程中的静态变量),那么您就可以了。

Lalaland answered 2019-11-14T12:58:37Z
translate from https://stackoverflow.com:/questions/5125241/how-to-make-an-application-thread-safe