Streams can be used in multi-threaded applications in the same way they are used in single-threaded applications. But the programmer must be aware of the possible complications. It is important to know about these also if the program one writes never use threads since the design and implementation of many stream functions are heavily influenced by the requirements added by multi-threaded programming.
The POSIX standard requires that by default the stream operations are atomic. I.e., issuing two stream operations for the same stream in two threads at the same time will cause the operations to be executed as if they were issued sequentially. The buffer operations performed while reading or writing are protected from other uses of the same stream. To do this each stream has an internal lock object which has to be (implicitly) acquired before any work can be done.
But there are situations where this is not enough and there are also situations where this is not wanted. The implicit locking is not enough if the program requires more than one stream function call to happen atomically. One example would be if an output line a program wants to generate is created by several function calls. The functions by themselves would ensure only atomicity of their own operation, but not atomicity over all the function calls. For this it is necessary to perform the stream locking in the application code.
void
flockfile (FILE *stream)
¶Preliminary: | MT-Safe | AS-Safe | AC-Unsafe lock | See POSIX Safety Concepts.
The flockfile
function acquires the internal locking object
associated with the stream stream. This ensures that no other
thread can explicitly through flockfile
/ftrylockfile
or
implicitly through the call of a stream function lock the stream. The
thread will block until the lock is acquired. An explicit call to
funlockfile
has to be used to release the lock.
int
ftrylockfile (FILE *stream)
¶Preliminary: | MT-Safe | AS-Safe | AC-Unsafe lock | See POSIX Safety Concepts.
The ftrylockfile
function tries to acquire the internal locking
object associated with the stream stream just like
flockfile
. But unlike flockfile
this function does not
block if the lock is not available. ftrylockfile
returns zero if
the lock was successfully acquired. Otherwise the stream is locked by
another thread.
void
funlockfile (FILE *stream)
¶Preliminary: | MT-Safe | AS-Safe | AC-Unsafe lock | See POSIX Safety Concepts.
The funlockfile
function releases the internal locking object of
the stream stream. The stream must have been locked before by a
call to flockfile
or a successful call of ftrylockfile
.
The implicit locking performed by the stream operations do not count.
The funlockfile
function does not return an error status and the
behavior of a call for a stream which is not locked by the current
thread is undefined.
The following example shows how the functions above can be used to
generate an output line atomically even in multi-threaded applications
(yes, the same job could be done with one fprintf
call but it is
sometimes not possible):
FILE *fp; { … flockfile (fp); fputs ("This is test number ", fp); fprintf (fp, "%d\n", test); funlockfile (fp) }
Without the explicit locking it would be possible for another thread to
use the stream fp after the fputs
call returns and before
fprintf
was called with the result that the number does not
follow the word ‘number’.
From this description it might already be clear that the locking objects
in streams are no simple mutexes. Since locking the same stream twice
in the same thread is allowed the locking objects must be equivalent to
recursive mutexes. These mutexes keep track of the owner and the number
of times the lock is acquired. The same number of funlockfile
calls by the same threads is necessary to unlock the stream completely.
For instance:
void
foo (FILE *fp)
{
ftrylockfile (fp);
fputs ("in foo\n", fp);
/* This is very wrong!!! */
funlockfile (fp);
}
It is important here that the funlockfile
function is only called
if the ftrylockfile
function succeeded in locking the stream. It
is therefore always wrong to ignore the result of ftrylockfile
.
And it makes no sense since otherwise one would use flockfile
.
The result of code like that above is that either funlockfile
tries to free a stream that hasn’t been locked by the current thread or it
frees the stream prematurely. The code should look like this:
void foo (FILE *fp) { if (ftrylockfile (fp) == 0) { fputs ("in foo\n", fp); funlockfile (fp); } }
Now that we covered why it is necessary to have locking it is necessary to talk about situations when locking is unwanted and what can be done. The locking operations (explicit or implicit) don’t come for free. Even if a lock is not taken the cost is not zero. The operations which have to be performed require memory operations that are safe in multi-processor environments. With the many local caches involved in such systems this is quite costly. So it is best to avoid the locking completely if it is not needed – because the code in question is never used in a context where two or more threads may use a stream at a time. This can be determined most of the time for application code; for library code which can be used in many contexts one should default to be conservative and use locking.
There are two basic mechanisms to avoid locking. The first is to use
the _unlocked
variants of the stream operations. The POSIX
standard defines quite a few of those and the GNU C Library adds a few
more. These variants of the functions behave just like the functions
with the name without the suffix except that they do not lock the
stream. Using these functions is very desirable since they are
potentially much faster. This is not only because the locking
operation itself is avoided. More importantly, functions like
putc
and getc
are very simple and traditionally (before the
introduction of threads) were implemented as macros which are very fast
if the buffer is not empty. With the addition of locking requirements
these functions are no longer implemented as macros since they would
expand to too much code.
But these macros are still available with the same functionality under the new
names putc_unlocked
and getc_unlocked
. This possibly huge
difference of speed also suggests the use of the _unlocked
functions even if locking is required. The difference is that the
locking then has to be performed in the program:
void foo (FILE *fp, char *buf) { flockfile (fp); while (*buf != '/') putc_unlocked (*buf++, fp); funlockfile (fp); }
If in this example the putc
function would be used and the
explicit locking would be missing the putc
function would have to
acquire the lock in every call, potentially many times depending on when
the loop terminates. Writing it the way illustrated above allows the
putc_unlocked
macro to be used which means no locking and direct
manipulation of the buffer of the stream.
A second way to avoid locking is by using a non-standard function which was introduced in Solaris and is available in the GNU C Library as well.
int
__fsetlocking (FILE *stream, int type)
¶Preliminary: | MT-Safe race:stream | AS-Unsafe lock | AC-Safe | See POSIX Safety Concepts.
The __fsetlocking
function can be used to select whether the
stream operations will implicitly acquire the locking object of the
stream stream. By default this is done but it can be disabled and
reinstated using this function. There are three values defined for the
type parameter.
FSETLOCKING_INTERNAL
¶The stream stream
will from now on use the default internal
locking. Every stream operation with exception of the _unlocked
variants will implicitly lock the stream.
FSETLOCKING_BYCALLER
¶After the __fsetlocking
function returns, the user is responsible
for locking the stream. None of the stream operations will implicitly
do this anymore until the state is set back to
FSETLOCKING_INTERNAL
.
FSETLOCKING_QUERY
¶__fsetlocking
only queries the current locking state of the
stream. The return value will be FSETLOCKING_INTERNAL
or
FSETLOCKING_BYCALLER
depending on the state.
The return value of __fsetlocking
is either
FSETLOCKING_INTERNAL
or FSETLOCKING_BYCALLER
depending on
the state of the stream before the call.
This function and the values for the type parameter are declared in stdio_ext.h.
This function is especially useful when program code has to be used
which is written without knowledge about the _unlocked
functions
(or if the programmer was too lazy to use them).