Tuesday, November 22, 2022

Digest for comp.lang.c++@googlegroups.com - 17 updates in 4 topics

Juha Nieminen <nospam@thanks.invalid>: Nov 22 09:52AM

Recently I made a post about references, about how I think many C++
programmers think of them in the wrong way (ie. they think of them
as being effectively an alternative syntax for pointers, a "more
limited pointer syntax", or "a safer pointer syntax", when in fact
references shouldn't be semantically thought of as pointers at all,
but as aliases for the objects they are referring to). In that thread
some criticism was presented about the use of references, and arguments
for their use.
 
For the sake of fairness and balance, here's an argument *against*
(the liberal use of) references.
 
Most C++ programmers have been conditioned to always take larger objects
(and even not so large objects) by const reference parameters in functions,
because that's more efficient. (The heavier an object is to deep-copy,
the more efficient a reference to it becomes, obviously.)
 
However, not many of them consider the *thread-safety* problem that taking
a parameter by reference introduces. In single-threaded programs this is
rather irrelevant, but multithreaded programming is becoming more and more
common every day. Also, if you are writing a library to be used in programs
out there, you have to consider its thread-safety even if the library itself
doesn't use threads.
 
What is this thread-safety problem introduced by references (especially
when we are writing a function that takes a parameter by reference)? The
fact that in theory another thread could modify the object being referred
to, at the same time that this function is trying to read it.
 
And there's nothing this function can do to defend against that. (Unless
this function cooperates with the calling code to make it thread-safe, eg.
by using a mutex offered by the calling code.) Even if the function is
internally thread-safe, it can't help the fact that it's using an external
resource without mutual exclusion (unless provided by the calling code).
 
How many times have you thought about the fact that taking a parameter
by reference makes the function automatically not-thread-safe? I know
I haven't. Like ever.
 
And, as has been pointed out, in the calling code itself it's not obvious
that a function is taking a parameter by reference, and thus might need
mutual exclusion.
"Fred. Zwarts" <F.Zwarts@KVI.nl>: Nov 22 11:23AM +0100

Op 22.von..2022 om 10:52 schreef Juha Nieminen:
 
> And, as has been pointed out, in the calling code itself it's not obvious
> that a function is taking a parameter by reference, and thus might need
> mutual exclusion.
 
I have seen such problems often. When possible, I try to make the class
itself thread-safe, such that it can only be accessed with thread-safe
member functions. But sometimes one has to use classes that are not
thread-safe themselves. Then this is something to think of. Using a copy
instead of a reference is not always the solution, because making a copy
is not always thread-safe either.
Paavo Helde <eesnimi@osa.pri.ee>: Nov 22 01:27PM +0200

22.11.2022 11:52 Juha Nieminen kirjutas:
> by using a mutex offered by the calling code.) Even if the function is
> internally thread-safe, it can't help the fact that it's using an external
> resource without mutual exclusion (unless provided by the calling code).
 
If the code passing a reference is not thread-safe, then just changing
it to pass by value will not magically make it thread-safe. Without
proper synchronization, the object may be changed by another thread at
any moment, for example in the middle of the copy operation, and thus
the copy might become internally inconsistent.
 
In multithreaded programs typically there are 3 types of objects:
 
1. Non-mutable shared objects which can be accessed by multiple threads
without locking. Can be passed by reference.
 
2. Shared objects which need locking when accessed. The locking will be
best placed inside the objects methods, so that the callers do not need
to worry about that. Can be passed by reference. Also lock-free data
structures would belong here.
 
3. Single-threaded objects which are accessed and modified in a single
thread only. Do not need locking, but require deep copying when passed
to another thread. The copying must happen before the copy will become
accessible in the other thread. This copying can be indeed done by
passing the object to a function by value - that's what is done e.g. by
the std::thread constructor.
 
In their own thread such objects can be accessed without locking and can
be passed via references, no problems.
 
In short, just casual pass-by-value is neither sufficient nor needed for
multi-thread safety. Copying is needed when passing over single-threaded
objects to other threads, which ought better to happen in clearly
defined points in the program.
Bonita Montero <Bonita.Montero@gmail.com>: Nov 22 03:36PM +0100

You've got problems that practical developers don't have.
Chris Vine <chris@cvine--nospam--.freeserve.co.uk>: Nov 22 03:21PM

On Tue, 22 Nov 2022 09:52:33 -0000 (UTC)
Juha Nieminen <nospam@thanks.invalid> wrote:
[snip]
 
> How many times have you thought about the fact that taking a parameter
> by reference makes the function automatically not-thread-safe? I know
> I haven't. Like ever.
 
Ah, the frailty of human memory! I drew it to your attention
some time ago. Anyway, I agree with your conclusions above.
 
********************************************************************
Date: Sat, 25 Aug 2018 01:38:29 +0100
From: Chris Vine <chris@cvine--nospam--.freeserve.co.uk>
Newsgroups: comp.lang.c++
Subject: Re: Should you use constexpr by default?
 
On Thu, 23 Aug 2018 05:48:38 -0000 (UTC)
Juha Nieminen <nospam@thanks.invalid> wrote:
> Even beyond that, in the era of C++11 and newer, the constness of
> a member function should indicate that it's thread-safe to call it
> without a locking mechanism.
 
It absolutely doesn't mean that. Locking of object (instance) data is
required in a const member function if it accesses those data at a time
when a non-const member function might concurrently mutate the data in
another thread.
 
You may have got this idea from a talk given by Herb Sutter in which he
asserted that "const means thread safe". It doesn't.
 
A const member function can also mutate static data.
********************************************************************
Richard Damon <Richard@Damon-Family.org>: Nov 22 10:48AM -0500

On 11/22/22 4:52 AM, Juha Nieminen wrote:
 
> And, as has been pointed out, in the calling code itself it's not obvious
> that a function is taking a parameter by reference, and thus might need
> mutual exclusion.
 
Taking a parameter by referance makes you no less thread safe than
taking a pointer as an parameter. In either case, the caller needs to
make sure it has proper "ownership" of the object it is passing a
reference or pointer to.
 
Yes, taking a parameter by (const) reference vs taking it by value
increases the exposure to thread safety issues.
 
I will agree that the presence of refernce arguments perhaps makes it
easier to miss some "sharing" that is happening, but ultimately, the C
and C++ philosophy is the programmer needs to know what he is doing.
They are NOT languages that coddle the programmer with extreme safety.
"Chris M. Thomasson" <chris.m.thomasson.1@gmail.com>: Nov 22 01:18PM -0800

On 11/22/2022 7:48 AM, Richard Damon wrote:
>> mutual exclusion.
 
> Taking a parameter by referance makes you no less thread safe than
> taking a pointer as an parameter. [...]
 
Agreed. Passing in a pointer vs a reference has no bearing on thread
safety. Think of, <pseudo-code>:
 
int
fetch_add(
int* const src,
int addend
){
return atomic_fetch_add(src, addend);
}
 
 
int
fetch_add(
int& src,
int addend
){
return atomic_fetch_add(&src, addend);
}
 
where this low level atomic_fetch_add function takes a pointer to an int
in src and an int as addend for its parameters. The way we pass in the
fetch_add::src parameter is irrelevant wrt reference vs. pointer wrt
thread safety. The atomic_fetch_add function takes care to make sure the
fetch_add RMW operation is atomic. Nothing to do with references vs
pointers...
El Jo <giorgio.zoppi@gmail.com>: Nov 22 01:41PM -0800

Il giorno martedì 22 novembre 2022 alle 09:52:49 UTC Juha Nieminen ha scritto:
> for their use.
 
> For the sake of fairness and balance, here's an argument *against*
> (the liberal use of) references.
 
Looks like a big post, but:
1. thread safety can only enforced by mutual-exclusion so I'd expect that before the function call, a mutex.
2. In 2021 we don't pass anymore by value big objects or by references, every we can we should transfer the ownership,
if this is not the case we should pass the pointer to the object. We pass by references just small objects that we don't own.
Just 1c.
BR,
"Chris M. Thomasson" <chris.m.thomasson.1@gmail.com>: Nov 22 01:51PM -0800

On 11/22/2022 1:41 PM, El Jo wrote:
>> (the liberal use of) references.
 
> Looks like a big post, but:
> 1. thread safety can only enforced by mutual-exclusion so I'd expect that before the function call, a mutex.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
Why do you say that? C++ has fairly decent atomic capabilities, that try
to avoid a mutex if the underlying architecture supports lock-free
atomic RMW's and loads/stores.
 
 
 
Bonita Montero <Bonita.Montero@gmail.com>: Nov 22 09:16PM +0100

I developed a little program which lists the process tree of Windows.
 
#if defined(_MSC_VER)
#define _CRT_SECURE_NO_WARNINGS 1

No comments: