我正在学习 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) */
我必须在哪里锁定,在哪里必须解锁? 我认为,情况可能更加复杂。我将不胜感激一个详尽的答案。
提前谢谢你。
—阿尔贝托
最佳答案
没有任何保护:
操作系统可能会随时中断您的每个线程,并将处理器交给另一个。 “随时”包括“实际上来自同一个 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 最终取回处理器,完成它正在做的事情,然后解锁互斥体。
那么,什么时候锁定?
至于很多事情,这取决于。主要是根据您的应用程序正常工作所需的特定行为顺序。您需要始终在读取或写入之前锁定以获得保护,然后再解锁。但是“锁定的代码块”可能有很多命令,也可能只有一个。牢记上面解释的舞蹈,并考虑您的应用程序应该如何表现。
还有性能问题。如果你围绕每一行代码进行锁定/解锁,你就会浪费时间进行锁定/解锁。如果您只在大块代码周围锁定/解锁,那么每个线程都会等待很长时间才能释放互斥量。
并不是真的“总是”
现在,有些情况下您可以跳过锁定-解锁。当您处理一个一位数(意味着一个处理器字)变量时,它们会发生,并且每个线程要么只读它,要么只写它,所以读取的值不会决定以后写入什么值。仅当您非常确定自己在做什么并且确实需要提高性能时才这样做。
关于c - 线程互斥行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/4718727/