On Aug 23, 4:49 am, James Kanze <
[email protected]> wrote:
[...]
Even after careful consideration of your points, I am still
not convinced that the conditional thread-safety the _r
variants give is the definition of POSIX thread-safe. If the
_r implementations are defining then it would be but then it's
rarely a good idea for implementations to define concepts.
I'm not too sure what your point is here. Are you implying that
the current implementations of the _r functions isn't conform?
Or something else?
I wasn't basing my argument on any specific implementation. In
some ways, I was basing it on common sense---localtime_r
obviously isn't going to work if two threads pass it the same
buffer, for the same reason localtime doesn't work. More
generally, the thread-safety guarantees in Posix consist of
several parts. On one hand, there is the guarantee that "All
functions defined by this volume of IEEE Std 1003.1-2001 shall
be thread-safe, except that the following functions1 need not be
thread-safe. [list of functions]" On the other, there are the
requirements placed on client code, for example "Applications
shall ensure that access to any memory location by more than one
thread of control (threads or processes) is restricted such that
no thread of control can read or modify a memory location while
another thread of control may be modifying it." It seems (to
me, at least) that calling localtime_r should be considered
modifying the memory locations pointed to by the buffer
parameter, in which case, calling the function from different
threads with the same buffer argument violates the requirements,
in the same way as e.g. calling it with a null pointer as the
buffer argument violates the requirements.
I suppose you might argue that they had a clear concept and _r
are just examples of that concept. However, in their design
rationale they considered other implementations for the _r
variants, namely dynamic allocation and thread-local storage,
both of which would have provided strong thread-safety. And it
seems to me the choice not to provide the thread-local storage
strong solution was simply incidental rather than fundamental.
In other words, some practical portability issues and not
fundamental concepts controlled the POSIX implementation
decision.
Are you saying that these functions violate the concepts
otherwise defined in the Posix standard? What about setjmp?
Most functions in the Posix standard place restrictions on their
arguments (e.g. no null pointer, etc.). If the client code
violates those restrictions, either the standard defines a
specific error behavior, or undefined behavior occurs. One of
those restrictions is that "Applications shall ensure that
access to any memory location by more than one thread of control
(threads or processes) is restricted such that no thread of
control can read or modify a memory location while another
thread of control may be modifying it." If the function
modifies memory, then this restriction applies; at least as I
read it, *p = ... isn't the only way of modifying memory.
Note that Posix doesn't really specify this as clearly as it
should, but it seems reasonable to consider that this
restriction only applies to modifications that the application
specifically requests. It applies to the memory pointed to by
the buffer argument of localtime_r, for example, but not to any
memory used internally by localtime_r---that's the
responsibility of the implementation.
Also, there are two interpretations as to how this applies to
C++ objects. I very strongly believe that it means that
external synchronization is only required if the application
requests a modification of the logical value of an object, but
others have argued that the const'ness of a function is
determinate. This has a definite effect for classes like
std::string---is something like:
if ( s[ 0 ] == t[ 0 ] )
require external synchronization if it occurs in two different
threads, and s is a non-const std::string?
Correct, the _r variants are "conditionally" thread-safe and I
think the implementation choice was largely incidental to the
concept of "thread-safe".
The problem with this point of view is that Posix very
explicitly states that they are thread safe (in §2.9.1). The
only functions Posix defines as conditionally thread-safe are
ctermid(), tmpnam(), wcrtomb() and wcstrtombs(). (In all cases,
they are required to be thread safe unless passed a null
pointer.)
Can you please tell us some of the experts you have in mind?
The authors of the Posix standard, naturally
. It's also the
position taken by the draft C++ standard with regards to
thread-safety. Although as far as I can see, the C++ standard
doesn't actually use the term "thread-safety" in this
regard---given the apparent ambiguity, that seems like a wise
decision.
Well for novice programmers it seems to mean something clear by
simple default of common sense language: if my use of it works
with a single thread then it works as-is with multiple threads.
In this context, I fear "clear and simple" is the equivalent of
"naive". As I said, I've seen code which carefully locks
internal accesses, then returns a reference to internal data. I
don't think we can base much on what "novice programmers" think
with regards to threading.
This is an interesting point. However, I think you might agree
there are some common-sense limits to what those contracts can
require.
I'm not sure. The point is that the code has defined a contract
that it claims to be valid in a threaded context. And IMHO,
that's the most important aspect if I want to use the code in a
multithreaded context---I know what the contract is, and what I,
as a client, have to do.
For example, would you consider:
/* thread-safety : foo must be wrapped in a mutex lock/unlock
* pairing. If this requirement is met then foo is thread-safe.
* /
int foo ( ) ;
the foo() above to be thread-safe?
Except that the wording of the guarantee doesn't seem very
precise, yes. The author has considered the issues, and decided
what he wants to contractually guarantee. (As I said, this is
*my* definition; I don't think it's widely shared.)
I wouldn't. And yet clearly it has a contract that defines its
"thread-safety". The POSIX _r functions have a contract like
/* thread-safety : the memory location *result must not be modified
* by another thread until localtime_r returns. If this requirement
* is met then localtime_r is thread-safe.
* /
struct tm *localtime_r(const time_t *restrict timer,
struct tm *restrict result);
which is of course more reasonable;
The contract is more complex than that. The contract says that
no other thread may access the memory locations defined by
*result, or undefined behavior occurs. And this requirement
holds not only during the call to localtime_r, but until the
pointed to buffer ceases to exist.
but, I'm still thinking this is "conditionally thread-safe"
not just "thread-safe".
That's your right. Just remember that you're using a different
definition than Posix.
Except that using precise terms helps to educate. So calling
it "conditionally thread-safe" would help to simultaneously
educate while just calling it "thread-safe" helps to introduce
bugs.
During the education process, you're obviously going to have to
define precisely what you mean by each term. And systematically
distinguishing between normal/basic thread safety and strong
thread safety might be a good idea---don't use "thread safety"
at all without a modifier.
vast majority of programmers don't understand threading
issues in general: I've seen more than a few cases of people
putting locks in functions like std::vector<>:
perator[],
which return references, and claiming the strong thread-safe
guarantee. Most of the time, when I hear people equating
strong thread-safety with thread-safety in general, they are
more or less at about this level---and the word naive really
does apply. Just telling them that boost::shared_ptr is not
thread safe in this sense is treating the symptom, not the
problem, and will cause problems further down the road.
I'm not saying we should tell them boost::shared_ptr is "not
thread-safe" because yes it would cause other problems. I'm
saying we should tell them it is "conditionally thread-safe"
or "as thread safe as a built-in type" which is what the Boost
documentation says. Because that at least encourages a
curious one to ask "what are the conditions?" what does "as a
built-in type mean?" etc.
We should reserve "thread-safe" for those structures that are
rock-solid, no-brainer safe with multi-threads ie strong
thread-safe.
I'd tend to avoid thread-safe completely with novices, given the
confusion surrounding the term. But I'd also treat
"thread-safe" much like "volatile", spending some time
explaining that it doesn't mean what you think it means. (And
that regardless of the definition, just using thread-safe
components everywhere doesn't guarantee thread safety.)
Can you please tell me what kind of limits if any you would
place on client code requirements? Is the foo() I gave earlier
"thread- safe"? Or what about a more complex where the
"contract" required synchronization between all calls of
multiple functions foo(), bar(), baz(), ...? Because at some
point it seems this definition of thread-safe would become
equally useless.
I don't think so. There are lots of components out there that
don't document anything, and that might have these sort of
requirements. If you know about them, you can take necessary
measures, whatever they might be, and safely use the components
in multithreaded code. If you don't know about them, you can't.
Reasonably, of course, there does occur a point where the
requirements are so restrictive that you won't use the
component.