Date: | June 29, 2004 / year-entry #258 |
Tags: | code |
Orig Link: | https://blogs.msdn.microsoft.com/oldnewthing/20040629-00/?p=38643 |
Comments: | 19 |
Summary: | An operation is "thread-safe" if it can be performed from multiple threads safely, even if the calls happen simultaneously on multiple threads. An operation is re-entrant if it can be performed while the operation is already in progress (perhaps in another context). This is a stronger concept than thread-safety, because the second attempt to perform... |
An operation is "thread-safe" if it can be performed from multiple threads safely, even if the calls happen simultaneously on multiple threads. An operation is re-entrant if it can be performed while the operation is already in progress (perhaps in another context). This is a stronger concept than thread-safety, because the second attempt to perform the operation can even come from within the same thread. Consider the following function: int length = 0; char *s = NULL; // Note: Since strings end with a 0, if we want to // add a 0, we encode it as "\0", and encode a // backslash as "\\". // WARNING! This code is buggy - do not use! void AddToString(int ch) { EnterCriticalSection(&someCriticalSection); // +1 for the character we're about to add // +1 for the null terminator char *newString = realloc(s, (length+1) * sizeof(char)); if (newString) { if (ch == '\0' || ch == '\\') { AddToString('\\'); // escape prefix } newString[length++] = ch; newString[length] = '\0'; s = newString; } LeaveCriticalSection(&someCriticalSection); } This function is thread-safe because the critical section prevents two threads from attempting to add to the string simultaneously. However, it is not re-entrant. The internal call to AddToString occurs while the data structures are unstable. At the point of the call, execution re-enters the start of the function AddToString, but this time the attempt to realloc the memory will use a pointer (s) that is no longer valid. (It was invalidated by the call to realloc performed by the caller.) |
Comments (19)
|
2nd reason why it’s not re-entrant is that it would ‘loop’ forever when ch== 0 or
Nah, eventually it’ll crash in some difficult to debug way.
Hi Raymond, I like your clean difference between re-entrant and thread-safe. Thanks.
(Rats, missed the infinite loop on . Okay, so remove the test against and then you’re set.)
And of course re-entrancy is an interesting issue with STA COM components, especially as if you don’t think you have to worry about it ‘cos you’re in an STA ;)
I can only assume that "someCriticalSection" is a global variable of some sort, whereas quite obviously, "EnterCriticalSection" blocks re-entrancy from other threads.
It is fairly obvious but not quite as obvious as it could be.
Do I understand it correctly if I believe that re-entrant being stronger than thread-safe means that a re-entrant operation is automagically thread-safe?
Or can an operation be re-entrant and not thread-safe at the same time?
A function can be reentrant but not thread safe. There are two types of reentrancy issues, if it is reentrant by the same thread only or if it is reentrant by multiple threads. The latter of the two is the strongest one (and makes it automagically thread safe). For example:
void foo(unsigned i)
{
if (i–) {
free(malloc(1));
foo(i);
}
}
If malloc and free access a global heap and are not protected by any locks, this function is reentrant but not multiple-thread reentrant.
Another source of re-entrancy is user-mode APCs. Any routine that enters an alertable wait (calls SleepEx() or WaitForXxxxEx() with Alertable == TRUE) can potentially cause queued APCs to run.
Consider:
ReadFileEx( …, &MyCompletionRoutine );
SomethingThatWaitsAlertably();
/* etc */
MyCompletionRoutine()
{
SomethingThatWaitsAlertably();
}
This is one of the reasons why critical sections, mutexes etc. shouldn’t allow recursive locks (at least not by default).
A deadlock is always better than undefined behavior.
Even if this were reentrant, it wouldn’t work. It’s impossible to have length correct for the AddToString(‘\’) at that point.
This is reentrant (assuming it’s never called from an interrupt) but not thread safe:
void AddToString(int ch)
{
char *newString;
if (ch == 0)
AddToString(‘\’);
newString = realloc(s, (length+1) * sizeof(char))
if (newString)
{
newString[length++] = ch;
newString[length] = 0;
s = newString;
}
}
Is reentrancy mostly related to recursive functions? What are other scenarios besides alertable waits and interrupts?
A really common reentrant function that isn’t recursive is a window’s wndproc. The most common case besides sending messages back and forth between windows is if you spawn something that uses it’s own message loop like a modal dialog or popup window calls your wndproc while you’re already in your wndproc. Only if you’re doing something really dumb can you have problems with that case.
If you put asserts (the kind that spawns message boxes) on certain messages in your wndproc, your program will just suddenly disapear because of stack overflow.
You seem to suggest that re-entrant implies thread-safe. This is not true.
"You seem to suggest that re-entrant implies thread-safe. This is not true."
Example please.
Re-entrant but not thread-safe..
char * strcopy(char * dst,const char * src)
{
assert(dst!=NULL);
assert(src!=NULL);
if(*src!=’