我正在学习C.我正在编写一个包含多个线程的应用程序;我知道当一个变量在两个或多个线程之间共享时,最好使用互斥锁来锁定/解锁,以避免死锁和变量的不一致。当我想要更改或查看一个变量时,这一点非常清楚。
int i = 0; /** Global */
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
/** Thread 1. */
pthread_mutex_lock(&mutex);
i++;
pthread_mutex_unlock(&mutex);
/** Thread 2. */
pthread_mutex_lock(&mutex);
i++;
pthread_mutex_unlock(&mutex);
我认为这是正确的。变量i
在执行结束时包含整数2
无论如何,在某些情况下,我不知道确切地将两个函数调用放在哪里。
例如,假设您有一个函数obtain()
,它返回一个全局变量。我需要从两个线程中调用该函数。我还有另外两个调用函数set()
的线程,用一些参数定义;此函数将设置相同的全局变量。在获取/设置var之前需要执行某些操作时,这两个函数是必需的。
/** (0) */
/** Thread 1, or 2, or 3... */
if(obtain() == something) {
if(obtain() == somethingElse) {
// Do this, sometimes obtain() and sometimes set(random number) (1)
} else {
// Do that, just obtain(). (2)
}
} else {
// Do this and do that (3)
// If # of thread * 3 > 10, then set(3*10) For example. (4)
}
/** (5) */
我必须锁定的地方,以及我必须解锁的地方? 我认为情况可能更复杂。我将非常感谢一个详尽的答案。
提前谢谢你 -Alberto
答案 0 :(得分:10)
没有任何保护:
操作系统可能会随时中断每个线程,并将处理器交给另一个线程。 “Anytime”包括“实际来自同一C命令的两个汇编指令之间”。
现在,假设您的变量在32位处理器中占用64位。这意味着您的变量占用了两个处理器“单词”。为了编写它,处理器需要两个汇编指令。同样适合阅读。如果线程在两者之间中断,则会遇到麻烦。
为了给出一个更清晰的例子,我将使用两个十进制数字的类比来表示两个二进制32位字。因此,假设您在1位处理器中递增两位十进制数。要增加19到20,必须读取19,进行数学运算,然后写入20.要编写20,必须写入2然后写入0(反之亦然)。如果你写2,然后在写0之前被中断,那么内存中的数字将是29,远远不是实际上正确的数字。然后另一个线程继续读取错误的数字。
即使你有一个数字,仍然有读取 - 修改 - 写问题Blank Xavier解释。
使用互斥锁:
当线程A锁定互斥锁时,线程A会检查互斥锁变量。如果它是空闲的,则线程A将其写为已采用。它使用原子指令,一个汇编指令来完成它,因此没有“介于两者之间”来中断。然后它继续增加19到20.它仍然可以在错误的29变量值期间被中断,但是没关系,因为现在没有其他人可以访问该变量。当线程B试图锁定互斥锁时,它会检查互斥锁变量,然后执行该操作。所以线程B知道它无法触及变量。然后它调用操作系统,说“我现在放弃处理器”。线程B将重复它,如果它再次获得处理器。然后再次。直到线程A最终得到处理器,完成它正在做的事情,然后解锁互斥锁。
那么,何时锁定?
这么多东西,取决于它。主要是关于应用程序需要正常工作的特定行为顺序。在阅读或写入以获得保护之前,您需要始终锁定,然后解锁。但是“锁定的代码块”可能有许多命令,或者只有一个命令。保持上面解释的舞蹈并考虑应用程序应该如何表现。
还存在性能问题。如果您锁定/解锁每一行代码,就会浪费时间锁定/解锁。如果只围绕大量代码锁定/解锁,那么每个线程将等待很长时间让另一个线程释放互斥锁。
不是“总是”
现在,在某些情况下您可以跳过锁定解锁。它们发生在处理一位数(意味着一个处理器字)变量时,并且每个线程要么只读取它,要么只写它,所以读取的值不会决定以后写入什么值。只有当您非常确定自己在做什么并且确实需要提高性能时才这样做。
答案 1 :(得分:3)
有些解释词。
在示例代码中,单个变量正在递增。
现在,内存和CPU缓存的组织方式使得无论何时访问内存,都可以一次访问高速缓存行的数据。这是因为内存开始访问速度非常慢,但随后相对快速地继续访问,并且因为通常情况下,当访问一位时,将访问相当数量的后续位。
所以,我们读入整数。假设整数长度为8个字节,高速缓存行也是8个字节(例如现代64位Intel CPU)。在这种情况下,读取是必要的,因为我们需要知道原始值。因此,读取发生并且高速缓存行进入L3,L2和L1高速缓存(英特尔使用包容性高速缓存; L1中的所有内容都存在于L2中,L2中的所有内容都存在于L3中等。)
现在,当你有多个CPU时,他们会密切注意其他人正在做什么,因为如果另一个CPU写入你缓存中的缓存行,你的副本就不再正确了。
如果我们有一个具有此缓存行的CPU在缓存中并且它递增该值,则具有此值副本的任何其他CPU将其副本标记为无效。
因此想象一下,我们在不同的CPU上有两个线程。它们都读入整数。此时,它们的缓存将此缓存行标记为共享。然后其中一个人写信给它。编写器将其缓存行标记为已修改,第二个CPU将其缓存行无效 - 因此当他尝试编写时,然后发生的是他再次尝试从内存中读取整数,但是因为存在于另一个中CPU缓存修改后的副本,他从第一个CPU中获取修改后的值的副本,第一个CPU将其副本标记为无效,现在第二个CPU写入自己的新值。
所以,到目前为止一切似乎都很好 - 我们怎么可能需要锁定?
问题在于此;一个CPU读取其缓存中的值 - 然后另一个CPU执行相同的操作。缓存行当前标记为已共享,因此这很好。然后它们都会增加。其中一个将回写,因此他的缓存行变为独占,而所有其他CPU的缓存行被标记为无效。然后第二个CPU回写,这使它从当前所有者那里获取一个缓存行的副本,然后修改它 - 写回相同的值。
因此,其中一个增量丢失了。
答案 2 :(得分:0)
如果您有obtain()
函数,则应该有release()
函数,对吗?然后在获取()中解锁并在release()中解锁。
答案 3 :(得分:0)
你必须锁定应该是 atomic 的整个操作 - 也就是说,应该作为一个不可分割的操作执行的块。