“代码重入”和“线程安全”的概念有什么区别?根据下面提到的链接,一段代码可以是其中之一,也可以是两者,也可以不是。
我无法清楚地理解解释。帮助将不胜感激。
可重入代码在单点上没有状态。您可以在代码中执行某些操作时调用代码。如果代码使用全局状态,一个调用可能会覆盖全局状态,从而中断另一个调用中的计算。
线程安全代码是没有竞争条件或其他并发问题的代码。竞争条件是两个线程做某事的顺序影响计算。典型的并发问题是对共享数据结构的更改可能部分完成并处于不一致状态。为了避免这种情况,您必须使用并发控制机制,例如互斥量的信号量,以确保在操作完成之前没有其他任何东西可以访问数据结构。
例如,一段代码可以是不可重入但线程安全的,如果它在外部由互斥锁保护但仍然具有全局数据结构,其中状态必须在整个调用期间保持一致。在这种情况下,同一个线程可以发起对过程的回调,同时仍然受到外部粗粒度互斥体的保护。如果回调发生在不可重入过程中,则该调用可能会使数据结构处于从调用者的角度来看可能会破坏计算的状态。
如果一段代码可以对共享(和可共享)数据结构进行非原子更改,该更改可能在更新过程中被中断,从而使数据结构处于不一致状态,则它可以是可重入但非线程安全的. 在这种情况下,访问数据结构的另一个线程可能会受到半更改的数据结构的影响,并且会崩溃或执行破坏数据的操作。
那篇文章说:
“一个函数可以是可重入的,线程安全的,两者都可以,或者都不是。”
它还说:
“不可重入函数是线程不安全的”。
我可以看到这可能会导致混乱。它们意味着记录为不需要可重入的标准函数也不需要是线程安全的,这对于 POSIX 库 iirc 是正确的(并且 POSIX 也声明它对于 ANSI/ISO 库也是如此,ISO 有没有线程的概念,因此没有线程安全的概念)。换句话说,“如果一个函数说它是不可重入的,那么它也说它是线程不安全的”。这不是逻辑上的必要性,它只是一个约定。
这是一些线程安全的伪代码(好吧,由于锁定反转,回调有很多机会创建死锁,但我们假设文档包含足够的信息让用户避免这种情况)但不能重入。它应该增加全局计数器,并执行回调:
take_global_lock();
int i = get_global_counter();
do_callback(i);
set_global_counter(i+1);
release_global_lock();
If the callback calls this routine again, resulting in another callback, then both levels of callback will get the same parameter (which might be OK, depending on the API), but the counter will only be incremented once (which is almost certainly not the API you want, so it would have to be banned).
That's assuming the lock is recursive, of course. If the lock is non-recursive, then of course the code is non-reentrant anyway, since taking the lock the second time won't work.
Here's some pseudo-code which is "weakly re-entrant" but not thread-safe:
int i = get_global_counter();
do_callback(i);
set_global_counter(get_global_counter()+1);
Now it's fine to call the function from the callback, but it's not safe to call the function concurrently from different threads. It's also not safe to call it from a signal handler, because re-entrancy from a signal handler could likewise break the count if the signal happened to occur at the right time. So the code is non-re-entrant by the proper definition.
Here's some code which arguably is fully re-entrant (except I think the standard distinguishes between reentrant and 'non-interruptible by signals', and I'm not sure where this falls), but still isn't thread-safe:
int i = get_global_counter();
do_callback(i);
disable_signals(); // and any other kind of interrupts on your system
set_global_counter(get_global_counter()+1);
restore_signal_state();
On a single-threaded app, this is fine, assuming that the OS supports disabling everything that needs to be disabled. It prevents re-entrancy from occurring at the critical point. Depending how signals are disabled, it may be safe to call from a signal handler, although in this particular example there's still the issue of the parameter passed to the callback being the same for separate calls. It can still go wrong multi-threaded, though.
In practice, non-thread-safe often implies non-re-entrant, since (informally) anything that can go wrong due to the thread being interrupted by the scheduler, and the function called again from another thread, can also go wrong if the thread is interrupted by a signal, and the function is called again from the signal handler. But then the "fix" to prevent signals (disabling them) is different from the "fix" to prevent concurrency (locks, usually). This is at best a rule of thumb.
Note that I've implied globals here, but exactly the same considerations would apply if the function took as a parameter a pointer to the counter and the lock. It's just that the various cases would be thread-unsafe or non-re-entrant when called with the same parameter, rather than when called at all.