32

我正在尝试使 C++ API(用于 Linux 和 Solaris)线程安全,以便可以从不同的线程调用它的函数而不会破坏内部数据结构。在我目前的方法中,我使用 pthread 互斥锁来保护对成员变量的所有访问。这意味着一个简单的 getter 函数现在可以锁定和解锁互斥体,我担心这样做的开销,特别是因为该 API 将主要用于单线程应用程序中,任何互斥体锁定似乎都是纯粹的开销。

所以,我想问一下:

  • 您对使用锁定与不使用锁定的单线程应用程序的性能有什么经验吗?
  • 与例如相比,这些锁定/解锁调用有多昂贵。对 bool 成员变量进行简单的“返回 this->isActive”访问?
  • 您知道保护此类变量访问的更好方法吗?
4

9 回答 9

39

所有现代线程实现都可以完全在用户空间中处理无争用互斥锁(只需几条机器指令)——只有当存在争用时,库才必须调用内核。

要考虑的另一点是,如果应用程序没有显式链接到 pthread 库(因为它是单线程应用程序),它只会获得虚拟 pthread 函数(根本不做任何锁定) - 只有当应用程序是多线程的(并链接到 pthread 库),将使用完整的 pthread 函数。

最后,正如其他人已经指出的那样,使用互斥锁保护诸如 isActive 之类的 getter 方法是没有意义的——一旦调用者有机会查看返回值,该值可能已经被更改(因为mutex 仅锁定在 getter 方法内)。

于 2009-08-14T16:56:00.460 回答
23

“互斥体需要操作系统上下文切换。这相当昂贵。”

  • 这在 Linux 上并非如此,其中互斥锁是使用称为 futex'es 的东西实现的。正如 cmeerw 指出的那样,获取一个无争议(即尚未锁定)的互斥体只需几个简单的指令,并且通常在 25 纳秒(w/当前硬件)的范围内。

欲了解更多信息: Futex

每个人都应该知道的数字

于 2011-04-28T16:46:48.050 回答
7

这有点跑题了,但是您似乎对线程很陌生-一方面,只锁定线程可以重叠的位置。然后,尽量减少这些地方。此外,与其尝试锁定每个方法,不如考虑线程正在(总体上)对一个对象做什么,然后进行一次调用,然后锁定它。尽量让你的锁尽可能高(这再次提高了效率并且可能/帮助/避免死锁)。但是锁不会“组合”,你必须在精神上至少通过线程所在的位置和重叠来交叉组织你的代码。

于 2009-08-14T16:45:39.620 回答
4

我做了一个类似的库,并且在锁定性能方面没有任何问题。(我不能确切地告诉你它们是如何实现的,所以我不能肯定地说这没什么大不了的。)

我会先把它弄好(即使用锁),然后再担心性能。我不知道更好的方法;这就是互斥锁的用途。

单线程客户端的另一种选择是使用预处理器来构建库的非锁定版本和锁定版本。例如:

#ifdef BUILD_SINGLE_THREAD
    inline void lock () {}
    inline void unlock () {}
#else
    inline void lock () { doSomethingReal(); }
    inline void unlock () { doSomethingElseReal(); }
#endif

当然,这增加了一个额外的构建来维护,因为您将分发单线程和多线程版本。

于 2009-08-14T12:44:58.977 回答
3

我可以从 Windows 告诉你,互斥锁是一个内核对象,因此会产生(相对)显着的锁定开销。为了获得性能更好的锁,当您只需要一个在线程中工作的锁时,就是使用临界区。这不适用于跨进程,仅适用于单个进程中的线程。

然而.. linux 与多进程锁定完全不同。我知道互斥锁是使用原子 CPU 指令实现的,并且仅适用于进程 - 因此它们将具有与 win32 关键部分相同的性能 - 即非常快。

当然,最快的锁定是根本没有,或者尽可能少地使用它们(但是如果你的库要在一个重线程环境中使用,你会希望锁定尽可能短的时间:锁定,做某事,解锁,做其他事情,然后再次锁定比在整个任务中保持锁定要好 - 锁定的成本不在于锁定所花费的时间,而是线程坐在那里摆弄它的拇指等待的时间让另一个线程释放它想要的锁!)

于 2009-08-14T12:53:44.357 回答
2

互斥体需要操作系统上下文切换。那是相当昂贵的。CPU 仍然可以每秒执行数十万次而不会有太多麻烦,但它比没有互斥锁要贵得多。把它放在每个变量访问上可能是矫枉过正。

它也可能不是你想要的。这种蛮力锁定往往会导致死锁。

您知道保护此类变量访问的更好方法吗?

设计您的应用程序,以便共享尽可能少的数据。某些代码部分应该同步,可能使用互斥锁,但仅限于那些实际需要的部分。通常不是单独的变量访问,而是包含必须以原子方式执行的变量访问组的任务。(也许您需要设置is_active标志以及其他一些修改。设置该标志并且不对对象进行进一步更改是否有意义?)

于 2009-08-14T12:44:19.430 回答
2

我很好奇使用pthred_mutex_lock/unlock. 我有一个场景,我需要在不使用互斥锁的情况下复制 1500-65K 字节的任何位置,或者使用互斥锁并单次写入指向所需数据的指针。

我写了一个短循环来测试每个

gettimeofday(&starttime, NULL)
COPY DATA
gettimeofday(&endtime, NULL)
timersub(&endtime, &starttime, &timediff)
print out timediff data

或者

ettimeofday(&starttime, NULL)
pthread_mutex_lock(&mutex);
gettimeofday(&endtime, NULL)
pthread_mutex_unlock(&mutex);
timersub(&endtime, &starttime, &timediff)
print out timediff data

如果我复制的字节数少于 4000 左右,那么直接复制操作花费的时间更少。但是,如果我复制了超过 4000 个字节,那么执行互斥锁/解锁的成本会更低。

互斥锁/解锁的时间在 3 到 5 微秒之间,包括当前时间的 gettimeofday 时间,大约需要 2 微秒

于 2010-08-07T16:14:42.727 回答
1

对于成员变量访问,您应该使用读/写锁,它的开销略小,并且允许多个并发读取而不会阻塞。

在许多情况下,如果您的编译器提供了原子内置函数(如果您使用 gcc 或 icc __sync_fetch*() 等),您可以使用它们,但众所周知,它们很难正确处理。

如果您可以保证访问是原子的(例如,在 x86 上,dword 读取或写入始终是原子的,如果它是对齐的,但不是读取-修改-写入),您通常可以完全避免锁定并使用 volatile 代替,但是这是不可移植的,需要硬件知识。

于 2009-08-14T16:39:04.610 回答
0

一个次优但简单的方法是在互斥锁和解锁周围放置宏。然后有一个编译器/makefile 选项来启用/禁用线程。

前任。

#ifdef THREAD_ENABLED
#define pthread_mutex_lock(x) ... //actual mutex call
#endif

#ifndef THREAD_ENABLED
#define pthread_mutex_lock(x) ... //do nothing
#endif

然后在编译时做一个gcc -DTHREAD_ENABLED启用线程。

同样,我不会在任何大型项目中使用这种方法。但前提是你想要一些相当简单的东西。

于 2009-08-14T20:43:26.677 回答