线程互斥行为

我正在学习C.我正在编写一个multithreading的应用程序; I know that when a variable is shared between two or more threads, it is better to lock/unlock using a mutex to avoid deadlock and inconsistency of variables. This is very clear when I want to change or view one variable.

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

没有任何保护:

操作系统可以随时中断每个线程,并将处理器交给另一个。 “Anytime”包括“实际来自同一C命令的两个汇编指令之间”。

现在,假设您的变量在32位处理器中占用64位。 这意味着您的变量占用两个处理器“单词”。 为了编写它,处理器需要两个汇编指令。 同样适合阅读。 如果线程在两者之间中断,则会遇到麻烦。

为了给出一个更清晰的例子,我将使用两个十进制数字的类比来表示两个二进制32位字。 因此,假设您在1位处理器中递增两位十进制数。 要增加19到20,必须读取19,进行数学运算,然后写入20.要编写20,必须写入2然后写入0(反之亦然)。 如果你写2,然后在写0之前被打断,内存中的数字将是29,远远不是实际上正确的数字。 然后另一个线程继续读取错误的数字。

即使你有一个数字,仍然有读取 – 修改 – 写入问题Blank Xavier解释。

使用互斥锁

当线程A锁定互斥锁时,线程A会检查互斥锁变量。 如果它是空闲的,则线程A将其写为已采用。 它使用primefaces指令,一个汇编指令来完成它,因此没有“介于两者之间”来中断。 然后它继续增加19到20.它仍然可以在错误的29变量值期间被中断,但是没关系,因为现在没有其他人可以访问该变量。 当线程B试图锁定互斥锁时,它会检查互斥锁变量,然后执行该操作。 所以线程B知道它不能触及变量。 然后它调用操作系统,说“我现在放弃处理器”。 线程B将重复它,如果它再次获得处理器。 然后再次。 直到线程A最终获得处理器,完成它正在做的事情,然后解锁互斥锁。

那么,何时锁定?

这么多东西,取决于它。 主要是关于应用程序需要正常工作的特定行为顺序。 您需要在读取写入之前始终锁定以获得保护,然后解锁。 但是“锁定的代码块”可能有许多命令,或者只有一个命令。 保持上面解释的舞蹈并考虑应用程序应该如何表现。

还存在性能问题。 如果围绕每一行代码锁定/解锁,则会浪费时间锁定/解锁。 如果只围绕大块代码锁定/解锁,那么每个线程将等待很长时间让另一个线程释放互斥锁。

不是真的“永远”

现在,在某些情况下您可以跳过锁定解锁。 它们发生在处理一位数(意味着一个处理器字)变量时,并且每个线程只是读取它,或者只是写它,所以读取的值不会决定稍后写入它的值。 只有当您非常确定自己在做什么时才这样做 ,并且确实需要提高性能

一些解释的话。

在示例代码中,单个变量正在递增。

现在,组织内存和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回写,这导致它从当前所有者获取缓存行的副本,然后修改它 – 写回相同的值。

因此,其中一个增量丢失了。

如果你有一个obtain()函数,应该有一个release()函数,对吧? 然后在gets()中执行锁定并在release()中解锁。

你必须锁定应该是primefaces的整个操作 – 也就是说,应该作为一个不可分割的操作执行的块。