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

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

可以请任何人 列出为了使应用程序线程安全而需要做或需要注意的事情。如果可能的话,给出一个关于 C/C + + 语言的答案。

52396 次浏览

函数可以通过几种方式实现线程安全。

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

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

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

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

正如疯子埃迪所说,这是一个巨大的主题。我建议阅读关于增强线程的文章,并相应地使用它们。

低级警告 : 编译器可以重新排序语句,这可能破坏线程安全。对于多个核心,每个核心都有自己的缓存,您需要正确地同步缓存以保证线程安全。此外,即使编译器不重新排序语句,硬件也可能。因此,完整的、有保证的线程安全性在今天实际上是不可能的。不过,您可以完成99.99% 的工作,目前正在与编译器供应商和 CPU 制造商合作,以修复这个挥之不去的警告。

无论如何,如果你正在寻找一个检查表,使类线程安全:

  • 识别跨线程共享的任何数据(如果错过了,就无法保护它)
  • 创建一个成员 boost::mutex m_mutex,并在尝试访问该共享成员数据时使用它(理想情况下,共享数据是该类的私有数据,因此您可以更加确定正在适当地保护它)。
  • 清理全局变量。全局变量无论如何都是不好的,如果尝试用全局变量做任何线程安全的事情,那就祝你好运了。
  • 小心 static关键字。它实际上不是线程安全的。所以如果你试图做一个单独的,它不会工作的权利。
  • 注意双重检查锁定范例。大多数使用它的人都会在一些微妙的方面出现错误,并且很容易因为低级别的警告而破坏它。

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

其中一个想法是将您的程序视为通过队列进行通信的一组线程。每个线程有一个队列,这些队列将被共享给所有线程(以及一个共享的数据同步方法(如互斥锁等))。

然后“解决”生产者/消费者问题,但是您希望保持队列不溢出或溢出

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

两件事:

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

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

不同类型的锁:

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

返回文章页面 窗户:

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

原子弹原子旗使线程处于旋转计数中。互斥只是睡了线。如果等待时间太长,也许是更好的睡眠线程。最后一个“ 关键部分”将线程保持在一个自旋计数中,直到耗尽一个时间,然后线程进入睡眠状态。

如何使用这些关键部分?

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


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

使用 raii 惯用语: 构造函数锁定临界区域,析构函数解锁临界区域。

例子

class MyClass {


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


}

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

希望这对你有帮助。

谢谢!