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

时间:2020-12-26 18:09:18

I thought thread safe, in particular, means it must satisfy the need for multiple threads to access the same shared data. But, it seems this definition is not enough.

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

Can anyone please list out the things to be done or taken care of to make an application thread safe. If possible, give an answer with respect to C/C++ language.

任何人都可以列出要做的事情或注意使应用程序线程安全。如果可能的话,就C/ c++语言给出一个答案。

4 个解决方案

#1


47  

There are several ways in which a function can be thread safe.

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

It can be reentrant. This means that a function has no state, and does not touch any global or static variables, so it can be called from multiple threads simultaneously. The term comes from allowing one thread to enter the function while another thread is already inside it.

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

It can have a critical section. This term gets thrown around a lot, but frankly I prefer critical data. A critical section occurs any time your code touches data that is shared across multiple threads. So I prefer to put the focus on that critical data.

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

If you use a mutex properly, you can synchronize access to the critical data, properly protecting from thread unsafe modifications. Mutexes and Locks are very useful, but with great power comes great responsibility. You must not lock the same mutex twice within the same thread (that is a self-deadlock). You must be careful if you acquire more than one mutex, as it increases your risk for deadlock. You must consistently protect your data with mutexes.

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

If all of your functions are thread safe, and all of your shared data properly protected, your application should be thread safe.

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

As Crazy Eddie said, this is a huge subject. I recommend reading up on boost threads, and using them accordingly.

艾迪说,这是个大问题。我建议阅读boost线程,并相应地使用它们。

low-level caveat: compilers can reorder statements, which can break thread safety. With multiple cores, each core has its own cache, and you need to properly sync the caches to have thread safety. Also, even if the compiler doesn't reorder statements, the hardware might. So, full, guaranteed thread safety isn't actually possible today. You can get 99.99% of the way there though, and work is being done with compiler vendors and cpu makers to fix this lingering caveat.

低层次警告:编译器可以重新排序语句,这会破坏线程安全性。对于多核,每个核都有自己的缓存,您需要正确地同步缓存以确保线程安全。而且,即使编译器不重新排序语句,硬件也可能如此。所以,保证线程安全在今天是不可能的。不过,您可以得到99.99%的方法,并且正在与编译器供应商和cpu制造商一起完成修复这一遗留问题的工作。

Anyway, if you're looking for a checklist to make a class thread-safe:

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

  • Identify any data that is shared across threads (if you miss it, you can't protect it)
  • 标识跨线程共享的任何数据(如果您错过了,就无法保护它)
  • create a member boost::mutex m_mutex and use it whenever you try to access that shared member data (ideally the shared data is private to the class, so you can be more certain that you're protecting it properly).
  • 创建一个成员增强:::mutex m_mutex,并在尝试访问共享成员数据时使用它(理想情况下,共享数据是类的私有数据,因此您可以更确定您正在正确地保护它)。
  • clean up globals. Globals are bad anyways, and good luck trying to do anything thread-safe with globals.
  • 清理全局。无论如何,全局变量是不好的,祝您好运,可以尝试使用全局变量执行任何线程安全的操作。
  • Beware the static keyword. It's actually not thread safe. So if you're trying to do a singleton, it won't work right.
  • 注意静态关键字。它实际上不是线程安全的。如果你想做单例,它就不能正常工作。
  • Beware the Double-Checked Lock Paradigm. Most people who use it get it wrong in some subtle ways, and it's prone to breakage by the low-level caveat.
  • 注意双重检查锁范例。大多数使用它的人在某些微妙的方式上犯了错误,并且它很容易被低级的警告所破坏。

That's an incomplete checklist. I'll add more if I think of it, but hopefully it's enough to get you started.

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

#2


11  

Two things:

两件事:

1. Make sure you use no globals. If you currently have globals, make them members of a per-thread state struct and then have the thread pass the struct to the common functions.

For example if we start with:

例如,如果我们从:

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

Once we add in a state struct the code becomes:

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

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

Now you may ask why not just pass x and y in as parameters. The reason is that this example is a simplification. In real life your state struct may have 20 fields and passing most of these parameters 4-5 functions down becomes daunting. You'd rather pass one parameter instead of many.

现在你可能会问为什么不把x和y作为参数。原因是这个例子是一个简化的例子。在现实生活中,您的状态结构可能有20个字段,将这些参数中的大部分4-5函数传递下来会让人望而生畏。您宁愿传递一个参数而不是多个参数。

2. If your threads have data in common that needs to be shared, then you need to look into critical sections and semaphores. Every time one of your threads accesses the data, it needs to block the other threads and then unblock them when it's done accessing the shared data.

#3


3  

If you want to make a exclusive access to the class' methods you have to use a lock at these functions.

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

The different type of locks:

不同类型的锁:

Using atomic_flg_lck:

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

Using atomic:

使用原子:

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

Using mutex:

使用互斥对象:

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

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

private:
  std::mutex lck;
};

Just for Windows:

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

The atomic and and atomic_flag keep the thread in a spin count. Mutex just sleeps the thread. If the wait time is too long maybe is better sleep the thread. The last one "CRITICAL_SECTION" keeps the thread in a spin count until a time is consumed, then the thread goes to sleep.

原子和atomic_flag保持线程的自旋计数。互斥量只是睡眠的线程。如果等待的时间太长,也许最好还是睡个好觉。最后一个“CRITICAL_SECTION”保持线程的旋转计数,直到消耗一个时间,然后线程进入休眠状态。

How to use these critical sections?

如何使用这些关键部分?

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

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

Using the raii idiom. The constructor to lock the critical section and the destructor to unlock it.

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

Example

例子

class MyClass {

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

}

This implementation is thread safe and exception safe because the variable lock is saved in the stack so when the function scope is ended (end of function or an exception) the destructor will be called.

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

I hope that you find this helpful.

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

Thanks!!

谢谢! !

#4


0  

One idea is to think of your program as a bunch of threads commutating through queues. Each thread would have one queue, and these queues would be shared (along with a shared data synchronization method(such as a mutex, etc) ) to all of the threads.

一种想法是把你的程序想象成一堆线程通过队列进行换向。每个线程将有一个队列,这些队列将与所有线程共享(以及共享数据同步方法(比如互斥))。

Then "solve" the producer/consumer problem however you want to keep the queues from underflowing or overflowing. http://en.wikipedia.org/wiki/Producer-consumer_problem

然后“解决”生产者/消费者问题,无论您希望如何避免队列欠流或溢出。http://en.wikipedia.org/wiki/Producer-consumer_problem

As long as you keep your threads localized, just sharing data with by sending copies over the queue, and not accessing thread unsafe things like (most) gui libraries and static variables in multiple threads, then you should be fine.

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

#1


47  

There are several ways in which a function can be thread safe.

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

It can be reentrant. This means that a function has no state, and does not touch any global or static variables, so it can be called from multiple threads simultaneously. The term comes from allowing one thread to enter the function while another thread is already inside it.

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

It can have a critical section. This term gets thrown around a lot, but frankly I prefer critical data. A critical section occurs any time your code touches data that is shared across multiple threads. So I prefer to put the focus on that critical data.

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

If you use a mutex properly, you can synchronize access to the critical data, properly protecting from thread unsafe modifications. Mutexes and Locks are very useful, but with great power comes great responsibility. You must not lock the same mutex twice within the same thread (that is a self-deadlock). You must be careful if you acquire more than one mutex, as it increases your risk for deadlock. You must consistently protect your data with mutexes.

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

If all of your functions are thread safe, and all of your shared data properly protected, your application should be thread safe.

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

As Crazy Eddie said, this is a huge subject. I recommend reading up on boost threads, and using them accordingly.

艾迪说,这是个大问题。我建议阅读boost线程,并相应地使用它们。

low-level caveat: compilers can reorder statements, which can break thread safety. With multiple cores, each core has its own cache, and you need to properly sync the caches to have thread safety. Also, even if the compiler doesn't reorder statements, the hardware might. So, full, guaranteed thread safety isn't actually possible today. You can get 99.99% of the way there though, and work is being done with compiler vendors and cpu makers to fix this lingering caveat.

低层次警告:编译器可以重新排序语句,这会破坏线程安全性。对于多核,每个核都有自己的缓存,您需要正确地同步缓存以确保线程安全。而且,即使编译器不重新排序语句,硬件也可能如此。所以,保证线程安全在今天是不可能的。不过,您可以得到99.99%的方法,并且正在与编译器供应商和cpu制造商一起完成修复这一遗留问题的工作。

Anyway, if you're looking for a checklist to make a class thread-safe:

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

  • Identify any data that is shared across threads (if you miss it, you can't protect it)
  • 标识跨线程共享的任何数据(如果您错过了,就无法保护它)
  • create a member boost::mutex m_mutex and use it whenever you try to access that shared member data (ideally the shared data is private to the class, so you can be more certain that you're protecting it properly).
  • 创建一个成员增强:::mutex m_mutex,并在尝试访问共享成员数据时使用它(理想情况下,共享数据是类的私有数据,因此您可以更确定您正在正确地保护它)。
  • clean up globals. Globals are bad anyways, and good luck trying to do anything thread-safe with globals.
  • 清理全局。无论如何,全局变量是不好的,祝您好运,可以尝试使用全局变量执行任何线程安全的操作。
  • Beware the static keyword. It's actually not thread safe. So if you're trying to do a singleton, it won't work right.
  • 注意静态关键字。它实际上不是线程安全的。如果你想做单例,它就不能正常工作。
  • Beware the Double-Checked Lock Paradigm. Most people who use it get it wrong in some subtle ways, and it's prone to breakage by the low-level caveat.
  • 注意双重检查锁范例。大多数使用它的人在某些微妙的方式上犯了错误,并且它很容易被低级的警告所破坏。

That's an incomplete checklist. I'll add more if I think of it, but hopefully it's enough to get you started.

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

#2


11  

Two things:

两件事:

1. Make sure you use no globals. If you currently have globals, make them members of a per-thread state struct and then have the thread pass the struct to the common functions.

For example if we start with:

例如,如果我们从:

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

Once we add in a state struct the code becomes:

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

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

Now you may ask why not just pass x and y in as parameters. The reason is that this example is a simplification. In real life your state struct may have 20 fields and passing most of these parameters 4-5 functions down becomes daunting. You'd rather pass one parameter instead of many.

现在你可能会问为什么不把x和y作为参数。原因是这个例子是一个简化的例子。在现实生活中,您的状态结构可能有20个字段,将这些参数中的大部分4-5函数传递下来会让人望而生畏。您宁愿传递一个参数而不是多个参数。

2. If your threads have data in common that needs to be shared, then you need to look into critical sections and semaphores. Every time one of your threads accesses the data, it needs to block the other threads and then unblock them when it's done accessing the shared data.

#3


3  

If you want to make a exclusive access to the class' methods you have to use a lock at these functions.

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

The different type of locks:

不同类型的锁:

Using atomic_flg_lck:

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

Using atomic:

使用原子:

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

Using mutex:

使用互斥对象:

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

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

private:
  std::mutex lck;
};

Just for Windows:

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

The atomic and and atomic_flag keep the thread in a spin count. Mutex just sleeps the thread. If the wait time is too long maybe is better sleep the thread. The last one "CRITICAL_SECTION" keeps the thread in a spin count until a time is consumed, then the thread goes to sleep.

原子和atomic_flag保持线程的自旋计数。互斥量只是睡眠的线程。如果等待的时间太长,也许最好还是睡个好觉。最后一个“CRITICAL_SECTION”保持线程的旋转计数,直到消耗一个时间,然后线程进入休眠状态。

How to use these critical sections?

如何使用这些关键部分?

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

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

Using the raii idiom. The constructor to lock the critical section and the destructor to unlock it.

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

Example

例子

class MyClass {

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

}

This implementation is thread safe and exception safe because the variable lock is saved in the stack so when the function scope is ended (end of function or an exception) the destructor will be called.

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

I hope that you find this helpful.

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

Thanks!!

谢谢! !

#4


0  

One idea is to think of your program as a bunch of threads commutating through queues. Each thread would have one queue, and these queues would be shared (along with a shared data synchronization method(such as a mutex, etc) ) to all of the threads.

一种想法是把你的程序想象成一堆线程通过队列进行换向。每个线程将有一个队列,这些队列将与所有线程共享(以及共享数据同步方法(比如互斥))。

Then "solve" the producer/consumer problem however you want to keep the queues from underflowing or overflowing. http://en.wikipedia.org/wiki/Producer-consumer_problem

然后“解决”生产者/消费者问题,无论您希望如何避免队列欠流或溢出。http://en.wikipedia.org/wiki/Producer-consumer_problem

As long as you keep your threads localized, just sharing data with by sending copies over the queue, and not accessing thread unsafe things like (most) gui libraries and static variables in multiple threads, then you should be fine.

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