Prime sieve language race
Petar
Petar
Thu Jul 15 16:51:02 UTC 2021
On Thursday, 15 July 2021 at 13:16:01 UTC, Sebastiaan Koppe wrote:
> On Thursday, 15 July 2021 at 12:35:29 UTC, Petar Kirov
> [ZombineDev] wrote:
>> On Wednesday, 14 July 2021 at 19:10:55 UTC, Sebastiaan Koppe
>>> Because member functions are harder to call from multiple
>>> threads than static functions are. For one, you will have to
>>> get the object on two threads first. Most functions that do
>>> that require a shared object, which requires a diligent
>>> programmer to do the casting.
>>
>> The problem with `std.stdio : std{in,out,err}` is they ought
>> to be defined (conceptually) as `shared Atomic!File`, where
>> `File` is essentially a wrapper around `SharedPtr!FileState`
>> (and `SharedPtr` does atomic ref-counting, if it's `shared`)
>> and until then, they shouldn't be `@trusted`, unless the
>> program is single-threaded.
>
> Yes that is the sensible thing to do. But I am not sure that is
> the right thing. I am afraid that it will lead to the
> conclusion that everything needs to be shared, because who is
> going to stop someone from taking your struct/class/function,
> moving it over to another thread and then complain it corrupts
> memory while it was advertised as having a @safe interface?
Not quite. If an aggregate has no methods marked as `shared`, it
means that in essence it's not designed to be shared across
threads (i.e it's not thread-safe). Just like `const` methods
define the API of `const` object instances, `shared` methods
define the API of `shared` objects. While it can be useful to
overload methods based on the `this` type qualifier (e.g. I added
`shared` overloads to the `lock`, `unlock` and `tryLock` methods
of [`core.sync.mutex : Mutex`][0] (*)), it's not strictly
necessary. It's perfectly possible to have a class which has one
set of functions of single-thread use and a complete separate set
of thread-safe functions. As an example, a simple non-thread-safe
queue class can have `front`, `push`, `pop` and `empty` methods,
while a thread-safe variant will instead have `tryGetFront`,
`tryPush`, `tryPop` (and no `empty`) methods.
> I am afraid that it will lead to the conclusion that everything
> needs to be shared
This is the sort thinking common in languages like C# and Java
(at least, in my experience), where you don't know whether your
class may be shared across threads, so you either find out
eventually the hard way (via bug reports), or (e.g. if requested
by code reviewers) you go in and preemptively add locks all over
the code (usually not tested well, since you your initial
use-case didn't involve sharing the object across threads).
This is not the case in D. If your aggregate doesn't have
`shared` methods it means that it must not be `shared`, plain and
simple.
That's why `__gshared` should be avoided - it shares both
thread-safe and non-thread-safe objects across threads. A
`__gshared` `Mutex` will work just fine (as the underlying
Posix/Win32 primitives are obviously designed support it), but
other types, like D's associative arrays would certainly go
kaboom, if access to them is not *synchronized externally* (**).
In case of Phobos, `std.stdio : std{in,out,err}` should really be
made thread-safe (you can find issues in bugzilla), as the whole
idea of making them global mutable properties is to allow any
thread to redirect them at any point of time. Whether that's a
good idea is a separate topic, but it was certainly an intended
case.
(*) `core.sync.mutex : Mutex.{lock, unlock, tryLock}` really
should have been `shared @safe nothrow @nogc` from the beginning,
but hey better late, then never :)
I considered removing the non-`shared` overloads, but I decided
against, as that would have been a breaking change. That said,
once we have enough high-quality APIs in Phobos to allow
ergonomic use of `shared` (i.e. not requiring people to cast-away
`shared` all over the place), we should consider deprecating them
(the non-`shared` overloads of `lock`/`unlock`/`tryLock`).
(**) Another way to discuss `shared` is to think in terms of
*internal* and *external synchronization*. If a method is
`shared`, it follows that access to the underlying object is
*internally synchronized*, i.e. you don't need an external mutex
to guard it. And vice versa - if the methods are not `shared`, it
means that you need to use external synchronization, and only
then (assuming you have implemented it correctly), you can cast
away `shared` and freely call the non-`shared` methods inside the
scope of the lock. See Rust's [`Mutex`][1] and more specifically
the [`MutexGuard`][2] types for a good example of this technique.
Given a type like `Rust`'s `MutexGuard`, casting-away `shared`
should really not be done in user-code - the idea is that the
`MutexGuard` will give you a safe `scope`-ed access to a
head-un-`shared` type (given `shared(SomeType**)` it will give
you `scope shared(SomeType*)*`).
P.S. I use the term "method" when I mean non-static member
function, and "aggregate" when I mean `struct`, `class`, or
`interface` type.
[0]: https://dlang.org/phobos/core_sync_mutex.html
[1]: https://doc.rust-lang.org/std/sync/struct.Mutex.html
[2]: https://doc.rust-lang.org/std/sync/struct.MutexGuard.html
More information about the Digitalmars-d
mailing list