| Posted by Jonathan M Davis in reply to mw | PermalinkReply |
|
Jonathan M Davis
| On Wednesday, July 12, 2023 10:32:22 AM MDT mw via Digitalmars-d wrote:
> On Wednesday, 12 July 2023 at 10:29:50 UTC, WebFreak001 wrote:
> > On Wednesday, 12 July 2023 at 03:00:11 UTC, mw wrote:
> >> On Monday, 13 March 2023 at 20:32:08 UTC, WebFreak001 wrote:
> >>> On Monday, 13 March 2023 at 02:53:00 UTC, Yuxuan Shui wrote:
> >>>> Example here:
> >>>>
> >>>> https://dlang.org/phobos/std_logger_core.html#.sharedLog
> >>>>
> >>>> Result:
> >>>>
> >>>> https://run.dlang.io/is/RMtF0I
> >>>
> >>> this changed in one of the most recent releases, but you can always expect breaking changes in an std.experimental module
> >>
> >> Can you please show the correct usage pattern of this sharedLog, in the new release?
> >
> > what I do now in my code:
> >
> > ```d
> > static if (__VERSION__ < 2101)
> >
> > sharedLog = new FileLogger(io.stderr);
> >
> > else
> >
> > sharedLog = (() @trusted => cast(shared) new
> >
> > FileLogger(io.stderr))();
> > ```
> >
> > not a big fan of the changes and the fact that this got made std.logger instead of the previous API, but it works at least
>
> 1) Is this `cast` ugly?
> 2) if it's a regular file, is this still thread safe to use?
>
>
> Without the `cast`, I got compiler error
>
> https://run.dlang.io/is/h3S0eb
> ```
> import std.stdio;
> import std.algorithm;
> import std.logger.core, std.logger.filelogger;
> import std.range;
>
> void main()
> {
> // sharedLog = new shared(FileLogger)("/dev/null");
>
> static if (__VERSION__ < 2101)
> sharedLog = new FileLogger(io.stderr);
> else
> sharedLog = (() @trusted => new shared(FileLogger)(stderr))();
>
> }
> ```
>
>
> onlineapp.d(13): Error: none of the overloads of `__ctor` are
> callable using a `shared` object
> /dlang/dmd/linux/bin64/../../src/phobos/std/logger/filelogger.d(40):
> Candidates are: `std.logger.filelogger.FileLogger.this(const(string) fn,
> const(LogLevel) lv = LogLevel.all)`
> /dlang/dmd/linux/bin64/../../src/phobos/std/logger/filelogger.d(66):
> `std.logger.filelogger.FileLogger.this(const(string) fn,
> const(LogLevel) lv, Flag createFileNameFolder)`
> /dlang/dmd/linux/bin64/../../src/phobos/std/logger/filelogger.d(105):
> `std.logger.filelogger.FileLogger.this(File file,
> const(LogLevel) lv = LogLevel.all)`
>
>
> Sigh, D is so broken on such basic stuff.
In general, objects are not supposed to be constructed as shared or even really operated on as shared. A shared object is shared across threads, and as long as that's done without protections in place, it's not thread-safe to actually do anything with the object. So, in most cases, what you need to do in order to operate on a shared object is to protect that section of code with a mutex, cast away shared to get a thread-local reference to the object, do whatever you need to do to the object while the mutex is locked, and then before you release the mutex, you make sure that no thread-local references to the object remain. That way, you don't ever operate on data while it's actively been shared across threads. The compiler prevents you from operating on the object through a shared reference precisely because doing so would not be thread-safe. This does of course result in some annoying casts, but it allows you to segregate the code that actually operates on shared objects, and because the language has shared, it prevents you from accidentally doing operations on shared objects that aren't thread-safe as long as the code is @safe (since the casts to remove shared have to be done in @trusted code to be callable by @safe code).
Outside of dealing with atomics, the only time that a shared object can be operated on when it's still shared is when it's a type that's specifically designed to work as shared. In that case, its member functions will be marked with shared so that they can be called when the object is shared, and instead of you casting away shared to do anything to the object, the mutexes and casting and whatnot are all handled internally within the member functions. But whether you're handling the mutexes and casting yourself, or whether the object itself handles that, the data within the object can't be operated one as long as it's shared (except for when using atomics).
Unfortunately, because most languages don't have shared in their type systems (and don't have objects be thread-local by default), shared is often misunderstood and misused.. And it is annoying to deal with, but that annoyance is largely on purpose, because it's the type system attempting to protect you from doing anything that isn't thread-safe. It would of course be nice if the type system were sophisticated enough to be able to automatically remove shared for you in some cases (e.g. because it knows that you have already protected the shared object with a mutex in a particular section of code), but it's difficult to give the compiler enough information for it to be able to do that. So, as things stand, it's up to the programmer to make sure that they've protected shared data appropriately before casting away shared and operating on it as thread-local while it's safe to do so.
Ultimately, you have to do basically the same stuff in languages like C or C++, but in those languages, you don't have the compiler trying to prevent you from doing anything that isn't thread-safe when it knows that data is shared across threads. They just leave everything as shared and leave it up to the programmer to deal with it all correctly. With D, you still have to get it right in the sections of code where you cast away shared, but the sections of code where that happens are then much better defined (since they have to be @trusted if you're using @safe), and in @safe code, you can't accidentally operate on shared data, because the compiler gives you an error when you attempt it. So, we're arguably much better off, but there is certainly some annoyance in the trade-off.
- Jonathan M Davis
|