在Linux上使用C语言的POSIX线程和全局变量

6
如果我有两个线程和一个全局变量(一个线程不断循环以读取变量;另一个线程不断循环以写入变量),会发生任何不应该发生的事情吗?(例如:异常、错误)。如果发生了,有什么方法可以防止这种情况发生。我读过互斥锁的相关内容,它们允许一个线程独占地访问变量。这是否意味着只有该线程可以读取和写入变量,而其他线程则不能?
5个回答

5

是否可能发生不应该发生的情况?

这要部分取决于变量的类型。如果变量是字符串(字符的长数组),那么如果写入程序和读取程序同时访问它,读取程序所看到的内容是完全未定义的。

这就是为什么 pthreads 提供了互斥锁和其他协调机制的原因。

这是否意味着只有该线程才能读写它,而其他线程则不能?

互斥锁确保最多只有一个正在使用互斥锁的线程可以获得继续执行的权限,所有其他使用相同互斥锁的线程将被阻塞,直到第一个线程释放互斥锁。因此,如果代码编写正确,任何时候只有一个线程将能够访问该变量。如果代码编写不正确,则可能出现以下情况:

  • 一个线程可能在未检查是否有权限的情况下访问该变量
  • 一个线程可能获取互斥锁并永远不释放它
  • 一个线程可能销毁互斥锁而不通知另一个线程

以上行为都是不希望出现的,但仅有互斥锁的存在并不能防止出现这些行为。

尽管如此,只要小心使用互斥锁,您的代码就可以合理地使用互斥锁,并且全局变量的访问将得到适当的控制。在通过互斥锁获得权限后,任何线程都可以修改变量或仅读取变量。无论哪种情况都不会受到其他线程的干扰。


全局数据是一个整数,用于睡眠计时器。一个线程从其他地方获取其他数据来确定睡眠时间,而另一个线程执行函数,然后休眠等待第一个线程设置的变量时间。睡眠全局变量不需要是“当前”的,即如果线程1将其设置为12,则线程2将其读取为12,就像线程1将其更改为16一样。是否仍建议实现锁定,或者我可以将其保留为原样,因为睡眠时间不需要是线程1设置的当前时间? - randy newfield
很有可能一个int在读写时是"原子性"的,但这并不是保证。你很可能可以不使用互斥锁来保护此值,但你可能会遇到奇怪的问题,并且时间问题非常难以复现和诊断。我可能不会冒险,但这取决于你的系统/应用程序有多重要。如果阅读器每次都要休眠12秒钟,那么如果编写者疯狂地改变它关于时间的想法,这并不重要。所以,是的,你有权利采取(经过计算的)风险。但是,如果事情出了问题,请不要哭泣。 - Jonathan Leffler
睡眠时间实际上不是以秒为单位的,那只是一个例子。写线程每秒更新一次睡眠时间,而读线程可能在下一次更新之前多次读取时间,因为睡眠时间实际上是一个空循环的大整数迭代次数,用于替换系统调用usleep()。无论如何,我认为我还是会实现互斥锁定。它不会太影响线程的速度,而且很可能会解决未来的头痛问题。 - randy newfield
一个忙循环总是一个设计缺陷。依赖于(现代的、可变速度的)CPU 的速度也是一个设计缺陷。这些设计缺陷旨在解决什么问题? - Brendan

2
这是什么意思,只有该线程可以读写它,其他线程不能吗?
这意味着每次只有一个线程可以读取或写入全局变量。两个线程不会相互竞争以访问全局变量,也不会在任何给定的时间同时访问它。
简而言之,对全局变量的访问是同步的。

让它们在没有锁的情况下读取和写入是否安全,还是应该让每个线程锁定变量,在进行读写操作时解锁? - randy newfield
我不确定我同意这个答案 - 只有一个进程可以获取互斥锁,但是另一个进程中的编程错误很容易让它在没有首先获取锁的情况下读/写变量。 - Soren
@bobmoch:一般来说,没有加锁的读写操作是不安全的。在某些非常特殊的情况下,这样做是安全的,但最好不要担心它,因为无争用锁非常快。 - Dietrich Epp
@Soren:不存在一种编程语言是完全“安全”的,可以避免编程错误。 - Dietrich Epp
@bobmoch:在读取或写入操作时,您必须锁定全局变量,否则您将会出现竞争条件。 - Alok Save
显示剩余2条评论

1

首先,对于C/C++中未同步读/写的变量并不会产生任何异常或系统错误,但它可能会生成应用程序级别的错误——主要是因为你很难完全理解内存如何被访问以及是否是原子性的,除非你查看生成汇编代码。当你在没有同步的情况下访问共享内存时,多核CPU可能会导致难以调试的竞争条件。

因此,第二点:在处理共享内存时,您应该始终使用同步,例如互斥锁。互斥锁很便宜,如果使用得当,它不会影响性能。经验法则是尽可能短地保持锁定状态,只需在读取/增加/写入共享内存期间保持锁定状态。

然而,根据您的描述,似乎其中一个线程除了等待共享内存改变状态之外什么也没有做——这是一种坏的多线程设计,会造成不必要的CPU负担,因此

第三点:如果您正在尝试从一个线程向另一个线程发送“消息”,请考虑使用信号量(sem_create/wait/post)进行线程间同步。


1

正如其他人所说,当通过“普通”对象在线程之间进行通信时,您必须注意竞争条件。除了相对较重的互斥锁和其他锁结构外,新的C标准(C11)提供了原子类型和操作,保证不会出现竞争。大多数现代处理器都为此类类型提供指令,并且许多现代编译器(特别是Linux上的gcc)已经为这些操作提供了适当的接口。


-1
如果线程确实只有一个生产者和一个消费者,那么(除编译器错误外):
1)将变量标记为volatile,并且
2)确保它正确对齐,以避免交错的获取和存储,
将允许您在不锁定的情况下执行此操作。

这仅适用于(1)某些体系结构,具有(2)某些数据类型,并且(3)如果您不需要阻塞语义并且(4)全局变量不引用共享状态。 除非在非常特殊的情况下,否则不可能推荐使用此方法。(顺便说一句,无论数据是否对齐,都将通过CPU和编译器重新排序获取和存储--使用“volatile”仅防止编译器进行某些特定更改,而CPU仍然可以重新排序它。) - Dietrich Epp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接