On Aug 30, 2011, at 1:41 PM, dsimcha wrote:
> == Quote from Sean Kelly ([email protected])'s article
>> I really need to fix this. It's a pain though, for the reasons related
>> to the ones you mention in "5. Shared". A tid, for example, fronts a
>> message queue object that has some shared interface elements and some
>> unshared interface elements. The shared portion, rather than labeling
>> functions as synchronized, uses synchronized internally to make the
>> mutex use as fine-grained as possible. And the logically unshared
>> portion only synchronizes when accessing the shared data in the object.
>> So to make Tid work with shared I would basically have to label the
>> shared methods as shared and re-evaluate the ASM output once the
>> compiler inserts memory barriers, and cast away shared when accessing
>> the message queue from within its owner thread. What gets me about all
>> this is that rather than helping me, the type system is working against
>> me. I love shared as far as its use for globals is concerned, and for
>> concrete variables, but not so much for classes.
>
> Yea, shared really was a blunder at least in anything like its current form.
> I
> think it's time we just admit it and do our best to mitigate it or find a way
> to
> massively overhaul it (though I'm skeptical that it can be overhauled
> successfully, especially with the constraints on backwards compatibility).
>
> The default thread-local/explicit global was a great idea, as was providing a
> share-nothing-except-immutable message passing-based concurrency module in
> Phobos
> for people who want safe, simple, coarse-grained concurrency. Message
> passing is
> safe and good for a lot of stuff, but not everything.
>
> Once you're trying to use a threading paradigm where you need shared mutable
> state, though, it can't be safe and it's a waste of time for the language to
> try.
I think the idea behind shared as it applies to classes is that the desired
application won't have many shared objects, and those that are shared should
typically be fairly simple things like containers of objects that don't have
external references. The typical "web of shared objects" as per Java is an
invitation to disaster, and so the model rightly discourages that.
Regarding classes, I think one fundamental problem is that shared is
transitive. Simply putting memory barriers between accesses to shared data
doesn't mean that the algorithm itself will be correct if executed
concurrently. Transitively applying shared to member data and allowing
lock-free operations on this data and having the compiler even insert memory
barriers suggests to the neophyte user (IMO) that this is a safe and acceptable
means for making a class multithreaded when nothing could be further from the
truth. Another issue with transitivity is how this affects C API calls, as the
recent flurry of Windows patches to Phobos can attest.
> Even if you get rid of low-level data races, you still need to worry about
> high-level invariants, so you've only won half the battle. Furthermore, the
> shared type constructor cripples shared-state multithreading so much that it's
> almost useless unless you do some casting, defeating its purpose. The way I
> see
> this evolving is that almost all multithreaded code in D will either:
>
> 1. Avoid sharing and just use some combination of straight message passing
> and
> immutable data.
>
> 2. Bypass shared with casts, use of core.thread, std.parallelism, etc. and
> just
> do threading the old-fashioned way. (Though shared-state multithreading can be
> made less dangerous by encapsulating high-level paradigms. In this respect
> std.parallelism represents a middle ground between the completely safe
> std.concurrency and the completely flexible core.thread.)
Regarding #1, I do think there's a case to be made for having mutable shared
data, but that data can't be terribly complex. I do very much dislike having
to cast away shared though, because it makes for a very awkward API design.
For example, say I want to use synchronized in a fine-grained manner. I have
to do something like this:
class MyClass {
shared void doSomething() {
cast(Unshared!(MyClass)).doSomething_();
}
private void doSomething_() {
…
synchronized(this) { … }
…
}
> Ironically, I don't see this as such a bad outcome, except for the wasted
> "shared"
> keyword and dead trees describing it. D is a systems language and there
> needs to
> be ways to do dangerous, unchecked multithreading. It's great to provide a
> safe
> but limited way to write concurrent programs (such as share-nothing message
> passing), it's a no-brainer to prohibit the dangerous ways in SafeD, and it's
> fine
> to require some explicitness when using the dangerous ways (such as importing
> a
> different module). However, fighting the type system every inch of the way
> while
> paying lip service to playing nice with shared is not an acceptable solution.
> If
> you need shared state then you're almost guaranteed to need to cast away
> shared
> all over the place, and if you need to do so then you may as well bypass it
> entirely because it's just getting in the way and not providing any safety.
> This
> is why I've been so adamant about keeping core.thread and std.parallelism the
> way
> they are unless shared massively improves in ways that I'm very skeptical are
> even
> possible.
And this is why I haven't made much effort to change core.thread. I've
actually tried twice so far and gave up each time after seeing the cascade of
changes necessary.
I'd like to say that the 'shared' attribute on member functions should just
mean "make this member visible through a shared reference" and do away with the
memory barriers idea entirely, except that D really does need some form of
atomics. I'm really not sure what the solution is here.