digitalmars.D - shared - i need it to be useful
- Manu (37/38) Oct 15 2018 Okay, so I've been thinking on this for a while... I think I have a
- Peter Alexander (5/10) Oct 15 2018 Just checking my understanding: are you saying here that shared
- Manu (20/30) Oct 15 2018 Burden of correctness that a `shared` method is indeed threadsafe is
- Nicholas Wilson (5/17) Oct 15 2018 Well `this` is still shared so you would be prevented from doing
- Manu (6/17) Oct 15 2018 Just to be clear:
- jmh530 (8/12) Oct 15 2018 Are you familiar with reference capabilities[1] in the pony
- Nicholas Wilson (4/5) Oct 15 2018 Keep in mind immutable is implicitly shared (i.e. not in tls)
- Manu (3/8) Oct 15 2018 Are you saying `is(immutable(int) == shared) == true)` ??
- jmh530 (13/15) Oct 15 2018 From the spec:
- Nicholas Wilson (7/23) Oct 15 2018 The philosophy of this is that: the value never changes,
- Manu (5/32) Oct 15 2018 I don't think what I describe affects immutable in any way;
- Nicholas Wilson (2/11) Oct 15 2018 OK, just making sure you've got this covered.
- Peter Alexander (19/20) Oct 15 2018 What you describe sounds better than what we currently have.
- Manu (24/44) Oct 15 2018 I think you can model this differently... perhaps rather than a single
- Peter Alexander (10/27) Oct 15 2018 If it is broken then why allow it? Why do we need to cast shared
- Manu (26/54) Oct 15 2018 It's not that it's 'allowed' other than, yes, access to atomic int's
- Stanislav Blinov (5/13) Oct 15 2018 You're still talking about implicit promotion? No, it does not
- Manu (11/26) Oct 15 2018 Absolutely. This is critical to make shared useful, and I think
- Stanislav Blinov (5/37) Oct 15 2018 No, on the contrary. Someone with an unshared pointer may call
- Manu (23/60) Oct 15 2018 If a shared method is incompatible with an unshared method, your class
- Stanislav Blinov (43/67) Oct 15 2018 What?!? So... my unshared methods should also perform all that's
- Nicholas Wilson (15/63) Oct 15 2018 No, its the other way around: a shared method that does extra
- Manu (14/36) Oct 15 2018 Yes, except maybe I didn't make it clear that I DO expect the
- Isaac S. (13/34) Oct 15 2018 I understand your point but I think the current shared (no
- Manu (34/102) Oct 15 2018 Of course! You're writing a threadsafe object... how could you expect ot...
- Stanislav Blinov (50/145) Oct 15 2018 I don't see how an *implicit* cast can be a restriction. At all.
- Manu (115/270) Oct 15 2018 Because a shared pointer can't access anything.
- Stanislav Blinov (197/351) Oct 16 2018 That just doesn't compute. You obviously *can* do "anything" with
- Timon Gehr (10/12) Oct 17 2018 Then what you want is not implicit unshared->shared conversion. What you...
- Timon Gehr (3/18) Oct 17 2018 (Also, with this new definition of 'shared', unshared -> shared
- Timon Gehr (2/3) Oct 17 2018 Unfortunate typo. This should be if, not iff (if and only if).
- Timon Gehr (12/13) Oct 17 2018 This is actually not necessary, let me reformulate:
- Manu (18/32) Oct 17 2018 No. c.x is mutable, s.x is shared (and inaccessible).
- 12345swordy (10/16) Oct 15 2018 Unless the compiler can show that it is ok to implicit/explicity
- ag0aep6g (31/42) Oct 15 2018 As far as I understand, the rule "can't read or write to members" is for...
- Jacob Carlborg (24/26) Oct 16 2018 Instead of having to explicitly cast away shared we could leverage the
- Kagamin (10/24) Oct 16 2018 The value of shared is existence of thread-local data that's
- Manu (18/42) Oct 16 2018 This isn't really an argument in favour of shared being able to
- Timon Gehr (5/14) Oct 16 2018 Unshared becomes useless, and in turn, shared becomes useless. You can't...
- Dominikus Dittes Scherkl (12/26) Oct 16 2018 why is unshared useless?
- Timon Gehr (14/40) Oct 16 2018 It can do harm to others who hold an unshared alias to the same data and...
- Manu (16/57) Oct 16 2018 Nobody else holds an unshared alias.
- Timon Gehr (38/110) Oct 17 2018 How so? If you allow implicit conversions from unshared to shared, then
- Steven Schveighoffer (13/16) Oct 17 2018 When you say "shared members", you mean all the data is shared too or
- Timon Gehr (10/31) Oct 17 2018 The specific proposal was that, for example, if a class is defined like
- Steven Schveighoffer (7/42) Oct 17 2018 OK, so the proposal is that all data and function members are shared.
- Manu (7/8) Oct 17 2018 OMFG, I just spent about 3 hours writing a super-detailed reply to all
- Paolo Invernizzi (7/19) Oct 18 2018 Never never write something super-detailed in a web-based "thing"!
- Steven Schveighoffer (4/15) Oct 18 2018 If it's gmail, it should be in sent folder, no?
- Manu (9/23) Oct 16 2018 What aliasing? Please show a reasonable and likely construction of the
- Timon Gehr (13/45) Oct 17 2018 Aliasing means you have two references to the same data. The two
- Steven Schveighoffer (26/75) Oct 16 2018 This is a step in the right direction. But there is still one problem --...
- Steven Schveighoffer (7/17) Oct 16 2018 Oh, I didn't see this part. Completely agree with Timon on this, no
- Manu (4/20) Oct 16 2018 Why?
- Steven Schveighoffer (13/39) Oct 16 2018 int x;
- Manu (31/70) Oct 16 2018 What does this mean? It can't do anything... that's the whole point here...
- Steven Schveighoffer (46/83) Oct 16 2018 OK, I wrote a whole big response to this, and I went and re-quoted the
- Nicholas Wilson (5/13) Oct 16 2018 If I understand Manu correctly the first should compile, and the
- Manu (5/21) Oct 16 2018 Why is the second an error?
- Nicholas Wilson (2/27) Oct 16 2018 I missed that the third example was *p3 = p; not *p3 = p2;
- Nicholas Wilson (5/18) Oct 16 2018 I think this comes up where the queue was originally shared, you
- Steven Schveighoffer (3/21) Oct 17 2018 Isn't that a locking queue? I thought we were talking lock-free?
- Manu (58/141) Oct 16 2018 It's critical that this is not allowed. It's totally unreasonable to
- Isaac S. (25/59) Oct 16 2018 Overloading for shared and unshared is my reason for not allowing
- Manu (19/65) Oct 16 2018 Okay, so just to be clear, you're objecting to an immensely useful
- Isaac S. (10/37) Oct 16 2018 I'm not objecting to the behavior (I actually really want it in
- Steven Schveighoffer (47/153) Oct 17 2018 OK, so even with synchronization in the second thread when you cast, you...
- Nicholas Wilson (4/32) Oct 17 2018 It isn't, you typo'd it (I originally missed it too).
- Steven Schveighoffer (15/25) Oct 17 2018 It wasn't a typo.
- Nicholas Wilson (4/18) Oct 17 2018 The first example assigns p2, the second assigns p (which is
- Steven Schveighoffer (28/47) Oct 17 2018 Here they are again:
- Manu (79/124) Oct 17 2018 This doesn't make sense... you're showing a thread-local program.
- Stanislav Blinov (6/10) Oct 17 2018 Oh God...
- Manu (16/27) Oct 17 2018 This function is effectively an intrinsic. It's unsafe by definition.
- Stanislav Blinov (32/70) Oct 17 2018 Only if implicit conversion is allowed. If it isn't, that's
- Manu (53/128) Oct 17 2018 In this case, with respect to the context (a single int) atomicInc()
- Stanislav Blinov (65/175) Oct 17 2018 Yes, *you* can. *Another* function can't unless *you* allow for
- Erik van Velzen (2/2) Oct 17 2018 I don't have anything to add that hasn't been said yet but it's
- Manu (70/226) Oct 17 2018 It can though, because `int` doesn't have any shared methods; it's
- Stanislav Blinov (33/33) Oct 18 2018 Manu,
- Simen =?UTF-8?B?S2rDpnLDpXM=?= (30/68) Oct 18 2018 Well, sorta. But that's not a problem, because you can't do
- Stanislav Blinov (37/107) Oct 18 2018 Yes you can. You silently agree to another function's assumption
- Simen =?UTF-8?B?S2rDpnLDpXM=?= (77/156) Oct 18 2018 No, you don't. If I give you a locked box with no obvious way to
-
Simen =?UTF-8?B?S2rDpnLDpXM=?=
(10/15)
Oct 18 2018
Sorry, small mistake here. You're correct that shared
- Stanislav Blinov (127/244) Oct 18 2018 You contradict yourself and don't even notice it. Per your rules,
- aliak (8/17) Oct 18 2018 Out of curiosity, when it comes to primitives, what could you do
- Stanislav Blinov (5/11) Oct 18 2018 1. Anything if int* implicitly converts to shared int* (per MP),
- aliak (5/17) Oct 18 2018 Right, but the argument is a shared int*, so from what I've
- Stanislav Blinov (4/7) Oct 18 2018 Obviously the implementation would cast `shared` away, just like
- aliak (28/36) Oct 18 2018 Sounds like one is encapsulated within a box that carefully
- Stanislav Blinov (34/72) Oct 18 2018 Unit of "encapsulation" in D is either a module or a package, not
- Simen =?UTF-8?B?S2rDpnLDpXM=?= (56/153) Oct 18 2018 Yes, and that's fine. Because it's thread-safe, remember?
- Stanislav Blinov (74/112) Oct 18 2018 Eh? If you can't read or write to your shared members, how *do*
- Manu (22/34) Oct 18 2018 It is easy to respond to these.
- Stanislav Blinov (31/65) Oct 18 2018 You don't say?.. And what, exactly, stops the optimizer from
- Manu (4/73) Oct 18 2018 You are an obscene person. I'm out.
- Stanislav Blinov (10/16) Oct 18 2018 Oooh, I'm soooorry, come baack!
- Manu (17/33) Oct 18 2018 I've given use cases constantly, about taking object ownership, promotio...
- Stanislav Blinov (57/77) Oct 18 2018 Manu, you haven't shown *any* code in which conversion from
- Norm (14/60) Oct 18 2018 There's another way; Stanislav isn't one you need to convince so
- rikki cattermole (5/15) Oct 18 2018 As long as it doesn't look like my idea[0] (Andrei doesn't like it, I
- Dominikus Dittes Scherkl (18/21) Oct 19 2018 This document provide no reasoning about what usecases it
- Manu (16/28) Oct 19 2018 No, a key misunderstanding. My proposal is @safe. The only thing an expe...
- rikki cattermole (2/3) Oct 19 2018 It was a basic idea of mine... It was never meant to be PR'd.
- Joakim (25/186) Oct 17 2018 Rather than answering all objections in this thread in detail, a
- Steven Schveighoffer (37/150) Oct 17 2018 It's assumed that shared int pointer can be passed to another thread,
- Manu (71/211) Oct 17 2018 And that shared(int)* provides no access. No other thread with that
- Steven Schveighoffer (23/67) Oct 17 2018 So then it's a misnomer -- it's not really shared, because I can't do
- Manu (76/143) Oct 17 2018 **EXACTLY**.. this is the key to the entire design that allows for the
- Steven Schveighoffer (51/66) Oct 18 2018 For example (your example):
- Steven Schveighoffer (5/27) Oct 18 2018 Another thing to point out -- I can make x public (not private), and
- Manu (14/40) Oct 18 2018 I'm not sure that's an interesting design goal though.
- Simen =?UTF-8?B?S2rDpnLDpXM=?= (38/51) Oct 18 2018 But this isn't thread-safe, for the exact reasons described
- Steven Schveighoffer (16/68) Oct 18 2018 Error: cannot call function shareAllOver(shared(ThreadSafe) *) with type...
- Manu (12/36) Oct 18 2018 And here you expect a user to perform an unsafe-cast (which they may
- Steven Schveighoffer (13/47) Oct 18 2018 No, I expect them to do:
- Manu (16/62) Oct 18 2018 I don't have any use for this design in my application.
- Steven Schveighoffer (33/104) Oct 18 2018 Huh? This is the same thing you are asking for. How were you intending
- Manu (49/148) Oct 18 2018 Things get promoted to shared and distributed on occasion, but there
- Manu (9/17) Oct 18 2018 No, you can never be sure. Your assumption depends on the *user*
- Steven Schveighoffer (7/27) Oct 18 2018 Not at all. No transfer of ownership is needed, no cast is needed. If
- Simen =?UTF-8?B?S2rDpnLDpXM=?= (19/90) Oct 18 2018 Sorry, typo. Should of course have been
- Manu (44/122) Oct 18 2018 I understand your argument, and I used to think this too... but I
- Stanislav Blinov (37/93) Oct 18 2018 No argument.
- Steven Schveighoffer (41/98) Oct 18 2018 You have not demonstrated why your proposal is usable, and the proposal
- Manu (79/174) Oct 18 2018 I don't think it introduces *more*, I think it's the same number of
- ag0aep6g (5/18) Oct 17 2018 In the `shared` method you'd get a nice error when attempting `++x;`,
- Walter Bright (48/49) Oct 16 2018 Shared has one incredibly valuable feature - it allows you, the programm...
- Manu (81/132) Oct 17 2018 What does it mean 'aliased' precisely? I'm trying to prevent that in
- jmh530 (6/11) Oct 17 2018 As far as I had known from reading the forums, shared was not
- Timon Gehr (10/19) Oct 17 2018 There is no "prejudice", just reasoning. Your proposal was "disallow
- Stanislav Blinov (10/10) Oct 17 2018 Jesus Manu, it's soon 8 pages of dancing around a trivial issue.
- Stanislav Blinov (3/4) Oct 17 2018 Blargh, to shared of course.
- Guillaume Piolat (8/15) Oct 17 2018 +1
- Stanislav Blinov (4/7) Oct 17 2018 Well, ISharedAllocator is indeed overboard, but at least
- Walter Bright (6/9) Oct 19 2018 Aliasing means there are two paths to the same piece of data. That could...
- Manu (30/42) Oct 19 2018 The reason I ask is because, by my definition, if you have:
- Walter Bright (6/14) Oct 20 2018 They are aliased, by code that believes it is unshared, and code that be...
- aliak (27/31) Oct 20 2018 When you say that, then under Manu's proposal and the code below:
- Stanislav Blinov (6/23) Oct 20 2018 Those are not "ok". They're only "ok" under Manu's proposal so
- Nicholas Wilson (43/70) Oct 20 2018 Backing up a bit and making a few observations (after adding
- Manu (21/47) Oct 20 2018 I only insist that if you write a shared method, you promise that it
- Stanislav Blinov (49/85) Oct 20 2018 ---
- Walter Bright (7/9) Oct 21 2018 What we're discussing is not an invalid program, but what guarantees the...
- Simen =?UTF-8?B?S2rDpnLDpXM=?= (9/20) Oct 21 2018 The only difference between this and Manu's proposal is when you
- Manu (34/45) Oct 21 2018 My proposal guarantees that too, but in a more interesting way, because ...
- rikki cattermole (4/62) Oct 21 2018 I'm excited, but you need to write a DIP even if preliminary which shows...
- aliak (5/11) Oct 21 2018 No he is not insisting you can statically enforce thread safety.
- Stanislav Blinov (10/21) Oct 21 2018 This ("ok") can only be achieved if the "implementor" (the
- Simen =?UTF-8?B?S2rDpnLDpXM=?= (22/33) Oct 21 2018 The onus is *always* on the user to write function calls in the
- Nicholas Wilson (12/29) Oct 20 2018 In this case given the above: `a[0]` does not alias `b[0]`
- Stanislav Blinov (11/40) Oct 20 2018 And that's already a bug, because the language can't enforce
- Nicholas Wilson (10/33) Oct 20 2018 access through `a` is through the owned reference threadsafety
- Walter Bright (5/7) Oct 21 2018 There is no purpose whatsoever to data that can be neither read nor writ...
- Stanislav Blinov (37/46) Oct 21 2018 Just a thought: if a hard requirement is made on `shared` data to
- Simen =?UTF-8?B?S2rDpnLDpXM=?= (51/60) Oct 21 2018 No, because every part of the public interface has to work
- Nicholas Wilson (10/21) Oct 21 2018 Indeed but there is a subtle difference between that and Manu's
- Timon Gehr (18/25) Oct 21 2018 Not all of the parties that participate in the data race are in @trusted...
- Manu (3/28) Oct 21 2018 Show me. Nobody has been able to show that yet. I'd really like to know ...
- Neia Neutuladh (12/20) Oct 21 2018 If we only used your proposal and only used @safe code, we wouldn't have...
- Nicholas Wilson (18/44) Oct 21 2018 Only of the @trusted implementation is thread safe, which it
- Manu (26/48) Oct 21 2018 I've shown an implementation of Atomic(T) 3 times now... no response
- Neia Neutuladh (33/65) Oct 21 2018 Yes, Atomic can be implemented here as @trusted code, and maybe using
- Timon Gehr (7/42) Oct 21 2018 I just did, but if you really need to, give me a non-trivial piece of
- Nicholas Wilson (2/3) Oct 21 2018 Link please?
- Timon Gehr (2/7) Oct 21 2018 https://forum.dlang.org/post/pqii8k$11u3$1@digitalmars.com
- Nicholas Wilson (11/23) Oct 21 2018 There are two cases of @trusted code, bad and good. Bad trusted
- Manu (5/47) Oct 21 2018 There's no code there... just a presumption that the person who wrote
- Timon Gehr (51/104) Oct 22 2018 Yes, because there is no way to write @trusted code that holds its
- Aliak (3/25) Oct 22 2018 hi, if you change the private val in Atomic to be “private shared
- Timon Gehr (13/42) Oct 22 2018 It's a bit different, because then there is no implicit unshared->shared...
- Timon Gehr (23/45) Oct 22 2018 Obviously, this should have been:
- Manu (4/49) Oct 22 2018 Nitpick; atomicOp does not receive a shared arg under my proposal,
- Simen =?UTF-8?B?S2rDpnLDpXM=?= (49/86) Oct 22 2018 Yes, so you need to place the @trusted code in a separate module.
- Timon Gehr (9/43) Oct 22 2018 module reborked;
- Simen =?UTF-8?B?S2rDpnLDpXM=?= (7/15) Oct 22 2018 Finally! Proof that MP is impossible. On the other hand, why the
- Timon Gehr (9/28) Oct 22 2018 Even if this is changed (and it probably should be), it does not fix the...
- Simen =?UTF-8?B?S2rDpnLDpXM=?= (34/64) Oct 22 2018 What do you mean by 'previously correct'?
- Manu (3/31) Oct 22 2018 I'm all ears.
- Walter Bright (7/15) Oct 22 2018 One possible workaround is to use PIMPL. Another is to put the @trusted ...
- 12345swordy (4/13) Oct 22 2018 Quick question, if a class import a module in its scope, does it
- Neia Neutuladh (7/9) Oct 22 2018 Importing a module doesn't make a copy of any code. Each module contains...
- 12345swordy (3/13) Oct 22 2018 Huh, that what I figured. Though it not easy to test it as you
- Manu (5/19) Oct 22 2018 Yeah, that's shockingly dangerous for all sorts of reasons!
- Timon Gehr (20/44) Oct 22 2018 It was a way to satisfy Simen's (imho, arbitrary) constraint that the
- Walter Bright (3/4) Oct 22 2018 Yes, tupleof is known to break through the private access barrier. This ...
- Manu (40/144) Oct 22 2018 How do my examples prior fail to hold their promise?
- Walter Bright (2/3) Oct 22 2018 Thank you, Timon, for a nice explanation of what I was trying to express...
- Manu (5/8) Oct 22 2018 You removed whatever comment you're referring to.
- Walter Bright (6/9) Oct 22 2018 If your newsreader cannot find the antecedent, you badly need to use a b...
- Manu (16/23) Oct 21 2018 There is no primitive type with implicit threadsafety... nor need there ...
- Timon Gehr (8/17) Oct 21 2018 I wonder where this "each piece of code is maintained by only one person...
- Nicholas Wilson (3/6) Oct 21 2018 This is the basis of the current @safe/@trusted/@system model.
- Manu (18/35) Oct 21 2018 Have you ever cracked open std::map and 'fixed' it because you thought
- Timon Gehr (4/35) Oct 21 2018 You are not proposing to let core.atomic.Atomic convert to shared
- Manu (10/45) Oct 21 2018 You can always implicitly convert to shared.
- Timon Gehr (16/70) Oct 22 2018 (Also, yes, some people do that because std::map does not provide an
- Manu (34/56) Oct 22 2018 Sorry, I read the "not proposing to let" as something like "proposing
- Manu (76/171) Oct 20 2018 `b` can't read or write `a`... accessing `a` is absolutely safe.
- Stanislav Blinov (83/236) Oct 21 2018 It's not, with or without your proposal. The purpose of sharing
- Simen =?UTF-8?B?S2rDpnLDpXM=?= (26/72) Oct 21 2018 Then someone has not done their job. Since the pieces of code
- Manu (30/80) Oct 21 2018 No, it is to assure that you write correct not-broken code.
- Stanislav Blinov (27/71) Oct 21 2018 You're conflating your assumptions about the code with the topic
- Neia Neutuladh (9/21) Oct 21 2018 I'd expect almost every nontrivial multithreaded program to do this. It'...
- Manu (25/41) Oct 20 2018 This situation could only occur if you do unsafe code badly.
- Walter Bright (42/49) Oct 21 2018 It has nothing at all to do with fairness. It is about what the type sys...
- Simen =?UTF-8?B?S2rDpnLDpXM=?= (13/37) Oct 21 2018 No. Instead, it proposes something more useful: once cast to
- Manu (85/128) Oct 21 2018 By the definition Nick pulled from Wikipedia and posted for you a few
- Walter Bright (3/3) Oct 21 2018 I'd like to add that if the compiler can prove that a T* points to a uni...
- 12345swordy (4/7) Oct 21 2018 This can be achieved by using the unique struct and enforce the
- Nicholas Wilson (8/30) Oct 21 2018 No, does not compile, lockedIncrement takes an int*
- Nicholas Wilson (6/13) Oct 21 2018 No
- Walter Bright (2/4) Oct 21 2018 It's Manu's example.
- Walter Bright (12/17) Oct 21 2018 Then I don't know what the proposal is. Pieces of it appear to be scatte...
- Nicholas Wilson (15/33) Oct 21 2018 The proposal is:
- Walter Bright (4/26) Oct 21 2018 That's what I was referring to, and Manu's example. It doesn't work, as ...
- Nicholas Wilson (3/8) Oct 21 2018 That is a given. You would do well to heed it for your own DIPs.
- Manu (9/35) Oct 21 2018 Would you please respond to my messages, and specifically, respond to
- Walter Bright (16/23) Oct 22 2018 That's just the problem. You've posted 62 messages so far in this thread...
- Manu (8/31) Oct 22 2018 I sent it twice... again just a short while ago right before this
- Walter Bright (8/10) Oct 22 2018 Posting it over and over is illustrative of the failure of posting propo...
- rikki cattermole (4/21) Oct 22 2018 As I've said previously, it doesn't need to be a good DIP or anywhere
- Walter Bright (3/6) Oct 22 2018 That's right, and it can be evolved so everyone involved can easily see ...
- Manu (7/17) Oct 22 2018 It hasn't changed. Not one single bit. I haven't changed a single
- Walter Bright (10/13) Oct 22 2018 A reader would not know that. Reposting the same thing over and over in ...
- Manu (42/60) Oct 21 2018 No no, they're repeated, not scattered, because I seem to have to keep
- Joakim (9/93) Oct 21 2018 I told you this is what happens with forum posts 4 days ago, yet
- Stanislav Blinov (79/89) Oct 22 2018 Right back at you.
- Manu (12/102) Oct 22 2018 I did correct that line (along with an apology) on my very next post;
- Atila Neves (30/39) Oct 22 2018 I've read every post on this thread and I also have the feeling
- John Colvin (10/27) Oct 22 2018 I can go look at the original post - and I have - but while it
- Manu (2/141) Oct 21 2018 Did you read this email? It seems you didn't read this email... Please r...
- Manu (87/130) Oct 22 2018 Third time's the charm.... maybe?
- Stanislav Blinov (4/12) Oct 17 2018 Here's one: shared -> const shared shouldn't be allowed. Mutable
- Nicholas Wilson (6/20) Oct 17 2018 Could you explain that a bit more? I don't understand it, mutable
- Timon Gehr (4/7) Oct 17 2018 Unfortunately not. For example, the thread with the mutable reference is...
- Nicholas Wilson (3/11) Oct 17 2018 Yes, but that is covered by not being able to read non-atomically
- Steven Schveighoffer (5/17) Oct 17 2018 All sides must participate in synchronization for it to make sense. The
- Stanislav Blinov (12/35) Oct 17 2018 Yes, but `shared` is not `const`. This 'might change' rule is
- jmh530 (4/15) Oct 17 2018 Isn't that also true for isolated data (data that only allows one
- Walter Bright (5/6) Oct 19 2018 That's colloquially called "unique" data. And yes, it is also true for t...
- rikki cattermole (5/12) Oct 19 2018 Actually we kind of have a way to do this (I think). Scope. It can't
- NX (15/15) Oct 17 2018 I don't see any problem with this proposal as long as these
- Atila Neves (40/67) Oct 18 2018 How is this significantly different from now?
- Steven Schveighoffer (3/12) Oct 18 2018 i = i + 1; // OK(!)
- Atila Neves (3/16) Oct 18 2018 Sigh... I'm sure there's a practical reason for it, but still.
- Stanislav Blinov (13/45) Oct 18 2018 i = 1;
- Steven Schveighoffer (5/25) Oct 18 2018 This should be fine, y is not shared when being created.
- Stanislav Blinov (3/27) Oct 18 2018 'y' isn't, but 'i' is. It's fine on amd64, but that's incidental.
- Steven Schveighoffer (6/32) Oct 18 2018 OH, I didn't even notice that `i` didn't have a type, so it was a
- Timon Gehr (5/17) Oct 18 2018 I'm pretty sure you will have to allow operations on shared local
- Stanislav Blinov (12/16) Oct 18 2018 Because you can't really "share" C (e.g. by value). You share a
- Timon Gehr (6/20) Oct 18 2018 Presumably you could have a local variable shared(C) c, then take its
- Stanislav Blinov (11/31) Oct 18 2018 In that case, it's already a pointer, and the only real issue is
- Erik van Velzen (7/22) Oct 18 2018 Manu said clearly that the receiving thread won't be able to read
- Stanislav Blinov (9/33) Oct 18 2018 Yes it will, by casting `shared` away. *Just like* his proposed
- Erik van Velzen (8/31) Oct 18 2018 Casting is inherently unsafe. Or at least, there's no threadsafe
- Stanislav Blinov (16/34) Oct 18 2018 So? That's the only way to implement required low-level access,
- Manu (4/21) Oct 18 2018 @trusted code exists, and it's the foundation of the @safe stack.
- Erik van Velzen (4/4) Oct 18 2018 Manu I'm also making a plea for you to write a document with your
- Manu (8/12) Oct 18 2018 I can start on this. I wouldn't take the time before having confidence
- Stanislav Blinov (3/3) Oct 18 2018 Manu, Erik, Simen... In what world can a person consciously say
- Simen =?UTF-8?B?S2rDpnLDpXM=?= (6/9) Oct 18 2018 In a world where the implicit casting always ends in a place
- Manu (11/14) Oct 18 2018 Implicit casting exists in a world where the conversion is guaranteed
- Stanislav Blinov (3/10) Oct 18 2018 Please see my exchange with Simen in case you're skipping my
- Erik van Velzen (14/49) Oct 18 2018 This may come as a surprise but I agree with this factually.
- Timon Gehr (16/18) Oct 18 2018 Then why do you not just make all members shared? Because with Manu's
- Stanislav Blinov (35/56) Oct 18 2018 Let's assume you have something like this:
- Manu (43/65) Oct 18 2018 No they don't, only facets that overlap with the shared method.
- Steven Schveighoffer (17/50) Oct 19 2018 I promised I wouldn't respond, I'm going to break that (obviously).
- Manu (50/100) Oct 19 2018 I'm glad that there's movement here... but I'm still not 100%
- Atila Neves (8/30) Oct 19 2018 Not directly - but obviously there must be *some* way to using
- Simen =?UTF-8?B?S2rDpnLDpXM=?= (12/14) Oct 19 2018 Atomic and thread-safe are two very different concepts.
- jmh530 (10/13) Oct 18 2018 I had posted your library before to no response...
- Atila Neves (6/22) Oct 19 2018 Yeah, I punted on making anything there @safe. I have to go back
- John Colvin (13/16) Oct 22 2018 I don't understand how you can safely have simultaneously shared
- Simen =?UTF-8?B?S2rDpnLDpXM=?= (36/53) Oct 23 2018 S's constructor is not written in such a way as to disallow
- John Colvin (7/45) Oct 23 2018 I don't think this direction of me providing examples and you
- Neia Neutuladh (6/14) Oct 22 2018 Somehow I lost track of your reason behind the promotion.
- Stanislav Blinov (9/14) Oct 23 2018 Because these have the same signature:
- Atila Neves (2/21) Oct 23 2018 This is easily achievable with template this.
Okay, so I've been thinking on this for a while... I think I have a pretty good feel for how shared is meant to be. 1. shared should behave exactly like const, except in addition to inhibiting write access, it also inhibits read access. I think this is the foundation for a useful definition for shared, and it's REALLY easy to understand and explain. Current situation where you can arbitrarily access shared members undermines any value it has. Shared must assure you don't access members unsafely, and the only way to do that with respect to data members, is to inhibit access completely. I think shared is just const without read access. Assuming this world... how do you use shared? 1. traditional; assert that the object become thread-local by acquiring a lock, cast shared away 2. object may have shared methods; such methods CAN be called on shared instances. such methods may internally implement synchronisation to perform their function. perhaps methods of a lock-free queue structure for instance, or operator overloads on `Atomic!int`, etc. In practise, there is no functional change in usage from the current implementation, except we disallow unsafe accesses (which will make the thing useful).From there, it opens up another critical opportunity; T* -> shared(T)*promotion. Const would be useless without T* -> const(T)* promotion. Shared suffers a similar problem. If you write a lock-free queue for instance, and all the methods are `shared` (ie, threadsafe), then under the current rules, you can't interact with the object when it's not shared, and that's fairly useless. Assuming the rules above: "can't read or write to members", and the understanding that `shared` methods are expected to have threadsafe implementations (because that's the whole point), what are the risks from allowing T* -> shared(T)* conversion? All the risks that I think have been identified previously assume that you can arbitrarily modify the data. That's insanity... assume we fix that... I think the promotion actually becomes safe now...? Destroy...
Oct 15 2018
On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:2. object may have shared methods; such methods CAN be called on shared instances. such methods may internally implement synchronisation to perform their function. perhaps methods of a lock-free queue structure for instance, or operator overloads on `Atomic!int`, etc.Just checking my understanding: are you saying here that shared methods can effectively do anything and the burden of correctness is on the author? Or do you still have to cast the shared away first?
Oct 15 2018
On Mon, Oct 15, 2018 at 12:15 PM Peter Alexander via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:Burden of correctness that a `shared` method is indeed threadsafe is absolutely on the author; there's nothing we can do to guarantee this (multithreading is hard!), but in this new world, a user of an API will be able to see shared methods and assume that they're threadsafe, since that would be (and should be!) the whole point. Since `shared` has no read or write access, at the bottom of the stack, it may be necessary that the function cast shared away to implement its magic. If you lock a mutex for instance, then you naturally need to cast it away; but that's solving the problem with a sledge-hammer. In my experience, most such functions are implemented with atomics; and the atomic API already receives shared args, ie: https://github.com/dlang/druntime/blob/master/src/core/atomic.d#L379 So, if you do your work with atomics, then you don't need any casts. Also, if the object is a composite, then a `shared` method is able to call `shared` methods on its members, and in that case, you are able to do useful aggregate work without casting. I think the interactions I describe above are all correct.2. object may have shared methods; such methods CAN be called on shared instances. such methods may internally implement synchronisation to perform their function. perhaps methods of a lock-free queue structure for instance, or operator overloads on `Atomic!int`, etc.Just checking my understanding: are you saying here that shared methods can effectively do anything and the burden of correctness is on the author? Or do you still have to cast the shared away first?
Oct 15 2018
On Monday, 15 October 2018 at 19:14:58 UTC, Peter Alexander wrote:On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:Well `this` is still shared so you would be prevented from doing anything non-atomically or lock cast away shared, but that will be enforced by the type system. So the burden of correctness is on the author, but the complier enforces correct behaviour.2. object may have shared methods; such methods CAN be called on shared instances. such methods may internally implement synchronisation to perform their function. perhaps methods of a lock-free queue structure for instance, or operator overloads on `Atomic!int`, etc.Just checking my understanding: are you saying here that shared methods can effectively do anything and the burden of correctness is on the author? Or do you still have to cast the shared away first?
Oct 15 2018
On Mon, Oct 15, 2018 at 12:15 PM Peter Alexander via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:Just to be clear:2. object may have shared methods; such methods CAN be called on shared instances. such methods may internally implement synchronisation to perform their function. perhaps methods of a lock-free queue structure for instance, or operator overloads on `Atomic!int`, etc.Just checking my understanding: are you saying here that shared methods can effectively do anything and the burden of correctness is on the author? Or do you still have to cast the shared away first?are you saying here that shared methods can effectively do anythingNo, quite the opposite. I am saying that shared objects have neither read nor write access to members. Shared means "no read or write access". You can only call shared methods.
Oct 15 2018
On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:Okay, so I've been thinking on this for a while... I think I have a pretty good feel for how shared is meant to be. 1. shared should behave exactly like const, except in addition to inhibiting write access, it also inhibits read access.Are you familiar with reference capabilities[1] in the pony language? They describe many of them in terms of read/write uniqueness. Another way they describe them [2] is in denying aliases, like deny global read alias. [1] https://tutorial.ponylang.io/capabilities/reference-capabilities.html [2] See page 12-14: http://www.doc.ic.ac.uk/~scd/Pony-WG2.16.pdf
Oct 15 2018
On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:Destroy...Keep in mind immutable is implicitly shared (i.e. not in tls) because nobody can change it. It should stay readable for this reason.
Oct 15 2018
On Mon, Oct 15, 2018 at 12:45 PM Nicholas Wilson via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:Are you saying `is(immutable(int) == shared) == true)` ??Destroy...Keep in mind immutable is implicitly shared (i.e. not in tls) because nobody can change it. It should stay readable for this reason.
Oct 15 2018
On Monday, 15 October 2018 at 20:44:35 UTC, Manu wrote:snip Are you saying `is(immutable(int) == shared) == true)` ??From the spec: "Applying any qualifier to immutable T results in immutable T. This makes immutable a fixed point of qualifier combinations and makes types such as const(immutable(shared T)) impossible to create." Example: import std.stdio : writeln; void main() { writeln(is(immutable(int) == shared immutable(int)) == true); //prints true }
Oct 15 2018
On Monday, 15 October 2018 at 20:54:12 UTC, jmh530 wrote:On Monday, 15 October 2018 at 20:44:35 UTC, Manu wrote:The philosophy of this is that: the value never changes, therefore only one copy of the variable needs to exist (i.e. immutable variables declared at module scope are _not_ thread local) and can be shared between threads with no race conditions. I'm saying that while what you propose sounds very reasonable, we should make sure that reading immutable variables is still good ;)snip Are you saying `is(immutable(int) == shared) == true)` ??From the spec: "Applying any qualifier to immutable T results in immutable T. This makes immutable a fixed point of qualifier combinations and makes types such as const(immutable(shared T)) impossible to create." Example: import std.stdio : writeln; void main() { writeln(is(immutable(int) == shared immutable(int)) == true); //prints true }
Oct 15 2018
On Mon, Oct 15, 2018 at 2:05 PM Nicholas Wilson via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Monday, 15 October 2018 at 20:54:12 UTC, jmh530 wrote:I don't think what I describe affects immutable in any way; `is(immutable(int) == shared) == false`, as it should be. Immutable wouldn't have its rules affected by any change to shared.On Monday, 15 October 2018 at 20:44:35 UTC, Manu wrote:The philosophy of this is that: the value never changes, therefore only one copy of the variable needs to exist (i.e. immutable variables declared at module scope are _not_ thread local) and can be shared between threads with no race conditions. I'm saying that while what you propose sounds very reasonable, we should make sure that reading immutable variables is still good ;)snip Are you saying `is(immutable(int) == shared) == true)` ??From the spec: "Applying any qualifier to immutable T results in immutable T. This makes immutable a fixed point of qualifier combinations and makes types such as const(immutable(shared T)) impossible to create." Example: import std.stdio : writeln; void main() { writeln(is(immutable(int) == shared immutable(int)) == true); //prints true }
Oct 15 2018
On Monday, 15 October 2018 at 21:08:38 UTC, Manu wrote:On Mon, Oct 15, 2018 at 2:05 PM Nicholas Wilson via Digitalmars-d <digitalmars-d puremagic.com> wrote:OK, just making sure you've got this covered.I'm saying that while what you propose sounds very reasonable, we should make sure that reading immutable variables is still good ;)I don't think what I describe affects immutable in any way; `is(immutable(int) == shared) == false`, as it should be. Immutable wouldn't have its rules affected by any change to shared.
Oct 15 2018
On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:Destroy...What you describe sounds better than what we currently have. I have at least two concerns: 1. A single producer, single consumer (SPSC) queue is necessarily shared, but is only safe if there is one writing thread and one reading thread. Is it ok if shared also requires user discipline and/or runtime checks to ensure correct usage? 2. In your scheme (as I understand), a struct composed entirely of atomics would be able to implement shared methods without any casts, but also be completely thread *unsafe*. Is this okay? struct TwoInts { Atomic!int x, y; void swap() shared { int z = x.load; x.store(y.load); y.store(z); } }
Oct 15 2018
On Mon, Oct 15, 2018 at 1:05 PM Peter Alexander via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:I think you can model this differently... perhaps rather than a single object, it's a coupled pair. For instance, a thread-local enque object, and a separate thread-local deque object. Those would express the 2 thread-local points of access to queue and dequeue, and the implementation shunts between them in a threadsafe way. I mean, it's not *really* a threadsafe primitive, so for the whole object to be 'shared', I think that might be a design-fail.Destroy...What you describe sounds better than what we currently have. I have at least two concerns: 1. A single producer, single consumer (SPSC) queue is necessarily shared, but is only safe if there is one writing thread and one reading thread. Is it ok if shared also requires user discipline and/or runtime checks to ensure correct usage?2. In your scheme (as I understand), a struct composed entirely of atomics would be able to implement shared methods without any casts, but also be completely thread *unsafe*. Is this okay?It would be as safe as the design intends. A struct with a bunch of public atomics effectively presents a set of distinct atomics, and each one is thread-safe relative to eachother. If the members are actually coupled into aggregate state, then you make then private and methods implement the state transitions in such a way guaranteeing atomicity. Like I say before, the language can't "make it threadsafe" for you... be producing a shared method, you have a responsibility to make sure it works right.struct TwoInts { Atomic!int x, y; void swap() shared { int z = x.load; x.store(y.load); y.store(z); } }Your swap function is plain broken; it doesn't do what the API promises. You can write all sorts of broken code, and this is a good example of just plain broken code. Also, `x` and `y` probably shouldn't be public, or it effectively communicates that they're de-coupled state.
Oct 15 2018
On Monday, 15 October 2018 at 20:53:32 UTC, Manu wrote:On Mon, Oct 15, 2018 at 1:05 PM Peter Alexander via Digitalmars-d <digitalmars-d puremagic.com> wrote:That's a nice design.On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote: 1. A single producer, single consumer (SPSC) queue is necessarily shared, but is only safe if there is one writing thread and one reading thread. Is it ok if shared also requires user discipline and/or runtime checks to ensure correct usage?I think you can model this differently... perhaps rather than a single object, it's a coupled pair.Your swap function is plain broken; it doesn't do what the API promises. You can write all sorts of broken code, and this is a good example of just plain broken code.If it is broken then why allow it? Why do we need to cast shared away if they weren't atomic and why do we allow it if they are atomic? I understand that shared can't magically tell you when code is thread safe or not. It does make sense to disallow almost everything and require casts. I'm just not seeing the value of allowing shared methods to access shared members if it isn't thread safe. Make it require casts.
Oct 15 2018
On Mon, Oct 15, 2018 at 4:25 PM Peter Alexander via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Monday, 15 October 2018 at 20:53:32 UTC, Manu wrote:It's not that it's 'allowed' other than, yes, access to atomic int's is allowed in a threadsafe way because atomic access to individual int's is threadsafe, so the primitive operation is perfectly acceptable. Your function models a higher-level concept; which is an atomic swap. You need to make sure that a threadsafe API you're authoring does actually deliver on the promise it makes.On Mon, Oct 15, 2018 at 1:05 PM Peter Alexander via Digitalmars-d <digitalmars-d puremagic.com> wrote:That's a nice design.On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote: 1. A single producer, single consumer (SPSC) queue is necessarily shared, but is only safe if there is one writing thread and one reading thread. Is it ok if shared also requires user discipline and/or runtime checks to ensure correct usage?I think you can model this differently... perhaps rather than a single object, it's a coupled pair.Your swap function is plain broken; it doesn't do what the API promises. You can write all sorts of broken code, and this is a good example of just plain broken code.If it is broken then why allow it? Why do we need to cast shared away if they weren't atomic and why do we allow it if they are atomic?I understand that shared can't magically tell you when code is thread safe or not. It does make sense to disallow almost everything and require casts. I'm just not seeing the value of allowing shared methods to access shared members if it isn't thread safe. Make it require casts.Because in an awful lot of cases, it is threadsafe. Implementing low-level machinery, and consuming such machinery should have a 1:many relationship. You're talking about adding friction to the 'many' such that the '1' knows that they need to implement their function right? I mean, they already know that, because they wrote 'shared' after their function declaration. In the common case, perhaps my object might aggregate a threadsafe queue, and my shared method prepares an item and then adds it to the queue. I think the vast majority case will be making use of utility functionality and that shouldn't present undue friction. It's not like atomic int's that are class members are going to be accidentally twiddled; you added Atomic!int's to your class, and that wasn't an accident... and if you're making use of the lowest-level atomic primitives, it's fair to presume you know how to use them. You're writing a shared method, which means you already encapsulate the promise that you are implementing a function that deals with thread-safety. I don't know what the cast in this case would add.
Oct 15 2018
On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:Assuming the rules above: "can't read or write to members", and the understanding that `shared` methods are expected to have threadsafe implementations (because that's the whole point), what are the risks from allowing T* -> shared(T)* conversion? All the risks that I think have been identified previously assume that you can arbitrarily modify the data. That's insanity... assume we fix that... I think the promotion actually becomes safe now...?You're still talking about implicit promotion? No, it does not become safe no matter what restrictions you put on `shared` instances, because the caller of any function that takes `shared` arguments remains blissfully unaware of this promotion.
Oct 15 2018
On Mon, Oct 15, 2018 at 1:15 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:Absolutely. This is critical to make shared useful, and I think there's a path to make it work.Assuming the rules above: "can't read or write to members", and the understanding that `shared` methods are expected to have threadsafe implementations (because that's the whole point), what are the risks from allowing T* -> shared(T)* conversion? All the risks that I think have been identified previously assume that you can arbitrarily modify the data. That's insanity... assume we fix that... I think the promotion actually becomes safe now...?You're still talking about implicit promotion?No, it does not become safe no matter what restrictions you put on `shared` instances, because the caller of any function that takes `shared` arguments remains blissfully unaware of this promotion.It doesn't matter... what danger is there of distributing a shared pointer? Are you concerned that someone with a shared reference might call a threadsafe method? If that's an invalid operation, then the method has broken it's basic agreed contract. And I agree that it's conceivable that you could contrive a bad program, but you can contrive a bad program with literally any language feature!
Oct 15 2018
On Monday, 15 October 2018 at 20:57:46 UTC, Manu wrote:On Mon, Oct 15, 2018 at 1:15 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:No, on the contrary. Someone with an unshared pointer may call unshared method or read/write data while someone else accesses it via `shared` interface precisely because you allow T to escape to shared(T). You *need* an explicit cast for this.On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:Absolutely. This is critical to make shared useful, and I think there's a path to make it work.Assuming the rules above: "can't read or write to members", and the understanding that `shared` methods are expected to have threadsafe implementations (because that's the whole point), what are the risks from allowing T* -> shared(T)* conversion? All the risks that I think have been identified previously assume that you can arbitrarily modify the data. That's insanity... assume we fix that... I think the promotion actually becomes safe now...?You're still talking about implicit promotion?No, it does not become safe no matter what restrictions you put on `shared` instances, because the caller of any function that takes `shared` arguments remains blissfully unaware of this promotion.It doesn't matter... what danger is there of distributing a shared pointer? Are you concerned that someone with a shared reference might call a threadsafe method? If that's an invalid operation, then the method has broken it's basic agreed contract.And I agree that it's conceivable that you could contrive a bad program, but you can contrive a bad program with literally any language feature!
Oct 15 2018
On Mon, Oct 15, 2018 at 2:25 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Monday, 15 October 2018 at 20:57:46 UTC, Manu wrote:If a shared method is incompatible with an unshared method, your class is broken. Explicit casting doesn't magically implement thread-safety, it basically just guarantees failure. What I suggest are rules that lead to proper behaviour with respect to writing a thread-safe API. You can write bad code with any feature in any number of ways. I see it this way: If your object has shared methods, then it is distinctly and *deliberately* involved in thread-safety. You have deliberately opted-in to writing a thread-safe object, and you must deliver on your promise. The un-shared API of an object that supports `shared` are not exempt from the thread-safety commitment, they are simply the subset of the API that may not be called from a shared context. If your shared method is incompatible with other methods, your class is broken, and you violate your promise. Nobody writes methods of an object such that they don't work with each other... methods are part of a deliberately crafted and packaged entity. If you write a shared object, you do so deliberately, and you buy responsibility of making sure your objects API is thread-safe. If your object is not thread-safe, don't write shared methods.On Mon, Oct 15, 2018 at 1:15 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:No, on the contrary. Someone with an unshared pointer may call unshared method or read/write data while someone else accesses it via `shared` interface precisely because you allow T to escape to shared(T). You *need* an explicit cast for this.On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:Absolutely. This is critical to make shared useful, and I think there's a path to make it work.Assuming the rules above: "can't read or write to members", and the understanding that `shared` methods are expected to have threadsafe implementations (because that's the whole point), what are the risks from allowing T* -> shared(T)* conversion? All the risks that I think have been identified previously assume that you can arbitrarily modify the data. That's insanity... assume we fix that... I think the promotion actually becomes safe now...?You're still talking about implicit promotion?No, it does not become safe no matter what restrictions you put on `shared` instances, because the caller of any function that takes `shared` arguments remains blissfully unaware of this promotion.It doesn't matter... what danger is there of distributing a shared pointer? Are you concerned that someone with a shared reference might call a threadsafe method? If that's an invalid operation, then the method has broken it's basic agreed contract.
Oct 15 2018
On Monday, 15 October 2018 at 21:51:43 UTC, Manu wrote:If a shared method is incompatible with an unshared method, your class is broken.What?!? So... my unshared methods should also perform all that's necessary for `shared` methods?Explicit casting doesn't magically implement thread-safety, it basically just guarantees failure.It doesn't indeed. It does, however, at least help prevent silent bugs. Via that same guaranteed failure. That failure is about all the help we can get from the compiler anyway.What I suggest are rules that lead to proper behaviour with respect to writing a thread-safe API. You can write bad code with any feature in any number of ways.Yup. For example, passing an int* to a function expecting shared int*.I see it this way: If your object has shared methods, then it is distinctly and *deliberately* involved in thread-safety. You have deliberately opted-in to writing a thread-safe object, and you must deliver on your promise.The un-shared API of an object that supports `shared` are not exempt from the thread-safety commitment, they are simply the subset of the API that may not be called from a shared context.And therefore they lack any synchronization. So I don't see how they *can* be "compatible" with `shared` methods.If your shared method is incompatible with other methods, your class is broken, and you violate your promise.Nope. class BigCounter { this() { /* don't even need the mutex if I'm not sharing this */ } this(Mutex m = null) shared { this.m = m ? m : new Mutex; } void increment() { value += 1; } void increment() shared { synchronized(m) *value.assumeUnshared += 1; } private: Mutex m; BigInt value; } They're not "compatible" in any shape or form. Or would you have the unshared ctor also create the mutex and unshared increment also take the lock? What's the point of having them then? Better disallow mixed implementations altogether (which is actually not that bad of an idea).Nobody writes methods of an object such that they don't work with each other... methods are part of a deliberately crafted and packaged entity. If you write a shared object, you do so deliberately, and you buy responsibility of making sure your objects API is thread-safe. If your object is not thread-safe, don't write shared methods.Ahem... Okay... import std.concurrency; import core.atomic; void thread(shared int* x) { (*x).atomicOp!"+="(1); } shared int c; void main() { int x; auto tid = spawn(&thread, &x); // "just" a typo } You're saying that's ok, it should "just" compile. It shouldn't. It should produce an error and a mild electric discharge into the developer's chair.
Oct 15 2018
On Monday, 15 October 2018 at 23:30:43 UTC, Stanislav Blinov wrote:On Monday, 15 October 2018 at 21:51:43 UTC, Manu wrote:No, its the other way around: a shared method that does extra synchronisation should work irrespective of wether or not the object needs that synchronisation. e.g. atomic loading a TLS variable is fine.If a shared method is incompatible with an unshared method, your class is broken.What?!? So... my unshared methods should also perform all that's necessary for `shared` methods?That is a reasonable thing to do if shared is const + no unsynched reads.Explicit casting doesn't magically implement thread-safety, it basically just guarantees failure.It doesn't indeed. It does, however, at least help prevent silent bugs. Via that same guaranteed failure. That failure is about all the help we can get from the compiler anyway.What I suggest are rules that lead to proper behaviour with respect to writing a thread-safe API. You can write bad code with any feature in any number of ways.Yup. For example, passing an int* to a function expecting shared int*.I think Manu means you have a shared object with some shared methods and some unshared methods. The shared methods deal with synchronisation and can therefore be call from anywhere by anyone, whereas the unshared methods must be called on a locked object.I see it this way: If your object has shared methods, then it is distinctly and *deliberately* involved in thread-safety. You have deliberately opted-in to writing a thread-safe object, and you must deliver on your promise. The un-shared API of an object that supports `shared` are not exempt from the thread-safety commitment, they are simply the subset of the API that may not be called from a shared context.And therefore they lack any synchronization. So I don't see how they *can* be "compatible" with `shared` methods.snipIndeed that is just a typo, just as that is a contrived example. You'd notice that pretty quick in a debugger.Nobody writes methods of an object such that they don't work with each other... methods are part of a deliberately crafted and packaged entity. If you write a shared object, you do so deliberately, and you buy responsibility of making sure your objects API is thread-safe. If your object is not thread-safe, don't write shared methods.Ahem... Okay... import std.concurrency; import core.atomic; void thread(shared int* x) { (*x).atomicOp!"+="(1); } shared int c; void main() { int x; auto tid = spawn(&thread, &x); // "just" a typo } You're saying that's ok, it should "just" compile. It shouldn't. It should produce an error and a mild electric discharge into the developer's chair.
Oct 15 2018
On Mon, Oct 15, 2018 at 5:10 PM Nicholas Wilson via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Monday, 15 October 2018 at 23:30:43 UTC, Stanislav Blinov wrote:Yes, except maybe I didn't make it clear that I DO expect the un-shared methods to be aware that a sibling shared method does exist (you wrote it!), and that it may manipulate some state, so *if* the un-shared method does interact with the same data that the shared method may manipulate (in many cases, it won't; it's likely only a small subset of an object's functionality that may have thread-safe access), then the un-shared method does need to acknowledge that functional overlap. So even though a method is un-shared, it still needs to be aware that it may have sibling methods that are shared. If they don't access an overlapping data-set, no special handling is required. If they do overlap, they may need to coordinate appropriately.On Monday, 15 October 2018 at 21:51:43 UTC, Manu wrote:I think Manu means you have a shared object with some shared methods and some unshared methods. The shared methods deal with synchronisation and can therefore be call from anywhere by anyone, whereas the unshared methods must be called on a locked object.I see it this way: If your object has shared methods, then it is distinctly and *deliberately* involved in thread-safety. You have deliberately opted-in to writing a thread-safe object, and you must deliver on your promise. The un-shared API of an object that supports `shared` are not exempt from the thread-safety commitment, they are simply the subset of the API that may not be called from a shared context.And therefore they lack any synchronization. So I don't see how they *can* be "compatible" with `shared` methods.
Oct 15 2018
On Tuesday, 16 October 2018 at 00:36:12 UTC, Manu wrote:*snip* Yes, except maybe I didn't make it clear that I DO expect the un-shared methods to be aware that a sibling shared method does exist (you wrote it!), and that it may manipulate some state, so *if* the un-shared method does interact with the same data that the shared method may manipulate (in many cases, it won't; it's likely only a small subset of an object's functionality that may have thread-safe access), then the un-shared method does need to acknowledge that functional overlap. So even though a method is un-shared, it still needs to be aware that it may have sibling methods that are shared. If they don't access an overlapping data-set, no special handling is required. If they do overlap, they may need to coordinate appropriately.I understand your point but I think the current shared (no implicit conversion) has its uses. It can be quite useful to have one interface for when an object is shared and one for when it is not (one with and without the synchronization cost). Sure, something as trivial as a counter can be re-implemented in both ways but more complex objects would easily result in extreme code duplication. It's also easy to acknowledge that implicit conversion to shared has its uses. Instead of forcing one way or another, how about we leave the decision up to the programmer? Say something like "alias this shared;" enables implicit conversion to shared.
Oct 15 2018
On Mon, Oct 15, 2018 at 7:15 PM Isaac S. via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Tuesday, 16 October 2018 at 00:36:12 UTC, Manu wrote:If you can give a single 'use', I'm all ears ;)*snip* Yes, except maybe I didn't make it clear that I DO expect the un-shared methods to be aware that a sibling shared method does exist (you wrote it!), and that it may manipulate some state, so *if* the un-shared method does interact with the same data that the shared method may manipulate (in many cases, it won't; it's likely only a small subset of an object's functionality that may have thread-safe access), then the un-shared method does need to acknowledge that functional overlap. So even though a method is un-shared, it still needs to be aware that it may have sibling methods that are shared. If they don't access an overlapping data-set, no special handling is required. If they do overlap, they may need to coordinate appropriately.I understand your point but I think the current shared (no implicit conversion) has its uses. It can be quite useful to have one interface for when an object is shared and one for when it is not (one with and without the synchronization cost). Sure, something as trivial as a counter can be re-implemented in both ways but more complex objects would easily result in extreme code duplication.It's also easy to acknowledge that implicit conversion to shared has its uses.I actually know of many real uses for this case.Instead of forcing one way or another, how about we leave the decision up to the programmer? Say something like "alias this shared;" enables implicit conversion to shared.That might be fine. Like I said in OP, the first point that I think needs to be agreed on, is that shared can not read or write members. I think that's a pre-requisite for any interesting development.
Oct 15 2018
On Tuesday, 16 October 2018 at 02:26:04 UTC, Manu wrote:My usages are a custom ref counted template and list types (which are built on top of the former). The ref counted template will initialize naively when not shared but utilize compare-and-set and thread yielding if needed when shared (ensureInitialized can occur any time after declaration). The list types (which are not shared-compatible *yet*) will utilize a read-write mutex only when shared (no cost beyond a little unused memory to non-shared objects). As such, casting any of the non-shared versions of these types to shared would be unsafe. (Technically the types can be safely casted to shared so long as no non-shared reference to them exists and vice versa.)I understand your point but I think the current shared (no implicit conversion) has its uses. It can be quite useful to have one interface for when an object is shared and one for when it is not (one with and without the synchronization cost). Sure, something as trivial as a counter can be re-implemented in both ways but more complex objects would easily result in extreme code duplication.If you can give a single 'use', I'm all ears ;)(There was a time when I wanted it as well. I've forgotten what for by now.)It's also easy to acknowledge that implicit conversion to shared has its uses.I actually know of many real uses for this case.That might be fine. Like I said in OP, the first point that I think needs to be agreed on, is that shared can not read or write members. I think that's a pre-requisite for any interesting development.I will agree that *outsiders* should not be able to read/write members of a shared object. I'm not sure whether your design means even within a shared function such members cannot be read from or written to. If it does, the casting required may get a little annoying but isn't unworkable (especially since an unshared template can do the cast in an trusted manner).
Oct 15 2018
On Mon, Oct 15, 2018 at 8:55 PM Isaac S. via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Tuesday, 16 October 2018 at 02:26:04 UTC, Manu wrote:Can you link to code? It doesn't sound incompatible with my proposal. Mutexes are blunt instruments, and easy to model. My whole suggestion has virtually no effect on the mutex protected case, which is what most people seem to talk about.My usages are a custom ref counted template and list types (which are built on top of the former). The ref counted template will initialize naively when not shared but utilize compare-and-set and thread yielding if needed when shared (ensureInitialized can occur any time after declaration). The list types (which are not shared-compatible *yet*) will utilize a read-write mutex only when shared (no cost beyond a little unused memory to non-shared objects). As such, casting any of the non-shared versions of these types to shared would be unsafe. (Technically the types can be safely casted to shared so long as no non-shared reference to them exists and vice versa.)I understand your point but I think the current shared (no implicit conversion) has its uses. It can be quite useful to have one interface for when an object is shared and one for when it is not (one with and without the synchronization cost). Sure, something as trivial as a counter can be re-implemented in both ways but more complex objects would easily result in extreme code duplication.If you can give a single 'use', I'm all ears ;)Absolutely. Shared references can not access members, that's the rule. It's completely unsafe to read or write to members of a shared thing. Period. Attributing a method shared doesn't change that fact. You can create a thread-local (ie, accessible) object from a shared object by acquiring a lock on it for some duration, and it would be helpful if the mechanism that fetches the lock also cast away shared as part of the lock operation.That might be fine. Like I said in OP, the first point that I think needs to be agreed on, is that shared can not read or write members. I think that's a pre-requisite for any interesting development.I will agree that *outsiders* should not be able to read/write members of a shared object. I'm not sure whether your design means even within a shared function such members cannot be read from or written to.If it does, the casting required may get a little annoying but isn't unworkable (especially since an unshared template can do the cast in an trusted manner).I agree users shouldn't write casts explicitly, they should use a lock helper or something that grabs the lock and returns the un-shared reference. The existing behaviour is totally unacceptable though; shared objects can read/write arbitrarily... what's the point of that? It gives the impression that it's okay, but it's completely broken. There is no situation where a shared thing can arbitrarily access its members.
Oct 15 2018
On Mon, Oct 15, 2018 at 4:35 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Monday, 15 October 2018 at 21:51:43 UTC, Manu wrote:Of course! You're writing a threadsafe object... how could you expect otherwise?If a shared method is incompatible with an unshared method, your class is broken.What?!? So... my unshared methods should also perform all that's necessary for `shared` methods?Just to be clear, what I'm suggesting is a significant *restriction* to what shared already does... there will be a whole lot more safety under my proposal. The cast gives exactly nothing that attributing a method as shared doesn't give you, except that attributing a method shared is so much more sanitary and clearly communicates intent at the API level.Explicit casting doesn't magically implement thread-safety, it basically just guarantees failure.It doesn't indeed. It does, however, at least help prevent silent bugs. Via that same guaranteed failure. That failure is about all the help we can get from the compiler anyway.I don't understand your example. What's the problem you're suggesting?What I suggest are rules that lead to proper behaviour with respect to writing a thread-safe API. You can write bad code with any feature in any number of ways.Yup. For example, passing an int* to a function expecting shared int*.I don't understand this statement either. Who said they lack synchronisation? If they need it, they will have it. There's a good chance they don't need it though, they might not interact with a thread-unsafe portion of the class.I see it this way: If your object has shared methods, then it is distinctly and *deliberately* involved in thread-safety. You have deliberately opted-in to writing a thread-safe object, and you must deliver on your promise.The un-shared API of an object that supports `shared` are not exempt from the thread-safety commitment, they are simply the subset of the API that may not be called from a shared context.And therefore they lack any synchronization. So I don't see how they *can* be "compatible" with `shared` methods.So certain...If your shared method is incompatible with other methods, your class is broken, and you violate your promise.Nope.class BigCounter { this() { /* don't even need the mutex if I'm not sharing this */ } this(Mutex m = null) shared { this.m = m ? m : new Mutex; } void increment() { value += 1; } void increment() shared { synchronized(m) *value.assumeUnshared += 1; } private: Mutex m; BigInt value; }You've just conflated 2 classes into one. One is a threadlocal counter, the other is a threadsafe counter. Which is it? Like I said before: "you can contrive a bad program with literally any language feature!"They're not "compatible" in any shape or form.Correct, you wrote 2 different things and mashed them together.Or would you have the unshared ctor also create the mutex and unshared increment also take the lock? What's the point of having them then? Better disallow mixed implementations altogether (which is actually not that bad of an idea).Right. This is key to my whole suggestion. If you write a shared thing, you accept that it's shared! You don't just accept it, you jam the stake in the ground. There's a relatively small number of things that need to be threadsafe, you won't see `shared` methods appearing at random. If you use shared, you promise threadsafety OR the members of the thing are inaccessible without some sort of lock-&-cast-away treatment.Yup. It's a typo. You passed a stack pointer to a scope that outlives the caller. That class of issue is not on trial here. There's DIP1000, and all sorts of things to try and improve safety in terms of lifetimes. You only managed to contrive this by spawning a thread. If it were just a normal function, this would be perfectly legitimate, and again, that's my whole point.Nobody writes methods of an object such that they don't work with each other... methods are part of a deliberately crafted and packaged entity. If you write a shared object, you do so deliberately, and you buy responsibility of making sure your objects API is thread-safe. If your object is not thread-safe, don't write shared methods.Ahem... Okay... import std.concurrency; import core.atomic; void thread(shared int* x) { (*x).atomicOp!"+="(1); } shared int c; void main() { int x; auto tid = spawn(&thread, &x); // "just" a typo } You're saying that's ok, it should "just" compile. It shouldn't. It should produce an error and a mild electric discharge into the developer's chair.
Oct 15 2018
On Tuesday, 16 October 2018 at 00:15:54 UTC, Manu wrote:On Mon, Oct 15, 2018 at 4:35 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:What?!? So... my unshared methods should also perform all that's necessary for `shared` methods?Of course! You're writing a threadsafe object... how could you expect otherwise?See below.Just to be clear, what I'm suggesting is a significant *restriction* to what shared already does... there will be a whole lot more safety under my proposal.I don't see how an *implicit* cast can be a restriction. At all.The cast gives exactly nothing that attributing a method as shared doesn't give you, except that attributing a method shared is so much more sanitary and clearly communicates intent at the API level.It's like we're talking about wholly different things here. Casting should be done by the caller, i.e. a programmer that uses some API. If that API expects shared arguments, the caller better make sure they pass shared values. Implicit conversion destroys any obligations between the caller and the API.The problem that I'm suggesting is exactly that: an `int*` is not, and can not, be a `shared int*` at the same time. Substitute int for any type. But D is not Rust and it can't statically prevent that, except for disallowing trivial programming mistakes, which, with implicit conversion introduced, would also go away.I don't understand your example. What's the problem you're suggesting?You can write bad code with any feature in any number of ways.Yup. For example, passing an int* to a function expecting shared int*.Or they might....And therefore they lack any synchronization. So I don't see how they *can* be "compatible" with `shared` methods.I don't understand this statement either. Who said they lack synchronisation? If they need it, they will have it. There's a good chance they don't need it though, they might not interact with a thread-unsafe portion of the class.Because that is exactly the code that a good amount of "developers" will write. Especially those of the "don't think about it" variety. Don't be mistaken for a second: if the language allows it, they'll write it.So certain...If your shared method is incompatible with other methods, your class is broken, and you violate your promise.Nope.class BigCounter { this() { /* don't even need the mutex if I'm not sharing this */ } this(Mutex m = null) shared { this.m = m ? m : new Mutex; } void increment() { value += 1; } void increment() shared { synchronized(m) *value.assumeUnshared += 1; } private: Mutex m; BigInt value; }You've just conflated 2 classes into one. One is a threadlocal counter, the other is a threadsafe counter. Which is it? Like I said before: "you can contrive a bad program with literally any language feature!"Can you actually provide an example of a mixed shared/unshared class that even makes sense then? As I said, at this point I'd rather see such definitions prohibited entirely.They're not "compatible" in any shape or form.Correct, you wrote 2 different things and mashed them together.Or would you have the unshared ctor also create the mutex and unshared increment also take the lock? What's the point of having them then? Better disallow mixed implementations altogether (which is actually not that bad of an idea).Right. This is key to my whole suggestion. If you write a shared thing, you accept that it's shared! You don't just accept it, you jam the stake in the ground.Then, once more, `shared` should then just be a type qualifier exclusively, and mixing shared/unshared methods should just not be allowed.There's a relatively small number of things that need to be threadsafe, you won't see `shared` methods appearing at random. If you use shared, you promise threadsafety OR the members of the thing are inaccessible without some sort of lock-&-cast-away treatment.As above.I'm sorry, I'm not very good at writing "real" examples for things that don't exist or don't compile. End of sarcasm. Let's come back to DIP1000 when it's actually implemented in it's entirety, ok? Anyway, you're nitpicking while actually missing the point altogether. The way `shared` is "implemented" today, the API (`thread` function) *requires* the caller to pass a `shared int*`. Implicit conversion breaks that contract. At the highest level, the only reason for taking a `shared` argument is to pass that argument to another thread. That is the *only* way to communicate that intent via the type system for the time being. You're suggesting to ignore that fact. `shared` was supposed to protect from unshared aliasing, not silently allow it. If you allow implicit conversion, there would literally be no way of knowing whether some API will access your data concurrently, other than plain old documentation (or sifting through it's code, which may not be available). This makes `shared` useless as a type qualifier.import std.concurrency; import core.atomic; void thread(shared int* x) { (*x).atomicOp!"+="(1); } shared int c; void main() { int x; auto tid = spawn(&thread, &x); // "just" a typo } You're saying that's ok, it should "just" compile. It shouldn't. It should produce an error and a mild electric discharge into the developer's chair.Yup. It's a typo. You passed a stack pointer to a scope that outlives the caller. That class of issue is not on trial here. There's DIP1000, and all sorts of things to try and improve safety in terms of lifetimes.You only managed to contrive this by spawning a thread. If it were just a normal function, this would be perfectly legitimate, and again, that's my whole point.I think you will agree that passing a pointer to a thread-local variable to another thread is not always a safe thing to do. Conditions do apply, which are on you (the programmer) to uphold, and the compiler can't help you with that. The only way the compiler *can* help you here is make sure you don't do that unintentionally. Which it won't be able to do if you allow such implicit conversion.
Oct 15 2018
On Mon, Oct 15, 2018 at 7:25 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Tuesday, 16 October 2018 at 00:15:54 UTC, Manu wrote:Because a shared pointer can't access anything. You can't do anything with a shared instance, so the can be no harm done. Only if there are shared methods (that promise thread-safety) is it that shared gets interesting. Without that, it's just a market for the existing recommended use of shared; which is lock and cast away.On Mon, Oct 15, 2018 at 4:35 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:What?!? So... my unshared methods should also perform all that's necessary for `shared` methods?Of course! You're writing a threadsafe object... how could you expect otherwise?See below.Just to be clear, what I'm suggesting is a significant *restriction* to what shared already does... there will be a whole lot more safety under my proposal.I don't see how an *implicit* cast can be a restriction. At all.Why? What could a function do with shared arguments?The cast gives exactly nothing that attributing a method as shared doesn't give you, except that attributing a method shared is so much more sanitary and clearly communicates intent at the API level.It's like we're talking about wholly different things here. Casting should be done by the caller, i.e. a programmer that uses some API. If that API expects shared arguments, the caller better make sure they pass shared values. Implicit conversion destroys any obligations between the caller and the API.Why not? The guy who receives the argument receives an argument that *may be shared*, and as such, he's restricted access to it appropriately. Just like if you receive a const thing, you can't write to it, even if the caller's thing isn't const. If you receive a shared thing, you can't read or write to it.The problem that I'm suggesting is exactly that: an `int*` is not, and can not, be a `shared int*` at the same time. Substitute int for any type. But D is not Rust and it can't statically prevent that, except for disallowing trivial programming mistakes, which, with implicit conversion introduced, would also go away.I don't understand your example. What's the problem you're suggesting?You can write bad code with any feature in any number of ways.Yup. For example, passing an int* to a function expecting shared int*.Then you will implement synchronisation, or have violated your thread-safety promise.Or they might....And therefore they lack any synchronization. So I don't see how they *can* be "compatible" with `shared` methods.I don't understand this statement either. Who said they lack synchronisation? If they need it, they will have it. There's a good chance they don't need it though, they might not interact with a thread-unsafe portion of the class.This is not even an argument. Atomic!int must be used with care. Any threading of ANY KIND must be handled with care. Saying we shouldn't make shared useful because someone can do something wrong is like saying we shouldn't have atomic int's and we shouldn't have spawn(). They're simply too dangerous to give to users...Because that is exactly the code that a good amount of "developers" will write. Especially those of the "don't think about it" variety. Don't be mistaken for a second: if the language allows it, they'll write it.So certain...If your shared method is incompatible with other methods, your class is broken, and you violate your promise.Nope.class BigCounter { this() { /* don't even need the mutex if I'm not sharing this */ } this(Mutex m = null) shared { this.m = m ? m : new Mutex; } void increment() { value += 1; } void increment() shared { synchronized(m) *value.assumeUnshared += 1; } private: Mutex m; BigInt value; }You've just conflated 2 classes into one. One is a threadlocal counter, the other is a threadsafe counter. Which is it? Like I said before: "you can contrive a bad program with literally any language feature!"I think this is a typical sort of construction: struct ThreadsafeQueue(T) { void QueueItem(T*) shared; T* UnqueueItem() shared; } struct SpecialWorkList { struct Job { ... } void MakeJob(int x, float y, string z) shared // <- any thread may produce a job { Job* job = new Job; // <- this is thread-local PopulateJob(job, x, y, z); // <- preparation of a job might be complex, and worthy of the SpecialWorkList implementation jobList.QueueItem(job); // <- QueueItem encapsulates thread-safety, no need for blunt casts } void Flush() // <- not shared, thread-local consumer { Job* job; while (job = jobList.UnqueueItem()) // <- it's obviously safe for a thread-local to call UnqueueItem even though the implementation is threadsafe { // thread-local dispatch of work... // perhaps rendering, perhaps deferred destruction, perhaps deferred resource creation... whatever! } } void GetSpecialSystemState() // <- this has NOTHING to do with the threadsafe part of SpecialWorkList { return os.functionThatChecksSystemState(); } // there may be any number of utility functions that don't interact with jobList. private: void PopulateJob(ref Job job, ...) { // expensive function; not thread-safe, and doesn't have any interaction with threading. } ThreadsafeQueue!Job jobList; } This isn't an amazing example, but it's typical of a thing that's mostly thread-local, and only a small controlled part of it's functionality is thread-safe. The thread-local method Flush() also deals with thread-safety internally... because it flushes a thread-safe queue. All thread-safety concerns are composed by a utility object, so there's no need for locks, magic, or casts here.Can you actually provide an example of a mixed shared/unshared class that even makes sense then? As I said, at this point I'd rather see such definitions prohibited entirely.They're not "compatible" in any shape or form.Correct, you wrote 2 different things and mashed them together.1. No. 2. I would have to repeat literally everything I've ever said on this topic to respond to this comment.Or would you have the unshared ctor also create the mutex and unshared increment also take the lock? What's the point of having them then? Better disallow mixed implementations altogether (which is actually not that bad of an idea).Right. This is key to my whole suggestion. If you write a shared thing, you accept that it's shared! You don't just accept it, you jam the stake in the ground.Then, once more, `shared` should then just be a type qualifier exclusively, and mixing shared/unshared methods should just not be allowed.I don't understand.There's a relatively small number of things that need to be threadsafe, you won't see `shared` methods appearing at random. If you use shared, you promise threadsafety OR the members of the thing are inaccessible without some sort of lock-&-cast-away treatment.As above.So? What does it mean to pass a `shared int*`?I'm sorry, I'm not very good at writing "real" examples for things that don't exist or don't compile. End of sarcasm. Let's come back to DIP1000 when it's actually implemented in it's entirety, ok? Anyway, you're nitpicking while actually missing the point altogether. The way `shared` is "implemented" today, the API (`thread` function) *requires* the caller to pass a `shared int*`. Implicit conversion breaks that contract.import std.concurrency; import core.atomic; void thread(shared int* x) { (*x).atomicOp!"+="(1); } shared int c; void main() { int x; auto tid = spawn(&thread, &x); // "just" a typo } You're saying that's ok, it should "just" compile. It shouldn't. It should produce an error and a mild electric discharge into the developer's chair.Yup. It's a typo. You passed a stack pointer to a scope that outlives the caller. That class of issue is not on trial here. There's DIP1000, and all sorts of things to try and improve safety in terms of lifetimes.At the highest level, the only reason for taking a `shared` argument is to pass that argument to another thread.Not even. This is the most un-useful application I can think of. It doesn't really model the problem at all. Transfer of ownership is a job for move semantics. shared is for interacting with objects that are *already* owned by many threads. shared needs to model mechanics to do a limited set of thread-safe interactions with shared objects that are shared. That would make shared a useful thing, rather than a giant stain.That is the *only* way to communicate that intent via the type system for the time being....but that's shit. And it doesn't communicate that intent at all. Bluntly casting attributes on things is a terrible solution to that proposed problem.You're suggesting to ignore that fact.Yes; everything we think about shared today is completely worthless. Under my proposal, some existing applications might remain untouched (they do), but they're not worth worrying about from a design point of view, because they're not really 'designs'. Focus on making shared a useful thing, and then see where we're at.`shared` was supposed to protect from unshared aliasing, not silently allow it.Inhibiting all access satisfies that protection. It doesn't matter if a pointer is distributed if you can't access the contents. Now from there, we need a way to make interacting with guaranteed thread-safe API's interesting and useful, and I'm describing how to do that.If you allow implicit conversion, there would literally be no way of knowing whether some API will access your data concurrently, other than plain old documentation (or sifting through it's code, which may not be available). This makes `shared` useless as a type qualifier.That's the whole point though. A thread-safe think couldn't care less if the data is shared or not, because it's threadsafe. Now we're able to describe what's thread-safe, and what's not. This makes shared *useful* as a type qualifier.That's the problem I'm trying to resolve by removing all access. I'm trying to make that interaction safe, and that's the key to moving forward as I see it. If the object is thread-local, then no other thread can access the object in any way, and it's just a fancy int.You only managed to contrive this by spawning a thread. If it were just a normal function, this would be perfectly legitimate, and again, that's my whole point.I think you will agree that passing a pointer to a thread-local variable to another thread is not always a safe thing to do.Conditions do apply, which are on you (the programmer) to uphold, and the compiler can't help you with that. The only way the compiler *can* help you here is make sure you don't do that unintentionally. Which it won't be able to do if you allow such implicit conversion.You need to demonstrate how the implicit conversion may lead to chaos. The conversion is immensely useful, and I haven't thought how it's a problem yet.
Oct 15 2018
On Tuesday, 16 October 2018 at 03:00:21 UTC, Manu wrote:I don't see how an *implicit* cast can be a restriction. At all.Because a shared pointer can't access anything. You can't do anything with a shared instance, so the can be no harm done.That just doesn't compute. You obviously *can* do "anything" with a shared instance, per your own requirements all you need is methods. But at this point, the caller has an unshared reference, and your API possibliy stole away a shared one without the caller ever knowing of it happening.How, exactly, do you propose getting shared state from one thread to another? Exclusively through globals?It's like we're talking about wholly different things here. Casting should be done by the caller, i.e. a programmer that uses some API. If that API expects shared arguments, the caller better make sure they pass shared values. Implicit conversion destroys any obligations between the caller and the API.Why? What could a function do with shared arguments?But the guy that *provides* that argument may have no idea of this happening. This is unshared aliasing. module yourapi; // My code does not know about this function at all void giveToThread(shared int* ptr) { /* ... */ } void yourAPI(int* ptr) { giveToThread(ptr); } module mycode; int x; void main() { yourAPI(&x); }The problem that I'm suggesting is exactly that: an `int*` is not, and can not, be a `shared int*` at the same time. Substitute int for any type. But D is not Rust and it can't statically prevent that, except for disallowing trivial programming mistakes, which, with implicit conversion introduced, would also go away.Why not? The guy who receives the argument receives an argument that *may be shared*, and as such, he's restricted access to it appropriately.Just like if you receive a const thing, you can't write to it, even if the caller's thing isn't const.You do know why those Rust guys disallowed mutable aliasing? That is, having both mutable and immutable references at the same time. That's a long known problem that in C++ and D is only "solved" by programmer discipline and not much else.If you receive a shared thing, you can't read or write to it.You can, per your model, through methods. All the while the original is being read and written freely.Such a good promise it is when it's simply ignored by an implicit cast.Then you will implement synchronisation, or have violated your thread-safety promise.a good chance they don't need it though, they might not interact with a thread-unsafe portion of the class.Or they might.Implicit casts are in another galaxy as far as handling with care is concerned.Because that is exactly the code that a good amount of "developers" will write. Especially those of the "don't think about it" variety. Don't be mistaken for a second: if the language allows it, they'll write it.This is not even an argument. Atomic!int must be used with care. Any threading of ANY KIND must be handled with care.Saying we shouldn't make shared useful because someone can do something wrong is like saying we shouldn't have atomic int's and we shouldn't have spawn(). They're simply too dangerous to give to users...We should make `shared` useful. We shouldn't allow unshared aliasing.That's an interface for a multi-producer multi-consumer, yet you're using it as a multi-producer single-consumer. Those would have very different implementations.Can you actually provide an example of a mixed shared/unshared class that even makes sense then? As I said, at this point I'd rather see such definitions prohibited entirely.I think this is a typical sort of construction: struct ThreadsafeQueue(T) { void QueueItem(T*) shared; T* UnqueueItem() shared; }struct SpecialWorkList { struct Job { ... } void MakeJob(int x, float y, string z) shared // <- any thread may produce a job { Job* job = new Job; // <- this is thread-local PopulateJob(job, x, y, z); // <- preparation of a job might be complex, and worthy of the SpecialWorkList implementation...except you can't call PopulateJob from MakeJob. The two methods after compiler rewrite are (pseudocode): void struct_method_SpecialWorkList_MakeJob(shared SpecialWorkList* this, int x, float y, string z); void struct_method_SpecialWorkList_PopulateJob(SpecialWorkList* this, Job* job, ...); Or are you also suggesting to allow implicitly demoting shared(T)* to T* ?!? Then you can just throw away `shared`.jobList.QueueItem(job); // <- QueueItem encapsulates thread-safety, no need for blunt castsNone were needed here regardless.void Flush() // <- not shared, thread-local consumer {I.e. this can only be called by the thread that owns this instance, because only that thread can have an unshared reference to it.void GetSpecialSystemState() // <- this has NOTHING to do with the threadsafe part of SpecialWorkList { return os.functionThatChecksSystemState(); }Therefore it's either static or out of SpecialWorkList. As someone in strong opposition to conflation you should've seen that.// there may be any number of utility functions that don't interact with jobList.So have a separate interface for them, why put them in the same namespace then?private: void PopulateJob(ref Job job, ...) { // expensive function; not thread-safe, and doesn't have any interaction with threading.And cannot be called from `shared` functions without at least casting away `shared`.This isn't an amazing example, but it's typical of a thing that's mostly thread-local, and only a small controlled part of it's functionality is thread-safe. The thread-local method Flush() also deals with thread-safety internally... because it flushes a thread-safe queue.All thread-safety concerns are composed by a utility object, so there's no need for locks, magic, or casts here.// Casts: auto assumeUnshared(T)(ref shared(T) x) { /* ... */ } struct MPSCQueue(T) { void put(T*) shared; T* remove(); // perhaps also this one: T* steal() shared; } struct Job { /* ... */ } struct JobList { void addJob(Args...)(Args args) shared { auto job = new Job; this.assumeUnshared.populateJob(*job, forward!args); jobs.put(job); } Job* removeJob() { return jobs.remove(); } Job* stealJob() shared { return jobs.steal(); } private: void populateJob(ref Job job, ...) { /* ... */ } MPSCQueue!Job jobs; } struct SpecialWorkList { shared JobList* getJobList() { return jobList; } // here's an implicit cast to the actually "shared" data if you need one: //alias getJobList this; void flush() { auto list = jobList.assumeUnshared; Job* job; while ((job = list.removeJob) !is null) { // ... } } // put anything non-shared here... private: // Even though your 'SpecialWorkList' is unshared, // you're sharing at least this reference with other threads: shared JobList* jobList; } SpecialWorkList makeSpecialWorkList() { SpecialWorkList result; // whatever, allocate the jobList, initialize other state, etc. return result; } void worker(shared int* keepMeltingCPU, shared JobList* jobList) { while (true) { if (!keepMeltingCPU.atomicLoad()) break; // Do some heavy processing... jobList.addJob(/* arguments */); jobList.addJob(/* arguments */); } } void thief(shared int* keepMeltingCPU, shared JobList* jobList) { while (true) { if (!keepMeltingCPU.atomicLoad()) break; jobList.addJob(/* arguments */); jobList.addJob(/* arguments */); /* ... */ // help with jobs Job* job; while ((job = jobList.steal) !is null) { // ... } } } bool readInput() { /* ... */ } void main() { Thread[2] threads; shared int[2] flags = [ 1, 1 ]; SpecialWorkList list = makeSpecialWorkList(); threads[0] = someFunctionThatMakesAThread(&worker, &flags[0], list.getJobList); threads[1] = someFunctionThatMakesAThread(&thief, &flags[1], list.getJobList); while (true) { list.flush(); if (!readInput()) break; } foreach (ref flag; flags) { flag.atomicStore(0); } foreach (t; threads) t.join(); } As you can see, the amount of casting is minimal. It is explicit and documenting the intent. And all the casts are actually the opposite way of what you're proposing. That's because the design lends to just not letting you share thread-local data. The "problem" here is that you can't add jobs from main thread without adding an unshared overload. Which is trivial, but would require a cast. There are, however, deeper problems that don't have anything to do with the `shared` keyword, and start with the call "new Job". I could assume it's just for the sake of example, and you'd actually store the jobs: a) not as pointers b) in aligned and padded storage but since we're nitpicking, and you're very specific in that approach, I'm not sure if I'm safe making such an assumption.Exactly what it says: passing a pointer to a shared int. Which is what int* is not. Adding `shared` to a type is not the same thing as "promoting" a mutable to a const, which, the way that's done in D, isn't that awesome a deal to begin with.missing the point altogether. The way `shared` is "implemented" today, the API (`thread` function) *requires* the caller to pass a `shared int*`. Implicit conversion breaks that contract.So? What does it mean to pass a `shared int*`?There's no feasible alternative in the current type system.At the highest level, the only reason for taking a `shared` argument is to pass that argument to another thread.Not even. This is the most un-useful application I can think of. It doesn't really model the problem at all.Transfer of ownership is a job for move semantics.There is no transfer of ownership when you're *sharing* data.shared is for interacting with objects that are *already* owned by many threads.There's only one owner, usually a thread that instantiated the thing in the first place. Ergo, there needs to be a way to "share" that thing. Explicitly.shared needs to model mechanics to do a limited set of thread-safe interactions with shared objects that are shared. That would make shared a useful thing, rather than a giant stain.Agreed.It's not a solution, it's a crutch. Presumably the programmer would know whether or not that cast is safe in that particular context. Can you propose a better way to distinguish shared and thread-local instances without introducing "thread" built-in type (at which point we should all probably just stop using D)? So long as threading is purely library code, there isn't exactly much room for that.That is the *only* way to communicate that intent via the type system for the time being....but that's shit. And it doesn't communicate that intent at all. Bluntly casting attributes on things is a terrible solution to that proposed problem.Yes; everything we think about shared today is completely worthless. Under my proposal, some existing applications might remain untouched (they do), but they're not worth worrying about from a design point of view, because they're not really 'designs'. Focus on making shared a useful thing, and then see where we're at.I quite like the idea of disallowing member access wholesale. For example, things like assignment, copy/postblit should be disabled for anything `shared` by default, shared destructors should not exist. But this doesn't *really* solve the problem.An API cannot be guaranteed thread-safe without some kind of contract in it's signature.`shared` was supposed to protect from unshared aliasing, not silently allow it.Inhibiting all access satisfies that protection. It doesn't matter if a pointer is distributed if you can't access the contents. Now from there, we need a way to make interacting with guaranteed thread-safe API's interesting and useful, and I'm describing how to do that.If you allow implicit conversion, there would literally be no way of knowing whether some API will access your data concurrently, other than plain old documentation (or sifting through it's code, which may not be available). This makes `shared` useless as a type qualifier.That's the whole point though. A thread-safe think couldn't care less if the data is shared or not, because it's threadsafe. Now we're able to describe what's thread-safe, and what's not. This makes shared *useful* as a type qualifier.You're looking at it backwards, I assume because you keep thinking about `shared` in terms of `const`. `shared` thing cares if the data is shared or not, to which with implicit casting in place there is no compile-time check.I think you will agree that passing a pointer to a thread-local variable to another thread is not always a safe thing to do.That's the problem I'm trying to resolve by removing all access. I'm trying to make that interaction safe, and that's the key to moving forward as I see it. If the object is thread-local, then no other thread can access the object in any way, and it's just a fancy int.Not true if you allow to silently cast a pointer to that fancy int to a pointer to a shared int.Conditions do apply, which are on you (the programmer) to uphold, and the compiler can't help you with that. The only way the compiler *can* help you here is make sure you don't do that unintentionally. Which it won't be able to do if you allow such implicit conversion.You need to demonstrate how the implicit conversion may lead to chaos. The conversion is immensely useful, and I haven't thought how it's a problem yet.I already have. With your implicit promotions, you can literally pass off anything as shared. No amount of DIP1000s will safeguard from this, because the only ways to pass data to threads is to either only use globals, or escape it. Therefore the language needs some way of annotating that intent. Currently, `shared` does that. If you don't like that, there needs to be some alternative.
Oct 16 2018
On 15.10.2018 23:51, Manu wrote:If a shared method is incompatible with an unshared method, your class is broken.Then what you want is not implicit unshared->shared conversion. What you want is a different way to type shared member access. You want a setup where shared methods are only allowed to access shared members and unshared methods are only allowed to access unshared members. I.e., what you want is that shared is not transitive. You want that if you have a shared(C) c, then it is an error to access c.m iff m is not shared. This way you can have partially shared classes, where part of the class is thread-local, and other parts are shared with other threads. Is this it?
Oct 17 2018
On 17.10.2018 14:24, Timon Gehr wrote:On 15.10.2018 23:51, Manu wrote:(Also, with this new definition of 'shared', unshared -> shared conversion would of course become sound.)If a shared method is incompatible with an unshared method, your class is broken.Then what you want is not implicit unshared->shared conversion. What you want is a different way to type shared member access. You want a setup where shared methods are only allowed to access shared members and unshared methods are only allowed to access unshared members. I.e., what you want is that shared is not transitive. You want that if you have a shared(C) c, then it is an error to access c.m iff m is not shared. This way you can have partially shared classes, where part of the class is thread-local, and other parts are shared with other threads. Is this it?
Oct 17 2018
On 17.10.2018 14:29, Timon Gehr wrote:to access c.m iff m is not sharedUnfortunate typo. This should be if, not iff (if and only if).
Oct 17 2018
On 17.10.2018 14:24, Timon Gehr wrote:and unshared methods are only allowed to access unshared members.This is actually not necessary, let me reformulate: You want: - if you have a C c and a shared(C) s, typeof(s.x) == typeof(c.x). - shared methods are not allowed to access unshared members. - shared is not transitive, and therefore unshared class references implicitly convert to shared class references Applied to pointers, this would mean that you can implicitly convert int* -> shared(int*), but not shared(int*)->int*, int* -> shared(int)* or shared(int)* -> int*. shared(int*) and shared(shared(int)*) would be different types, such that shared(int*) cannot be dereferenced but shared(shared(int)*) can.
Oct 17 2018
On Wed, Oct 17, 2018 at 6:15 AM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 17.10.2018 14:24, Timon Gehr wrote:No. c.x is mutable, s.x is shared (and inaccessible).and unshared methods are only allowed to access unshared members.This is actually not necessary, let me reformulate: You want: - if you have a C c and a shared(C) s, typeof(s.x) == typeof(c.x).- shared methods are not allowed to access unshared members.No. Shared methods are not allowed to access ANY members. Shared data has no access. I don't believe there's any rules that can make raw access to shared data safe. We must depend on threadsafe tooling (libraries).- shared is not transitive, and therefore unshared class references implicitly convert to shared class referencesNo. Shared is transitive.Applied to pointers, this would mean that you can implicitly convert int* -> shared(int*), but not shared(int*)->int*, int* -> shared(int)* or shared(int)* -> int*.Correct. This is acceptable because shared(int)* can not be accessed; can't produce to a race. shared -> unshared conversion produces an unshared alias, and is obviously invalid.shared(int*) and shared(shared(int)*) would be different types, such that shared(int*) cannot be dereferenced but shared(shared(int)*) can.No. No manner of shared(T) is safely accessible under any circumstance. This is just the reality, and there's no way it can be allowed. The escape hatch is that T may have some shared methods which implement threadsafe interaction with T, and THAT is the only safe way to interact with shared data.
Oct 17 2018
On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:If you write a lock-free queue for instance, and all the methods are `shared` (ie, threadsafe), then under the current rules, you can't interact with the object when it's not shared, and that's fairly useless.Unless the compiler can show that it is ok to implicit/explicity convert the object to share without any unintended consequences. It should reject it. It seems that the better solution would to implement a Operators https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/using-conversion-operators But that itself requires an DIP itself. -Alex
Oct 15 2018
On 10/15/2018 08:46 PM, Manu wrote:1. traditional; assert that the object become thread-local by acquiring a lock, cast shared away 2. object may have shared methods; such methods CAN be called on shared instances. such methods may internally implement synchronisation to perform their function. perhaps methods of a lock-free queue structure for instance, or operator overloads on `Atomic!int`, etc.[...]Assuming the rules above: "can't read or write to members", and the understanding that `shared` methods are expected to have threadsafe implementations (because that's the whole point), what are the risks from allowing T* -> shared(T)* conversion?As far as I understand, the rule "can't read or write to members" is for the compiler, right? I can still read and write members, but I have to cast `shared` away and ensure thread-safety myself? If that's so, then my example from the last thread might still apply: ---- struct Bob { int* p; void doThing() shared { p = &s; /* Might need a cast or two here, and a lock or an atomic store or whatever. */ } } shared int s; ---- When the needed casts etc. are added, is `doThing` allowed? If not, I think you have to specify more precisely what a method can and can't do. If `doThing` is ok, you can't allow T* -> shared(T)*. You'd be allowing aliasing an unqualified int* with a shared(int*): ---- void main() { Bob* b = new Bob; shared(Bob)* sb = b; /* You'd allow this line. */ sb.doThing(); /* Now the unqualified int* b.p points to the shared int s. */ } ----
Oct 15 2018
On 2018-10-15 20:46, Manu wrote:1. traditional; assert that the object become thread-local by acquiring a lock, cast shared awayInstead of having to explicitly cast away shared we could leverage the synchronized statement. It could be enhanced to allow the following: shared int a; Mutex mutex; synchronized(tlsA; a, mutex) { // within this scope "tlsA" is the same as "a" but without // the "shared" qualifier. "tlsA" is not allowed to escape the block } This could also be implemented as a library function, but that would require casting away shared inside the implementation: guard(a, mutex, (tlsA){ }); void guard(T)(scope shared T value, Mutex mutex, scope void delegate (scope T) block) { mutex.lock_nothrow(); scope (exit) mutex.unlock_nothrow(); block(cast(T) value); } -- /Jacob Carlborg
Oct 16 2018
On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:Current situation where you can arbitrarily access shared members undermines any value it has.The value of shared is existence of thread-local data that's guaranteed to be not shared, so you don't need to worry about thread-local data being shared, and shared protects that value well.Assuming this world... how do you use shared?Unique solution for each case.If you write a lock-free queue for instance, and all the methods are `shared` (ie, threadsafe), then under the current rules, you can't interact with the object when it's not shared, and that's fairly useless.Create it as shared.Assuming the rules above: "can't read or write to members", and the understanding that `shared` methods are expected to have threadsafe implementations (because that's the whole point), what are the risks from allowing T* -> shared(T)* conversion?All data becomes possibly shared, so you can't assume it's unshared, effectively C-style sharing. BTW D supports the latter already.
Oct 16 2018
On Tue, Oct 16, 2018 at 2:25 AM Kagamin via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:This isn't really an argument in favour of shared being able to arbitrarily access all its members though. I'm not sure how your points addresses my claim? this sounds like that old quote "there can not be peace without war". It's not shared that makes not-shared thread-local by default, it's just that's how D is defined. Shared allows you to break that core assumption. Without shared, it would still be thread-local by default... you just wouldn't have an escape hatch ;)Current situation where you can arbitrarily access shared members undermines any value it has.The value of shared is existence of thread-local data that's guaranteed to be not shared, so you don't need to worry about thread-local data being shared, and shared protects that value well.Then you can't use it locally.Assuming this world... how do you use shared?Unique solution for each case.If you write a lock-free queue for instance, and all the methods are `shared` (ie, threadsafe), then under the current rules, you can't interact with the object when it's not shared, and that's fairly useless.Create it as shared.No data becomes 'possibly shared', because it's all inaccessible. Only if your object specifies a threadsafe API may some controlled data become shared... and that's the whole point of writing a threadsafe object. If the threadsafe object doesn't support sharing its own data, then it's not really a threadsafe object.Assuming the rules above: "can't read or write to members", and the understanding that `shared` methods are expected to have threadsafe implementations (because that's the whole point), what are the risks from allowing T* -> shared(T)* conversion?All data becomes possibly shared, so you can't assume it's unshared, effectively C-style sharing. BTW D supports the latter already.
Oct 16 2018
On 15.10.2018 20:46, Manu wrote:Assuming the rules above: "can't read or write to members", and the understanding that `shared` methods are expected to have threadsafe implementations (because that's the whole point), what are the risks from allowing T* -> shared(T)* conversion?Unshared becomes useless, and in turn, shared becomes useless. You can't have unshared/shared aliasing.All the risks that I think have been identified previously assume that you can arbitrarily modify the data. That's insanity... assume we fix that... I think the promotion actually becomes safe now...?But useless, because there is no way to ensure thread safety of reads and writes if only one party to the shared state knows about the sharing.
Oct 16 2018
On Tuesday, 16 October 2018 at 10:15:51 UTC, Timon Gehr wrote:On 15.10.2018 20:46, Manu wrote:why is unshared useless? Unshared means you can read an write to it. If you give it to a function that expect something shared, the function you had given it to can't read or write it, so it can't do any harm. Of course it can handle it threadsave, but as it is local, that is only overhead - reading or changing the value can't do any harm either. I like the idea.Assuming the rules above: "can't read or write to members", and the understanding that `shared` methods are expected to have threadsafe implementations (because that's the whole point), what are the risks from allowing T* -> shared(T)* conversion?Unshared becomes useless, and in turn, shared becomes useless.But useless, because there is no way to ensure thread safety of reads and writes if only one party to the shared state knows about the sharing.Of course there is. Giving an unshared value to a function that even can handle shared values may create some overhead, but is indeed threadsave.
Oct 16 2018
On 16.10.2018 13:04, Dominikus Dittes Scherkl wrote:On Tuesday, 16 October 2018 at 10:15:51 UTC, Timon Gehr wrote:It can do harm to others who hold an unshared alias to the same data and are operating on it concurrently.On 15.10.2018 20:46, Manu wrote:why is unshared useless? Unshared means you can read an write to it. If you give it to a function that expect something shared, the function you had given it to can't read or write it, so it can't do any harm.Assuming the rules above: "can't read or write to members", and the understanding that `shared` methods are expected to have threadsafe implementations (because that's the whole point), what are the risks from allowing T* -> shared(T)* conversion?Unshared becomes useless, and in turn, shared becomes useless.Of course it can handle it threadsave, but as it is local, that is only overhead - reading or changing the value can't do any harm either. I like the idea.Please do enlighten me. You have two processors operating (reading/writing) on the same address space on a modern computer architecture with a weak memory model, and you are using an optimizing compiler. How do you ensure sensible results without cooperation from both of them? (Hint: you don't.)But useless, because there is no way to ensure thread safety of reads and writes if only one party to the shared state knows about the sharing.Of course there is.Giving an unshared value to a function that even can handle shared values may create some overhead, but is indeed threadsave.Yes, if you give it to one function only, that is the case. However, as you may know, concurrency means that there may be multiple functions operating on the data _at the same time_. If one of them operates on the data as if it was not shared, you will run into trouble. You are arguing as if there was either no concurrency or no mutable aliasing.
Oct 16 2018
On Tue, Oct 16, 2018 at 6:25 AM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 16.10.2018 13:04, Dominikus Dittes Scherkl wrote:Nobody else holds an unshared alias. If you pass a value as const, you don't fear that it will become mutable.On Tuesday, 16 October 2018 at 10:15:51 UTC, Timon Gehr wrote:It can do harm to others who hold an unshared alias to the same data and are operating on it concurrently.On 15.10.2018 20:46, Manu wrote:why is unshared useless? Unshared means you can read an write to it. If you give it to a function that expect something shared, the function you had given it to can't read or write it, so it can't do any harm.Assuming the rules above: "can't read or write to members", and the understanding that `shared` methods are expected to have threadsafe implementations (because that's the whole point), what are the risks from allowing T* -> shared(T)* conversion?Unshared becomes useless, and in turn, shared becomes useless.What? This is a weird statement. So, you're saying that nobody has successfully written any threadsafe code, ever... we should stop trying, and we should admit that threadsafe queues and atomics, and mutexes and stuff all don't exist?Of course it can handle it threadsave, but as it is local, that is only overhead - reading or changing the value can't do any harm either. I like the idea.Please do enlighten me. You have two processors operating (reading/writing) on the same address space on a modern computer architecture with a weak memory model, and you are using an optimizing compiler. How do you ensure sensible results without cooperation from both of them? (Hint: you don't.)But useless, because there is no way to ensure thread safety of reads and writes if only one party to the shared state knows about the sharing.Of course there is.without cooperation from both of them?Perhaps this is the key to your statement? Yes. 'cooperation from both of them' in this case means, they are both interacting with a threadsafe api, and they are blocked from accessing members, or any non-threadsafe api.Who's doing this, and how?Giving an unshared value to a function that even can handle shared values may create some overhead, but is indeed threadsave.Yes, if you give it to one function only, that is the case. However, as you may know, concurrency means that there may be multiple functions operating on the data _at the same time_. If one of them operates on the data as if it was not shared, you will run into trouble.You are arguing as if there was either no concurrency or no mutable aliasing.If a class has no shared methods, there's no possibility for mutable aliasing. If the class has shared methods, then the class was carefully designed to be threadsafe.
Oct 16 2018
On 16.10.2018 20:07, Manu wrote:On Tue, Oct 16, 2018 at 6:25 AM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:How so? If you allow implicit conversions from unshared to shared, then you immediately get this situation.On 16.10.2018 13:04, Dominikus Dittes Scherkl wrote:Nobody else holds an unshared alias.On Tuesday, 16 October 2018 at 10:15:51 UTC, Timon Gehr wrote:It can do harm to others who hold an unshared alias to the same data and are operating on it concurrently.On 15.10.2018 20:46, Manu wrote:why is unshared useless? Unshared means you can read an write to it. If you give it to a function that expect something shared, the function you had given it to can't read or write it, so it can't do any harm.Assuming the rules above: "can't read or write to members", and the understanding that `shared` methods are expected to have threadsafe implementations (because that's the whole point), what are the risks from allowing T* -> shared(T)* conversion?Unshared becomes useless, and in turn, shared becomes useless.If you pass a value as const, you don't fear that it will become mutable. ...No, but as I already explained last time, mutable -> const is not at all like unshared -> shared. const only takes away capabilities, shared adds new capabilities, such as sending a reference to another thread. If you have two threads that share data, you need cooperation from both to properly synchronize accesses.Obviously I am not saying that.What? This is a weird statement. So, you're saying that nobody has successfully written any threadsafe code, ever... we should stop trying, and we should admit that threadsafe queues and atomics, and mutexes and stuff all don't exist?Of course it can handle it threadsave, but as it is local, that is only overhead - reading or changing the value can't do any harm either. I like the idea.Please do enlighten me. You have two processors operating (reading/writing) on the same address space on a modern computer architecture with a weak memory model, and you are using an optimizing compiler. How do you ensure sensible results without cooperation from both of them? (Hint: you don't.)But useless, because there is no way to ensure thread safety of reads and writes if only one party to the shared state knows about the sharing.Of course there is.Yes.without cooperation from both of them?Perhaps this is the key to your statement?Yes. 'cooperation from both of them' in this case means, they are both interacting with a threadsafe api, and they are blocked from accessing members, or any non-threadsafe api. ...Yes. Your proposal only enforces this for the shared alias.Anyone, it really does not matter. One major point of the type system is to ensure that _all_ safe code has defined behavior. You can convert between shared and unshared, just not in safe code.Who's doing this,Giving an unshared value to a function that even can handle shared values may create some overhead, but is indeed threadsave.Yes, if you give it to one function only, that is the case. However, as you may know, concurrency means that there may be multiple functions operating on the data _at the same time_. If one of them operates on the data as if it was not shared, you will run into trouble.and how? ...They create a mutable instance of a class, they create a shared alias using one of your proposed holes, then send the shared alias to another thread, call some methods on it in both threads and get race conditions.Not necessarily. Counterexample: safe: class C{ int x; void foo(){ x+=1; // this can still race with atomicIncrement } void bar()shared{ atomicIncrement(x); // presumably you want to allow this } } void main(){ auto c=new C(); shared s=c; // depending on your exact proposed rules, this step may be more cumbersome spawn!(()=>s.bar()); s.foo(); // race } Now, if a class has only shared members, that is another story. In this case, all references should implicitly convert to shared. There's a DIP I meant to write about this. (For all qualifiers, not just shared).You are arguing as if there was either no concurrency or no mutable aliasing.If a class has no shared methods, there's no possibility for mutable aliasing. If the class has shared methods, then the class was carefully designed to be threadsafe.
Oct 17 2018
On 10/17/18 8:02 AM, Timon Gehr wrote:Now, if a class has only shared members, that is another story. In this case, all references should implicitly convert to shared. There's a DIP I meant to write about this. (For all qualifiers, not just shared).When you say "shared members", you mean all the data is shared too or just the methods are shared? If not the data, D has a problem with encapsulation. Not only all the methods on the class must be shared, but ALL code in the entire module must be marked as using a shared class instance. Otherwise, other functions could modify the private data without using the proper synch mechanisms. We are better off requiring the cast, or enforcing that one must use a shared object to begin with. I think any sometimes-shared object is in any case going to benefit from parallel implementations for when the thing is unshared. -Steve
Oct 17 2018
On 17.10.2018 15:40, Steven Schveighoffer wrote:On 10/17/18 8:02 AM, Timon Gehr wrote:The specific proposal was that, for example, if a class is defined like this: shared class C{ // ... } then shared(C) and C are implicitly convertible to each other. The change is not fully backwards-compatible, because right now, this annotation just makes all members (data and methods) shared, but child classes may introduce unshared members.Now, if a class has only shared members, that is another story. In this case, all references should implicitly convert to shared. There's a DIP I meant to write about this. (For all qualifiers, not just shared).When you say "shared members", you mean all the data is shared too or just the methods are shared? If not the data, D has a problem with encapsulation. Not only all the methods on the class must be shared, but ALL code in the entire module must be marked as using a shared class instance. Otherwise, other functions could modify the private data without using the proper synch mechanisms. We are better off requiring the cast, or enforcing that one must use a shared object to begin with. I think any sometimes-shared object is in any case going to benefit from parallel implementations for when the thing is unshared. -Steve
Oct 17 2018
On 10/17/18 10:18 AM, Timon Gehr wrote:On 17.10.2018 15:40, Steven Schveighoffer wrote:OK, so the proposal is that all data and function members are shared. That makes sense. In one sense, because the class reference is conflated with the type modifier, having a C that isn't shared, actually have it's class data be shared, would be useful. -SteveOn 10/17/18 8:02 AM, Timon Gehr wrote:The specific proposal was that, for example, if a class is defined like this: shared class C{ // ... } then shared(C) and C are implicitly convertible to each other. The change is not fully backwards-compatible, because right now, this annotation just makes all members (data and methods) shared, but child classes may introduce unshared members.Now, if a class has only shared members, that is another story. In this case, all references should implicitly convert to shared. There's a DIP I meant to write about this. (For all qualifiers, not just shared).When you say "shared members", you mean all the data is shared too or just the methods are shared? If not the data, D has a problem with encapsulation. Not only all the methods on the class must be shared, but ALL code in the entire module must be marked as using a shared class instance. Otherwise, other functions could modify the private data without using the proper synch mechanisms. We are better off requiring the cast, or enforcing that one must use a shared object to begin with. I think any sometimes-shared object is in any case going to benefit from parallel implementations for when the thing is unshared. -Steve
Oct 17 2018
On Wed, Oct 17, 2018 at 5:05 AM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:[... all text ...]OMFG, I just spent about 3 hours writing a super-detailed reply to all of Timon's posts in aggregate... I clicked send... and it's gone. I don't know if this is a gmail thing, a mailing list thing... no idea... but it's... gone. I can't repeat that effort :(
Oct 17 2018
On Thursday, 18 October 2018 at 06:20:02 UTC, Manu wrote:On Wed, Oct 17, 2018 at 5:05 AM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:Never never write something super-detailed in a web-based "thing"! Native application, and copy-past! :-O But' now I'm curious about your reply! Timon argumentation are really strong (IMHO), so it's a double effort! :-/ /Paolo[... all text ...]OMFG, I just spent about 3 hours writing a super-detailed reply to all of Timon's posts in aggregate... I clicked send... and it's gone. I don't know if this is a gmail thing, a mailing list thing... no idea... but it's... gone. I can't repeat that effort :(
Oct 18 2018
On 10/18/18 2:20 AM, Manu wrote:On Wed, Oct 17, 2018 at 5:05 AM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:If it's gmail, it should be in sent folder, no? I've never had a gmail message that got sent fail to go into the sent box. -Steve[... all text ...]OMFG, I just spent about 3 hours writing a super-detailed reply to all of Timon's posts in aggregate... I clicked send... and it's gone. I don't know if this is a gmail thing, a mailing list thing... no idea... but it's... gone. I can't repeat that effort :(
Oct 18 2018
On Tue, Oct 16, 2018 at 3:20 AM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 15.10.2018 20:46, Manu wrote:What aliasing? Please show a reasonable and likely construction of the problem. I've been trying to think of it.Assuming the rules above: "can't read or write to members", and the understanding that `shared` methods are expected to have threadsafe implementations (because that's the whole point), what are the risks from allowing T* -> shared(T)* conversion?Unshared becomes useless, and in turn, shared becomes useless. You can't have unshared/shared aliasing.What? I don't understand this sentence. If a shared method is not threadsafe, then it's an implementation error. A user should expect that a shared method is threadsafe, otherwise it shouldn't be a shared method! Thread-local (ie, normal) methods are for not-threadsafe functionality.All the risks that I think have been identified previously assume that you can arbitrarily modify the data. That's insanity... assume we fix that... I think the promotion actually becomes safe now...?But useless, because there is no way to ensure thread safety of reads and writes if only one party to the shared state knows about the sharing.
Oct 16 2018
On 16.10.2018 19:25, Manu wrote:On Tue, Oct 16, 2018 at 3:20 AM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:Aliasing means you have two references to the same data. The two references are then said to alias. An implicit conversion from unshared to shared by definition introduces aliasing, where one of the two references is unshared and the other is shared.On 15.10.2018 20:46, Manu wrote:What aliasing?Assuming the rules above: "can't read or write to members", and the understanding that `shared` methods are expected to have threadsafe implementations (because that's the whole point), what are the risks from allowing T* -> shared(T)* conversion?Unshared becomes useless, and in turn, shared becomes useless. You can't have unshared/shared aliasing.Please show a reasonable and likely construction of the problem. I've been trying to think of it. ...I have given you an example. I don't care whether it is "reasonable" or "likely". The point of safe is to have a subset of the language with a sound type system.Your function can be thread safe all you want. If you have a thread unsafe function also operating on the same state, you will still get race conditions. E.g. mutual exclusion is based on cooperation from all threads. If one of them forgets to lock, it does not matter how threadsafe all the others are.What? I don't understand this sentence. If a shared method is not threadsafe, then it's an implementation error. A user should expect that a shared method is threadsafe, otherwise it shouldn't be a shared method! Thread-local (ie, normal) methods are for not-threadsafe functionality.All the risks that I think have been identified previously assume that you can arbitrarily modify the data. That's insanity... assume we fix that... I think the promotion actually becomes safe now...?But useless, because there is no way to ensure thread safety of reads and writes if only one party to the shared state knows about the sharing.
Oct 17 2018
On 10/15/18 2:46 PM, Manu wrote:Okay, so I've been thinking on this for a while... I think I have a pretty good feel for how shared is meant to be. 1. shared should behave exactly like const, except in addition to inhibiting write access, it also inhibits read access. I think this is the foundation for a useful definition for shared, and it's REALLY easy to understand and explain. Current situation where you can arbitrarily access shared members undermines any value it has. Shared must assure you don't access members unsafely, and the only way to do that with respect to data members, is to inhibit access completely. I think shared is just const without read access. Assuming this world... how do you use shared? 1. traditional; assert that the object become thread-local by acquiring a lock, cast shared away 2. object may have shared methods; such methods CAN be called on shared instances. such methods may internally implement synchronisation to perform their function. perhaps methods of a lock-free queue structure for instance, or operator overloads on `Atomic!int`, etc. In practise, there is no functional change in usage from the current implementation, except we disallow unsafe accesses (which will make the thing useful).This is a step in the right direction. But there is still one problem -- shared is inherently transitive. So casting away shared is super-dangerous, even if you lock the shared data, because any of the subreferences will become unshared and read/writable. For instance: struct S { int x; int *y; } shared int z; auto s1 = shared(S)(1, &z); auto s2 = shared(S)(2, &z); S* s1locked = s1.lock; Now I have access to z via s1locked as an unshared int, and I never locked z. Potentially one could do the same thing via s2, and now there are 2 mutable references, potentially in 2 threads. All of this, of course, is manual. So technically we could manually implement it properly inside S. But this means shared doesn't help us much. We really need on top of shared, a way to specify something is tail-shared. That is, all the data in S is unshared, but anything it points to is still shared. That at least helps the person implementing the manual locking from doing stupid things himself. -SteveFrom there, it opens up another critical opportunity; T* -> shared(T)*promotion. Const would be useless without T* -> const(T)* promotion. Shared suffers a similar problem. If you write a lock-free queue for instance, and all the methods are `shared` (ie, threadsafe), then under the current rules, you can't interact with the object when it's not shared, and that's fairly useless. Assuming the rules above: "can't read or write to members", and the understanding that `shared` methods are expected to have threadsafe implementations (because that's the whole point), what are the risks from allowing T* -> shared(T)* conversion? All the risks that I think have been identified previously assume that you can arbitrarily modify the data. That's insanity... assume we fix that... I think the promotion actually becomes safe now...? Destroy...
Oct 16 2018
On 10/16/18 9:25 AM, Steven Schveighoffer wrote:On 10/15/18 2:46 PM, Manu wrote:Oh, I didn't see this part. Completely agree with Timon on this, no implicit conversions should be allowed. If you want to have a lock-free implementation of something, you can abstract the assignments and reads behind the proper mechanisms anyway, and still avoid locking (casting is not locking). -SteveFrom there, it opens up another critical opportunity; T* -> shared(T)*promotion. Const would be useless without T* -> const(T)* promotion. Shared suffers a similar problem. If you write a lock-free queue for instance, and all the methods are `shared` (ie, threadsafe), then under the current rules, you can't interact with the object when it's not shared, and that's fairly useless.
Oct 16 2018
On Tue, Oct 16, 2018 at 6:35 AM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/16/18 9:25 AM, Steven Schveighoffer wrote:Why?On 10/15/18 2:46 PM, Manu wrote:Oh, I didn't see this part. Completely agree with Timon on this, no implicit conversions should be allowed.From there, it opens up another critical opportunity; T* -> shared(T)*promotion. Const would be useless without T* -> const(T)* promotion. Shared suffers a similar problem. If you write a lock-free queue for instance, and all the methods are `shared` (ie, threadsafe), then under the current rules, you can't interact with the object when it's not shared, and that's fairly useless.If you want to have a lock-free implementation of something, you can abstract the assignments and reads behind the proper mechanisms anyway, and still avoid locking (casting is not locking).Sorry, I don't understand what you're saying. Can you clarify?
Oct 16 2018
On 10/16/18 2:10 PM, Manu wrote:On Tue, Oct 16, 2018 at 6:35 AM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:int x; shared int *p = &x; // allow implicit conversion, currently error passToOtherThread(p); useHeavily(&x); How is this safe? Thread1 is using x without locking, while the other thread has to lock. In order for synchronization to work, both sides have to agree on a synchronization technique and abide by it.On 10/16/18 9:25 AM, Steven Schveighoffer wrote:Why?On 10/15/18 2:46 PM, Manu wrote:Oh, I didn't see this part. Completely agree with Timon on this, no implicit conversions should be allowed.From there, it opens up another critical opportunity; T* -> shared(T)*promotion. Const would be useless without T* -> const(T)* promotion. Shared suffers a similar problem. If you write a lock-free queue for instance, and all the methods are `shared` (ie, threadsafe), then under the current rules, you can't interact with the object when it's not shared, and that's fairly useless.I'd still mark a lock-free implementation shared, and all its methods shared. shared does not mean you have to lock, just cast away shared. A lock-free container still has to do some special things to make sure it avoids races, and having an "unusable" state aids in enforcing this. -SteveIf you want to have a lock-free implementation of something, you can abstract the assignments and reads behind the proper mechanisms anyway, and still avoid locking (casting is not locking).Sorry, I don't understand what you're saying. Can you clarify?
Oct 16 2018
On Tue, Oct 16, 2018 at 11:30 AM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/16/18 2:10 PM, Manu wrote:What does this mean? It can't do anything... that's the whole point here. I think I'm struggling here with people bringing presumptions to the thread. You need to assume the rules I define in the OP for the experiment to work.On Tue, Oct 16, 2018 at 6:35 AM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:int x; shared int *p = &x; // allow implicit conversion, currently error passToOtherThread(p); useHeavily(&x);On 10/16/18 9:25 AM, Steven Schveighoffer wrote:Why?On 10/15/18 2:46 PM, Manu wrote:Oh, I didn't see this part. Completely agree with Timon on this, no implicit conversions should be allowed.From there, it opens up another critical opportunity; T* -> shared(T)*promotion. Const would be useless without T* -> const(T)* promotion. Shared suffers a similar problem. If you write a lock-free queue for instance, and all the methods are `shared` (ie, threadsafe), then under the current rules, you can't interact with the object when it's not shared, and that's fairly useless.How is this safe?Because useHeavily() can't read or write to x.Thread1 is using x without locking, while the other thread has to lock. In order for synchronization to work, both sides have to agree on a synchronization technique and abide by it.Only the owning thread can access x, the shared instance can't access x at all. If some code somewhere decides it wants to cast-away shared, then it needs to determine ownership via some other means. It needs to be confident that the original owner yielded ownership, and any API that leads to this behaviour would need to be designed in such a way to encourage correct behaviour. That's *exactly* how it is now... I haven't changed anything from this perspective.Shared *should* mean that the function is threadsafe, and you are safe to call it from a shared instance. If a function is not shared, then you MUST cast away shared, and that implies that you need to use external means to create a context where you have thread-local ownership of the instance (usually with a mutex). You shouldn't need to cast-away shared to make a safe function call. By casting away shared, you also gain access to all the non-threadsafe methods and members. Casting shared away to call a shared method makes a safe access into a potentially unsafe access. It's unacceptable to case-away shared. It should be as unacceptable as casting const away. The only situation where it's okay is where you're externally verifying thread-locality by external means, and that's subject to your broader systemic design.I'd still mark a lock-free implementation shared, and all its methods shared. shared does not mean you have to lock, just cast away shared.If you want to have a lock-free implementation of something, you can abstract the assignments and reads behind the proper mechanisms anyway, and still avoid locking (casting is not locking).Sorry, I don't understand what you're saying. Can you clarify?A lock-free container still has to do some special things to make sure it avoids races, and having an "unusable" state aids in enforcing this.Can you explain how my proposal doesn't model this very neatly?
Oct 16 2018
On 10/16/18 4:26 PM, Manu wrote:On Tue, Oct 16, 2018 at 11:30 AM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:OK, I wrote a whole big response to this, and I went and re-quoted the above, and now I think I understand what the point of your statement is. I'll first say that if you don't want to allow implicit casting of shared to mutable, then you can't allow implicit casting from mutable to shared. Because it's mutable, races can happen. There is in fact, no difference between: int *p; shared int *p2 = p; int *p3 = cast(int*)p2; and this: int *p; shared int *p2 = p; int *p3 = p; So really, the effort to prevent the reverse cast is defeated by allowing the implicit cast. There is a reason we disallow assigning from mutable to immutable without a cast. Yet, it is done in many cases, because you are sometimes building an immutable object with mutable pieces, and want to cast the final result. In this case, it's ON YOU to make sure it's correct, and the traditional mechanism for the compiler giving you the responsibility is to require a cast. ----- OK, so here is where I think I misunderstood your point. When you said a lock-free queue would be unusable if it wasn't shared, I thought you meant it would be unusable if we didn't allow the implicit cast. But I realize now, you meant you should be able to use a lock-free queue without it being actually shared anywhere. What I say to this is that it doesn't need to be usable. I don't care to use a lock-free queue in a thread-local capacity. I'll just use a normal queue, which is easy to implement, and doesn't have to worry about race conditions or using atomics. A lock free queue is a special thing, very difficult to get right, and only really necessary if you are going to share it. And used for performance reasons! Why would I want to incur performance penalties when using a lock-free queue in an unshared mode? I would actually expect 2 separate implementations of the primitives, one for shared one for unshared. What about primitives that would be implemented the same? In that case, the shared method becomes: auto method() { return (cast(Queue*)&this).method; } Is this "unusable"? Without a way to say, you can call this on shared or unshared instances, then we need to do it this way. But I would trust the queue to handle this properly depending on whether it was typed shared or not. -SteveOn 10/16/18 2:10 PM, Manu wrote:What does this mean? It can't do anything... that's the whole point here. I think I'm struggling here with people bringing presumptions to the thread. You need to assume the rules I define in the OP for the experiment to work.On Tue, Oct 16, 2018 at 6:35 AM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:int x; shared int *p = &x; // allow implicit conversion, currently error passToOtherThread(p); useHeavily(&x);On 10/16/18 9:25 AM, Steven Schveighoffer wrote:Why?On 10/15/18 2:46 PM, Manu wrote:Oh, I didn't see this part. Completely agree with Timon on this, no implicit conversions should be allowed.From there, it opens up another critical opportunity; T* -> shared(T)*promotion. Const would be useless without T* -> const(T)* promotion. Shared suffers a similar problem. If you write a lock-free queue for instance, and all the methods are `shared` (ie, threadsafe), then under the current rules, you can't interact with the object when it's not shared, and that's fairly useless.
Oct 16 2018
On Tuesday, 16 October 2018 at 21:19:26 UTC, Steven Schveighoffer wrote:There is in fact, no difference between: int *p; shared int *p2 = p; int *p3 = cast(int*)p2; and this: int *p; shared int *p2 = p; int *p3 = p;If I understand Manu correctly the first should compile, and the second should error, just like if you replaces shared with const in the above.
Oct 16 2018
On Tue, Oct 16, 2018 at 3:25 PM Nicholas Wilson via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Tuesday, 16 October 2018 at 21:19:26 UTC, Steven Schveighoffer wrote:Why is the second an error? Right, for the sake of the thought experiment, replace shared with const. The first is an abomination (casting away const)... the second is just a no-op.There is in fact, no difference between: int *p; shared int *p2 = p; int *p3 = cast(int*)p2; and this: int *p; shared int *p2 = p; int *p3 = p;If I understand Manu correctly the first should compile, and the second should error, just like if you replaces shared with const in the above.
Oct 16 2018
On Wednesday, 17 October 2018 at 00:29:04 UTC, Manu wrote:On Tue, Oct 16, 2018 at 3:25 PM Nicholas Wilson via Digitalmars-d <digitalmars-d puremagic.com> wrote:I missed that the third example was *p3 = p; not *p3 = p2;On Tuesday, 16 October 2018 at 21:19:26 UTC, Steven Schveighoffer wrote:Why is the second an error? Right, for the sake of the thought experiment, replace shared with const. The first is an abomination (casting away const)... the second is just a no-op.There is in fact, no difference between: int *p; shared int *p2 = p; int *p3 = cast(int*)p2; and this: int *p; shared int *p2 = p; int *p3 = p;If I understand Manu correctly the first should compile, and the second should error, just like if you replaces shared with const in the above.
Oct 16 2018
On Tuesday, 16 October 2018 at 21:19:26 UTC, Steven Schveighoffer wrote:OK, so here is where I think I misunderstood your point. When you said a lock-free queue would be unusable if it wasn't shared, I thought you meant it would be unusable if we didn't allow the implicit cast. But I realize now, you meant you should be able to use a lock-free queue without it being actually shared anywhere. What I say to this is that it doesn't need to be usable. I don't care to use a lock-free queue in a thread-local capacity. I'll just use a normal queue, which is easy to implement, and doesn't have to worry about race conditions or using atomics. A lock free queue is a special thing, very difficult to get right, and only really necessary if you are going to share it. And used for performance reasons!I think this comes up where the queue was originally shared, you acquired a lock on the thing it is a member of, and you want to continue using it through your exclusive reference.
Oct 16 2018
On 10/16/18 6:24 PM, Nicholas Wilson wrote:On Tuesday, 16 October 2018 at 21:19:26 UTC, Steven Schveighoffer wrote:Isn't that a locking queue? I thought we were talking lock-free? -SteveOK, so here is where I think I misunderstood your point. When you said a lock-free queue would be unusable if it wasn't shared, I thought you meant it would be unusable if we didn't allow the implicit cast. But I realize now, you meant you should be able to use a lock-free queue without it being actually shared anywhere. What I say to this is that it doesn't need to be usable. I don't care to use a lock-free queue in a thread-local capacity. I'll just use a normal queue, which is easy to implement, and doesn't have to worry about race conditions or using atomics. A lock free queue is a special thing, very difficult to get right, and only really necessary if you are going to share it. And used for performance reasons!I think this comes up where the queue was originally shared, you acquired a lock on the thing it is a member of, and you want to continue using it through your exclusive reference.
Oct 17 2018
On Tue, Oct 16, 2018 at 2:20 PM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/16/18 4:26 PM, Manu wrote:It's critical that this is not allowed. It's totally unreasonable to cast from shared to thread-local without synchronisation. It's as bad as casting away const.On Tue, Oct 16, 2018 at 11:30 AM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:OK, I wrote a whole big response to this, and I went and re-quoted the above, and now I think I understand what the point of your statement is. I'll first say that if you don't want to allow implicit casting of shared to mutable,On 10/16/18 2:10 PM, Manu wrote:What does this mean? It can't do anything... that's the whole point here. I think I'm struggling here with people bringing presumptions to the thread. You need to assume the rules I define in the OP for the experiment to work.On Tue, Oct 16, 2018 at 6:35 AM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:int x; shared int *p = &x; // allow implicit conversion, currently error passToOtherThread(p); useHeavily(&x);On 10/16/18 9:25 AM, Steven Schveighoffer wrote:Why?On 10/15/18 2:46 PM, Manu wrote:Oh, I didn't see this part. Completely agree with Timon on this, no implicit conversions should be allowed.From there, it opens up another critical opportunity; T* -> shared(T)*promotion. Const would be useless without T* -> const(T)* promotion. Shared suffers a similar problem. If you write a lock-free queue for instance, and all the methods are `shared` (ie, threadsafe), then under the current rules, you can't interact with the object when it's not shared, and that's fairly useless.then you can't allow implicit casting from mutable to shared. Because it's mutable, races can happen.I don't follow...There is in fact, no difference between: int *p; shared int *p2 = p; int *p3 = cast(int*)p2;Totally illegal!! You casted away shared. That's as bad as casting away const.and this: int *p; shared int *p2 = p; int *p3 = p;There's nothing wrong with this... I don't understand the point?So really, the effort to prevent the reverse cast is defeated by allowing the implicit cast.Only the caller has the thread-local instance. You can take a thread-local pointer to a thread-local within the context of a single thread. So, it's perfectly valid for `p` and `p3` to exist in a single scope. `p2` is fine here too... and if that shared pointer were to escape to another thread, it wouldn't be a threat, because it's not readable or writable, and you can't make it back into a thread-local pointer without carefully/deliberately deployed machinery.There is a reason we disallow assigning from mutable to immutable without a cast. Yet, it is done in many cases, because you are sometimes building an immutable object with mutable pieces, and want to cast the final result.I don't think analogy to immutable has a place in this discussion, or at least, I don't understand the relevance... I think the reasonable analogy is const.In this case, it's ON YOU to make sure it's correct, and the traditional mechanism for the compiler giving you the responsibility is to require a cast.I think what you're talking about are behaviours relating to casting shared *away*, and that's some next-level shit. Handling in that case is no different to the way it exists today. You must guarantee that the pointer you possess becomes thread-local before casting it to a thread-local pointer. In my application framework, I will never cast shared away under my proposed design. We don't have any such global locks.----- OK, so here is where I think I misunderstood your point. When you said a lock-free queue would be unusable if it wasn't shared, I thought you meant it would be unusable if we didn't allow the implicit cast. But I realize now, you meant you should be able to use a lock-free queue without it being actually shared anywhere.Right, a lock-free queue is a threadsafe object, and it's methods work whether the queue is shared or not. The methods are attributed shared because they can be called on shared instances... but they can ALSO be called from a thread-local instance, and under my suggested promotion rules, it's fine for the this-pointer to promote to shared to make the call.What I say to this is that it doesn't need to be usable. I don't care to use a lock-free queue in a thread-local capacity. I'll just use a normal queue, which is easy to implement, and doesn't have to worry about race conditions or using atomics. A lock free queue is a special thing, very difficult to get right, and only really necessary if you are going to share it. And used for performance reasons!I'm more interested in the object that has that lock-free queue as a member... it is probably a mostly thread-local object, but may have a couple of shared methods. I have a whole lot of objects which have 3 tiers of API access; the thread-local part, the threadsafe part, and the const part. Just as a mutable instance can call a const method, there's no reason a thread-local instance can't call a threadsafe method. You can ask 'why', and I can't give any more satisfactory answer than "you can call a const method from a mutable object", this is commonsense, and should be possible. We want to call shared methods from threadlocal objects all the time. It's possible, and it's safe... there's no reason we shouldn't be able to.Why would I want to incur performance penalties when using a lock-free queue in an unshared mode? I would actually expect 2 separate implementations of the primitives, one for shared one for unshared.Overloading for shared and unshared is possible, and may be desirable in many cases. There are also many cases where the code duplication and tech-debt does not carry its weight. It should not be required, because it's not technically required.What about primitives that would be implemented the same? In that case, the shared method becomes: auto method() { return (cast(Queue*)&this).method; } Is this "unusable"? Without a way to say, you can call this on shared or unshared instances, then we need to do it this way.I don't understand here...? I'm saying, we *don't* need a way to say "you can call on shared or unshared instances*, because that's always safe to do. If it wasn't safe to call a const method with a mutable instance, something is terrible wrong; same applies here, it is always safe to call a threadsafe method with a thread-local instance. If that's not true, the method is objectively not threadsafe.But I would trust the queue to handle this properly depending on whether it was typed shared or not.The queue is less interesting than the object that aggregates the queue.
Oct 16 2018
On Tuesday, 16 October 2018 at 06:21:22 UTC, Manu wrote:On Mon, Oct 15, 2018 at 8:55 PM Isaac S. via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Wednesday, 17 October 2018 at 00:26:58 UTC, Manu wrote:On Tuesday, 16 October 2018 at 02:26:04 UTC, Manu wrote:Can you link to code? It doesn't sound incompatible with my proposal. Mutexes are blunt instruments, and easy to model. My whole suggestion has virtually no effect on the mutex protected case, which is what most people seem to talk about.My usages are a custom ref counted template and list types (which are built on top of the former). The ref counted template will initialize naively when not shared but utilize compare-and-set and thread yielding if needed when shared (ensureInitialized can occur any time after declaration). The list types (which are not shared-compatible *yet*) will utilize a read-write mutex only when shared (no cost beyond a little unused memory to non-shared objects). As such, casting any of the non-shared versions of these types to shared would be unsafe. (Technically the types can be safely casted to shared so long as no non-shared reference to them exists and vice versa.)I understand your point but I think the current shared (no implicit conversion) has its uses. *snip*If you can give a single 'use', I'm all ears ;)*snip* Overloading for shared and unshared is possible, and may be desirable in many cases. There are also many cases where the code duplication and tech-debt does not carry its weight. It should not be required, because it's not technically required.Overloading for shared and unshared is my reason for not allowing implicit conversion on my types (I have no problems with implicit conversion being optional or disableable). The unshared function performs no synchronization of any kind while the shared function always does. This means that having an unshared and shared reference to the same object is unsafe. As for an actual link to code, I can really only link to my ref counted template as I haven't gotten around to making the containers shared-compatible yet. The main point of interest is at lines 103-131: https://github.com/isaacs-dev/Familiar/blob/66f1a94fc099601465e755d40a2c68bf4200cabd/containers/familiar/containers/safe_ref_counted.d#L103 The unshared ensureInitialized() doesn't worry about threads simultaneously calling it. The shared version of it uses compare-and-set to prevent two threads from initializing it at the same time. An unsafe example would require a shared reference/pointer to an unshared SafeRefCounted (which would be fairly weird to do) and both calling ensureInitialized. While an example using the ref counted template is fairly contrived, a list-type that only uses a read-write mutex when it's shared isn't. As to the larger part of preventing reading/writing of shared objects, I agree with its purpose and have no problems with it.
Oct 16 2018
On Tue, Oct 16, 2018 at 8:20 PM Isaac S. via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Tuesday, 16 October 2018 at 06:21:22 UTC, Manu wrote:Okay, so just to be clear, you're objecting to an immensely useful behaviour because you exploit the current design as an optimisation potential? That said, I'm still not taking anything away from you... you can still implement your class that way if you like, no change will affect that decision. You obviously have mechanisms in place to guarantee the thread-local-ness of your object (otherwise it wouldn't work), so, assuming you implement the unshared overload to be unsafe, then situation hasn't changed for you at all...? I don't think a robust middleware would make that trade-off, but an application can do that if it wishes. Trading implicit safety (enforcing it externally via application context) for perf is not unusual at all.On Mon, Oct 15, 2018 at 8:55 PM Isaac S. via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Wednesday, 17 October 2018 at 00:26:58 UTC, Manu wrote:On Tuesday, 16 October 2018 at 02:26:04 UTC, Manu wrote:Can you link to code? It doesn't sound incompatible with my proposal. Mutexes are blunt instruments, and easy to model. My whole suggestion has virtually no effect on the mutex protected case, which is what most people seem to talk about.My usages are a custom ref counted template and list types (which are built on top of the former). The ref counted template will initialize naively when not shared but utilize compare-and-set and thread yielding if needed when shared (ensureInitialized can occur any time after declaration). The list types (which are not shared-compatible *yet*) will utilize a read-write mutex only when shared (no cost beyond a little unused memory to non-shared objects). As such, casting any of the non-shared versions of these types to shared would be unsafe. (Technically the types can be safely casted to shared so long as no non-shared reference to them exists and vice versa.)I understand your point but I think the current shared (no implicit conversion) has its uses. *snip*If you can give a single 'use', I'm all ears ;)*snip* Overloading for shared and unshared is possible, and may be desirable in many cases. There are also many cases where the code duplication and tech-debt does not carry its weight. It should not be required, because it's not technically required.Overloading for shared and unshared is my reason for not allowing implicit conversion on my types (I have no problems with implicit conversion being optional or disableable). The unshared function performs no synchronization of any kind while the shared function always does. This means that having an unshared and shared reference to the same object is unsafe.As to the larger part of preventing reading/writing of shared objects, I agree with its purpose and have no problems with it.Right, the implicit cast thing is secondary to this fundamental part. I think it's worth first focusing on getting that rule right. shared = no read + no write .. I don't think that's objectionable.
Oct 16 2018
On Wednesday, 17 October 2018 at 03:50:44 UTC, Manu wrote:On Tue, Oct 16, 2018 at 8:20 PM Isaac S. via Digitalmars-d <digitalmars-d puremagic.com> wrote:I'm not objecting to the behavior (I actually really want it in the language as it allows for some really useful designs) but more-so saying it would be nice if it could be enabled or disabled. If its enabled-by-default, the disable attribute could be used somehow (maybe " disable alias this shared;"). Even without the ability to enable/disable shared implicit conversion it won't break my code, it'll just allow a potential bug. Since it does allow for extremely useful designs, I'll support this change (*even if it can't be enabled/disabled*).*snip* Overloading for shared and unshared is my reason for not allowing implicit conversion on my types (I have no problems with implicit conversion being optional or disableable). The unshared function performs no synchronization of any kind while the shared function always does. This means that having an unshared and shared reference to the same object is unsafe.Okay, so just to be clear, you're objecting to an immensely useful behaviour because you exploit the current design as an optimisation potential? That said, I'm still not taking anything away from you... you can still implement your class that way if you like, no change will affect that decision. You obviously have mechanisms in place to guarantee the thread-local-ness of your object (otherwise it wouldn't work), so, assuming you implement the unshared overload to be unsafe, then situation hasn't changed for you at all...? I don't think a robust middleware would make that trade-off, but an application can do that if it wishes. Trading implicit safety (enforcing it externally via application context) for perf is not unusual at all.
Oct 16 2018
On 10/16/18 8:26 PM, Manu wrote:On Tue, Oct 16, 2018 at 2:20 PM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:OK, so even with synchronization in the second thread when you cast, you still have a thread-local pointer in the originating thread WITHOUT synchronization.On 10/16/18 4:26 PM, Manu wrote:It's critical that this is not allowed. It's totally unreasonable to cast from shared to thread-local without synchronisation.On Tue, Oct 16, 2018 at 11:30 AM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:OK, I wrote a whole big response to this, and I went and re-quoted the above, and now I think I understand what the point of your statement is. I'll first say that if you don't want to allow implicit casting of shared to mutable,int x; shared int *p = &x; // allow implicit conversion, currently error passToOtherThread(p); useHeavily(&x);What does this mean? It can't do anything... that's the whole point here. I think I'm struggling here with people bringing presumptions to the thread. You need to assume the rules I define in the OP for the experiment to work.It's as bad as casting away const.Of course! But shared has a different problem from const. Const allows the data to change through another reference, shared cannot allow changes without synchronization. Changes without synchronization are *easy* with an unshared reference. Data can't be shared and unshared at the same time.You seem to be saying that shared data is unusable. But why the hell have it then? At some point it has to be usable. And the agreed-upon use is totally defeated if you also have some stray non-shared reference to it.then you can't allow implicit casting from mutable to shared. Because it's mutable, races can happen.I don't follow...But if you can't do anything with shared data, how do you use it?There is in fact, no difference between: int *p; shared int *p2 = p; int *p3 = cast(int*)p2;Totally illegal!! You casted away shared. That's as bad as casting away const.It's identical to the top one. You now have a new unshared reference to shared data. This is done WITHOUT any agreed-upon synchronization.and this: int *p; shared int *p2 = p; int *p3 = p;There's nothing wrong with this... I don't understand the point?Huh? If shared data can never be used, why have it? Pretend that p is not a pointer to an int, but a pointer to an UNSHARED type that has shared methods on it and unshared methods (for when you don't need any sync). Now the shared methods will obey the sync, but the unshared ones won't. The result is races. I can't understand how you don't see that.So really, the effort to prevent the reverse cast is defeated by allowing the implicit cast.Only the caller has the thread-local instance. You can take a thread-local pointer to a thread-local within the context of a single thread. So, it's perfectly valid for `p` and `p3` to exist in a single scope. `p2` is fine here too... and if that shared pointer were to escape to another thread, it wouldn't be a threat, because it's not readable or writable, and you can't make it back into a thread-local pointer without carefully/deliberately deployed machinery.No, immutable is more akin to shared because immutable and mutable are completely different. const can point at mutable or immutable data. shared can't be both shared and unshared. There's no comparison. Data is either shared or not shared, there is no middle ground. There is no equivalent of const to say "this data could be shared, or could be unshared".There is a reason we disallow assigning from mutable to immutable without a cast. Yet, it is done in many cases, because you are sometimes building an immutable object with mutable pieces, and want to cast the final result.I don't think analogy to immutable has a place in this discussion, or at least, I don't understand the relevance... I think the reasonable analogy is const.OK, so how does shared data actually operate? Somewhere, the magic has to turn into real code. If not casting away shared, what do you suggest?In this case, it's ON YOU to make sure it's correct, and the traditional mechanism for the compiler giving you the responsibility is to require a cast.I think what you're talking about are behaviours relating to casting shared *away*, and that's some next-level shit. Handling in that case is no different to the way it exists today. You must guarantee that the pointer you possess becomes thread-local before casting it to a thread-local pointer. In my application framework, I will never cast shared away under my proposed design. We don't have any such global locks.It's fine in terms of a specific object we are talking about, with pre-determined agreements as to whether the data will be passed to other threads. But the compiler has no idea about these agreements. It can't logically determine that this is safe without you telling it, hence the requirement for casting.----- OK, so here is where I think I misunderstood your point. When you said a lock-free queue would be unusable if it wasn't shared, I thought you meant it would be unusable if we didn't allow the implicit cast. But I realize now, you meant you should be able to use a lock-free queue without it being actually shared anywhere.Right, a lock-free queue is a threadsafe object, and it's methods work whether the queue is shared or not. The methods are attributed shared because they can be called on shared instances... but they can ALSO be called from a thread-local instance, and under my suggested promotion rules, it's fine for the this-pointer to promote to shared to make the call.I get that, I think the shared methods just need to have unshared versions for the case when the queue is fully thread-local.What I say to this is that it doesn't need to be usable. I don't care to use a lock-free queue in a thread-local capacity. I'll just use a normal queue, which is easy to implement, and doesn't have to worry about race conditions or using atomics. A lock free queue is a special thing, very difficult to get right, and only really necessary if you are going to share it. And used for performance reasons!I'm more interested in the object that has that lock-free queue as a member... it is probably a mostly thread-local object, but may have a couple of shared methods.I have a whole lot of objects which have 3 tiers of API access; the thread-local part, the threadsafe part, and the const part. Just as a mutable instance can call a const method, there's no reason a thread-local instance can't call a threadsafe method.If you call a const method, it can squirrel away a const pointer to the otherwise mutable data, which is then usable later (and safe to do so). The same cannot be said for a shared pointer. That can be then easily moved to another thread, WITHOUT the expectation that it was. And in that case, the now thread-local queue is actually shared between threads, and calling the thread-local API will cause races. I didn't respond to the rest of the comments, because they were simply another form of "shared is similar to const", which is not true. threadsafe use of shared data depends on ALL threads using those same mechanisms. If one uses simple thread-local use, then it all falls apart. -Steve
Oct 17 2018
On Wednesday, 17 October 2018 at 13:25:28 UTC, Steven Schveighoffer wrote:On 10/16/18 8:26 PM, Manu wrote:It isn't, you typo'd it (I originally missed it too).On Tue, Oct 16, 2018 at 2:20 PM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:But if you can't do anything with shared data, how do you use it?There is in fact, no difference between: int *p; shared int *p2 = p; int *p3 = cast(int*)p2;Totally illegal!! You casted away shared. That's as bad as casting away const.It's identical to the top one. You now have a new unshared reference to shared data. This is done WITHOUT any agreed-upon synchronization.and this: int *p; shared int *p2 = p; int *p3 = p;There's nothing wrong with this... I don't understand the point?int *p3 = cast(int*)p2;vsint *p3 = p;
Oct 17 2018
On 10/17/18 9:58 AM, Nicholas Wilson wrote:On Wednesday, 17 October 2018 at 13:25:28 UTC, Steven Schveighoffer wrote:It wasn't a typo. It's identical in that both result in a thread-local pointer equivalent to p. Effectively, you can "cast" away shared without having to write a cast. I was trying to demonstrate the ineffectiveness of preventing implicit casting from shared to mutable if you allow unshared data to implicitly cast to shared. It's the same problem with mutable and immutable. It's why we can't allow the implicit casting. Explicit casting is OK as long as you don't later modify the data. In the same vein, explicit casting of local to shared is OK as long as you don't ever treat the data as local again. Which should requires a cast to say "I know what I'm doing, compiler". -SteveIt's identical to the top one. You now have a new unshared reference to shared data. This is done WITHOUT any agreed-upon synchronization.It isn't, you typo'd it (I originally missed it too).int *p3 = cast(int*)p2;vsint *p3 = p;
Oct 17 2018
On Wednesday, 17 October 2018 at 15:51:04 UTC, Steven Schveighoffer wrote:On 10/17/18 9:58 AM, Nicholas Wilson wrote:The first example assigns p2, the second assigns p (which is thread local) _not_ p2 (which is shared), I'm confused.On Wednesday, 17 October 2018 at 13:25:28 UTC, Steven Schveighoffer wrote:It wasn't a typo.It's identical to the top one. You now have a new unshared reference to shared data. This is done WITHOUT any agreed-upon synchronization.It isn't, you typo'd it (I originally missed it too).int *p3 = cast(int*)p2;vsint *p3 = p;
Oct 17 2018
On 10/17/18 12:27 PM, Nicholas Wilson wrote:On Wednesday, 17 October 2018 at 15:51:04 UTC, Steven Schveighoffer wrote:Here they are again: int *p; shared int *p2 = p; int *p3 = cast(int*)p2; int *p; shared int *p2 = p; int *p3 = p; I'll put some asserts in that show they accomplish the same thing: assert(p3 is p2); assert(p3 is p); assert(p2 is p); What the example demonstrates is that while you are trying to disallow implicit casting of a shared pointer to an unshared pointer, you have inadvertently allowed it by leaving behind an unshared pointer that is the same thing. While we do implicitly allow mutable to cast to const, it's because const is a weak guarantee. It's a guarantee that the data may not change via *this* reference, but could change via other references. Shared doesn't have the same characteristics. In order for a datum to be safely shared, it must be accessed with synchronization or atomics by ALL parties. If you have one party that can simply change it without those, you will get races. That's why shared/unshared is more akin to mutable/immutable than mutable/const. It's true that only one thread will have thread-local access. It's not valid any more than having one mutable alias to immutable data. -SteveOn 10/17/18 9:58 AM, Nicholas Wilson wrote:The first example assigns p2, the second assigns p (which is thread local) _not_ p2 (which is shared), I'm confused.On Wednesday, 17 October 2018 at 13:25:28 UTC, Steven Schveighoffer wrote:It wasn't a typo.It's identical to the top one. You now have a new unshared reference to shared data. This is done WITHOUT any agreed-upon synchronization.It isn't, you typo'd it (I originally missed it too).int *p3 = cast(int*)p2;vsint *p3 = p;
Oct 17 2018
On Wed, Oct 17, 2018 at 10:30 AM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/17/18 12:27 PM, Nicholas Wilson wrote:This doesn't make sense... you're showing a thread-local program. The thread owning the unshared pointer is entitled to the unshared pointer. It can make as many copies at it likes. They are all thread-local. There's only one owning thread, and you can't violate that without unsafe casts.On Wednesday, 17 October 2018 at 15:51:04 UTC, Steven Schveighoffer wrote:Here they are again: int *p; shared int *p2 = p; int *p3 = cast(int*)p2; int *p; shared int *p2 = p; int *p3 = p; I'll put some asserts in that show they accomplish the same thing: assert(p3 is p2); assert(p3 is p); assert(p2 is p); What the example demonstrates is that while you are trying to disallow implicit casting of a shared pointer to an unshared pointer, you have inadvertently allowed it by leaving behind an unshared pointer that is the same thing.On 10/17/18 9:58 AM, Nicholas Wilson wrote:The first example assigns p2, the second assigns p (which is thread local) _not_ p2 (which is shared), I'm confused.On Wednesday, 17 October 2018 at 13:25:28 UTC, Steven Schveighoffer wrote:It wasn't a typo.It's identical to the top one. You now have a new unshared reference to shared data. This is done WITHOUT any agreed-upon synchronization.It isn't, you typo'd it (I originally missed it too).int *p3 = cast(int*)p2;vsint *p3 = p;In order for a datum to be safely shared, it must be accessed with synchronization or atomics by ALL parties.** Absolutely **If you have one party that can simply change it without those, you will get races.*** THIS IS NOT WHAT I'M PROPOSING *** I've explained it a few times now, but people aren't reading what I actually write, and just assume based on what shared already does that they know what I'm suggesting. You need to eject all presumptions from your mind, take the rules I offer as verbatim, and do thought experiments from there.That's why shared/unshared is more akin to mutable/immutable than mutable/const.Only if you misrepresent my suggestion.It's true that only one thread will have thread-local access. It's not valid any more than having one mutable alias to immutable data.And this is why the immutable analogy is invalid. It's like const. shared offers restricted access (like const), not a different class of thing. There is one thread with thread-local access, and many threads with shared access. If a shared (threadsafe) method can be defeated by threadlocal access, then it's **not threadsafe**, and the program is invalid. struct NotThreadsafe { int x; void local() { ++x; // <- invalidates the method below, you violate the other function's `shared` promise } void notThreadsafe() shared { atomicIncrement(&x); } } struct Atomic(T) { void opUnary(string op : "++")() shared { atomicIncrement(&val); } private T val; } struct Threadsafe { Atomic!int x; void local() { ++x; } void threadsafe() shared { ++x; } } Naturally, local() is redundant, and it's perfectly fine for a thread-local to call threadsafe() via implicit conversion. Here's another one, where only a subset of the object is modeled to be threadsafe (this is particularly interesting to me): struct Threadsafe { int x; Atomic!int y; void notThreadsafe() { ++x; ++y; } void threadsafe() shared { ++y; } } In these examples, the thread-local function *does not* undermine the threadsafety of threadsafe(), it MUST NOT undermine the threadsafety of threadsafe(), or else threadsafe() **IS NOT THREADSAFE**. In the second example, you can see how it's possible and useful to do thread-local work without invalidating the objects threadsafety commitments. I've said this a bunch of times, there are 2 rules: 1. shared inhibits read and write access to members 2. `shared` methods must be threadsafeFrom there, shared becomes interesting and useful.
Oct 17 2018
On Wednesday, 17 October 2018 at 18:46:18 UTC, Manu wrote:I've said this a bunch of times, there are 2 rules: 1. shared inhibits read and write access to members 2. `shared` methods must be threadsafeOh God... void atomicInc(shared int* i) { /* ... */ } Now what? There are no "methods" for ints, only UFCS. Those functions can be as safe as you like, but if you allow implicit promotion of int* to shared int*, you *allow implicit races*.From there, shared becomes interesting and useful.
Oct 17 2018
On Wed, Oct 17, 2018 at 12:05 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Wednesday, 17 October 2018 at 18:46:18 UTC, Manu wrote:This function is effectively an intrinsic. It's unsafe by definition. It's a tool for implementing threadsafe machinery. No user can just start doing atomic operations on random ints and say "it's threadsafe", you must encapsulate the threadsafe functionality into some sort of object that aggregates all concerns and presents an intellectually sound api. Let me try one: void free(void*) { ... } Now what? I might have dangling pointers... it's a catastrophe! It's essentially the same argument. This isn't a function that professes to do something that people might misunderstand and try to use in an unsafe way, it's a low-level implementation device, which is used to build larger *useful* constructs.I've said this a bunch of times, there are 2 rules: 1. shared inhibits read and write access to members 2. `shared` methods must be threadsafeOh God... void atomicInc(shared int* i) { /* ... */ } Now what? There are no "methods" for ints, only UFCS. Those functions can be as safe as you like, but if you allow implicit promotion of int* to shared int*, you *allow implicit races*.From there, shared becomes interesting and useful.
Oct 17 2018
On Wednesday, 17 October 2018 at 19:25:33 UTC, Manu wrote:On Wed, Oct 17, 2018 at 12:05 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:Only if implicit conversion is allowed. If it isn't, that's likely trusted, and this: void atomicInc(ref shared int); can even be safe.On Wednesday, 17 October 2018 at 18:46:18 UTC, Manu wrote:This function is effectively an intrinsic. It's unsafe by definition.I've said this a bunch of times, there are 2 rules: 1. shared inhibits read and write access to members 2. `shared` methods must be threadsafeOh God... void atomicInc(shared int* i) { /* ... */ } Now what? There are no "methods" for ints, only UFCS. Those functions can be as safe as you like, but if you allow implicit promotion of int* to shared int*, you *allow implicit races*.From there, shared becomes interesting and useful.It's a tool for implementing threadsafe machinery. No user can just start doing atomic operations on random ints and say "it's threadsafe", you must encapsulate the threadsafe functionality into some sort of object that aggregates all concerns and presents an intellectually sound api.Threadsafety starts and ends with the programmer. By your logic *all* functions operating on `shared` are unsafe then. As far as compiler is concerned, there would be no difference between these two: struct S {} void atomicInc(ref shared S); and struct S { void atomicInc() shared { /* ... */ } } The signatures of those two functions are exactly the same. How is that different from a function taking a shared int pointer or reference?Let me try one: void free(void*) { ... } Now what? I might have dangling pointers... it's a catastrophe!One could argue that it should be void free(ref void* p) { /* ... */ p = null; } As a matter of fact, in my own allocators memory blocks allocated by them are passed by value and are non-copyable, they're not just void[] as in std.experimental.allocator. One must 'move' them to pass ownership, and that includes deallocation. But that's another story altogether.It's essentially the same argument. This isn't a function that professes to do something that people might misunderstand and try to use in an unsafe way, it's a low-level implementation device, which is used to build larger *useful* constructs.You're missing the point, again. You have an int. You pass a pointer to it to some API that takes an int*. You continue to use your int as just an int. The API changes, and now the function you called previously takes a shared int*. Implicit conversion works, everything compiles, you have a race. Now, that's of course an extremely stupid scenario. The point is: the caller of some API *must* assert that they indeed pass shared data. It's insufficient for the API alone to "promise" taking shared data. That's the difference with promotion to `const`.
Oct 17 2018
On Wed, Oct 17, 2018 at 2:15 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Wednesday, 17 October 2018 at 19:25:33 UTC, Manu wrote:In this case, with respect to the context (a single int) atomicInc() is ALWAYS safe, even with implicit conversion. You can atomicInc() a thread-local int perfectly safely.On Wed, Oct 17, 2018 at 12:05 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:Only if implicit conversion is allowed. If it isn't, that's likely trusted, and this: void atomicInc(ref shared int); can even be safe.On Wednesday, 17 October 2018 at 18:46:18 UTC, Manu wrote:This function is effectively an intrinsic. It's unsafe by definition.I've said this a bunch of times, there are 2 rules: 1. shared inhibits read and write access to members 2. `shared` methods must be threadsafeOh God... void atomicInc(shared int* i) { /* ... */ } Now what? There are no "methods" for ints, only UFCS. Those functions can be as safe as you like, but if you allow implicit promotion of int* to shared int*, you *allow implicit races*.From there, shared becomes interesting and useful.It's not, atomicInc() of an int is always safe with respect to the int itself. You can call atomicInc() on an unshared int and it's perfectly fine, but now you need to consider context, and that's a problem for the design of the higher-level scope. To maintain thread-safety, the int in question must be appropriately contained. The problem is that the same as the example I presented before, which I'll repeat: struct InvalidProgram { int x; void fun() { ++x; } void gun() shared { atomicInc(&x); } } The method gun() (and therefore the whole object) is NOT threadsafe by my definition, because fun() violates the threadsafety of gun(). The situation applies equally here that: int x; atomicInc(&x); ++x; // <- by my definition, this 'API' (increment an int) violates the threadsafety of atomicInc(), and atomicInc() is therefore not threadsafe. `int` doesn't present a threadsafe API, so int is by definition, NOT threadsafe. atomicInc() should be system, and not trusted. If you intend to share an int, use Atomic!int, because it has a threadsafe API. atomicInc(shared int*) is effectively just an unsafe intrinsic, and its only use is at ground-level implementation of threadsafe machinery, like malloc() and free().It's a tool for implementing threadsafe machinery. No user can just start doing atomic operations on random ints and say "it's threadsafe", you must encapsulate the threadsafe functionality into some sort of object that aggregates all concerns and presents an intellectually sound api.Threadsafety starts and ends with the programmer. By your logic *all* functions operating on `shared` are unsafe then. As far as compiler is concerned, there would be no difference between these two: struct S {} void atomicInc(ref shared S); and struct S { void atomicInc() shared { /* ... */ } } The signatures of those two functions are exactly the same. How is that different from a function taking a shared int pointer or reference?void *p2 = p; free(p); p2.crash();Let me try one: void free(void*) { ... } Now what? I might have dangling pointers... it's a catastrophe!One could argue that it should be void free(ref void* p) { /* ... */ p = null; }As a matter of fact, in my own allocators memory blocks allocated by them are passed by value and are non-copyable, they're not just void[] as in std.experimental.allocator. One must 'move' them to pass ownership, and that includes deallocation. But that's another story altogether.Right, now you're talking about move semantics to implement transfer of ownership... you might recall I was arguing this exact case to express transferring ownership of objects between threads earlier. This talk of blunt casts and "making sure everything is good" is all just great, but it doesn't mean anything interesting with respect to `shared`. It should be interesting even without unsafe casts.You have written an invalid program. I can think of an infinite number of ways to write an invalid program. In this case, don't have an `int`, instead, have an Atomic!int; you now guarantee appropriate access, problem solved! If you do have an int, don't pass it to other threads at random when you don't have any idea what they intend to do with it! That's basic common sense. You don't pass a pointer to a function if you don't know what it does with the pointer!It's essentially the same argument. This isn't a function that professes to do something that people might misunderstand and try to use in an unsafe way, it's a low-level implementation device, which is used to build larger *useful* constructs.You're missing the point, again. You have an int. You pass a pointer to it to some API that takes an int*. You continue to use your int as just an int.The API changes, and now the function you called previously takes a shared int*. Implicit conversion works, everything compiles, you have a race. Now, that's of course an extremely stupid scenario.Yes.The point is: the caller of some API *must* assert that they indeed pass shared data. It's insufficient for the API alone to "promise" taking shared data. That's the difference with promotion to `const`.The caller doesn't care if it's true that the callee can't do anything with it that's unsafe anyway. We effect that state by removing all non-threadsafe access.
Oct 17 2018
On Wednesday, 17 October 2018 at 23:12:48 UTC, Manu wrote:On Wed, Oct 17, 2018 at 2:15 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:Yes, *you* can. *Another* function can't unless *you* allow for it to be safe. You can't do that if that function silently assumes you gave it shared data, when in fact you did not.On Wednesday, 17 October 2018 at 19:25:33 UTC, Manu wrote:In this case, with respect to the context (a single int) atomicInc() is ALWAYS safe, even with implicit conversion. You can atomicInc() a thread-local int perfectly safely.On Wed, Oct 17, 2018 at 12:05 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:Only if implicit conversion is allowed. If it isn't, that's likely trusted, and this: void atomicInc(ref shared int); can even be safe.On Wednesday, 17 October 2018 at 18:46:18 UTC, Manu wrote:This function is effectively an intrinsic. It's unsafe by definition.I've said this a bunch of times, there are 2 rules: 1. shared inhibits read and write access to members 2. `shared` methods must be threadsafeOh God... void atomicInc(shared int* i) { /* ... */ } Now what? There are no "methods" for ints, only UFCS. Those functions can be as safe as you like, but if you allow implicit promotion of int* to shared int*, you *allow implicit races*.From there, shared becomes interesting and useful.Exactly. And that means it can't convert to shared without my say so :)The signatures of those two functions are exactly the same. How is that different from a function taking a shared int pointer or reference?It's not, atomicInc() of an int is always safe with respect to the int itself. You can call atomicInc() on an unshared int and it's perfectly fine, but now you need to consider context, and that's a problem for the design of the higher-level scope. To maintain thread-safety, the int in question must be appropriately contained.The problem is that the same as the example I presented before, which I'll repeat: struct InvalidProgram { int x; void fun() { ++x; } void gun() shared { atomicInc(&x); } } The method gun() (and therefore the whole object) is NOT threadsafe by my definition, because fun() violates the threadsafety of gun(). The situation applies equally here that: int x; atomicInc(&x); ++x; // <- by my definition, this 'API' (increment an int) violates the threadsafety of atomicInc(), and atomicInc() is therefore not threadsafe.No. The 'API' is just the atomicInc function. You, the user of that API, own the int. If the API wants a shared int from you, you have to be in agreement. You can't have any agreement if the API is only making promises and assumptions.`int` doesn't present a threadsafe API, so int is by definition, NOT threadsafe. atomicInc() should be system, and not trusted.Exactly. `int` isn't threadsafe and therefore cannot automatically convert to `shared int`.If you intend to share an int, use Atomic!int, because it has a threadsafe API.No. With current implementation of `shared`, which disallows your automatic promotion, your intent is enforced. You cannot share a local `int` unless *you know* it's safe to do so and therefore can cast that int to shared.atomicInc(shared int*) is effectively just an unsafe intrinsic, andIt is only unsafe if you allow int* to silently convert to shared int*. If you can't do that, you can't call `atomicInc` on an int*.One could argue that it should be void free(ref void* p) { /* ... */ p = null; }void *p2 = p; free(p); p2.crash();That's exactly analogous to what you're proposing: leaking `shared` references while keeping unshared data.You went off on an irrelevant tangent there, and I feel like you didn't even see my reply. You don't pass any ownership when you share. You just share. As an owner, you get to access the un-`shared` interface freely. Others do not.As a matter of fact, in my own allocators memory blocks allocated by them are passed by value and are non-copyable, they're not just void[] as in std.experimental.allocator. One must 'move' them to pass ownership, and that includes deallocation. But that's another story altogether.Right, now you're talking about move semantics to implement transfer of ownership... you might recall I was arguing this exact case to express transferring ownership of objects between threads earlier.This talk of blunt casts and "making sure everything is good" is all just great, but it doesn't mean anything interesting with respect to `shared`. It should be interesting even without unsafe casts.Again, I feel like you didn't see my reply. It's not talk about just blunt casts to make sure everything is good. You either have shared data to begin with, and so can share it freely, or you *know* that you can share this particular piece of data, even if it itself isn't marked `shared`, and you *assert* that by deliberately casting. *You* know, *you* cast, not "some function expects you to know, and just auto-casts".You're missing the point, again. You have an int. You pass a pointer to it to some API that takes an int*. You continue to use your int as just an int.You have written an invalid program. I can think of an infinite number of ways to write an invalid program.No, I have not. I didn't make any promises that my data was shared, and I wasn't expecting it to be treated as such. I didn't even author that API. The other guy (the API) made wrong assumptions. Don't you see the difference here?In this case, don't have an `int`, instead, have an Atomic!int; you now guarantee appropriate access, problem solved!No, it isn't. My int *is* thread-local, *I* don't need an Atomic!int, I didn't sanction that int's use as shared in any way. Yet the automatic conversion presumes that I have. As I stated previously, there's no difference between Atomic!int and free functions operating on shared int* (or ref shared int). Struct methods are sugared versions of those free functions, *nothing more*. That's why we have UFCS.If you do have an int, don't pass it to other threads at random when you don't have any idea what they intend to do with it!*I* don't pass it to other threads at random, and I expect other code to not do so without *my* approval. What approval can I give if *other* code can silently assume I'm giving it shared data, when in fact I'm not?That's basic common sense. You don't pass a pointer to a function if you don't know what it does with the pointer!I should know what the function does with the pointer from it's signature. Now, currently in D that very blurry. *Hopefully* with 'scope', DIP25 and DIP1000 this becomes more common. But that's at least what we should strive for. If a function takes `shared`, I better be sure I'm giving it `shared`. The only way to do so is either have `shared` to begin with, or explicitly cast when I know I can do so.The point is: the caller of some API *must* assert that they indeed pass shared data. It's insufficient for the API alone to "promise" taking shared data. That's the difference with promotion to `const`.The caller doesn't care if it's true that the callee can't do anything with it that's unsafe anyway. We effect that state by removing all non-threadsafe access.You allow non-threadsafe access with implicit cast, not remove it. This broken record is getting very tiresome... Let me ask you this once again: *why* are you so bent on this implicit conversion from mutable to shared? So far the only reason I've seen is to just avoid writing additional methods that forward to `shared` methods. Most of your casts will be the other way around, will have to be explicit and there's nothing that can be done about that. You'll only have mutable->shared casts in few select cases, exactly because they're corner cases where you *need* to make the decision clear.
Oct 17 2018
I don't have anything to add that hasn't been said yet but it's good to see some thinking on this subject. It feels like progress.
Oct 17 2018
On Wed, Oct 17, 2018 at 5:35 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Wednesday, 17 October 2018 at 23:12:48 UTC, Manu wrote:It can though, because `int` doesn't have any shared methods; it's inaccessible. You can't do anything with a shared int, so it's safe to distribute regardless.On Wed, Oct 17, 2018 at 2:15 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:Yes, *you* can. *Another* function can't unless *you* allow for it to be safe. You can't do that if that function silently assumes you gave it shared data, when in fact you did not.On Wednesday, 17 October 2018 at 19:25:33 UTC, Manu wrote:In this case, with respect to the context (a single int) atomicInc() is ALWAYS safe, even with implicit conversion. You can atomicInc() a thread-local int perfectly safely.On Wed, Oct 17, 2018 at 12:05 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:Only if implicit conversion is allowed. If it isn't, that's likely trusted, and this: void atomicInc(ref shared int); can even be safe.On Wednesday, 17 October 2018 at 18:46:18 UTC, Manu wrote:This function is effectively an intrinsic. It's unsafe by definition.I've said this a bunch of times, there are 2 rules: 1. shared inhibits read and write access to members 2. `shared` methods must be threadsafeOh God... void atomicInc(shared int* i) { /* ... */ } Now what? There are no "methods" for ints, only UFCS. Those functions can be as safe as you like, but if you allow implicit promotion of int* to shared int*, you *allow implicit races*.From there, shared becomes interesting and useful.Exactly. And that means it can't convert to shared without my say so :)The signatures of those two functions are exactly the same. How is that different from a function taking a shared int pointer or reference?It's not, atomicInc() of an int is always safe with respect to the int itself. You can call atomicInc() on an unshared int and it's perfectly fine, but now you need to consider context, and that's a problem for the design of the higher-level scope. To maintain thread-safety, the int in question must be appropriately contained.Okay, here's an idea... atomicInc() should NOT take `shared int*`, change atomicInt() to receive int*. We agree that `int` is not a threadsafe type, atomicInc() is not a threadsafe method, and therefore shouldn't be shared. If you have a shared(int)*, and you want to do un-threadsafe atomicInc(), you need to cast to int*. I think this is actually correct by my definition, because we recognise int is not a threadsafe type, and atomicInc() is not a threadsafe method, so hard-cast is required, and programmer must appropriately validate the context when doing an unsafe operation.The problem is that the same as the example I presented before, which I'll repeat: struct InvalidProgram { int x; void fun() { ++x; } void gun() shared { atomicInc(&x); } } The method gun() (and therefore the whole object) is NOT threadsafe by my definition, because fun() violates the threadsafety of gun(). The situation applies equally here that: int x; atomicInc(&x); ++x; // <- by my definition, this 'API' (increment an int) violates the threadsafety of atomicInc(), and atomicInc() is therefore not threadsafe.No. The 'API' is just the atomicInc function. You, the user of that API, own the int. If the API wants a shared int from you, you have to be in agreement. You can't have any agreement if the API is only making promises and assumptions.It can convert to shared int, because it's inaccessible... you can't do anything with a `shared int` except unsafe cast shared away. Once you're in unsafe land, you are responsible for assuring correct conditions.`int` doesn't present a threadsafe API, so int is by definition, NOT threadsafe. atomicInc() should be system, and not trusted.Exactly. `int` isn't threadsafe and therefore cannot automatically convert to `shared int`.Yes, but we're not talking about current definition of shared. Stop confusing the conversation. This thread is about the rules I define.If you intend to share an int, use Atomic!int, because it has a threadsafe API.No. With current implementation of `shared`, which disallows your automatic promotion, your intent is enforced. You cannot share a local `int` unless *you know* it's safe to do so and therefore can cast that int to shared.I allow conversion to shared. Assume that, and then solve. Actually, I think I realise the problem. We're not stuck on a failure of definition, we're stressing over an invalid API! It's incorrect for atomicInt to receive a shared(int)*, because it doesn't (and can't) promise thread-safety. It must receive int*. The implementation of Atomic which encapsulates access (guards against unsafe access) to the int will do the proper cast.atomicInc(shared int*) is effectively just an unsafe intrinsic, andIt is only unsafe if you allow int* to silently convert to shared int*. If you can't do that, you can't call `atomicInc` on an int*.Right, and I'm saying it's completely acceptable and normal. We don't need to be blocked on this matter. It's a low-level intrinsic, it isn't a general use API. Can we please look past it. void atomicInc(int*); // <- we'll assume this moving forward. As far as I can tell, this is the ONLY issue you have with the design. Please find another issue.One could argue that it should be void free(ref void* p) { /* ... */ p = null; }void *p2 = p; free(p); p2.crash();That's exactly analogous to what you're proposing: leaking `shared` references while keeping unshared data.Sorry, I can't understand this paragraph...This talk of blunt casts and "making sure everything is good" is all just great, but it doesn't mean anything interesting with respect to `shared`. It should be interesting even without unsafe casts.Again, I feel like you didn't see my reply. It's not talk about just blunt casts to make sure everything is good. You either have shared data to begin with, and so can share it freely, or you *know* that you can share this particular piece of data, even if it itself isn't marked `shared`, and you *assert* that by deliberately casting. *You* know, *you* cast, not "some function expects you to know, and just auto-casts".I do not. I see it black-and-white, it you write `shared` on a method, you made a promise to do threadsafety. You must deliver on that promise. If you do not deliver that promise, the program is invalid. If you *can not* deliver on that promise, then you can not attribute the method shared (or receive a shared argument).You're missing the point, again. You have an int. You pass a pointer to it to some API that takes an int*. You continue to use your int as just an int.You have written an invalid program. I can think of an infinite number of ways to write an invalid program.No, I have not. I didn't make any promises that my data was shared, and I wasn't expecting it to be treated as such. I didn't even author that API. The other guy (the API) made wrong assumptions. Don't you see the difference here?As I stated previously, there's no difference between Atomic!int and free functions operating on shared int* (or ref shared int). Struct methods are sugared versions of those free functions, *nothing more*. That's why we have UFCS.I'm not sure what you're saying. Atomic encapsulates guaranteed threadsafe access to an int, whereas shared(int)* doesn't and can't. Any threadsafe API that receives an int will necessarily receive an Atomic!int, unless it's deliberately doing un-safety.You don't care, because you are certain that it can't do anything that's unsafe with your object.That's basic common sense. You don't pass a pointer to a function if you don't know what it does with the pointer!I should know what the function does with the pointer from it's signature. Now, currently in D that very blurry. *Hopefully* with 'scope', DIP25 and DIP1000 this becomes more common. But that's at least what we should strive for. If a function takes `shared`, I better be sure I'm giving it `shared`.Why do you keep saying this? threadsafe methods! I **ONLY** allow threadsafe access by allowing implicit conversion.The point is: the caller of some API *must* assert that they indeed pass shared data. It's insufficient for the API alone to "promise" taking shared data. That's the difference with promotion to `const`.The caller doesn't care if it's true that the callee can't do anything with it that's unsafe anyway. We effect that state by removing all non-threadsafe access.You allow non-threadsafe access with implicit cast, not remove it.This broken record is getting very tiresome...It is. I don't know how to be clearer than I have been.Let me ask you this once again: *why* are you so bent on this implicit conversion from mutable to shared? So far the only reason I've seen is to just avoid writing additional methods that forward to `shared` methods.Because users of an API shouldn't have to riddle their code with manual casts to call perfectly safe functions. That would be a plain-as-day indicator that the design is wrong. I need a some real examples of how the implicit cast violates threadsafety... We've identified the issue with atomicInc(), and it's the only thing you're stuck on, and you've never picked any general holes in the design. Please find a legitimate hole in the design...Most of your casts will be the other way around, will have to be explicit and there's nothing that can be done about that. You'll only have mutable->shared casts in few select cases, exactly because they're corner cases where you *need* to make the decision clear.If shared means "threadsafe or no access", then you don't need to make the decision clear... you don't need to make the decision at all. It's safe by definition. I'm trying to create a situation where users are not concerned that they were able to use an API correctly unless they're *implementing* threadsafety; that's when they need to think, and any requirement for un-safety and casting should be factored into that space, not the calling space.
Oct 17 2018
Manu, how is it that you can't see what *your own* proposal means??? Implicit casting from mutable to shared means that everything is shared by default! Precisely the opposite of what D proclaims. You also essentially forbid defining *any* functions that take `shared T*` argument(s). You keep asking for concrete "holes". Don't you see what the previous "atomicInc" example implies??? If *any* free function `foo(shared T* bar)`, per your definition, is not threadsafe, then no other function with shared argument(s) can be threadsafe at all. So how do you call functions on shared data then? You keep saying "methods, methods..." struct Other { /* ... */ } struct S { void foo(shared Other*) shared; } Per your rules, there would be *nothing* in the language to prevent calling S.foo with an unshared Other. So the only way to make your proposal work would be to forbid all functions from taking `shared T*` or `ref shared T` argument. Except we can't do that, because a method is just a function with an implicit first argument. The code above is the same as this: void foo(ref shared S, shared Other*); It's literally *the same signature*. So there's nothing in the language to prevent calling that on an unshared S either. To sum up, things you implied but never specified in your proposal: 1. Primitive types can't be explicitly `shared`. 2. Free functions taking `shared` arguments are not allowed. 3. Only `shared` methods can implement threadsafe operations on `shared` data (which contradicts (2) already) <- this one you did specify. 4. Every variable is implicitly shared, whether intended so or not.
Oct 18 2018
On Thursday, 18 October 2018 at 10:08:48 UTC, Stanislav Blinov wrote:Manu, how is it that you can't see what *your own* proposal means??? Implicit casting from mutable to shared means that everything is shared by default! Precisely the opposite of what D proclaims.Well, sorta. But that's not a problem, because you can't do anything that's not threadsafe to something that's shared.You also essentially forbid defining *any* functions that take `shared T*` argument(s). You keep asking for concrete "holes". Don't you see what the previous "atomicInc" example implies???I certainly don't. Please do elucidate.If *any* free function `foo(shared T* bar)`, per your definition, is not threadsafe, then no other function with shared argument(s) can be threadsafe at all. So how do you call functions on shared data then? You keep saying "methods, methods..." struct Other { /* ... */ } struct S { void foo(shared Other*) shared; } Per your rules, there would be *nothing* in the language to prevent calling S.foo with an unshared Other.That's true. And you can't do anything to it, so that's fine.So the only way to make your proposal work would be to forbid all functions from taking `shared T*` or `ref shared T` argument.No. Please read this thread again. From the beginning, every word. Actually, don't do that, because Manu's proposal is simple and elegant:1. the rule must be applied that shared object can not be read or written 2. attributing a method shared is a statement and a promise that the method is threadsafe The rest just follows naturally.There's actually one more thing: The one and only thing you can do (without unsafe casting) with a shared object, is call shared methods and free functions on it.To sum up, things you implied but never specified in your proposal: 1. Primitive types can't be explicitly `shared`.Sure they can, they just can't present a thread-safe interface, so you can't do anything with a shared(int).2. Free functions taking `shared` arguments are not allowed.Yes, they are. They would be using other shared methods or free functions on the shared argument, and would thus be thread-safe. If defined in the same module as the type on which they operate, they would have access to the internal state of the object, and would have to be written in such a way as to not violate the thread-safety of other methods and free functions that operate on it.3. Only `shared` methods can implement threadsafe operations on `shared` data (which contradicts (2) already) <- this one you did specify.Non-shared methods are perfectly free to be thread-safe (and they should be, in the sense that they shouldn't interfere with shared methods). A better way to state this is that only shared methods may be called on a shared object. A shared object may also be passed to a function taking a shared parameter.4. Every variable is implicitly shared, whether intended so or not.Well, yes, in the same sense that every variable is also implicitly const, whether intended so or not. -- Simen
Oct 18 2018
On Thursday, 18 October 2018 at 11:35:21 UTC, Simen Kjærås wrote:On Thursday, 18 October 2018 at 10:08:48 UTC, Stanislav Blinov wrote:Yes you can. You silently agree to another function's assumption that you pass shared data, while actually passing thread-local data and keeping treating it as thread-local. I.e. you silently agree to a race.Manu, how is it that you can't see what *your own* proposal means??? Implicit casting from mutable to shared means that everything is shared by default! Precisely the opposite of what D proclaims.Well, sorta. But that's not a problem, because you can't do anything that's not threadsafe to something that's shared.What the hell? I do in the very next paragraph. Do people read sentence by sentence and assume context does not exist or what?You also essentially forbid defining *any* functions that take `shared T*` argument(s). You keep asking for concrete "holes". Don't you see what the previous "atomicInc" example implies???I certainly don't. Please do elucidate.Yes you can do "anything" to it. If you couldn't, you wouldn't be able to implement `shared` at all. Forbidding reads and writes isn't enough to guarantee that you "can't do anything with it". *Unless* you forbid implicit conversion from mutable to shared. Then, and only then, your statement can hold.If *any* free function `foo(shared T* bar)`, per your definition, is not threadsafe, then no other function with shared argument(s) can be threadsafe at all. So how do you call functions on shared data then? You keep saying "methods, methods..." struct Other { /* ... */ } struct S { void foo(shared Other*) shared; } Per your rules, there would be *nothing* in the language to prevent calling S.foo with an unshared Other.That's true. And you can't do anything to it, so that's fine.Are you kidding me? Maybe it's *you* who should do that?..So the only way to make your proposal work would be to forbid all functions from taking `shared T*` or `ref shared T` argument.No. Please read this thread again. From the beginning, every word.Actually, don't do that, because Manu's proposal is simple and elegant:No objection there, I fully support that. I even stated multiple times how it can be extended and why.1. the rule must be applied that shared object can not be read or writtenNo objection here either.2. attributing a method shared is a statement and a promise that the method is threadsafeNothing follows naturally. The proposal doesn't talk at all about the fact that you can't have "methods" on primitives, that you can't distinguish between shared and unshared data if that proposal is realized, that you absolutely destroy D's TLS-by-default treatment...The rest just follows naturally.There's actually one more thing: The one and only thing you can do (without unsafe casting) with a shared object, is call shared methods and free functions on it.Functions that you must not be allowed to write per this same proposal. How quaint.Ergo... you can't have functions taking pointers to shared primitives. Ergo, `shared <primitive type>` becomes a useless language construct.To sum up, things you implied but never specified in your proposal: 1. Primitive types can't be explicitly `shared`.Sure they can, they just can't present a thread-safe interface, so you can't do anything with a shared(int).This contradicts (1). Either you can have functions taking shared T* arguments, thus creating threadsafe interface for them, or you can't. If, per (1) as you say, you can't2. Free functions taking `shared` arguments are not allowed.Yes, they are. They would be using other shared methods or free functions on the shared argument, and would thus be thread-safe. If defined in the same module as the type on which they operate, they would have access to the internal state of the object, and would have to be written in such a way as to not violate the thread-safety of other methods and free functions that operate on it.3. Only `shared` methods can implement threadsafe operations on `shared` data (which contradicts (2) already) <- this one you did specify.Non-shared methods are perfectly free to be thread-safe (and they should be, in the sense that they shouldn't interfere with shared methods). A better way to state this is that only shared methods may be called on a shared object. A shared object may also be passed to a function taking a shared parameter.4. Every variable is implicitly shared, whether intended so or not.Well, yes, in the same sense that every variable is also implicitly const, whether intended so or not.I sort of expected that answer. No, nothing is implicitly const. When you pass a reference to a function taking const, *you keep mutable reference*, the function agrees to that, and it's only "promise" is to not modify data through the reference you gave it. But *you still keep mutable reference*. Just as you would keep *unshared mutable* reference if implicit conversion from mutable to shared existed.
Oct 18 2018
On Thursday, 18 October 2018 at 12:15:07 UTC, Stanislav Blinov wrote:On Thursday, 18 October 2018 at 11:35:21 UTC, Simen Kjærås wrote:No, you don't. If I give you a locked box with no obvious way to open it, I can expect you not to open it. It's the same thing. If you have a shared(T), and it doesn't define a thread-safe interface, you can do nothing with it. If you are somehow able to cause a race with something with which you can do nothing, please tell me how, because I'm pretty sure that implies the very laws of logic are invalid.On Thursday, 18 October 2018 at 10:08:48 UTC, Stanislav Blinov wrote:Yes you can. You silently agree to another function's assumption that you pass shared data, while actually passing thread-local data and keeping treating it as thread-local. I.e. you silently agree to a race.Manu, how is it that you can't see what *your own* proposal means??? Implicit casting from mutable to shared means that everything is shared by default! Precisely the opposite of what D proclaims.Well, sorta. But that's not a problem, because you can't do anything that's not threadsafe to something that's shared.No, you can't. You can do thread-safe things to it. That's nothing, *unless* Other defines a shared (thread-safe) interface, in which case it's safe, and everything is fine. Example: struct Other { private Data payload; // shared function. Thread-safe, can be called from a // shared object, or from an unshared object. void twiddle() shared { payload.doSharedStuff(); } // unshared function. Cannot be called from a shared object. // Promises not to interfere with shared data, or to so only // in thread-safe ways (by calling thread-safe methods, or // by taking a mutex or equivalent). void twaddle() { payload.doSharedThings(); } // Bad function. Promises not to interfere with shared data, // but does so anyway. // Give the programmer a stern talking-to. void twank() { payload.fuckWith(); } } struct S { void foo(shared Other* o) shared { // No can do - can't call non-shared functions on shared object. // o.twaddle(); // Can do - twiddle is always safe to call. o.twiddle(); } }Yes you can do "anything" to it.If *any* free function `foo(shared T* bar)`, per your definition, is not threadsafe, then no other function with shared argument(s) can be threadsafe at all. So how do you call functions on shared data then? You keep saying "methods, methods..." struct Other { /* ... */ } struct S { void foo(shared Other*) shared; } Per your rules, there would be *nothing* in the language to prevent calling S.foo with an unshared Other.That's true. And you can't do anything to it, so that's fine.If you couldn't, you wouldn't be able to implement `shared` at all. Forbidding reads and writes isn't enough to guarantee that you "can't do anything with it".Alright, so I have this shared object that I can't read from, and can't write to. It has no public shared members. What can I do with it? I can pass it to other guys, who also can't do anything with it. Are there other options?You can't have thread-safe methods operating directly on primitives, because they already present a non-thread-safe interface. This is true. This follows naturally from the rules.Nothing follows naturally. The proposal doesn't talk at all about the fact that you can't have "methods" on primitives,The rest just follows naturally.that you can't distinguish between shared and unshared data if that proposal is realized,And you can't do that currently either. Just like today, shared(T) means the T may or may not be shared with other thread. Nothing more, nothing less.that you absolutely destroy D's TLS-by-default treatment...I'm unsure what you mean by this.What? Which functions can't I write? // Safe, regular function operating on shared data. void foo(shared(Other)* o) { o.twiddle(); // Works great! } // Unsafe function. Should belong somewhere deep in druntime // and only be used by certified wizards. void bar(shared(int)* i) { atomicOp!"++"(i); }There's actually one more thing: The one and only thing you can do (without unsafe casting) with a shared object, is call shared methods and free functions on it.Functions that you must not be allowed to write per this same proposal. How quaint.Yup, this is correct. But wrap it in a struct, like e.g. Atomic!int, and everything's hunky-dory.Ergo... you can't have functions taking pointers to shared primitives. Ergo, `shared <primitive type>` becomes a useless language construct.1. Primitive types can't be explicitly `shared`.Sure they can, they just can't present a thread-safe interface, so you can't do anything with a shared(int).I have no idea where I or Manu have said you can't make functions that take shared(T)*.This contradicts (1). Either you can have functions taking shared T* arguments, thus creating threadsafe interface for them, or you can't. If, per (1) as you say, you can't2. Free functions taking `shared` arguments are not allowed.Yes, they are. They would be using other shared methods or free functions on the shared argument, and would thus be thread-safe. If defined in the same module as the type on which they operate, they would have access to the internal state of the object, and would have to be written in such a way as to not violate the thread-safety of other methods and free functions that operate on it.Yup, and that's perfectly fine, because 'shared' means 'thread-safe'. I think Manu might have mentioned that once. If a type presents both a shared and a non-shared interface, and the non-shared interface may do things that impact the shared part, these things must be done in a thread-safe manner. If that's not the case, you have a bug. The onus is on the creator of a type to do this. Let's say it together: for a type to be thread-safe, all of its public members must be written in a thread-safe way. If a non-shared method may jeopardize this, the type is not thread-safe, and shouldn't provide a shared interface. -- Simen4. Every variable is implicitly shared, whether intended so or not.Well, yes, in the same sense that every variable is also implicitly const, whether intended so or not.I sort of expected that answer. No, nothing is implicitly const. When you pass a reference to a function taking const, *you keep mutable reference*, the function agrees to that, and it's only "promise" is to not modify data through the reference you gave it. But *you still keep mutable reference*. Just as you would keep *unshared mutable* reference if implicit conversion from mutable to shared existed.
Oct 18 2018
On Thursday, 18 October 2018 at 13:09:10 UTC, Simen Kjærås wrote:Sorry, small mistake here. You're correct that shared <primitive type> becomes useless, except as a way to signal that there's something there, and you can't touch it. I was not replying to the part saying 'you can't have functions taking pointers to shared primitives'. That's just patently false. However, I don't see why you'd want such a function, since it can't do anything with what you pass it. -- SimenErgo... you can't have functions taking pointers to shared primitives. Ergo, `shared <primitive type>` becomes a useless language construct.Yup, this is correct. But wrap it in a struct, like e.g. Atomic!int, and everything's hunky-dory.
Oct 18 2018
On Thursday, 18 October 2018 at 13:09:10 UTC, Simen Kjærås wrote:You contradict yourself and don't even notice it. Per your rules, the way to open that locked box is have shared methods that access data via casting. Also per your rules, there is absolutely no way for the programmer to control whether they're actually sharing the data. Therefore, some API can steal a shared reference without your approval, use that with your "safe" shared methods, while you're continuing to threat your data as not shared.No, you don't. If I give you a locked box with no obvious way to open it, I can expect you not to open it.Well, sorta. But that's not a problem, because you can't do anything that's not threadsafe to something that's shared.Yes you can. You silently agree to another function's assumption that you pass shared data, while actually passing thread-local data and keeping treating it as thread-local. I.e. you silently agree to a race.It's the same thing. If you have a shared(T), and it doesn't define a thread-safe interface, you can do nothing with it. If you are somehow able to cause a race with something with which you can do nothing, please tell me how, because I'm pretty sure that implies the very laws of logic are invalid.You and Manu both seem to think that methods allow you to "define a thread-safe interface". struct S { void foo() shared; } Per your rules, S.foo is thread-safe. It is here that I remind you, *again*, what S.foo actually looks like, given made-up easy-to-read mangling: void struct_S_foo(ref shared S); And yet, for some reason, you think that these are not thread-safe: void foo(shared int*); void bar(ref shared int); I mean, how does that logic even work with you?That's already wrong starting at line 2. It should be: struct Other { private shared Data payload; // shared, there's no question about it // shared function. Thread-safe, can be called from a // shared object, or from an unshared object. void twiddle() shared { payload.doSharedStuff(); } // unshared function. Cannot be called from a shared object. // Promises not to interfere with shared data, or to so only // in thread-safe ways (by calling thread-safe methods, or // by taking a mutex or equivalent). void twaddle() { // fine so long as there's a // 'auto doSharedThings(ref shared Data)' // or an equivalent method for Data. // Otherwise it just wouldn't compile, as it should. payload.doSharedThings(); } // No longer a bad function, because it doesn't compile, and the // programmer can do their own auto-spanking. void twank() { payload.fuckWith(); // Error: cannot fuckWith() 'shared Data' } } struct S { void foo(shared Other* o) shared { // No can do - can't call non-shared functions on shared object. // o.twaddle(); // ^Yep, agreed // Can do - twiddle is always safe to call. o.twiddle(); } } Well, that was easy, wasn't it? Your implementation of 'twaddle' is *unsafe*, because the compiler doesn't know that 'payload' is shared. For example, when inlining, it may reorder the calls in it and cause races or other UB. At least one of the reasons behind `shared` *was* to serve as compiler barrier. What I don't see in your example is where it would be necessary to cast mutable to shared, let alone auto-cast it. And that's the heart of this discussion. If you just do this: auto other = new /*shared*/ Other; ...then at the moment, per current rules, you can either twiddle or twaddle (depending on whether you remove the comment or not), but not both, despite being a sole owner of 'Other'. This is the only place where I can see *some small* value in automatic conversion. But I'd much rather have a language that strictly forbids me to do nasty things than provides small conveniences in corner cases.No, you can't. You can do thread-safe things to it. That's nothing, *unless* Other defines a shared (thread-safe) interface, in which case it's safe, and everything is fine. Example: struct Other { private Data payload; // shared function. Thread-safe, can be called from a // shared object, or from an unshared object. void twiddle() shared { payload.doSharedStuff(); } // unshared function. Cannot be called from a shared object. // Promises not to interfere with shared data, or to so only // in thread-safe ways (by calling thread-safe methods, or // by taking a mutex or equivalent). void twaddle() { payload.doSharedThings(); } // Bad function. Promises not to interfere with shared data, // but does so anyway. // Give the programmer a stern talking-to. void twank() { payload.fuckWith(); } } struct S { void foo(shared Other* o) shared { // No can do - can't call non-shared functions on shared object. // o.twaddle(); // Can do - twiddle is always safe to call. o.twiddle(); } }Yes you can do "anything" to it.Per your rules, there would be *nothing* in the language to prevent calling S.foo with an unshared Other.That's true. And you can't do anything to it, so that's fine.It can have any number of public shared "members" per UFCS. The fact that you forget is that there's no difference between a method and a free function, other than syntax sugar. Well, OK, there's guaranteed private access for methods, but same is true for module members.If you couldn't, you wouldn't be able to implement `shared` at all. Forbidding reads and writes isn't enough to guarantee that you "can't do anything with it".Alright, so I have this shared object that I can't read from, and can't write to. It has no public shared members. What can I do with it? I can pass it to other guys, who also can't do anything with it. Are there other options?Everything in D already presents a non-threadsafe interface. Things that you advocate included. struct S { void foo() shared; } That is not threadsafe. This *sort of* is: struct S { disable this(this); disable void opAssign(S); void foo() shared; } ...except not quite still. Now, if the compiler generated above in the presence of any `shared` members or methods, then we could begin talking about it being threadsafe. But that part is mysteriously missing from Manu's proposal, even though I keep reminding of this in what feels like every third post or so (I'm probably grossly exaggerating).You can't have thread-safe methods operating directly on primitives, because they already present a non-thread-safe interface. This is true. This follows naturally from the rules.Nothing follows naturally. The proposal doesn't talk at all about the fact that you can't have "methods" on primitives,The rest just follows naturally.that you can't distinguish between shared and unshared data if that proposal is realized,And you can't do that currently either. Just like today, shared(T) means the T may or may not be shared with other thread. Nothing more, nothing less.I don't think it means what you think it means. "May or may not be shared with other thread" means "you MUST treat it as if it's shared with other thread". That's it. That's why automatic conversion doesn't make *any* sense, and that's why compiler error on attempting to pass over mutable as shared makes *perfect* sense.You lose the ability to distinguish thread-local and shared data.that you absolutely destroy D's TLS-by-default treatment...I'm unsure what you mean by this.Uh-huh, only due to some weird convention that "methods" are somehow safer than free functions. Which they're not.What? Which functions can't I write? // Safe, regular function operating on shared data. void foo(shared(Other)* o) { o.twiddle(); // Works great! } // Unsafe function. Should belong somewhere deep in druntime // and only be used by certified wizards. void bar(shared(int)* i) { atomicOp!"++"(i); }There's actually one more thing: The one and only thing you can do (without unsafe casting) with a shared object, is call shared methods and free functions on it.Functions that you must not be allowed to write per this same proposal. How quaint.So again, void atomicInc(shared int*); // is "not safe", but void struct_Atomic_int_opUnary_plus_plus(ref shared Atomic); // is "safe" just because latter is a "method". And that, by you, is hunky-dory? Whether it's a method or a free function, it's written to work on *shared* data. Of course it wouldn't be safe if you allow any non-shared data to become shared without the programmer having a say in this.Yup, this is correct. But wrap it in a struct, like e.g. Atomic!int, and everything's hunky-dory.Ergo... you can't have functions taking pointers to shared primitives. Ergo, `shared <primitive type>` becomes a useless language construct.1. Primitive types can't be explicitly `shared`.Sure they can, they just can't present a thread-safe interface, so you can't do anything with a shared(int).I have no idea where I or Manu have said you can't make functions that take shared(T)*.Because you keep saying they're unsafe and that you should wrap them up in a struct for no other reason than just "because methods are kosher".Yes, that part is perfectly fine with me.I sort of expected that answer. No, nothing is implicitly const. When you pass a reference to a function taking const, *you keep mutable reference*, the function agrees to that, and it's only "promise" is to not modify data through the reference you gave it. But *you still keep mutable reference*. Just as you would keep *unshared mutable* reference if implicit conversion from mutable to shared existed.Yup, and that's perfectly fine, because 'shared' means 'thread-safe'. I think Manu might have mentioned that once. If a type presents both a shared and a non-shared interface, and the non-shared interface may do things that impact the shared part, these things must be done in a thread-safe manner. If that's not the case, you have a bug. The onus is on the creator of a type to do this.Let's say it together: for a type to be thread-safe, all of its public members must be written in a thread-safe way.It's shared private parts also must be written in a thread-safe way. Yes, they're private, but they still may be shared. Welcome to the communism of multithreading.If a non-shared method may jeopardize this, the type is not thread-safe, and shouldn't provide a shared interface.It can't jeopardize anything so long as you actually treat your shared data accordingly, and don't just magically assume unshared parts to be shared for some reason.
Oct 18 2018
On Thursday, 18 October 2018 at 16:31:02 UTC, Stanislav Blinov wrote:So again, void atomicInc(shared int*); // is "not safe", but void struct_Atomic_int_opUnary_plus_plus(ref shared Atomic); // is "safe" just because latter is a "method". And that, by you, is hunky-dory? Whether it's a method or a free function, it's written to work on *shared* data. Of course it wouldn't be safe if you allow any non-shared data to become shared without the programmer having a say in this.Out of curiosity, when it comes to primitives, what could you do under MP in void "atomicInc(shared int*)" that would be problematic? void atomicInc(shared int*) { // i.e. what goes here? }
Oct 18 2018
On Thursday, 18 October 2018 at 17:10:03 UTC, aliak wrote:Out of curiosity, when it comes to primitives, what could you do under MP in void "atomicInc(shared int*)" that would be problematic? void atomicInc(shared int*) { // i.e. what goes here? }1. Anything if int* implicitly converts to shared int* (per MP), because then that function is indeed unsafe. 2. Only actual platform-specific implementation bugs otherwise, and these are beyond what `shared` can provide.
Oct 18 2018
On Thursday, 18 October 2018 at 17:23:36 UTC, Stanislav Blinov wrote:On Thursday, 18 October 2018 at 17:10:03 UTC, aliak wrote:Right, but the argument is a shared int*, so from what I've understood... you can't do anything with it since it has no shared members. i.e. you can't read or write to it. No?Out of curiosity, when it comes to primitives, what could you do under MP in void "atomicInc(shared int*)" that would be problematic? void atomicInc(shared int*) { // i.e. what goes here? }1. Anything if int* implicitly converts to shared int* (per MP), because then that function is indeed unsafe.2. Only actual platform-specific implementation bugs otherwise, and these are beyond what `shared` can provide.
Oct 18 2018
On Thursday, 18 October 2018 at 18:05:51 UTC, aliak wrote:Right, but the argument is a shared int*, so from what I've understood... you can't do anything with it since it has no shared members. i.e. you can't read or write to it. No?Obviously the implementation would cast `shared` away, just like it would if it were Atomic!int. But for some reason, Manu thinks that latter is OK doing that, but former is voodoo. Go figure.
Oct 18 2018
On Thursday, 18 October 2018 at 18:12:03 UTC, Stanislav Blinov wrote:On Thursday, 18 October 2018 at 18:05:51 UTC, aliak wrote:Sounds like one is encapsulated within a box that carefully handles thread safety and makes promises with the API and the other is not. I don't think you can apply shared on a free function, i.e.: void increment(shared int*) shared; in which case increment would not, and cannot be a threadsafe api in Manu's world. So once you throw an Object in to shared land all you could do is call shared methods on it, and since they'd have been carefully written with sharing in mind... it does seem a lot more usable. On these two cases: increment(shared int* p1) { // I have no guarantees that protecting and accessing p1 will not cause problems // // but you don't have this guarantee in any world (current nor MP) because you can // never be sure that p1 was not cast from a mutable. } int* p2; increment(p2); // I have no guarantee that accessing p2 is safe anymore. // // But that would apply only if the author of increment was being unsafe. // and "increment" cannot be marked as shared.Right, but the argument is a shared int*, so from what I've understood... you can't do anything with it since it has no shared members. i.e. you can't read or write to it. No?Obviously the implementation would cast `shared` away, just like it would if it were Atomic!int. But for some reason, Manu thinks that latter is OK doing that, but former is voodoo. Go figure.
Oct 18 2018
On Thursday, 18 October 2018 at 21:51:52 UTC, aliak wrote:On Thursday, 18 October 2018 at 18:12:03 UTC, Stanislav Blinov wrote:Unit of "encapsulation" in D is either a module or a package, not a struct. Free functions are a very valid way of accessing "encapsulated" data.On Thursday, 18 October 2018 at 18:05:51 UTC, aliak wrote:Sounds like one is encapsulated within a box that carefullyRight, but the argument is a shared int*, so from what I've understood... you can't do anything with it since it has no shared members. i.e. you can't read or write to it. No?Obviously the implementation would cast `shared` away, just like it would if it were Atomic!int. But for some reason, Manu thinks that latter is OK doing that, but former is voodoo. Go figure.handles thread safety and makes promises with the API and the other is not.Nope. void foo(const T* x); makes a promise to not write through x. It assumes '*x' itself may not be const. void foo(shared T* x); makes a promise to threat '*x' in a thread-safe manner. But per MP, it *assumes* that '*x' is shared. And if it isn't, good luck finding that spot in your code.I don't think you can apply shared on a free function, i.e.: void increment(shared int*) shared; in which case increment would not, and cannot be a threadsafe api in Manu's world.Wrong. In Manu's "world", this is somehow considered "safe": void T_method_increment(ref shared T); ...because that is what a method is, while this: void increment(shared T*); void increment(ref shared T); ...is considered "unsafe" because reasons. Do you see the difference in signatures? I sure don't.So once you throw an Object in to shared land all you could do is call shared methods on it, and since they'd have been carefully written with sharing in mind... it does seem a lot more usable.Same goes with free functions.On these two cases: increment(shared int* p1) { // I have no guarantees that protecting and accessing p1 will not cause problems // // but you don't have this guarantee in any world (current nor MP) because you can // never be sure that p1 was not cast from a mutable. }Except that you *have to* *explicitly* cast it, which is: a) documentation b) greppable c) easily fails review for people not authorized to do soint* p2; increment(p2); // I have no guarantee that accessing p2 is safe anymore. // But that would apply only if the author of increment was being unsafe. // and "increment" cannot be marked as shared.No. *You*, the caller of an API (the "increment"), do not necessarily control that API. By allowing implicit conversion you waive all claims on your own data. In Manu's world, "increment" *assumes* you're doing the right thing. Yet at the same time, Manu happily talks about how only "experts" can do the right thing. How these two things co-exist in his world, I have no idea. The "have no guarantee" holds in both cases. Except case (1) would require actually checking what the hell you're doing before making a cast, while in case (2) you just blindly write unsafe code.
Oct 18 2018
On Thursday, 18 October 2018 at 16:31:02 UTC, Stanislav Blinov wrote:You contradict yourself and don't even notice it. Per your rules, the way to open that locked box is have shared methods that access data via casting. Also per your rules, there is absolutely no way for the programmer to control whether they're actually sharing the data. Therefore, some API can steal a shared reference without your approval, use that with your "safe" shared methods, while you're continuing to threat your data as not shared.Yes, and that's fine. Because it's thread-safe, remember?You and Manu both seem to think that methods allow you to "define a thread-safe interface". struct S { void foo() shared; } Per your rules, S.foo is thread-safe. It is here that I remind you, *again*, what S.foo actually looks like, given made-up easy-to-read mangling: void struct_S_foo(ref shared S); And yet, for some reason, you think that these are not thread-safe: void foo(shared int*); void bar(ref shared int);Again, no. No. No, no, no, no, no. We have not said that, we are not saying that, and we will not say that. Because it's not true, and I pointed out exactly this in a previous post. I have no idea where you got this idea, and I hope we can excise it. There is absolutely nothing wrong with void foo(shared int*). (apart from the fact it can't safely do anything) For clarity: the interface of a type is any method, function, delegate or otherwise that may affect its internals. That means any free function in the same module, and any non-private members.Your implementation of 'twaddle' is *unsafe*, because the compiler doesn't know that 'payload' is shared. For example, when inlining, it may reorder the calls in it and cause races or other UB. At least one of the reasons behind `shared` *was* to serve as compiler barrier.Ah, now this is a good point - thanks! That does seem like it's a harder problem than has come up thus far.This again? See point 2, above. I hope we can stop this silliness soon.Alright, so I have this shared object that I can't read from, and can't write to. It has no public shared members. What can I do with it? I can pass it to other guys, who also can't do anything with it. Are there other options?It can have any number of public shared "members" per UFCS. The fact that you forget is that there's no difference between a method and a free function, other than syntax sugar. Well, OK, there's guaranteed private access for methods, but same is true for module members.Again, this is good stuff. This is an actual example of what can go wrong. Thanks!Everything in D already presents a non-threadsafe interface. Things that you advocate included. struct S { void foo() shared; } That is not threadsafe. This *sort of* is: struct S { disable this(this); disable void opAssign(S); void foo() shared; } ...except not quite still. Now, if the compiler generated above in the presence of any `shared` members or methods, then we could begin talking about it being threadsafe. But that part is mysteriously missing from Manu's proposal, even though I keep reminding of this in what feels like every third post or so (I'm probably grossly exaggerating).You can't have thread-safe methods operating directly on primitives, because they already present a non-thread-safe interface. This is true. This follows naturally from the rules.Nothing follows naturally. The proposal doesn't talk at all about the fact that you can't have "methods" on primitives,The rest just follows naturally.Yup, hence 'shared' on a method meaning 'thread-safe'. So it's fine. I think this has been mentioned before.that you can't distinguish between shared and unshared data if that proposal is realized,And you can't do that currently either. Just like today, shared(T) means the T may or may not be shared with other thread. Nothing more, nothing less.I don't think it means what you think it means. "May or may not be shared with other thread" means "you MUST treat it as if it's shared with other thread". That's it.That's why automatic conversion doesn't make *any* sense, and that's why compiler error on attempting to pass over mutable as shared makes *perfect* sense.No. Because shared access is thread-safe.And when is this a problem? Again, anything that has shared access to something is incapable of doing anything non-thread-safe to it.You lose the ability to distinguish thread-local and shared data.that you absolutely destroy D's TLS-by-default treatment...I'm unsure what you mean by this.No. Again, point 2. Nobody says this.Uh-huh, only due to some weird convention that "methods" are somehow safer than free functions. Which they're not.Functions that you must not be allowed to write per this same proposal. How quaint.What? Which functions can't I write?No, void atomicInc(shared int*) is perfectly safe, as long as it doesn't cast away shared. Again, the problem is int already has a non-thread-safe interface, which Atomic!int doesn't. And once more, for clarity, this interface includes any function that has access to its private members, free function, method, delegate return value from a function/method, what have you. Since D's unit of encapsulation is the module, this has to be the case. For int, the members of that interface include all operators. For pointers, it includes deref and pointer arithmetic. For arrays indexing, slicing, access to .ptr, etc. None of these lists are necessarily complete.Yup, this is correct. But wrap it in a struct, like e.g. Atomic!int, and everything's hunky-dory.So again, void atomicInc(shared int*); // is "not safe", but void struct_Atomic_int_opUnary_plus_plus(ref shared Atomic); // is "safe"Again, point 2. I think we have been remiss in the explanation of what we consider the interface.I have no idea where I or Manu have said you can't make functions that take shared(T)*.Because you keep saying they're unsafe and that you should wrap them up in a struct for no other reason than just "because methods are kosher".For the public members to be thread-safe, yes, the private parts must be thread-safe. That's always the case. If I'm going to build a skyscraper, I will not make the foundation out of cardboard. Now, Two very good points came up in this post, and I think it's worth stating them again, because they do present possible issues with MP: 1) How does MP deal with reorderings in non-shared methods? I don't know. I'd hide behind 'that's for the type implementor to handle', but it's a subtle enough problem that I'm not happy with that answer. 2) What about default members like opAssign and postblit? The obvious solution is for the compiler to not generate these when a type has a shared method or is taken as shared by a free function in the same module. I don't like the latter part of that, but it should work. -- SimenLet's say it together: for a type to be thread-safe, all of its public members must be written in a thread-safe way.It's shared private parts also must be written in a thread-safe way. Yes, they're private, but they still may be shared. Welcome to the communism of multithreading.
Oct 18 2018
On Thursday, 18 October 2018 at 22:08:14 UTC, Simen Kjærås wrote:On Thursday, 18 October 2018 at 16:31:02 UTC, Stanislav BlinovNow, if the compiler generated above in the presence of any `shared` members or methods, then we could begin talking about it being threadsafe...Again, this is good stuff. This is an actual example of what can go wrong. Thanks!You're welcome.No, void atomicInc(shared int*) is perfectly safe, as long as it doesn't cast away shared.Eh? If you can't read or write to your shared members, how *do* you implement your "safe" shared methods without casting away shared? Magic?!Again, the problem is int already has a non-thread-safe interface, which Atomic!int doesn't.*All* structs and classes have a non-thread-safe interface, as I have demonstrated, and thankfully you agree with that. But *there is no mention of it* in the OP, nor there was *any recognition* of it up to this point. When I asked about copying, assignment and destructors in the previous thread (thread, not post), Manu quite happily proclaimed those were fine given some arbitrary conditions. Yet those are quite obviously *not fine*, especially *if* you want to have a "safe" implicit conversion. *That* prompted me to assume that Manu didn't actually think long and hard about his proposal and what it implies. Without recognizing those issues: struct S { private int x; void foo() shared; } void shareWithThread(shared S* s); auto s = make!S; // or new, whatever shareWithThread(&s); // Manu's implicit conversion // 10 kLOC below, written by some other guy 2 years later: *s = S.init; ^ that is a *terrible*, and un-greppable BUG. How fast would you spot that in a review? Pretty fast if you saw or wrote the other code yesterday. A week later? A month? A year?.. Again, that is assuming *only* what I'm *certain about* in Manu's proposal, not something he or you assumed but didn't mention.And once more, for clarity, this interface includes any function that has access to its private members, free function, method, delegate return value from a function/method, what have you. Since D's unit of encapsulation is the module, this has to be the case. For int, the members of that interface include all operators. For pointers, it includes deref and pointer arithmetic. For arrays indexing, slicing, access to .ptr, etc. None of these lists are necessarily complete.Aight, now, *now* I can perhaps try to reason about this from your point of view. Still, I would need some experimentation to see if such approach could actually work. And that would mean digging out old non-`shared`-aware code and performing some... dubious... activities.I have no idea where I or Manu have said you can't make functions that take shared(T)*.Because that was the only way to reason about your interpretations of various examples until you said this:I think we have been remiss in the explanation of what we consider the interface. For clarity: the interface of a type is any method, function, delegate or otherwise that may affect its internals. That means any free function in the same module, and any non-private members.Now compare that to what is stated in the OP and correlate with what I'm saying, you might understand where my opposition comes from.Now, Two very good points came up in this post, and I think it's worth stating them again, because they do present possible issues with MP: 1) How does MP deal with reorderings in non-shared methods? I don't know. I'd hide behind 'that's for the type implementor to handle', but it's a subtle enough problem that I'm not happy with that answer. 2) What about default members like opAssign and postblit? The obvious solution is for the compiler to not generate these when a type has a shared method or is taken as shared by a free function in the same module. I don't like the latter part of that, but it should work.Something I didn't yet stress about (I think only mentioned briefly somewhere) is, sigh, destructors. Right now, `shared` allows you to either have a `~this()` or a `~this() shared`, but not both. In my mind, `~this() shared` is an abomination. One should either: 1) have data that starts life shared (a global, or e.g. new shared(T)), and simply MUST NOT have a destructor. Such data is ownerless, or you can say that everybody owns it. Therefore there's no deterministic way of knowing whether or not or when to call the destructor. You can think of it as an analogy with current stance on finalizers with GC. 2) have data that starts life locally (e.g. it's not declared `shared`, but converted later). Such types MAY have a destructor, because they always have a cleanly defined owner: whoever holds the non-`shared` reference (recall that copying MUST be *disabled* for any shared-aware type). But that destructor MUST NOT be `shared`. Consequently, types such as these: shared struct S { /* ... */ } MUST NOT define a destructor, either explicitly, or implicitly through members, i.e. it's a compile error if an __xdtor needs to be generated for such type. However, in practice this would mean that practically all types couldn't have a destructor: struct S { private shared X x; // X must not have a destructor, even though S can?.. } Perhaps, an exception to (1) above could be made for such cases, but I'm too tired to think about wording at the moment. Also, the proposal has no mention of interaction of `shared` and the GC, which AFAIK is also missing pretty much everywhere you can even get some information on current state of `shared`. This *needs* to be addressed.
Oct 18 2018
On Thu, Oct 18, 2018 at 3:10 PM Simen Kjærås via Digitalmars-d <digitalmars-d puremagic.com> wrote:Now, Two very good points came up in this post, and I think it's worth stating them again, because they do present possible issues with MP:It is easy to respond to these.1) How does MP deal with reorderings in non-shared methods? I don't know. I'd hide behind 'that's for the type implementor to handle', but it's a subtle enough problem that I'm not happy with that answer.This is a red-herring. Opaque function calls are never reordered. If they are inlined, the compiler has full visibility to the internal machinery present. If you call one function that performs an atomic op, then another that performs an atomic op, it is impossible for the CPU to reorder atomic op's around eachother, that would defeat the entire point of hardware atomic operations. If the functions used mutexes, or semaphores, exactly the same applies; they are either function calls (not reordered), or they use hardware primitives, which have scheduling fences. If none of those things are present in the threadsafe functions, then the functions aren't threadsafe in the first place! In short, he made up this issue, it doesn't exist.2) What about default members like opAssign and postblit? The obvious solution is for the compiler to not generate these when a type has a shared method or is taken as shared by a free function in the same module. I don't like the latter part of that, but it should work.These aren't issues either. There's nothing wrong with atomic assignment; you just have to implement an atomic assignment. Postblit is being replaced with copy-ctor's and `shared` is one of the explicit reasons why! Copy-ctor's are also fine, it would express an atomic assignment. This is just hot air, and only strengthen my conviction.
Oct 18 2018
On Friday, 19 October 2018 at 01:22:53 UTC, Manu wrote:On Thu, Oct 18, 2018 at 3:10 PM Simen Kjærås via Digitalmars-d <digitalmars-d puremagic.com> wrote:You don't say?.. And what, exactly, stops the optimizer from removing "unnecessary" reads or rearranging them with stores, given the original code, which, if you freaking read it, you'd see there's no indication that it's not allowed to do so.Now, Two very good points came up in this post, and I think it's worth stating them again, because they do present possible issues with MP:It is easy to respond to these.1) How does MP deal with reorderings in non-shared methods? I don't know. I'd hide behind 'that's for the type implementor to handle', but it's a subtle enough problem that I'm not happy with that answer.This is a red-herring. Opaque function calls are never reordered. If they are inlined, the compiler has full visibility to the internal machinery present.If you call one function that performs an atomic op, then another that performs an atomic op, it is impossible for the CPU to reorder atomic op's around eachother, that would defeat the entire point of hardware atomic operations.I'm not talking about CPU reordering at all. I'm talking about the optimizer.In short, he made up this issue, it doesn't exist.Yeeees, of course I have. What else have I made up, can you tell? You know what doesn't exist though? Even one example of a useful implicit conversion form mutable to shared from you. Not even one.You just haven't read the code. Those members aren't even `shared`. The *least* you can do is disable them *if* you're going to cast your variable to `shared`. Otherwise your "interface" remains non-threadsafe.2) What about default members like opAssign and postblit? The obvious solution is for the compiler to not generate these when a type has a shared method or is taken as shared by a free function in the same module. I don't like the latter part of that, but it should work.These aren't issues either. There's nothing wrong with atomic assignment; you just have to implement an atomic assignment.Postblit is being replaced with copy-ctor's and `shared` is one of the explicit reasons why! Copy-ctor's are also fine, it would express an atomic assignment.And this strengthens *my* belief that you haven't at all thought about this. There is literally *no* purpose for any `shared` types to have any copy-ctors. The only feasible copy primitives are from shared to local and from local to shared. Not to mention that again, to even talk about your "implicit" conversions, you must first think about what can happen to the *owned* (non-`shared`) reference after the conversion. Hint: you can't copy it. You can't assign *to it*. Not via default-generated postblits and opAssigns, which are not, and can not, be "atomic". I'm fully aware about postblits being "replaced" by copy-ctors, I'm also fully aware how "much" thought was put into that wrt. `shared`.This is just hot air, and only strengthen my conviction.You know what, I'm fed up with you too. Just show me one, *one* non-contrived example of useful implicit conversion from mutable to shared. So far you haven't produced *any at all*. Then we can talk about what is hot air here. Produce, or drop this presumptious crap.
Oct 18 2018
On Thu, Oct 18, 2018 at 6:50 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Friday, 19 October 2018 at 01:22:53 UTC, Manu wrote:You are an obscene person. I'm out. You win.On Thu, Oct 18, 2018 at 3:10 PM Simen Kjærås via Digitalmars-d <digitalmars-d puremagic.com> wrote:You don't say?.. And what, exactly, stops the optimizer from removing "unnecessary" reads or rearranging them with stores, given the original code, which, if you freaking read it, you'd see there's no indication that it's not allowed to do so.Now, Two very good points came up in this post, and I think it's worth stating them again, because they do present possible issues with MP:It is easy to respond to these.1) How does MP deal with reorderings in non-shared methods? I don't know. I'd hide behind 'that's for the type implementor to handle', but it's a subtle enough problem that I'm not happy with that answer.This is a red-herring. Opaque function calls are never reordered. If they are inlined, the compiler has full visibility to the internal machinery present.If you call one function that performs an atomic op, then another that performs an atomic op, it is impossible for the CPU to reorder atomic op's around eachother, that would defeat the entire point of hardware atomic operations.I'm not talking about CPU reordering at all. I'm talking about the optimizer.In short, he made up this issue, it doesn't exist.Yeeees, of course I have. What else have I made up, can you tell? You know what doesn't exist though? Even one example of a useful implicit conversion form mutable to shared from you. Not even one.You just haven't read the code. Those members aren't even `shared`. The *least* you can do is disable them *if* you're going to cast your variable to `shared`. Otherwise your "interface" remains non-threadsafe.2) What about default members like opAssign and postblit? The obvious solution is for the compiler to not generate these when a type has a shared method or is taken as shared by a free function in the same module. I don't like the latter part of that, but it should work.These aren't issues either. There's nothing wrong with atomic assignment; you just have to implement an atomic assignment.Postblit is being replaced with copy-ctor's and `shared` is one of the explicit reasons why! Copy-ctor's are also fine, it would express an atomic assignment.And this strengthens *my* belief that you haven't at all thought about this. There is literally *no* purpose for any `shared` types to have any copy-ctors. The only feasible copy primitives are from shared to local and from local to shared. Not to mention that again, to even talk about your "implicit" conversions, you must first think about what can happen to the *owned* (non-`shared`) reference after the conversion. Hint: you can't copy it. You can't assign *to it*. Not via default-generated postblits and opAssigns, which are not, and can not, be "atomic". I'm fully aware about postblits being "replaced" by copy-ctors, I'm also fully aware how "much" thought was put into that wrt. `shared`.This is just hot air, and only strengthen my conviction.You know what, I'm fed up with you too. Just show me one, *one* non-contrived example of useful implicit conversion from mutable to shared. So far you haven't produced *any at all*. Then we can talk about what is hot air here. Produce, or drop this presumptious crap.
Oct 18 2018
On Friday, 19 October 2018 at 01:53:00 UTC, Manu wrote:This is a red-herring. In short, he made up this issue, it doesn't exist. This is just hot air, and only strengthen my conviction.Produce, or drop this presumptious crap.You are an obscene person. I'm out.Oooh, I'm soooorry, come baack! Really though, what is it that you wanted to achieve here? You ask for counter-arguments, are given them on *17 pages already*, are asked numerous times to actually demonstrate the value of a small contained portion of your proposal, and all you do is shrug this all off just because you presume to "know better", and on top of that have the audacity to call someone else *obscene*? Wow... just... wow!You win.I didn't know it was a contest.
Oct 18 2018
On Thu., 18 Oct. 2018, 7:10 pm Stanislav Blinov via Digitalmars-d, < digitalmars-d puremagic.com> wrote:On Friday, 19 October 2018 at 01:53:00 UTC, Manu wrote:I've given use cases constantly, about taking object ownership, promotions, and distribution for periods (think parallel for), I can achieve all my goals with full safety, absolutely no casts in user code, and I have infrastructure in production that applies these patterns successfully. It's worth pursuing. I've spent years thinking on this, I'm trying to move the needle on this issue for the first time in over a decade at least, and you have violently opposed, in principle, from the very first post, and make no effort to actually understand the proposition. It's clearly a contest from your insurance that my proposal in worthless in every single post you've made. You want me to admit defeat and desist. Fuck you. You win. I don't have the time or energy to argue against a wall. You are obscene, you're complete unproductive, and destructive from no apparent reason. I hope you continue to love shared, just the way it is... useless.This is a red-herring. In short, he made up this issue, it doesn't exist. This is just hot air, and only strengthen my conviction.Produce, or drop this presumptious crap.You are an obscene person. I'm out.Oooh, I'm soooorry, come baack! Really though, what is it that you wanted to achieve here? You ask for counter-arguments, are given them on *17 pages already*, are asked numerous times to actually demonstrate the value of a small contained portion of your proposal, and all you do is shrug this all off just because you presume to "know better", and on top of that have the audacity to call someone else *obscene*? Wow... just... wow!You win.I didn't know it was a contest.
Oct 18 2018
On Friday, 19 October 2018 at 02:20:22 UTC, Manu wrote:I've given use cases constantly, about taking object ownership, promotions, and distribution for periods (think parallel for),Manu, you haven't shown *any* code in which conversion from mutable to shared, an *implicit* one at that, was even present, let alone useful. While at the same time blatantly dismissing *all* examples to the contrary as "bad code" or any other reason, especially the "you just don't understand". How do you expect us to understand if all you do is talk about how it's useful, but don't explain why? You can talk and brag all you want, but until you do show at least one example, I'm sorry but you have no case. And what's this all about? A *small*, localized portion of your proposal, that isn't event the source of the problems you're talking about in the current state of affairs. In all examples flying about in this thread, all of *required* casts were *the opposite*, from shared to mutable, and you yourself acknowledge that those are indeed required on the lowest level. As I've said numerous times, from all that's been said and implied in this thread, the *only* use of implicit casting that comes to mind is avoiding writing some forwarding methods, that's about it. I honestly can't see how this is valuable to warrant such a drastic change in the language. Now maybe it's that I'm dumb, then just say so already and we'll all move on. Just don't act like you're the only one who knows it all and everyone else is an unwashed pleb clinging to worshiping the faces in the trees.I can achieve all my goals with full safety, absolutely no casts in user code, and I have infrastructure in production that applies these patterns successfully. It's worth pursuing.Okay... Then please do show *one* example of useful implicit conversion from mutable to shared.I've spent years thinking on this, I'm trying to move the needle on this issue for the first time in over a decade at least,And you're behaving in this thread as if everyone else, myself included, were sitting on our thumbs counting flies for this same decade.and you have violently opposed, in principle, from the very first post, and make no effort to actually understand the proposition.I haven't done such a thing. I have asked you, numerous times, one, and only one question, and you never so much as replied to *that question*. What is the big value of this implicit conversion that it would warrant changing current language rules regarding type conversions?It's clearly a contest from your insurance that my proposal in worthless in every single post you've made. You want me to admit defeat and desist.And here you are, continuing to presume. Now you suddenly know what I want. Marvelous. I *don't* want you to admit any defeat, *or* desist. I *want* you to succeed. I *want* to help make `shared` useful so that I and everyone else can actually start writing code with it, not in spite of it. I've agreed with you on pretty much everything in your proposal, *except one thing*. I want you to demonstrate the practical value of *that thing*, and it's benefits over the current state of affairs, and I asked you several times to explain to us mere unwashed mortals exactly how it's useful. What have we been doing for 17 pages? Discussing the benefits of disabling reads/writes on shared? No. Estimating how much existing code could go to trash, what parts of DRuntime/Phobos would need a rewrite? No. We were in constant back-and-forth of "But... nope... but... nope" about this implicit conversion, which you value so much yet for some reason fail to defend. Saying "I had good time with it" is not a very practical defense, not for a language construct anyway.Fuck you. You win. I don't have the time or energy to argue against a wall.If you ask to destroy, be prepared for a fight. Or don't ask. Just stop appealing to your own authority.You are obscene, you're complete unproductive, and destructive from no apparent reason.Give it all you've got, please. Let it all out all at once.I hope you continue to love shared, just the way it is... useless.Yet another presumption. Good on you.
Oct 18 2018
On Friday, 19 October 2018 at 02:20:22 UTC, Manu wrote:On Thu., 18 Oct. 2018, 7:10 pm Stanislav Blinov via Digitalmars-d, < digitalmars-d puremagic.com> wrote:There's another way; Stanislav isn't one you need to convince so if that particular discussion is unproductive and disruptive just ignore it. I.e technical discussions should be robust but once they become personal just ignore that input and move on. Isn't always possible I know but in this case I reckon you can. Convincing Walter, Andrei and the rest of the core dev team of course will require a DIP. Keep going on this, it is the first hint of movement with shared since like foreva! bye, norm bye, NormOn Friday, 19 October 2018 at 01:53:00 UTC, Manu wrote:I've given use cases constantly, about taking object ownership, promotions, and distribution for periods (think parallel for), I can achieve all my goals with full safety, absolutely no casts in user code, and I have infrastructure in production that applies these patterns successfully. It's worth pursuing. I've spent years thinking on this, I'm trying to move the needle on this issue for the first time in over a decade at least, and you have violently opposed, in principle, from the very first post, and make no effort to actually understand the proposition. It's clearly a contest from your insurance that my proposal in worthless in every single post you've made. You want me to admit defeat and desist. Fuck you. You win. I don't have the time or energy to argue against a wall. You are obscene, you're complete unproductive, and destructive from no apparent reason. I hope you continue to love shared, just the way it is... useless.This is a red-herring. In short, he made up this issue, it doesn't exist. This is just hot air, and only strengthen my conviction.Produce, or drop this presumptious crap.You are an obscene person. I'm out.Oooh, I'm soooorry, come baack! Really though, what is it that you wanted to achieve here? You ask for counter-arguments, are given them on *17 pages already*, are asked numerous times to actually demonstrate the value of a small contained portion of your proposal, and all you do is shrug this all off just because you presume to "know better", and on top of that have the audacity to call someone else *obscene*? Wow... just... wow!You win.I didn't know it was a contest.
Oct 18 2018
On 19/10/2018 7:09 PM, Norm wrote:There's another way; Stanislav isn't one you need to convince so if that particular discussion is unproductive and disruptive just ignore it. I.e technical discussions should be robust but once they become personal just ignore that input and move on. Isn't always possible I know but in this case I reckon you can.Trust me, I think a few of us at least have already figured that out.Convincing Walter, Andrei and the rest of the core dev team of course will require a DIP. Keep going on this, it is the first hint of movement with shared since like foreva!As long as it doesn't look like my idea[0] (Andrei doesn't like it, I may have asked) it should have some sort of legs. [0] https://github.com/rikkimax/DIPs/blob/shared/DIPs/DIP1xxx-RC2.md
Oct 18 2018
On Friday, 19 October 2018 at 06:25:00 UTC, rikki cattermole wrote:On 19/10/2018 7:09 PM, Norm wrote:[0] https://github.com/rikkimax/DIPs/blob/shared/DIPs/DIP1xxx-RC2.mdThis document provide no reasoning about what usecases it supports: Is it possible to create objects that are shared just for short periods during their livetime and guarantee that they can be used threadsave like Manu want it to be? Does it prohibit misuse any better than Manus proposal (that requires the "Expert" to implement all theadsave API)? Is the "normal" User still enforced to do some unsave casts? Has the normal User to have high knownledge of how a threadsave API is to be used or can the compiler provide any guarantees that using them can only fail if the implementation behind the API has bugs (e.g. provide some encapsulation)? Or any other usecases why and how this design is better than what we have now? And also some ideas how to implement some useacases (examples) are completely missing.
Oct 19 2018
On Fri., 19 Oct. 2018, 6:10 am Dominikus Dittes Scherkl via Digitalmars-d, < digitalmars-d puremagic.com> wrote:On Friday, 19 October 2018 at 06:25:00 UTC, rikki cattermole wrote:No, a key misunderstanding. My proposal is safe. The only thing an expert must do is write the few trusted implementations that live at the very bottom of the stack. That would always be in a lib. When was the last time you rewrote std::map because you thought you could do better? The whole stack from there on up (the user stack) is safe, and you can have confidence in the safe-ty. My goal is to make it safe, clearly communicate how a user interact with the API, and mechanically confirm that users do the right stuff. My proposal is specifically structured to not require *any* unsafe interactions at the user level. Only core machinery that is trusted needs expert attention. I don't think it's possible to invent a proposal with a higher degree of verifiable safety.On 19/10/2018 7:09 PM, Norm wrote:[0] https://github.com/rikkimax/DIPs/blob/shared/DIPs/DIP1xxx-RC2.mdThis document provide no reasoning about what usecases it supports: Is it possible to create objects that are shared just for short periods during their livetime and guarantee that they can be used threadsave like Manu want it to be? Does it prohibit misuse any better than Manus proposal (that requires the "Expert" to implement all theadsave API)?
Oct 19 2018
On 20/10/2018 2:07 AM, Dominikus Dittes Scherkl wrote:This document provide no reasoning about what usecases it supports:It was a basic idea of mine... It was never meant to be PR'd.
Oct 19 2018
On Wednesday, 17 October 2018 at 23:12:48 UTC, Manu wrote:On Wed, Oct 17, 2018 at 2:15 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:Rather than answering all objections in this thread in detail, a mistake I've made before too, I suggest you put up a 3-5 page document somewhere explaining your proposal in detail. You can use the feedback here as a guide on what to explain more and what not to. Be sure to include code samples of how you see everything ultimately working in your external document. Two mistakes you may be making in writing responses in this thread, which I've made before too: 1. Assuming people read anything more than fragments of what you're writing. My experience suggests people just read bits and pieces quickly, then compose them together in the way that makes most sense to them based on how _past_ technology works. 2. Assuming people have any idea how the underlying implementation of shared and multi-threading works. I know little to nothing about how shared systems work and how that interacts with type systems, which is why I haven't responded to your proposal, but that doesn't stop others from responding who may not know much more. Neither of these will be solved by writing an external document, but at least Walter and others more seriously interested will benefit from a better-motivated explanation with more detail. I won't read it, ;) but I think we can avoid another long thread like this if you go this route. I'm planning the same approach with the technical topic I raised before, along with working code.On Wednesday, 17 October 2018 at 19:25:33 UTC, Manu wrote:In this case, with respect to the context (a single int) atomicInc() is ALWAYS safe, even with implicit conversion. You can atomicInc() a thread-local int perfectly safely.On Wed, Oct 17, 2018 at 12:05 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:Only if implicit conversion is allowed. If it isn't, that's likely trusted, and this: void atomicInc(ref shared int); can even be safe.On Wednesday, 17 October 2018 at 18:46:18 UTC, Manu wrote:This function is effectively an intrinsic. It's unsafe by definition.I've said this a bunch of times, there are 2 rules: 1. shared inhibits read and write access to members 2. `shared` methods must be threadsafeOh God... void atomicInc(shared int* i) { /* ... */ } Now what? There are no "methods" for ints, only UFCS. Those functions can be as safe as you like, but if you allow implicit promotion of int* to shared int*, you *allow implicit races*.From there, shared becomes interesting and useful.It's not, atomicInc() of an int is always safe with respect to the int itself. You can call atomicInc() on an unshared int and it's perfectly fine, but now you need to consider context, and that's a problem for the design of the higher-level scope. To maintain thread-safety, the int in question must be appropriately contained. The problem is that the same as the example I presented before, which I'll repeat: struct InvalidProgram { int x; void fun() { ++x; } void gun() shared { atomicInc(&x); } } The method gun() (and therefore the whole object) is NOT threadsafe by my definition, because fun() violates the threadsafety of gun(). The situation applies equally here that: int x; atomicInc(&x); ++x; // <- by my definition, this 'API' (increment an int) violates the threadsafety of atomicInc(), and atomicInc() is therefore not threadsafe. `int` doesn't present a threadsafe API, so int is by definition, NOT threadsafe. atomicInc() should be system, and not trusted. If you intend to share an int, use Atomic!int, because it has a threadsafe API. atomicInc(shared int*) is effectively just an unsafe intrinsic, and its only use is at ground-level implementation of threadsafe machinery, like malloc() and free().It's a tool for implementing threadsafe machinery. No user can just start doing atomic operations on random ints and say "it's threadsafe", you must encapsulate the threadsafe functionality into some sort of object that aggregates all concerns and presents an intellectually sound api.Threadsafety starts and ends with the programmer. By your logic *all* functions operating on `shared` are unsafe then. As far as compiler is concerned, there would be no difference between these two: struct S {} void atomicInc(ref shared S); and struct S { void atomicInc() shared { /* ... */ } } The signatures of those two functions are exactly the same. How is that different from a function taking a shared int pointer or reference?void *p2 = p; free(p); p2.crash();Let me try one: void free(void*) { ... } Now what? I might have dangling pointers... it's a catastrophe!One could argue that it should be void free(ref void* p) { /* ... */ p = null; }As a matter of fact, in my own allocators memory blocks allocated by them are passed by value and are non-copyable, they're not just void[] as in std.experimental.allocator. One must 'move' them to pass ownership, and that includes deallocation. But that's another story altogether.Right, now you're talking about move semantics to implement transfer of ownership... you might recall I was arguing this exact case to express transferring ownership of objects between threads earlier. This talk of blunt casts and "making sure everything is good" is all just great, but it doesn't mean anything interesting with respect to `shared`. It should be interesting even without unsafe casts.You have written an invalid program. I can think of an infinite number of ways to write an invalid program. In this case, don't have an `int`, instead, have an Atomic!int; you now guarantee appropriate access, problem solved! If you do have an int, don't pass it to other threads at random when you don't have any idea what they intend to do with it! That's basic common sense. You don't pass a pointer to a function if you don't know what it does with the pointer!It's essentially the same argument. This isn't a function that professes to do something that people might misunderstand and try to use in an unsafe way, it's a low-level implementation device, which is used to build larger *useful* constructs.You're missing the point, again. You have an int. You pass a pointer to it to some API that takes an int*. You continue to use your int as just an int.The API changes, and now the function you called previously takes a shared int*. Implicit conversion works, everything compiles, you have a race. Now, that's of course an extremely stupid scenario.Yes.The point is: the caller of some API *must* assert that they indeed pass shared data. It's insufficient for the API alone to "promise" taking shared data. That's the difference with promotion to `const`.The caller doesn't care if it's true that the callee can't do anything with it that's unsafe anyway. We effect that state by removing all non-threadsafe access.
Oct 17 2018
On 10/17/18 2:46 PM, Manu wrote:On Wed, Oct 17, 2018 at 10:30 AM Steven Schveighoffer viaIt's assumed that shared int pointer can be passed to another thread, right? Do I have to write a full program to demonstrate?What the example demonstrates is that while you are trying to disallow implicit casting of a shared pointer to an unshared pointer, you have inadvertently allowed it by leaving behind an unshared pointer that is the same thing.This doesn't make sense... you're showing a thread-local program. The thread owning the unshared pointer is entitled to the unshared pointer. It can make as many copies at it likes. They are all thread-local.There's only one owning thread, and you can't violate that without unsafe casts.The what is the point of shared? Like why would you share data that NOBODY CAN USE? At SOME POINT, shared data needs to be readable and writable. Any correct system is going to dictate how that works. It's a good start to make shared data unusable unless you cast. But then to make it implicitly castable from unshared defeats the whole purpose.What seems to be a mystery here is how one is to actually manipulate shared data. If it's not usable as shared data, how does one use it?In order for a datum to be safely shared, it must be accessed with synchronization or atomics by ALL parties.** Absolutely **If you have one party that can simply change it without those, you will get races.*** THIS IS NOT WHAT I'M PROPOSING *** I've explained it a few times now, but people aren't reading what I actually write, and just assume based on what shared already does that they know what I'm suggesting. You need to eject all presumptions from your mind, take the rules I offer as verbatim, and do thought experiments from there.It's not misrepresentation, I'm trying to fill in the holes with the only logical possibilities I can think of.That's why shared/unshared is more akin to mutable/immutable than mutable/const.Only if you misrepresent my suggestion.No, not at all. Somehow one must manipulate shared data. If shared data cannot be read or written, there is no reason to share it. So LOGICALLY, we have to assume, yes there actually IS a way to manipulate shared data through these very carefully constructed and guarded things.It's true that only one thread will have thread-local access. It's not valid any more than having one mutable alias to immutable data.And this is why the immutable analogy is invalid. It's like const. shared offers restricted access (like const), not a different class of thing.There is one thread with thread-local access, and many threads with shared access. If a shared (threadsafe) method can be defeated by threadlocal access, then it's **not threadsafe**, and the program is invalid. struct NotThreadsafe { int x; void local() { ++x; // <- invalidates the method below, you violate the other function's `shared` promise } void notThreadsafe() shared { atomicIncrement(&x); } }So the above program is invalid. Is it compilable with your added allowance of implicit casting to shared? If it's not compilable, why not? If it is compilable, how in the hell does your proposal help anything? I get the exact behavior today without any changes (except today, I need to explicitly cast, which puts the onus on me).struct Atomic(T) { void opUnary(string op : "++")() shared { atomicIncrement(&val); } private T val; } struct Threadsafe { Atomic!int x; void local() { ++x; } void threadsafe() shared { ++x; } } Naturally, local() is redundant, and it's perfectly fine for a thread-local to call threadsafe() via implicit conversion.In this case, yes. But that's not because of anything the compiler can prove. How does Atomic work? I thought shared data was not usable? I'm being pedantic because every time I say "well at some point you must be able to modify things", you explode. Complete the sentence: "In order to read or write shared data, you have to ..."Here's another one, where only a subset of the object is modeled to be threadsafe (this is particularly interesting to me): struct Threadsafe { int x; Atomic!int y; void notThreadsafe() { ++x; ++y; } void threadsafe() shared { ++y; } } In these examples, the thread-local function *does not* undermine the threadsafety of threadsafe(), it MUST NOT undermine the threadsafety of threadsafe(), or else threadsafe() **IS NOT THREADSAFE**. In the second example, you can see how it's possible and useful to do thread-local work without invalidating the objects threadsafety commitments. I've said this a bunch of times, there are 2 rules: 1. shared inhibits read and write access to members 2. `shared` methods must be threadsafeGiven rule 1, how does Atomic!int actually work, if it can't read or write shared members? For rule 2, how does the compiler actually prove this? Any programming by convention, we can do today. We can implement Atomic!int with the current compiler, using unsafe casts inside trusted blocks. -SteveFrom there, shared becomes interesting and useful.
Oct 17 2018
On Wed, Oct 17, 2018 at 12:35 PM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/17/18 2:46 PM, Manu wrote:And that shared(int)* provides no access. No other thread with that pointer can do anything with it.On Wed, Oct 17, 2018 at 10:30 AM Steven Schveighoffer viaIt's assumed that shared int pointer can be passed to another thread, right? Do I have to write a full program to demonstrate?What the example demonstrates is that while you are trying to disallow implicit casting of a shared pointer to an unshared pointer, you have inadvertently allowed it by leaving behind an unshared pointer that is the same thing.This doesn't make sense... you're showing a thread-local program. The thread owning the unshared pointer is entitled to the unshared pointer. It can make as many copies at it likes. They are all thread-local.You can call shared methods. They promise threadsafety. That's a small subset of the program, but that's natural; only a very small subset of the program is safe to be called from a shared context. In addition, traditional unsafe interactions which may involve acquiring locks and doing casts remain exactly the same, and the exact same design patterns must apply which assure that the object is handled correctly. I'm not suggesting any changes that affect that workflow.There's only one owning thread, and you can't violate that without unsafe casts.The what is the point of shared? Like why would you share data that NOBODY CAN USE?At SOME POINT, shared data needs to be readable and writable. Any correct system is going to dictate how that works. It's a good start to make shared data unusable unless you cast. But then to make it implicitly castable from unshared defeats the whole purpose.No. No casting! This is antiquated workflow.. I'm not trying to take it away from you, but it's not an interesting model for the future. `shared` can model more than just that. You can call threadsafe methods. Shared methods explicitly dictate how the system works, and in a very clear and obvious/intuitive way. The implicit cast makes using threadsafe objects more convenient when you only have one, which is extremely common.Call shared methods. It's not like I haven't been saying this in every post since my OP. The only possible thing that you can safely do with a shared object is call a method that has been carefully designed for thread-safe calling. Any other access is invalid under any circumstance.What seems to be a mystery here is how one is to actually manipulate shared data. If it's not usable as shared data, how does one use it?In order for a datum to be safely shared, it must be accessed with synchronization or atomics by ALL parties.** Absolutely **If you have one party that can simply change it without those, you will get races.*** THIS IS NOT WHAT I'M PROPOSING *** I've explained it a few times now, but people aren't reading what I actually write, and just assume based on what shared already does that they know what I'm suggesting. You need to eject all presumptions from your mind, take the rules I offer as verbatim, and do thought experiments from there.Call shared methods.No, not at all. Somehow one must manipulate shared data. If shared data cannot be read or written, there is no reason to share it.It's true that only one thread will have thread-local access. It's not valid any more than having one mutable alias to immutable data.And this is why the immutable analogy is invalid. It's like const. shared offers restricted access (like const), not a different class of thing.So LOGICALLY, we have to assume, yes there actually IS a way to manipulate shared data through these very carefully constructed and guarded things.Yes, call the methods, they were carefully constructed to be threadsafe. Only functions that have made a promise to be threadsafe and implemented that complexity are valid interactions.All my examples assume my implicit conversion rule. But regardless, under my proposal, the above program is invalid. This violates the threadsafe promise.There is one thread with thread-local access, and many threads with shared access. If a shared (threadsafe) method can be defeated by threadlocal access, then it's **not threadsafe**, and the program is invalid. struct NotThreadsafe { int x; void local() { ++x; // <- invalidates the method below, you violate the other function's `shared` promise } void notThreadsafe() shared { atomicIncrement(&x); } }So the above program is invalid. Is it compilable with your added allowance of implicit casting to shared? If it's not compilable, why not?If it is compilable, how in the hell does your proposal help anything? I get the exact behavior today without any changes (except today, I need to explicitly cast, which puts the onus on me).My proposal doesn't help this program, it's invalid. I'm just demonstrating what an invalid program looks like.The compiler can't 'prove' anything at all related to threadsafety. We need to make one assumption; that `shared` methods are expected to be threadsafe, and from there the compiler can prove correctness with respect to that assumption. In this case, `Atomic.opUnary("++")` promises that it's threadsafe, and as such, the aggregate can safely use that tool to implement higher-level logic.struct Atomic(T) { void opUnary(string op : "++")() shared { atomicIncrement(&val); } private T val; } struct Threadsafe { Atomic!int x; void local() { ++x; } void threadsafe() shared { ++x; } } Naturally, local() is redundant, and it's perfectly fine for a thread-local to call threadsafe() via implicit conversion.In this case, yes. But that's not because of anything the compiler can prove.How does Atomic work? I thought shared data was not usable? I'm being pedantic because every time I say "well at some point you must be able to modify things", you explode.Atomic implements a safe utility using unsafe primitives (atomic increment intrinsic). Atomic wraps the unsafe call to an intrinsic into a box that's safe, and can be used by clients. In my worldview, atomic is at the bottom of the chain-of-trust. It's effectively a trusted implementation of a foundational tool. Almost every low level tool is of this nature.Complete the sentence: "In order to read or write shared data, you have to ..."Call a shared method, or at the bottom of the stack, you need to do unsafe ( trusted?) implementations of the foundational machinery (possibly using casts), and package into boxes that are safe to interact with and build out from.It's an intrinsic. You use it the same as malloc() or free(). It's a piece of low-level mechanical tooling which you use in an unsafe way, but you then wrap it in a layer that introduces the type-safety. malloc() returns a void*, which you cast to the intended type, and then perform construction. You can't implement a typesafe new without malloc() at the bottom of the stack. You need to raise your vision one-level higher to users of Atomic to see interesting interactions.Here's another one, where only a subset of the object is modeled to be threadsafe (this is particularly interesting to me): struct Threadsafe { int x; Atomic!int y; void notThreadsafe() { ++x; ++y; } void threadsafe() shared { ++y; } } In these examples, the thread-local function *does not* undermine the threadsafety of threadsafe(), it MUST NOT undermine the threadsafety of threadsafe(), or else threadsafe() **IS NOT THREADSAFE**. In the second example, you can see how it's possible and useful to do thread-local work without invalidating the objects threadsafety commitments. I've said this a bunch of times, there are 2 rules: 1. shared inhibits read and write access to members 2. `shared` methods must be threadsafeGiven rule 1, how does Atomic!int actually work, if it can't read or write shared members?From there, shared becomes interesting and useful.For rule 2, how does the compiler actually prove this? Any programming by convention, we can do today. We can implement Atomic!int with the current compiler, using unsafe casts inside trusted blocks.It can't. I don't know what can be done to mechanically enforce this requirement, but I would suggest that it's a goal to work towards in the future with any technology possible. In the meantime though, if we accept that the user writing a threadsafe tool is responsible for delivering on their promise, then the system that emerges is widely useful. The higher-level becomes generally interesting, the low-level will remain to be implemented by experts, and is no change from the situation today. The low level doesn't really care much about type-safety, it's the high-level I'm interested in. We can tell a MUCH better story about how users can interact with shared machinery, and we should.
Oct 17 2018
On 10/17/18 6:37 PM, Manu wrote:On Wed, Oct 17, 2018 at 12:35 PM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:So then it's a misnomer -- it's not really shared, because I can't do anything with it.On 10/17/18 2:46 PM, Manu wrote:And that shared(int)* provides no access. No other thread with that pointer can do anything with it.On Wed, Oct 17, 2018 at 10:30 AM Steven Schveighoffer viaIt's assumed that shared int pointer can be passed to another thread, right? Do I have to write a full program to demonstrate?What the example demonstrates is that while you are trying to disallow implicit casting of a shared pointer to an unshared pointer, you have inadvertently allowed it by leaving behind an unshared pointer that is the same thing.This doesn't make sense... you're showing a thread-local program. The thread owning the unshared pointer is entitled to the unshared pointer. It can make as many copies at it likes. They are all thread-local.All I can see is that a shared method promises to be callable on shared or unshared data. In essence, it promises nothing. It's the programmer who must implement the thread safety, and there really is no help at all from the compiler for this. At some level, there will be either casts, or intrinsics, both of which are unsafe without knowing all the context of the object. In any case, it's simply a false guarantee of thread safety, which might as well be a convention of "any function which starts with TS_ is supposed to be thread safe". shared in the current form promises one thing and one thing only -- data marked as shared is actually sharable between threads, and data not marked as shared is actually not shared between threads. This new regime you are proposing does nothing extra or new, except break that guarantee.You can call shared methods. They promise threadsafety. That's a small subset of the program, but that's natural; only a very small subset of the program is safe to be called from a shared context.There's only one owning thread, and you can't violate that without unsafe casts.The what is the point of shared? Like why would you share data that NOBODY CAN USE?The implicit cast means that you have to look at more than just your method. You have to look at the entire module, and figure out all the interactions, to see if the thread safe method actually is thread safe. That's programming by convention, and fully trusting the programmer. I don't think this thread is going anywhere, so I'll just have to wait and see if someone else can explain it better. I'm a firm no on implicit casting from mutable to shared. -SteveAt SOME POINT, shared data needs to be readable and writable. Any correct system is going to dictate how that works. It's a good start to make shared data unusable unless you cast. But then to make it implicitly castable from unshared defeats the whole purpose.No. No casting! This is antiquated workflow.. I'm not trying to take it away from you, but it's not an interesting model for the future. `shared` can model more than just that. You can call threadsafe methods. Shared methods explicitly dictate how the system works, and in a very clear and obvious/intuitive way. The implicit cast makes using threadsafe objects more convenient when you only have one, which is extremely common.
Oct 17 2018
On Wed, Oct 17, 2018 at 6:50 PM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/17/18 6:37 PM, Manu wrote:**EXACTLY**.. this is the key to the entire design that allows for the implicit promotion.On Wed, Oct 17, 2018 at 12:35 PM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:So then it's a misnomer -- it's not really shared, because I can't do anything with it.On 10/17/18 2:46 PM, Manu wrote:And that shared(int)* provides no access. No other thread with that pointer can do anything with it.On Wed, Oct 17, 2018 at 10:30 AM Steven Schveighoffer viaIt's assumed that shared int pointer can be passed to another thread, right? Do I have to write a full program to demonstrate?What the example demonstrates is that while you are trying to disallow implicit casting of a shared pointer to an unshared pointer, you have inadvertently allowed it by leaving behind an unshared pointer that is the same thing.This doesn't make sense... you're showing a thread-local program. The thread owning the unshared pointer is entitled to the unshared pointer. It can make as many copies at it likes. They are all thread-local.How is that nothing? It promises threadsafety, such that it can be called on shared data. Normally, you can do NOTHING with shared data short of unsafe operations. It is always safe to call a threadsafe function on an unshared thing, so it should be possible. `shared` opts into a strong commitment to threadsafety that allows for a shared thing to interact with it, otherwise, no interaction is possible. I don't understand how you can claim it promises nothing; this is an extremely strong promise!All I can see is that a shared method promises to be callable on shared or unshared data. In essence, it promises nothing.You can call shared methods. They promise threadsafety. That's a small subset of the program, but that's natural; only a very small subset of the program is safe to be called from a shared context.There's only one owning thread, and you can't violate that without unsafe casts.The what is the point of shared? Like why would you share data that NOBODY CAN USE?It's the programmer who must implement the thread safety, and there really is no help at all from the compiler for this.I hope we can work on this moving forwards. There are probably some things we can do... but even without compiler support, in reality, there are probably only a small number of core pieces of tooling, but most authors of a shared thing will just aggregate core tools and call through to their API's. Most shared code will use tools written by experts, and implement nothing fancy or magical themselves.At some level, there will be either casts, or intrinsics, both of which are unsafe without knowing all the context of the object.This will tend to be in the core tooling, written by an expert. The whole point of my design is to remove friction from the USERS of those tools, such that they can create aggregate functionality safely without doing anything unsafe.In any case, it's simply a false guarantee of thread safety, which might as well be a convention of "any function which starts with TS_ is supposed to be thread safe".What makes it a false guarantee? It allows you to have confidence in the stack built on top of some core tools. The whole point here is to minimise the number of people writing code like that. I'm trying to make shared generally useful so that people don't have to engage with it at a low-level. If you see an unsafe case in a shared function (beyond the core tooling), then you should immediately be suspicious, and this is a deliberate part of my goal here. The point here is to produce safer and more reliable code by defining access rules that lead to confident threadsafety. What you describe is what we have now.shared in the current form promises one thing and one thing only -- data marked as shared is actually sharable between threads, and data not marked as shared is actually not shared between threads.Ummm. The current form might *say* that, but you still have full unregulated access to all members, and it's completely unsafe, with no attempt to mitigate that. This is a new definition. Take it for what it's worth. The old definition is mostly worthless. I'm trying to make something that's interesting and useful.This new regime you are proposing does nothing extra or new, except break that guarantee.There are heaps of advantages: 1. If I have a shared thing, I know I can't ruin its state by manipulating it arbitrarily like I can now 2. I am now able to describe threadsafe objects 3. I relieve users from mental strain of trying to understand how to correctly and safely interact with shared API's, because they can have confidence that a shared thing can only perform threadsafe activity 4. I'm trying to give shared a simple, meaningful and *useful* definition, that's easy to understand and communicate. I believe the only reason people are having trouble, because they are taking it in contrast, rather than as it is.I don't understand... how can the outer context affect the threadsafety of a properly encapsulated thing? Back to this trivial example: struct Atomic(T) { void opUnary(string op : "++")() shared { atomicInc(cast(T*)&val); } private T val; } This is properly encapsulated... the promise is firm. If some threadsafe machinery is sufficiently complex that it has a wide roam and you can't reason about whether the method is actually threadsafe, then you have no business writing threadsafe machinery; use a library. This design, nor the current design, nor any other design can possibly help you.The implicit cast means that you have to look at more than just your method. You have to look at the entire module, and figure out all the interactions, to see if the thread safe method actually is thread safe. That's programming by convention, and fully trusting the programmer.At SOME POINT, shared data needs to be readable and writable. Any correct system is going to dictate how that works. It's a good start to make shared data unusable unless you cast. But then to make it implicitly castable from unshared defeats the whole purpose.No. No casting! This is antiquated workflow.. I'm not trying to take it away from you, but it's not an interesting model for the future. `shared` can model more than just that. You can call threadsafe methods. Shared methods explicitly dictate how the system works, and in a very clear and obvious/intuitive way. The implicit cast makes using threadsafe objects more convenient when you only have one, which is extremely common.I don't think this thread is going anywhere, so I'll just have to wait and see if someone else can explain it better. I'm a firm no on implicit casting from mutable to shared.You need to take it for an intellectual spin. Show me how it's corrupt rather than just presenting discomfort with the idea in theory. You're addicted to some concepts that you've carried around for a long time. There is no value in requiring casts, they're just a funky smell, and force the user to perform potentially unsafe manual conversions, or interactions that they don't understand. Implicit conversion is for allowing/encouraging what is safe. Calling threadsafe functions is safe. You should be able to call threadsafe functions.
Oct 17 2018
On 10/17/18 10:26 PM, Manu wrote:On Wed, Oct 17, 2018 at 6:50 PM Steven Schveighoffer via Digitalmars-d[snip]The implicit cast means that you have to look at more than just your method. You have to look at the entire module, and figure out all the interactions, to see if the thread safe method actually is thread safe. That's programming by convention, and fully trusting the programmer.I don't understand... how can the outer context affect the threadsafety of a properly encapsulated thing?You need to take it for an intellectual spin. Show me how it's corrupt rather than just presenting discomfort with the idea in theory. You're addicted to some concepts that you've carried around for a long time. There is no value in requiring casts, they're just a funky smell, and force the user to perform potentially unsafe manual conversions, or interactions that they don't understand.For example (your example): struct NotThreadsafe { private int x; void local() { ++x; // <- invalidates the method below, you violate the other function's `shared` promise } void notThreadsafe() shared { atomicIncrement(&x); } } First, note the comment. I can't look ONLY at the implementation of "notThreadSafe" (assuming the function name is less of a giveaway) in order to guarantee that it's actually thread safe. I have to look at the WHOLE MODULE. Anything could potentially do what local() does. I added private to x to at least give the appearance of thread safety. But on top of that, if I can't implicitly cast mutable to shared, then this ACTUALLY IS thread safe, as long as all the casting in the module is sound (easy to search and verify), and hopefully all the casting is encapsulated in primitives like you have written. Because someone on the outside would have to cast a mutable item into a shared item, and this puts the responsibility on them to make sure it works. I'm ALL FOR having shared be completely unusable as-is unless you cast (thanks for confirming what I suspected in your last post). It's the implicit casting which I think makes things way more difficult, and completely undercuts the utility of the compiler's mechanical checking. And on top of that, I WANT that implementation. If I know something is not shared, why would I ever want to use atomics on it? I don't like needlessly throwing away performance. This is how I would write it: struct ThreadSafe { private int x; void increment() { ++x; // I know this is not shared, so no reason to use atomics } void increment() shared { atomicIncrement(&x); // use atomics, to avoid races } } The beauty of shared not being implicitly castable, is it allows you to focus on the implementation at hand, with the knowledge that nothing else can meddle with it. The goal of mechanical checking should be to narrow the focus of what needs to be proven correct. -Steve
Oct 18 2018
On 10/18/18 9:35 AM, Steven Schveighoffer wrote:struct NotThreadsafe { private int x; void local() { ++x; // <- invalidates the method below, you violate the other function's `shared` promise } void notThreadsafe() shared { atomicIncrement(&x); } }[snip]But on top of that, if I can't implicitly cast mutable to shared, then this ACTUALLY IS thread safe, as long as all the casting in the module is sound (easy to search and verify), and hopefully all the casting is encapsulated in primitives like you have written. Because someone on the outside would have to cast a mutable item into a shared item, and this puts the responsibility on them to make sure it works.Another thing to point out -- I can make x public (not private), and it's STILL THREAD SAFE. -Steve
Oct 18 2018
On Thu, Oct 18, 2018 at 6:50 AM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/18/18 9:35 AM, Steven Schveighoffer wrote:I'm not sure that's an interesting design goal though. Most things don't have shared methods. If you're writing a thing with shared methods, you're very in the business of implementing threadsafety... you're going to want to make it the tightest, most-unlikely-to-have-threading-bugs thing you can write. I predict that you're not going to be upset about the restriction. And if you are, AND you're confident in your application to maintain a mutually-exclusive shared/TL separation, then you can do this to your hearts content! Nobody will stop you, and it will be fine. But I don't think it should be default, because the rules as designed that way, enforce *users* to perform unsafe casts when they're, on average, not qualified to make those decisions.struct NotThreadsafe { private int x; void local() { ++x; // <- invalidates the method below, you violate the other function's `shared` promise } void notThreadsafe() shared { atomicIncrement(&x); } }[snip]But on top of that, if I can't implicitly cast mutable to shared, then this ACTUALLY IS thread safe, as long as all the casting in the module is sound (easy to search and verify), and hopefully all the casting is encapsulated in primitives like you have written. Because someone on the outside would have to cast a mutable item into a shared item, and this puts the responsibility on them to make sure it works.Another thing to point out -- I can make x public (not private), and it's STILL THREAD SAFE.
Oct 18 2018
On Thursday, 18 October 2018 at 13:35:22 UTC, Steven Schveighoffer wrote:struct ThreadSafe { private int x; void increment() { ++x; // I know this is not shared, so no reason to use atomics } void increment() shared { atomicIncrement(&x); // use atomics, to avoid races } }But this isn't thread-safe, for the exact reasons described elsewhere in this thread (and in fact, incorrectly leveled at Manu's proposal). Someone could write this code: void foo() { ThreadSafe* a = new ThreadSafe(); shareAllOver(a); a.increment(); // unsafe, non-shared method call } When a.increment() is being called, you have no idea if anyone else is using the shared interface. This is one of the issues that MP (Manu's Proposal) tries to deal with. Under MP, your code would *not* be considered thread-safe, because the non-shared portion may interfere with the shared portion. You'd need to write two types: struct ThreadSafe { private int x; void increment() shared { atomicIncrement(&x); } } struct NotThreadSafe { private int x; void increment() { ++x; } } These two are different types with different semantics, and forcing them both into the same struct is an abomination. In your case, the user of your type will need to ensure thread-safety. You may not have any control over how he's doing things, while you *do* control the code in your own type (and module, since that also affects things). Under MP, the type is what needs to be thread-safe, and once it is, the chance of a user mucking things up is much lower. -- Simen
Oct 18 2018
On 10/18/18 10:11 AM, Simen Kjærås wrote:On Thursday, 18 October 2018 at 13:35:22 UTC, Steven Schveighoffer wrote:Error: cannot call function shareAllOver(shared(ThreadSafe) *) with type ThreadSafe *struct ThreadSafe { private int x; void increment() { ++x; // I know this is not shared, so no reason to use atomics } void increment() shared { atomicIncrement(&x); // use atomics, to avoid races } }But this isn't thread-safe, for the exact reasons described elsewhere in this thread (and in fact, incorrectly leveled at Manu's proposal). Someone could write this code: void foo() { ThreadSafe* a = new ThreadSafe(); shareAllOver(a);a.increment(); // unsafe, non-shared method call } When a.increment() is being called, you have no idea if anyone else is using the shared interface.I do, because unless you have cast the type to shared, I'm certain there is only thread-local aliasing to it.This is one of the issues that MP (Manu's Proposal) tries to deal with. Under MP, your code would *not* be considered thread-safe, because the non-shared portion may interfere with the shared portion. You'd need to write two types: struct ThreadSafe { private int x; void increment() shared { atomicIncrement(&x); } } struct NotThreadSafe { private int x; void increment() { ++x; } } These two are different types with different semantics, and forcing them both into the same struct is an abomination.Why? What if I wanted to have an object that is local for a while, but then I want it to be shared (and I ensure carefully when I cast to shared that there are no other aliases to that)?In your case, the user of your type will need to ensure thread-safety.No, the contract the type provides is: if you DON'T cast unshared to shared or vice versa, the type is thread-safe. If you DO cast unshared to shared, then the type is thread-safe as long as you no longer use the unshared reference. This is EXACTLY how immutable works.You may not have any control over how he's doing things, while you *do* control the code in your own type (and module, since that also affects things). Under MP, the type is what needs to be thread-safe, and once it is, the chance of a user mucking things up is much lower.Under MP, the type is DEFENSIVELY thread-safe, locking or using atomics unnecessarily when it's thread-local. -Steve
Oct 18 2018
On Thu, Oct 18, 2018 at 7:20 AM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/18/18 10:11 AM, Simen Kjærås wrote:And here you expect a user to perform an unsafe-cast (which they may not understand), and we have no language semantics to enforce the transfer of ownership. How do you assure that the user yields the thread-local instance? I think requiring the cast is un-principled in every way that D values. My proposal doesn't rely on convention (that we *hope* the user does correctly yield the thread-local instance)... it assures a set of safe rules by default. This is the core value proposition of my proposal. It's literally the entire point.On Thursday, 18 October 2018 at 13:35:22 UTC, Steven Schveighoffer wrote:Error: cannot call function shareAllOver(shared(ThreadSafe) *) with type ThreadSafe *struct ThreadSafe { private int x; void increment() { ++x; // I know this is not shared, so no reason to use atomics } void increment() shared { atomicIncrement(&x); // use atomics, to avoid races } }But this isn't thread-safe, for the exact reasons described elsewhere in this thread (and in fact, incorrectly leveled at Manu's proposal). Someone could write this code: void foo() { ThreadSafe* a = new ThreadSafe(); shareAllOver(a);
Oct 18 2018
On 10/18/18 2:55 PM, Manu wrote:On Thu, Oct 18, 2018 at 7:20 AM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:No, I expect them to do: auto a = new shared(ThreadSafe)();On 10/18/18 10:11 AM, Simen Kjærås wrote:And here you expect a user to perform an unsafe-cast (which they may not understand), and we have no language semantics to enforce the transfer of ownership. How do you assure that the user yields the thread-local instance?On Thursday, 18 October 2018 at 13:35:22 UTC, Steven Schveighoffer wrote:Error: cannot call function shareAllOver(shared(ThreadSafe) *) with type ThreadSafe *struct ThreadSafe { private int x; void increment() { ++x; // I know this is not shared, so no reason to use atomics } void increment() shared { atomicIncrement(&x); // use atomics, to avoid races } }But this isn't thread-safe, for the exact reasons described elsewhere in this thread (and in fact, incorrectly leveled at Manu's proposal). Someone could write this code: void foo() { ThreadSafe* a = new ThreadSafe(); shareAllOver(a);I think requiring the cast is un-principled in every way that D values.No cast is required. If you have shared data, it's shared. If you have thread local data, it's unshared. Allocate the data the way you expect to use it. It's only if you intend to turn unshared data into shared data where you need an unsafe cast. It's not even as difficult as immutable, because you can still modify shared data. For instance, the shared constructor doesn't have to have special rules about initialization, it can just assume shared from the beginning. -Steve
Oct 18 2018
On Thu, Oct 18, 2018 at 12:15 PM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/18/18 2:55 PM, Manu wrote:I don't have any use for this design in my application. I can't use the model you prescribe, at all.On Thu, Oct 18, 2018 at 7:20 AM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:No, I expect them to do: auto a = new shared(ThreadSafe)();On 10/18/18 10:11 AM, Simen Kjærås wrote:And here you expect a user to perform an unsafe-cast (which they may not understand), and we have no language semantics to enforce the transfer of ownership. How do you assure that the user yields the thread-local instance?On Thursday, 18 October 2018 at 13:35:22 UTC, Steven Schveighoffer wrote:Error: cannot call function shareAllOver(shared(ThreadSafe) *) with type ThreadSafe *struct ThreadSafe { private int x; void increment() { ++x; // I know this is not shared, so no reason to use atomics } void increment() shared { atomicIncrement(&x); // use atomics, to avoid races } }But this isn't thread-safe, for the exact reasons described elsewhere in this thread (and in fact, incorrectly leveled at Manu's proposal). Someone could write this code: void foo() { ThreadSafe* a = new ThreadSafe(); shareAllOver(a);All data is thread-local, and occasionally becomes shared during periods. I can't make use of the model you describe. My proposal is more permissive, and allows a wider range of application designs. What are the disadvantages?I think requiring the cast is un-principled in every way that D values.No cast is required. If you have shared data, it's shared. If you have thread local data, it's unshared. Allocate the data the way you expect to use it.It's only if you intend to turn unshared data into shared data where you need an unsafe cast.It's unnecessary though, because threadsafe functions are threadsafe! You're pointlessly forcing un-safety. Why would I prefer a design that forces unsafe interactions to perform safe operations?It's not even as difficult as immutable, because you can still modify shared data. For instance, the shared constructor doesn't have to have special rules about initialization, it can just assume shared from the beginning.Your design us immutable, mine is const. Tell me, how many occurrences of 'immutable' can you find in your software? ... how about const? Which is more universally useful? If you had to choose one or the other, which one could you live without?
Oct 18 2018
On 10/18/18 5:22 PM, Manu wrote:On Thu, Oct 18, 2018 at 12:15 PM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:Huh? This is the same thing you are asking for. How were you intending to make a thread-safe thing sharable? Surely it will be typed as shared, right? How else will you pass it to multiple threads?On 10/18/18 2:55 PM, Manu wrote:I don't have any use for this design in my application. I can't use the model you prescribe, at all.On Thu, Oct 18, 2018 at 7:20 AM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:No, I expect them to do: auto a = new shared(ThreadSafe)();On 10/18/18 10:11 AM, Simen Kjærås wrote:And here you expect a user to perform an unsafe-cast (which they may not understand), and we have no language semantics to enforce the transfer of ownership. How do you assure that the user yields the thread-local instance?On Thursday, 18 October 2018 at 13:35:22 UTC, Steven Schveighoffer wrote:Error: cannot call function shareAllOver(shared(ThreadSafe) *) with type ThreadSafe *struct ThreadSafe { private int x; void increment() { ++x; // I know this is not shared, so no reason to use atomics } void increment() shared { atomicIncrement(&x); // use atomics, to avoid races } }But this isn't thread-safe, for the exact reasons described elsewhere in this thread (and in fact, incorrectly leveled at Manu's proposal). Someone could write this code: void foo() { ThreadSafe* a = new ThreadSafe(); shareAllOver(a);If data is shared, it is shared. Once it is shared, it never goes back. In your model, everything is *assumed* shared, so that's what you need to do, initialize it as shared. It still works just as you like. Even if you never actually share it, or share it periodically.All data is thread-local, and occasionally becomes shared during periods. I can't make use of the model you describe.I think requiring the cast is un-principled in every way that D values.No cast is required. If you have shared data, it's shared. If you have thread local data, it's unshared. Allocate the data the way you expect to use it.My proposal is more permissive, and allows a wider range of application designs. What are the disadvantages?The opposite is true. More designs are allowed by restricting casting as I have demonstrated many times.No unsafe interactions are required for a type that defensively is shared. Just make it always shared, and you don't have any problems.It's only if you intend to turn unshared data into shared data where you need an unsafe cast.It's unnecessary though, because threadsafe functions are threadsafe! You're pointlessly forcing un-safety. Why would I prefer a design that forces unsafe interactions to perform safe operations?No, your design is not const, const works on normal types. It's applicable to anything. Your design is only applicable to special types that experts write. It's not applicable to int, for instance. It feels more like a special library than a compiler feature.It's not even as difficult as immutable, because you can still modify shared data. For instance, the shared constructor doesn't have to have special rules about initialization, it can just assume shared from the beginning.Your design us immutable, mine is const.Tell me, how many occurrences of 'immutable' can you find in your software? ... how about const?I generally use inout whenever possible, or const when that is more appropriate. But that is for methods. For data, I generally use immutable when I want a constant. But like I said, something can't be both shared and unshared. So having shared pointers point at unshared data makes no sense -- once it's shared, it's shared. So shared really can't be akin to const.Which is more universally useful? If you had to choose one or the other, which one could you live without?I would hate to have a const where you couldn't read the data, I probably would rather have immutable. I said I would stop commenting on this thread, and I didn't keep that promise. I really am going to stop now. I'm pretty sure Walter will not agree with this mechanism, so until you convince him, I don't really need to be spending time on this. We seem to be completely understanding each others mechanisms, but not agreeing which one is correct, based on (from both sides) hypothetical types and usages. -Steve
Oct 18 2018
On Thu, Oct 18, 2018 at 3:40 PM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/18/18 5:22 PM, Manu wrote:Things get promoted to shared and distributed on occasion, but there is only one owner, and he's the only guy with thread-local access.On Thu, Oct 18, 2018 at 12:15 PM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:Huh? This is the same thing you are asking for. How were you intending to make a thread-safe thing sharable? Surely it will be typed as shared, right? How else will you pass it to multiple threads?On 10/18/18 2:55 PM, Manu wrote:I don't have any use for this design in my application. I can't use the model you prescribe, at all.On Thu, Oct 18, 2018 at 7:20 AM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:No, I expect them to do: auto a = new shared(ThreadSafe)();On 10/18/18 10:11 AM, Simen Kjærås wrote:And here you expect a user to perform an unsafe-cast (which they may not understand), and we have no language semantics to enforce the transfer of ownership. How do you assure that the user yields the thread-local instance?On Thursday, 18 October 2018 at 13:35:22 UTC, Steven Schveighoffer wrote:Error: cannot call function shareAllOver(shared(ThreadSafe) *) with type ThreadSafe *struct ThreadSafe { private int x; void increment() { ++x; // I know this is not shared, so no reason to use atomics } void increment() shared { atomicIncrement(&x); // use atomics, to avoid races } }But this isn't thread-safe, for the exact reasons described elsewhere in this thread (and in fact, incorrectly leveled at Manu's proposal). Someone could write this code: void foo() { ThreadSafe* a = new ThreadSafe(); shareAllOver(a);I'm suggesting that; data *may be shared* (ie, has valid threadsafe interaction), or it may not (has no access). NOT that data "is shared", or "is not". I don't see value in that proposition. I don't know how to make that useful to me. It's impossible for me to interact with that model safely. The ONLY way to interact with that model safely is to allocate objects shared, or not... there is no safe transition, and I am totally concerned with the transition.If data is shared, it is shared. Once it is shared, it never goes back.All data is thread-local, and occasionally becomes shared during periods. I can't make use of the model you describe.I think requiring the cast is un-principled in every way that D values.No cast is required. If you have shared data, it's shared. If you have thread local data, it's unshared. Allocate the data the way you expect to use it.In your model, everything is *assumed* shared, so that's what you need to do, initialize it as shared. It still works just as you like. Even if you never actually share it, or share it periodically.You can't do that... cus you can't call anything's unshared methods.No you haven't. What design is enabled by it that is inhibited by my design? I can't see this... Your demonstrated design if fundamentally unsafe, unless you *allocate* things as shared, and accept that no transition is possible. This is not a flexible design. My design also allows that if that's what you want. Under my proposal, you can equally allocate things shared to the exact same effect; I haven't taken anything away.My proposal is more permissive, and allows a wider range of application designs. What are the disadvantages?The opposite is true. More designs are allowed by restricting casting as I have demonstrated many times.Then you completely lose access to the unshared API. This is not workable.No unsafe interactions are required for a type that defensively is shared. Just make it always shared, and you don't have any problems.It's only if you intend to turn unshared data into shared data where you need an unsafe cast.It's unnecessary though, because threadsafe functions are threadsafe! You're pointlessly forcing un-safety. Why would I prefer a design that forces unsafe interactions to perform safe operations?But... that's the same here. const restricts you to calling const methods... shared restricts you to calling shared methods.No, your design is not const, const works on normal types. It's applicable to anything.It's not even as difficult as immutable, because you can still modify shared data. For instance, the shared constructor doesn't have to have special rules about initialization, it can just assume shared from the beginning.Your design us immutable, mine is const.You're dodging the question... I think you know that nothing can work without const, but we could live without immutable. Certainly, immutable wouldn't work without const! The exact same connundrum applies here. Imagine that we only had immutable, and any time you wanted to call a const method, you had to cast to immutable...Tell me, how many occurrences of 'immutable' can you find in your software? ... how about const?I generally use inout whenever possible, or const when that is more appropriate. But that is for methods. For data, I generally use immutable when I want a constant.But like I said, something can't be both shared and unshared. So having shared pointers point at unshared data makes no sense -- once it's shared, it's shared. So shared really can't be akin to const.Yes, but I'm saying the mutually-exclusive state isn't useful. No transition is possible, and necessitates unsafety to do anything (that I want to do). I'm trying to create a world where the mutual-exclusion is effected in practise (can define and perform threadsafe interaction), but not in strict terms, such that transition is impossible... I'm very carefully designing a solution where the worlds aren't isolated, and that world is 100% more useful to me and my entire ecosystem. I don't know how to use shared safely if it's not designed that way, just like I don't know how to generally use immutable in lieu of const. immutable wouldn't be practical without const.If an object has no const methods, you can't do anything with it. The usefulness of const depends on people recognising and support const.Which is more universally useful? If you had to choose one or the other, which one could you live without?I would hate to have a const where you couldn't read the data, I probably would rather have immutable.I said I would stop commenting on this thread, and I didn't keep that promise. I really am going to stop now. I'm pretty sure Walter will not agree with this mechanism, so until you convince him, I don't really need to be spending time on this. We seem to be completely understanding each others mechanisms, but not agreeing which one is correct, based on (from both sides) hypothetical types and usages.I'm still not at all clear on how I'm excluding any interesting use cases... I have deliberately tried to preserve all the valuable use cases I'm aware of. I don't understand your perspective, because I don't understand your sense of loss.
Oct 18 2018
On Thu, Oct 18, 2018 at 7:20 AM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/18/18 10:11 AM, Simen Kjærås wrote:No, you can never be sure. Your assumption depends on the *user* engaging in an unsafe operation (the cast), and correctly perform a conventional act; they must correctly the safely transfer ownership. My proposal puts all requirements on the author, not the user. I think this is a much more trustworthy relationship, and in terms of cognitive load, author:users is a 1:many relationship, and I place the load on the '1', not the 'many.a.increment(); // unsafe, non-shared method call } When a.increment() is being called, you have no idea if anyone else is using the shared interface.I do, because unless you have cast the type to shared, I'm certain there is only thread-local aliasing to it.
Oct 18 2018
On 10/18/18 2:59 PM, Manu wrote:On Thu, Oct 18, 2018 at 7:20 AM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:Not at all. No transfer of ownership is needed, no cast is needed. If you want to share something declare it shared.On 10/18/18 10:11 AM, Simen Kjærås wrote:No, you can never be sure. Your assumption depends on the *user* engaging in an unsafe operation (the cast), and correctly perform a conventional act; they must correctly the safely transfer ownership.a.increment(); // unsafe, non-shared method call } When a.increment() is being called, you have no idea if anyone else is using the shared interface.I do, because unless you have cast the type to shared, I'm certain there is only thread-local aliasing to it.My proposal puts all requirements on the author, not the user. I think this is a much more trustworthy relationship, and in terms of cognitive load, author:users is a 1:many relationship, and I place the load on the '1', not the 'many.Sure, but we can create a system today where smart people make objects that do the right thing without compiler help. We don't need to break the guarantees of shared to do it. -Steve
Oct 18 2018
On Thursday, 18 October 2018 at 14:19:41 UTC, Steven Schveighoffer wrote:On 10/18/18 10:11 AM, Simen Kjærås wrote:Sorry, typo. Should of course have been shareAllOver(cast(shared)a);On Thursday, 18 October 2018 at 13:35:22 UTC, Steven Schveighoffer wrote:Error: cannot call function shareAllOver(shared(ThreadSafe) *) with type ThreadSafe *struct ThreadSafe { private int x; void increment() { ++x; // I know this is not shared, so no reason to use atomics } void increment() shared { atomicIncrement(&x); // use atomics, to avoid races } }But this isn't thread-safe, for the exact reasons described elsewhere in this thread (and in fact, incorrectly leveled at Manu's proposal). Someone could write this code: void foo() { ThreadSafe* a = new ThreadSafe(); shareAllOver(a);a.increment(); // unsafe, non-shared method call } When a.increment() is being called, you have no idea if anyone else is using the shared interface.I do, because unless you have cast the type to shared, I'm certain there is only thread-local aliasing to it.Yes, and that means the user of the type will need to follow these rules to ensure thread-safety. Which is what I said. Under MP, it's simply safe.This is one of the issues that MP (Manu's Proposal) tries to deal with. Under MP, your code would *not* be considered thread-safe, because the non-shared portion may interfere with the shared portion. You'd need to write two types: struct ThreadSafe { private int x; void increment() shared { atomicIncrement(&x); } } struct NotThreadSafe { private int x; void increment() { ++x; } } These two are different types with different semantics, and forcing them both into the same struct is an abomination.Why? What if I wanted to have an object that is local for a while, but then I want it to be shared (and I ensure carefully when I cast to shared that there are no other aliases to that)?In your case, the user of your type will need to ensure thread-safety.No, the contract the type provides is: if you DON'T cast unshared to shared or vice versa, the type is thread-safe. If you DO cast unshared to shared, then the type is thread-safe as long as you no longer use the unshared reference. This is EXACTLY how immutable works.Yes, because safety >> efficiency. There's nothing stopping you from making fast, unsafe functions under MP. Call them unsafe_<foo> to make them easy to grep for. Since only the type implementer, not the users, write this code, it's not an undue burden. One of the greatest benefits of MP is that all the potential problem points are in one place. In a large codebase with multiple developers, anyone anywhere could be using a dangling unshared reference under the current schema. Under MP, that's confined to the type, not its uses. -- SimenYou may not have any control over how he's doing things, while you *do* control the code in your own type (and module, since that also affects things). Under MP, the type is what needs to be thread-safe, and once it is, the chance of a user mucking things up is much lower.Under MP, the type is DEFENSIVELY thread-safe, locking or using atomics unnecessarily when it's thread-local.
Oct 18 2018
On Thu, Oct 18, 2018 at 6:40 AM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/17/18 10:26 PM, Manu wrote:I understand your argument, and I used to think this too... but I concluded differently for 1 simple reason: usability. I have demonstrated these usability considerations in production. I am confident it's the right balance. I propose: 1. Normal people don't write thread-safety, a very small number of unusual people do this. I feel very good about biasing 100% of the cognitive load INSIDE the shared method. This means the expert, and ONLY the expert, must make decisions about thread-safety implementation. 2. Implicit conversion allows users to safely interact with safe things without doing unsafe casts. I think it's a complete design fail if you expect any user anywhere to perform an unsafe cast to call a perfectly thread-safe function. The user might not properly understand their obligations. 3. The practical result of the above is, any complexity relating to safety is completely owned by the threadsafe author, and not cascaded to the user. You can't expect users to understand, and make correct decisions about threadsafety. Safety should be default position. I recognise the potential loss of an unsafe optimised thread-local path. 1. This truly isn't a big deal. If this is really hurting you, you will notice on the profiler, and deploy a thread-exclusive path assuming the context supports it. 2. I will trade that for confidence in safe interaction every day of the week. Safety is the right default position here. 2. You just need to make the unsafe thread-exclusive variant explicit, eg:On Wed, Oct 17, 2018 at 6:50 PM Steven Schveighoffer via Digitalmars-d[snip]The implicit cast means that you have to look at more than just your method. You have to look at the entire module, and figure out all the interactions, to see if the thread safe method actually is thread safe. That's programming by convention, and fully trusting the programmer.I don't understand... how can the outer context affect the threadsafety of a properly encapsulated thing?You need to take it for an intellectual spin. Show me how it's corrupt rather than just presenting discomfort with the idea in theory. You're addicted to some concepts that you've carried around for a long time. There is no value in requiring casts, they're just a funky smell, and force the user to perform potentially unsafe manual conversions, or interactions that they don't understand.For example (your example): struct NotThreadsafe { private int x; void local() { ++x; // <- invalidates the method below, you violate the other function's `shared` promise } void notThreadsafe() shared { atomicIncrement(&x); } } First, note the comment. I can't look ONLY at the implementation of "notThreadSafe" (assuming the function name is less of a giveaway) in order to guarantee that it's actually thread safe. I have to look at the WHOLE MODULE. Anything could potentially do what local() does. I added private to x to at least give the appearance of thread safety. But on top of that, if I can't implicitly cast mutable to shared, then this ACTUALLY IS thread safe, as long as all the casting in the module is sound (easy to search and verify), and hopefully all the casting is encapsulated in primitives like you have written. Because someone on the outside would have to cast a mutable item into a shared item, and this puts the responsibility on them to make sure it works. I'm ALL FOR having shared be completely unusable as-is unless you cast (thanks for confirming what I suspected in your last post). It's the implicit casting which I think makes things way more difficult, and completely undercuts the utility of the compiler's mechanical checking. And on top of that, I WANT that implementation. If I know something is not shared, why would I ever want to use atomics on it? I don't like needlessly throwing away performance. This is how I would write it: struct ThreadSafe { private int x; void increment() { ++x; // I know this is not shared, so no reason to use atomics } void increment() shared { atomicIncrement(&x); // use atomics, to avoid races } } The beauty of shared not being implicitly castable, is it allows you to focus on the implementation at hand, with the knowledge that nothing else can meddle with it. The goal of mechanical checking should be to narrow the focus of what needs to be proven correct. -Stevestruct ThreadSafe { private int x; void unsafeIncrement() // <- make it explicit { ++x; // User has asserted that no sharing is possible, no reason to use atomics } void increment() shared { atomicIncrement(&x); // object may be shared } }I think this is quiet a reasonable and clearly documented compromise. I think absolutely-reliably-threadsafe-by-default is the right default position. And if you want to accept unsafe operations for optimsation circumstances, then you're welcome to deploy that in your code as you see fit. If the machinery is not a library for distribution and local to your application, and you know for certain that your context is such that thread-local and shared are mutually exclusive, then you're free to make the unshared overload not-threadsafe; you can do this because you know your application context. You just shouldn't make widely distributed tooling this way. I will indeed do this myself in some cases, because I know those facts about my application. But I wouldn't compromise the default design of shared for this optimisation potential... deliberately deployed optimisation is okay to be unsafe when taken in context.
Oct 18 2018
On Thursday, 18 October 2018 at 18:24:47 UTC, Manu wrote:I have demonstrated these usability considerations in production. I am confident it's the right balance.Then convince us. So far you haven't.I propose: 1. Normal people don't write thread-safety, a very small number of unusual people do this. I feel very good about biasing 100% of the cognitive load INSIDE the shared method. This means the expert, and ONLY the expert, must make decisions about thread-safety implementation.No argument.2. Implicit conversion allows users to safely interact with safe things without doing unsafe casts. I think it's a complete design fail if you expect any user anywhere to perform an unsafe cast to call a perfectly thread-safe function. The user might not properly understand their obligations.Disagreed. "Normal" people wouldn't be doing any unsafe casts and *must not be able to* do something as unsafe as silent promotion of thread-local mutable data to shared. But it's perfectly fine for the "expert" user to do such a promotion, explicitly.3. The practical result of the above is, any complexity relating to safety is completely owned by the threadsafe author, and not cascaded to the user. You can't expect users to understand, and make correct decisions about threadsafety. Safety should be default position.Exactly. And an implicit conversion from mutable to shared isn't safe at all.I recognise the potential loss of an unsafe optimised thread-local path. 1. This truly isn't a big deal. If this is really hurting you, you will notice on the profiler, and deploy a thread-exclusive path assuming the context supports it.2. I will trade that for confidence in safe interaction every day of the week. Safety is the right default position here.Does not compute. Either you're an "expert" from above and live with that burden, or you're not.2. You just need to make the unsafe thread-exclusive variant explicit, eg:No. The above code is not thread-safe at all. The private int *must* be declared shared. Then it becomes: struct ThreadSafe { // These must be *required* if you want to assert any thread safety. Be nice // if the compiler did that for us. disable this(this); disable void opAssign(typeof(this)); private shared int x; // <- *must* be shared void unsafeIncrement() system { x.assumeUnshared += 1; } // or deduced: void unsafeIncrement()() // assumeUnshared must be system, thus this unsafeIncrement will also be deduced system { x.assumeUnshared += 1; } void increment() shared { x.atomicOp!"+="(1); } }struct ThreadSafe { private int x; void unsafeIncrement() // <- make it explicit { ++x; // User has asserted that no sharing is possible, no reason to use atomics } void increment() shared { atomicIncrement(&x); // object may be shared } }I think this is quiet a reasonable and clearly documented compromise.With the fixes above, it is. Without them, it will only be apparent from documentation, and who writes, or reads, that?..I think absolutely-reliably-threadsafe-by-default is the right default position.But it is exactly the opposite of automatic promotions from mutable to shared!
Oct 18 2018
On 10/18/18 2:24 PM, Manu wrote:I understand your argument, and I used to think this too... but I concluded differently for 1 simple reason: usability.You have not demonstrated why your proposal is usable, and the proposal to simply make shared not accessible while NOT introducing implicit conversion is somehow not usable. I find quite the opposite -- the implicit conversion introduces more pitfalls and less guarantees from the compiler.I have demonstrated these usability considerations in production. I am confident it's the right balance.Are these considerations the list below, or are they something else? If so, can you list them?I propose: 1. Normal people don't write thread-safety, a very small number of unusual people do this. I feel very good about biasing 100% of the cognitive load INSIDE the shared method. This means the expert, and ONLY the expert, must make decisions about thread-safety implementation.Thread safety is not easy. But it's also not generic. In terms of low-level things like atomics and lock-free implementations, those ARE generic and SHOULD only be written by experts. But other than that, you can't know how someone has designed all the conditions in their code. For example, you can have an expert write mutex locks and semaphores. But they can't tell you the proper order to lock different objects to ensure there's no deadlock. That's application specific.2. Implicit conversion allows users to safely interact with safe things without doing unsafe casts. I think it's a complete design fail if you expect any user anywhere to perform an unsafe cast to call a perfectly thread-safe function. The user might not properly understand their obligations.I also do not expect anyone to perform unsafe casts in normal use. I expect them to use more generic well-written types in a shared-object library. Casting should be very rare.3. The practical result of the above is, any complexity relating to safety is completely owned by the threadsafe author, and not cascaded to the user. You can't expect users to understand, and make correct decisions about threadsafety. Safety should be default position.I think these are great rules, and none are broken by keeping the explicit cast requirement in place.I recognise the potential loss of an unsafe optimised thread-local path. 1. This truly isn't a big deal. If this is really hurting you, you will notice on the profiler, and deploy a thread-exclusive path assuming the context supports it.This is a mischaracterization. The thread-local path is perfectly safe because only one thread can be accessing the data. That's why it's thread-local and not shared.2. I will trade that for confidence in safe interaction every day of the week. Safety is the right default position here.You can be confident that any shared data is properly synchronized via the API provided. No confidence should be lost here.2. You just need to make the unsafe thread-exclusive variant explicit, eg:It is explicit, the thread-exclusive variant is not marked shared, and cannot be called on data that is actually shared and needs synchronization.This is more design by convention.struct ThreadSafe { private int x; void unsafeIncrement() // <- make it explicit { ++x; // User has asserted that no sharing is possible, no reason to use atomics } void increment() shared { atomicIncrement(&x); // object may be shared } }I think this is quiet a reasonable and clearly documented compromise. I think absolutely-reliably-threadsafe-by-default is the right default position. And if you want to accept unsafe operations for optimsation circumstances, then you're welcome to deploy that in your code as you see fit.All thread-local operations are thread-safe by default, because there can be only one thread using it. That is the beauty of the current regime, regardless of how broken shared is -- unshared is solid. We shouldn't want to break that guarantee.If the machinery is not a library for distribution and local to your application, and you know for certain that your context is such that thread-local and shared are mutually exclusive, then you're free to make the unshared overload not-threadsafe; you can do this because you know your application context. You just shouldn't make widely distributed tooling this way.I can make widely distributed tooling that does both shared and unshared versions of the code, and ALL are thread safe. No choices are necessary, no compromise on performance, and no design by convention.I will indeed do this myself in some cases, because I know those facts about my application. But I wouldn't compromise the default design of shared for this optimisation potential... deliberately deployed optimisation is okay to be unsafe when taken in context.Except it's perfectly thread safe to use data without synchronization in one thread -- which is supported by having unshared data. Unshared means only one thread. In your proposal, anything can be seen from one or more threads. -Steve
Oct 18 2018
On Thu, Oct 18, 2018 at 12:10 PM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/18/18 2:24 PM, Manu wrote:I don't think it introduces *more*, I think it's the same number of compiler guarantees, but it rearranges them into what I feel are a more satisfactory and reliable configuration. Ie, I rearrange such that the compiler guarantees are applicable to the 'many' case rather than the '1' case, and from that perspective, it means the compiler guarantees are more widely deployed.I understand your argument, and I used to think this too... but I concluded differently for 1 simple reason: usability.You have not demonstrated why your proposal is usable, and the proposal to simply make shared not accessible while NOT introducing implicit conversion is somehow not usable. I find quite the opposite -- the implicit conversion introduces more pitfalls and less guarantees from the compiler.They are generic in lots of instances. A lock-free queue object is a generic container object which can be deployed safely. A threadsafe state-machine implementation which implements reliable and valid state transitions using atomics is generic. Most of the threadsafe tooling I've ever seen expose at the user-facing level is absolutely generic, and can be packaged as a safe and user-friendly abstraction.I have demonstrated these usability considerations in production. I am confident it's the right balance.Are these considerations the list below, or are they something else? If so, can you list them?I propose: 1. Normal people don't write thread-safety, a very small number of unusual people do this. I feel very good about biasing 100% of the cognitive load INSIDE the shared method. This means the expert, and ONLY the expert, must make decisions about thread-safety implementation.Thread safety is not easy. But it's also not generic.In terms of low-level things like atomics and lock-free implementations, those ARE generic and SHOULD only be written by experts. But other than that, you can't know how someone has designed all the conditions in their code.That guy is implementing the machinery, and he is the best possible person to validate that he delivered on his promises. Nobody should have to perform un-safety to interact with his promises. There is greater chance of user error than expert failure (users are, by definition, more numerous in number, and almost certainly less qualified).For example, you can have an expert write mutex locks and semaphores. But they can't tell you the proper order to lock different objects to ensure there's no deadlock. That's application specific.A mutex-style API has an element of un-safety by definition. My proposal doesn't affect lock and cast-away workflows. That workflow remains the same, and depends on unsafe interactions, and I don't think it's possible to arrange that any other way. What I'm trying to do is express another form of safe interaction tools with threadsafe devices, and in my work, I would use those exclusively. I have no use or desire for unsafe lock-and-cast workflows in our ecosystem. I'm trying to add a new possibility for expressing threadsafety that doesn't exist with strong guarantees today.You're resistant to implicit conversion to shared. Casting to shared is unsafe, and depending on them to yield thread-local ownership is a 'hope' at best; your worldview depends on users performing unsafe interactions with otherwise safe (threadsafe) API's. I'm trying to reposition away from that world into a safe-by-default place.2. Implicit conversion allows users to safely interact with safe things without doing unsafe casts. I think it's a complete design fail if you expect any user anywhere to perform an unsafe cast to call a perfectly thread-safe function. The user might not properly understand their obligations.I also do not expect anyone to perform unsafe casts in normal use. I expect them to use more generic well-written types in a shared-object library. Casting should be very rare.They are expressly broken whenever anyone has to cast to shared. That is unsafe, and they're expected to yield the thread-local ownership by convention. That is the cancer at the core of my worldview, which I'm trying to factor away. I think my proposal delivers on that very elegantly. There will be *no casts* anywhere, outside of the low-level implementation methods written by the expert. I am aiming for safe-by-default. If you want to step outside that place, you do so deliberate, and carefully, and it's easy to search for.3. The practical result of the above is, any complexity relating to safety is completely owned by the threadsafe author, and not cascaded to the user. You can't expect users to understand, and make correct decisions about threadsafety. Safety should be default position.I think these are great rules, and none are broken by keeping the explicit cast requirement in place.It's not safe. There may be a thread-local instance at any time, because we have no mechanism to concretely transfer ownership. You rely on unsafe cast and convention to yield ownership to perform the transition. I'm saying, I don't find that acceptable, and I'm designing for that reality, rather than wishful thinking.I recognise the potential loss of an unsafe optimised thread-local path. 1. This truly isn't a big deal. If this is really hurting you, you will notice on the profiler, and deploy a thread-exclusive path assuming the context supports it.This is a mischaracterization. The thread-local path is perfectly safe because only one thread can be accessing the data. That's why it's thread-local and not shared.But you can't safely call a threadsafe method, and you can't transition TL -> shared data. My proposal addresses those issues.2. I will trade that for confidence in safe interaction every day of the week. Safety is the right default position here.You can be confident that any shared data is properly synchronized via the API provided. No confidence should be lost here.It's defeated by the other considerations though. Implicit conversion is the only way to allow data to become shared safely, and the design works elegantly.2. You just need to make the unsafe thread-exclusive variant explicit, eg:It is explicit, the thread-exclusive variant is not marked shared, and cannot be called on data that is actually shared and needs synchronization.You want to do an unsafe thing. You need to prescribe convention when you want to do unsafe things. I don't recommend this, I'm telling you that you can do it in your case of desired optimisation.This is more design by convention.struct ThreadSafe { private int x; void unsafeIncrement() // <- make it explicit { ++x; // User has asserted that no sharing is possible, no reason to use atomics } void increment() shared { atomicIncrement(&x); // object may be shared } }Right, but you just said it; shared is useless by extension. I'm trying to reconcile shared with the current design in such a way to express a useful concept. I'm not undermining how thread-local is threadsafe by default, that promise still exists unchanged. What I'm saying is, promise of threadsafety must be true with respect to the threadlocal implementation, that is all. I think it's a reasonable definition for threadsafety, and the value is evident; you don't need to perform unsafe operations and convention to do interaction with threadsafe machinery (as it should be, because it is *threadsafe*).I think this is quiet a reasonable and clearly documented compromise. I think absolutely-reliably-threadsafe-by-default is the right default position. And if you want to accept unsafe operations for optimsation circumstances, then you're welcome to deploy that in your code as you see fit.All thread-local operations are thread-safe by default, because there can be only one thread using it. That is the beauty of the current regime, regardless of how broken shared is -- unshared is solid. We shouldn't want to break that guarantee.You provide no path to make an unshared thing shared. I don't think the language has any tools to express this; we can't express an ownership transfer. The mutually-exclusive shared-ness design fails here. I'm designing for reality.If the machinery is not a library for distribution and local to your application, and you know for certain that your context is such that thread-local and shared are mutually exclusive, then you're free to make the unshared overload not-threadsafe; you can do this because you know your application context. You just shouldn't make widely distributed tooling this way.I can make widely distributed tooling that does both shared and unshared versions of the code, and ALL are thread safe. No choices are necessary, no compromise on performance, and no design by convention.Right, but as I've tried to demonstrate, shared can't exist safely with this construction, no transition is possible. I consider that a non-starter, and my design addresses that reality.I will indeed do this myself in some cases, because I know those facts about my application. But I wouldn't compromise the default design of shared for this optimisation potential... deliberately deployed optimisation is okay to be unsafe when taken in context.Except it's perfectly thread safe to use data without synchronization in one thread -- which is supported by having unshared data. Unshared means only one thread. In your proposal, anything can be seen from one or more threads.
Oct 18 2018
On 17.10.18 20:46, Manu wrote:struct NotThreadsafe { int x; void local() { ++x; // <- invalidates the method below, you violate the other function's `shared` promise } void notThreadsafe() shared { atomicIncrement(&x); } }In the `shared` method you'd get a nice error when attempting `++x;`, because it's not thread-safe. With your proposal, it's just as unsafe in `local`, but you don't get any help from the compiler in spotting it. `local` is effectively ` trusted` without being marked as such.
Oct 17 2018
On 10/15/2018 11:46 AM, Manu wrote:[...]Shared has one incredibly valuable feature - it allows you, the programmer, to identify data that can be accessed by multiple threads. There are so many ways that data can be shared, the only way to comprehend what is going on is to build a wall around shared data. (The exception to this is immutable data. Immutable data does not need synchronization, so there is no need to distinguish between shared and unshared immutable data.) This falls completely apart if shared and unshared data can be aliased. When Andrei and I came up with the rules for: mutable const shared const shared immutable and which can be implicitly converted to what, so far nobody has found a fault in those rules. Timon Gehr has done a good job showing that they still stand unbreached. So, how can mutable data become shared, and vice versa? I've never found a way to do that that is proveably safe. Therefore, it's up to the programmer. If a programmer does the following: T* p; shared(T)* sp; p = cast(T*)sp; (1) sp = cast(shared(T)*) p; (2) those casts will be rejected in safe code. They'll have to be in trusted or system code. It will be up to the programmer to ensure (via mutexes, locks, uniqueness, etc.) that: (1) sp is a unique pointer and that ownership of the data is thus transferred for the lifetime of p (2) p is a unique pointer and that ownership of the data is thus transferred for the lifetime of sp A sensible program should try to minimize the places where there are these "gates" through the wall, as those gates will be where you'll be looking when threading bugs appear. Allowing implicit aliasing between shared and unshared data means the entire program needs to reviewed for threading bugs, rather than just the shared parts. One might as well just not have shared as a language feature at all. D being a systems programming language, one can certainly write code that way, just like one does with C, and with all of the threading bugs one gets doing that. ---------- Currently, the compiler allows you to read/write shared data without any locks or atomics. I.e. the compiler doesn't attempt to insert any synchronization on your behalf. Given all the ways synchronization can be done, it just seems presumptive and restrictive to impose a particular scheme. It's pretty much left up to the user. Which is why I recommend minimizing the amount of code that actually has to deal with shared data.
Oct 16 2018
On Tue, Oct 16, 2018 at 10:45 PM Walter Bright via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/15/2018 11:46 AM, Manu wrote:What does it mean 'aliased' precisely? I'm trying to prevent that in practical terms. If you can't read/write to a shared object... is it actually aliased? Or do you just have a fancy int that happens to look like a pointer?[...]Shared has one incredibly valuable feature - it allows you, the programmer, to identify data that can be accessed by multiple threads. There are so many ways that data can be shared, the only way to comprehend what is going on is to build a wall around shared data. (The exception to this is immutable data. Immutable data does not need synchronization, so there is no need to distinguish between shared and unshared immutable data.) This falls completely apart if shared and unshared data can be aliased.When Andrei and I came up with the rules for: mutable const shared const shared immutable and which can be implicitly converted to what, so far nobody has found a fault in those rules.Oh bollocks... everyone has been complaining about this for at least the 10 years I've been here! Shared is uninteresting and mostly useless as spec-ed, everyone knows this. Interaction with shared via barely-controlled blunt casting in trusted blocks is feeble and boring. It doesn't really give us anything in practice that we don't have in C++. It's an uninspired design. I am working against a design where *everything* may be shared, and it's a very interesting design space. I have no doubt it represents the future of my field. We are limited by what the type system can express on this front. I'm trying to explore opportunities to make shared interesting and useful... and I'm mostly being met with prejudice here. Eject what you think you know about shared from your brain, and try and take a fresh look from the perspective I'm proposing. I think you'll find that the current wisdom and interactions with shared are actually preserved in practice, but new opportunities become available. If not, I want to understand how the design is broken, so I can continue to iterate. If D offered a real advantage here over C++, this would really represent a strong attracting force.Timon Gehr has done a good job showing that they still stand unbreached.His last comment was applied to a different proposal. His only comment on this thread wasn't in response to the proposal in this thread. If you nominate Timon as your proxy, then he needs to destroy my proposal, or at least comment on it, rather than make some prejudiced comment generally.So, how can mutable data become shared, and vice versa? I've never found a way to do that that is proveably safe. Therefore, it's up to the programmer.I'm proposing a way, and that is: 1. the rule must be applied that shared object can not be read or written 2. attributing a method shared is a statement and a promise that the method is threadsafe The rest just follows naturally.If a programmer does the following: T* p; shared(T)* sp; p = cast(T*)sp; (1) sp = cast(shared(T)*) p; (2) those casts will be rejected in safe code. They'll have to be in trusted or system code. It will be up to the programmer to ensure (via mutexes, locks, uniqueness, etc.) that: (1) sp is a unique pointer and that ownership of the data is thus transferred for the lifetime of p (2) p is a unique pointer and that ownership of the data is thus transferred for the lifetime of sp'Transfer' is a nice idea, but it's not what's happening here. That's not what these operations do. Transferring ownership is a job for move semantics, and that's nowhere in sight. Also, there's no need to 'transfer' a this pointer to shared to call a threadsafe method. You can always just call it safely.A sensible program should try to minimize the places where there are these "gates" through the wall, as those gates will be where you'll be looking when threading bugs appear.I agree, that's my goal here. Shared right now is an unsafe mess; I want to apply aggressive restriction so that you can't do unsafe operations without explicit casts.Allowing implicit aliasing between shared and unshared data means the entire program needs to reviewed for threading bugs, rather than just the shared parts.That's backwards, I'm suggesting conversion TO shared, such that you are able to call threadsafe methods on thread-local things. There's no possible harm in that. I'm trying to eliminate aliasing by removing read/write access. The result is, the only thing you are able to do with a shared reference are explicitly threadsafe things. Any other operation is inaccessible without deliberate blunt casting, and the rules surrounding that are the same as always; those that you listed above.One might as well just not have shared as a language feature at all. D being a systems programming language, one can certainly write code that way, just like one does with C, and with all of the threading bugs one gets doing that.And this is a silly claim that's been made before in this thread. It doesn't make sense, and I kinda have to presume here you have either not read or do not understand my proposal... Are you suggesting that I hate shared, and I want to make it worthless, therefore I'm trying to change the rules to eliminate it from practical existence? I promise you, I'd *really* like it if shared were a _useful_ language feature. The suggestion that I intend to undermine it such that it not be a language feature at all is obviously ridiculous.---------- Currently, the compiler allows you to read/write shared data without any locks or atomics.Right, this is the core of the problem, and it must change for shared to model anything useful.I.e. the compiler doesn't attempt to insert any synchronization on your behalf. Given all the ways synchronization can be done, it just seems presumptive and restrictive to impose a particular scheme. It's pretty much left up to the user.But it's a useless model; it's just a boring sentinel tag with no functional application. The shared rules don't model any useful behaviour. I'm trying to express how shared could model a useful behaviour, and coincidentally be *more* conceptually safe, and intuitively model and communicate appropriate interactions with threadsafe objects.Which is why I recommend minimizing the amount of code that actually has to deal with shared data.Well, the future is SMP. 100% of our code potentially deals with shared data, and it would be idea to do so in a typesafe manner. We can't model this with C++. We can't model it in D either, because D's `shared` doesn't model anything... but with just the one change that shared can't read or write, we would have the foundation of something useful. Being able to read/write to shared objects is weird; it's completely unsafe in every event. It's a useless access right, and changing it such that shared can not read/write would improve the model in such a way that it starts to become useful.
Oct 17 2018
On Wednesday, 17 October 2018 at 07:20:20 UTC, Manu wrote:[snip] Oh bollocks... everyone has been complaining about this for at least the 10 years I've been here! [snip]As far as I had known from reading the forums, shared was not feature complete. Also, are you familiar with Atila's fearless library for safe sharing? https://github.com/atilaneves/fearless
Oct 17 2018
On 17.10.2018 09:20, Manu wrote:There is no "prejudice", just reasoning. Your proposal was "disallow member access on shared aggregates, allow implicit conversion from unshared to shared and keep everything else the same". This is a bad proposal. There may be a good proposal that allows the things you want, but you have not stated what they are, your OP was just: "look at this bad proposal, it might work, no?" I said no, then was met with some hostility. You should focus on finding a good proposal that achieves what you want without breaking the type system instead of attacking me.Timon Gehr has done a good job showing that they still stand unbreached.His last comment was applied to a different proposal. His only comment on this thread wasn't in response to the proposal in this thread. If you nominate Timon as your proxy, then he needs to destroy my proposal, or at least comment on it, rather than make some prejudiced comment generally.
Oct 17 2018
Jesus Manu, it's soon 8 pages of dancing around a trivial issue. Implicit casting from mutable to shared is unsafe, case closed. Explicit cast from mutable to unsafe, on the other hand: - is an assertion (on programmer's behalf) that this instance is indeed unique - is self-documenting - is greppable (especially if implemented as assumeShared or other descriptive name). I don't understand why are you so fixated on this. The other parts of your proposal are much more important.
Oct 17 2018
On Wednesday, 17 October 2018 at 13:36:53 UTC, Stanislav Blinov wrote:Explicit cast from mutable to unsafe, on the other hand:Blargh, to shared of course.
Oct 17 2018
On Wednesday, 17 October 2018 at 07:20:20 UTC, Manu wrote:Shared is uninteresting and mostly useless as spec-ed, everyone knows this. Interaction with shared via barely-controlled blunt casting in trusted blocks is feeble and boring. It doesn't really give us anything in practice that we don't have in C++. It's an uninspired design.+1 I'm pretty sure it's great in theory, in practice it's another invasive type constructor, and not a particular well-understood or easy to deal with one. The fact that this _type constructor_ finds its way into _identifiers_ create some concern: https://github.com/dlang/phobos/blob/656798f2b385437c239246b59e0433148190938c/std/experimental/allocator/package.d#L642
Oct 17 2018
On Wednesday, 17 October 2018 at 14:44:19 UTC, Guillaume Piolat wrote:The fact that this _type constructor_ finds its way into _identifiers_ create some concern: https://github.com/dlang/phobos/blob/656798f2b385437c239246b59e0433148190938c/std/experimental/allocator/package.d#L642Well, ISharedAllocator is indeed overboard, but at least `allocateShared` would've been a very useful identifier indeed.
Oct 17 2018
On 10/17/2018 12:20 AM, Manu wrote:What does it mean 'aliased' precisely?Aliasing means there are two paths to the same piece of data. That could be two pointers pointing to the same data, or one pointer to a variable that is accessible by name.It doesn't really give us anything in practice that we don't have in C++.It provides a standard, enforced way to distinguish shared data from unshared data, and no way to bypass it in safe code. There's no way to do that in C++.
Oct 19 2018
On Fri., 19 Oct. 2018, 3:10 am Walter Bright via Digitalmars-d, < digitalmars-d puremagic.com> wrote:On 10/17/2018 12:20 AM, Manu wrote:The reason I ask is because, by my definition, if you have: int* a; shared(int)* b = a; While you have 2 numbers that address the same data, it is not actually aliased because only `a` can access it. It is not aliased in any practical sense.What does it mean 'aliased' precisely?Aliasing means there are two paths to the same piece of data. That could be two pointers pointing to the same data, or one pointer to a variable that is accessible by name.It doesn't really give usRight, but we can do so much better. I want shared to model "what is thread- safe to do", because that models what you are able to do, and what API's should encourage when operating on `shared` things. Exclusively distinguishing shared and unshared data is not an interesting distinction if shared data has no access. I've been trying to say over and over; ignore what you think you know about that definition, accept my rules strictly as given (they're very simple and concise, there's only 2 rules), such that shared will mean "is threadsafe to call with this data" when applied to function args... Build the thought experiment outward from there. That's an interesting and useful definition for shared, and it leads to a framework where shared is useful in a fully safe SMP program, and even models safe transitions across unshared -> shared boundaries (parallel for, map/reduce, etc, fork and join style workloads), which are typical lock-free patterns. Lock-and-cast semantics are preserved unchanged for those that interact in the way shared is prescribed today, but strictly modeling that workflow is uninteresting, because it's unsafe by definition. I'm not losing that, but I'm trying to introduce a safe workflow that exists in complement, and my model works. I don't even know if we have a mutex defined in our codebase, we don't use them... but we can max out a 64core thread ripper.anything in practice that we don't have in C++.It provides a standard, enforced way to distinguish shared data from unshared data, and no way to bypass it in safe code. There's no way to do that in C++.
Oct 19 2018
On 10/19/2018 11:18 PM, Manu wrote:The reason I ask is because, by my definition, if you have: int* a; shared(int)* b = a; While you have 2 numbers that address the same data, it is not actually aliased because only `a` can access it.They are aliased, by code that believes it is unshared, and code that believes it is shared. This is not going to work.Exclusively distinguishing shared and unshared data is not an interesting distinction if shared data has no access.Somehow, you still have to find a way to give the shared path access, through a gate or a cast or a lock or whatever. And then it breaks, because two different threads are accessing the same data each thinking that data is not shared.
Oct 20 2018
On Saturday, 20 October 2018 at 09:04:17 UTC, Walter Bright wrote:Somehow, you still have to find a way to give the shared path access, through a gate or a cast or a lock or whatever. And then it breaks, because two different threads are accessing the same data each thinking that data is not shared.When you say that, then under Manu's proposal and the code below: class C { void f(); void g() shared; } void t1(shared C c) { c.g; // ok c.f; // error } void t2(shared C c) { c.g; // ok c.f; // error } auto c = new C(); spawn(&t1, c); spawn(&t2, c); c.f; // ok c.g; // ok Do you mean the implementation of C.g? Since that is shared wouldn't that just be a normal understanding that you'd need to synchronize the data access in shared since it's a shared method? And if you mean C.f, then if that accessed data (that was accessed by C.g) unsafely, then that's just a bad implementation no? Or? Cheers, - Ali
Oct 20 2018
On Saturday, 20 October 2018 at 16:18:53 UTC, aliak wrote:class C { void f(); void g() shared; } void t1(shared C c) { c.g; // ok c.f; // error } void t2(shared C c) { c.g; // ok c.f; // error } auto c = new C(); spawn(&t1, c); spawn(&t2, c); c.f; // ok c.g; // okThose are not "ok". They're only "ok" under Manu's proposal so long as the author of C promises (via documentation) that that's indeed "ok". There can be no statically-enforced guarantees that those calls are "ok", or that issuing them in that order is "ok". Yet Manu keeps insisting that somehow there is.
Oct 20 2018
On Saturday, 20 October 2018 at 16:41:41 UTC, Stanislav Blinov wrote:On Saturday, 20 October 2018 at 16:18:53 UTC, aliak wrote:Backing up a bit and making a few observations (after adding imports and wrapping the bottom code in a function): 1. the code above currently does not compile, error messages are: i. line 20 & 21: spawn fails to instantiate because c is not shared ii. line 23: shared method C.g is not callable using a non-shared object iii. the lines already marked // error 2. in order to fix 1.i, one must cast c to shared at the call site, this is not safe 3 fixing 1.ii requires doing `(cast(shared)c).g`, this is also not safe 4 fixing 1.iii fixing requires casting away shared, this is not only not safe, but also wrong. c is a class so one could try locking it although I'm not sure what the implications are for doing that when another thread owns the data, probably bad. 5 the current means of dealing with shared with lock and cast away shared is also not safe 6 under Manu's proposal reading and writing shared objects results in compilation error 7 The static guarantees we have in the language are type safety and safe 8 under Manu's proposal to do anything one must call shared functions on said object, this implies a " trusted" implementation at the bottom of the stack for ensuring thread safety (atomics and lock + cast (assuming it is not wrong), other sync primitives) that are not safe, but not outright wrong either. The question then becomes: assuming the implementation _is_ safe type correct and thread safe etc., can the author of C provide guarantees of safe and type correctness? and can this guarantee be free of false positives? Currently the answer is no: the requirement to cast to and from shared is un- safe and that burden is on the user which means that they must understand the inner workings of C to know it that is the case. Manu's proposal is slightly more interesting. shared becomes a guarantee that accesses to that object will not race, assuming that the trusted implementation at the bottom of the stack are correct. In the above if t1 and t2 took `const shared C` and `g` was also const shared, then I think that it could.class C { void f(); void g() shared; } void t1(shared C c) { c.g; // ok c.f; // error } void t2(shared C c) { c.g; // ok c.f; // error } auto c = new C(); spawn(&t1, c); // line 20 spawn(&t2, c); // line 21 c.f; // ok c.g; // ok // line 23Those are not "ok". They're only "ok" under Manu's proposal so long as the author of C promises (via documentation) that that's indeed "ok". There can be no statically-enforced guarantees that those calls are "ok", or that issuing them in that order is "ok". Yet Manu keeps insisting that somehow there is.
Oct 20 2018
On Sat, Oct 20, 2018 at 9:45 AM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Saturday, 20 October 2018 at 16:18:53 UTC, aliak wrote:I only insist that if you write a shared method, you promise that it is threadsafe. If f() undermines g() threadsafety, then **g() is NOT threadsafe**, and you just write an invalid program. You can write an invalid program in any imaginable number of ways; that's just not an interesting discussion. An interesting discussion is what we might to to help prevent writing such an invalid program... I don't suggest here what we can do to statically encorce this, but I suspect there does exist *some* options which may help, which can be further developments. What I also assert is that *this unsafe code is rare*... it exists only at the bottom of the tooling stack, and anyone else using a shared object will not do unsafe, and therefore will not be able to create the problem. If you do unsafety anywhere near `shared`, you should feel nervous. I'm trying to make a world where you aren't *required* to do unsafety at every single interaction. Understand: f() can only undermine g() promise of threadsafety **if f() is not safe**. Users won't create this situation accidentally, they can only do it deliberately.class C { void f(); void g() shared; } void t1(shared C c) { c.g; // ok c.f; // error } void t2(shared C c) { c.g; // ok c.f; // error } auto c = new C(); spawn(&t1, c); spawn(&t2, c); c.f; // ok c.g; // okThose are not "ok". They're only "ok" under Manu's proposal so long as the author of C promises (via documentation) that that's indeed "ok". There can be no statically-enforced guarantees that those calls are "ok", or that issuing them in that order is "ok". Yet Manu keeps insisting that somehow there is.
Oct 20 2018
On Saturday, 20 October 2018 at 18:30:59 UTC, Manu wrote:On Sat, Oct 20, 2018 at 9:45 AM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:--- module expertcode; safe: struct FileHandle { safe: void[] read(void[] storage) shared; void[] write(const(void)[] buffer) shared; } FileHandle openFile(string path); // only the owner can close void closeFile(ref FileHandle); void shareWithThreads(shared FileHandle*); // i.e. generate a number of jobs in some queue void waitForThreads(); // waits until all processing is done module usercode; import expertcode; void processHugeFile(string path) { FileHandle file = openFile(path); shareWithThreads(&file); // implicit cast waitForThreads(); file.closeFile(); } --- Per your proposal, everything in 'expertcode' can be written safe, i.e. not violating any of the points that safe forbids, or doing so only in a trusted manner. As far as the language is concerned, this would mean that processHugeFile can be safe as well. Remove the call to `waitForThreads()` (assume user just forgot that, i.e. the "accident"). Nothing would change for the compiler: all calls remain safe. And yet, if we're lucky, we get a consistent instacrash. If we're unlucky, we get memory corruption, or an unsolicited write to another currently open file, either of which can go unnoticed for some time. Of course the program becomes invalid if you do that, there's no question about it, this goes for all buggy code. The problem is, definition of "valid" lies beyond the type system: it's an agreement between different parts of code, i.e. between expert programmers who wrote FileHandle et al., and users who write processHugeFile(). The main issue is that certain *runtime* conditions can still violate safe-ty. Your proposal makes the language more strict wrt. to writing safe 'expertmodule', thanks to disallowing reads and writes through `shared`, which is great. However the implicit conversion to `shared` doesn't in any way improve the situation as far as user code is concerned, unless I'm still missing something.On Saturday, 20 October 2018 at 16:18:53 UTC, aliak wrote:I only insist that if you write a shared method, you promise that it is threadsafe. If f() undermines g() threadsafety, then **g() is NOT threadsafe**, and you just write an invalid program. You can write an invalid program in any imaginable number of ways; that's just not an interesting discussion. An interesting discussion is what we might to to help prevent writing such an invalid program... I don't suggest here what we can do to statically encorce this, but I suspect there does exist *some* options which may help, which can be further developments. What I also assert is that *this unsafe code is rare*... it exists only at the bottom of the tooling stack, and anyone else using a shared object will not do unsafe, and therefore will not be able to create the problem. If you do unsafety anywhere near `shared`, you should feel nervous. I'm trying to make a world where you aren't *required* to do unsafety at every single interaction. Understand: f() can only undermine g() promise of threadsafety **if f() is not safe**. Users won't create this situation accidentally, they can only do it deliberately.class C { void f(); void g() shared; }Those are not "ok". They're only "ok" under Manu's proposal so long as the author of C promises (via documentation) that that's indeed "ok". There can be no statically-enforced guarantees that those calls are "ok", or that issuing them in that order is "ok". Yet Manu keeps insisting that somehow there is.
Oct 20 2018
On 10/20/2018 11:30 AM, Manu wrote:You can write an invalid program in any imaginable number of ways; that's just not an interesting discussion.What we're discussing is not an invalid program, but what guarantees the type system can provide. D's current type system guarantees that a T* and a shared(T)* do not point to the same memory location in safe code. To get them to point to the same memory location, you've got to dip into system code, where *you* become responsible for maintaining the guarantees.
Oct 21 2018
On Sunday, 21 October 2018 at 09:04:34 UTC, Walter Bright wrote:On 10/20/2018 11:30 AM, Manu wrote:The only difference between this and Manu's proposal is when you need to dip into system code - in MP it's perfectly fine for the pointers to be equal, but when you want to read from or write to the address, you'll need to use system. In other words, the dip into system happens deeper in the codebase, meaning more code can be safe. -- SimenYou can write an invalid program in any imaginable number of ways; that's just not an interesting discussion.What we're discussing is not an invalid program, but what guarantees the type system can provide. D's current type system guarantees that a T* and a shared(T)* do not point to the same memory location in safe code. To get them to point to the same memory location, you've got to dip into system code, where *you* become responsible for maintaining the guarantees.
Oct 21 2018
On Sun., 21 Oct. 2018, 2:05 am Walter Bright via Digitalmars-d, < digitalmars-d puremagic.com> wrote:On 10/20/2018 11:30 AM, Manu wrote:My proposal guarantees that too, but in a more interesting way, because it opens the door to a whole working model. And it's totally safe. To get them to point to the same memory location, you've got to dip intoYou can write an invalid program in any imaginable number of ways; that's just not an interesting discussion.What we're discussing is not an invalid program, but what guarantees the type system can provide. D's current type system guarantees that a T* and a shared(T)* do not point to the same memory location in safe code.system code, where *you* become responsible for maintaining the guarantees.My model preserves that property. Why do you think I'm running that static guarantee? It's all irrelevant if you don't express any mechanism to *do* anything. Shared today does not have any use. It simply expresses that data *is* shared, and says nothing about what you can do with it. If you don't express a safe mechanism for interacting with shared data, then simply expressing the distinction of shared data really is completely uninteresting. It's just a marker that's mixed up in a bunch of unsafe code. I'm no more satisfied than I am with C++. Shared needs to do something; I propose that it strictly models operations that are threadsafe and semantic restrictions required to support that, and then you have a *usage* scheme, which is safe, and API conveys proper interaction.. not just an uninteresting marker. I'm genuinely amazed that you're not intrigued by a safe shared proposition. Nobly likes safe more than you. I could run our entire SMP stack 100% safe. I am going to fork D with this feature one way or another. It's the most meaningful and compelling opportunity I've seen in ever. If there's ever been a single thing that could truly move a bunch of C++ programmers, this is it. C++ can do a crappy job of modelling most stuff in D, but it simply can't go anywhere near this, and I've been working on competing C++ models for months. SMP is the future, we're going all-in this generation. Almost every function in our codebase runs in an SMP environment... And I was staggered that I was able to work this definition through to such a simple and elegant set of rules. I can't get my head around why people aren't more excited about this... fully safe SMP is huge!
Oct 21 2018
On 21/10/2018 10:41 PM, Manu wrote:On Sun., 21 Oct. 2018, 2:05 am Walter Bright via Digitalmars-d, <digitalmars-d puremagic.com <mailto:digitalmars-d puremagic.com>> wrote: On 10/20/2018 11:30 AM, Manu wrote: > You can write an invalid program in any imaginable number of ways; > that's just not an interesting discussion. What we're discussing is not an invalid program, but what guarantees the type system can provide. D's current type system guarantees that a T* and a shared(T)* do not point to the same memory location in safe code. My proposal guarantees that too, but in a more interesting way, because it opens the door to a whole working model. And it's totally safe. To get them to point to the same memory location, you've got to dip into system code, where *you* become responsible for maintaining the guarantees. My model preserves that property. Why do you think I'm running that static guarantee? It's all irrelevant if you don't express any mechanism to *do* anything. Shared today does not have any use. It simply expresses that data *is* shared, and says nothing about what you can do with it. If you don't express a safe mechanism for interacting with shared data, then simply expressing the distinction of shared data really is completely uninteresting. It's just a marker that's mixed up in a bunch of unsafe code. I'm no more satisfied than I am with C++. Shared needs to do something; I propose that it strictly models operations that are threadsafe and semantic restrictions required to support that, and then you have a *usage* scheme, which is safe, and API conveys proper interaction.. not just an uninteresting marker. I'm genuinely amazed that you're not intrigued by a safe shared proposition. Nobly likes safe more than you. I could run our entire SMP stack 100% safe. I am going to fork D with this feature one way or another. It's the most meaningful and compelling opportunity I've seen in ever. If there's ever been a single thing that could truly move a bunch of C++ programmers, this is it. C++ can do a crappy job of modelling most stuff in D, but it simply can't go anywhere near this, and I've been working on competing C++ models for months. SMP is the future, we're going all-in this generation. Almost every function in our codebase runs in an SMP environment... And I was staggered that I was able to work this definition through to such a simple and elegant set of rules. I can't get my head around why people aren't more excited about this... fully safe SMP is huge!I'm excited, but you need to write a DIP even if preliminary which shows both new semantics but also shows both working and current code to compare them.
Oct 21 2018
On Saturday, 20 October 2018 at 16:41:41 UTC, Stanislav Blinov wrote:Those are not "ok". They're only "ok" under Manu's proposal so long as the author of C promises (via documentation) that that's indeed "ok". There can be no statically-enforced guarantees that those calls are "ok", or that issuing them in that order is "ok". Yet Manu keeps insisting that somehow there is.No he is not insisting you can statically enforce thread safety. When I say ok, I mean assuming the implementer actually wrote correct code. This applies to any shared method today as well.
Oct 21 2018
On Sunday, 21 October 2018 at 11:25:16 UTC, aliak wrote:On Saturday, 20 October 2018 at 16:41:41 UTC, Stanislav Blinov wrote:I stand corrected, it would seem so.Those are not "ok". They're only "ok" under Manu's proposal so long as the author of C promises (via documentation) that that's indeed "ok". There can be no statically-enforced guarantees that those calls are "ok", or that issuing them in that order is "ok". Yet Manu keeps insisting that somehow there is.No he is not insisting you can statically enforce thread safety.When I say ok, I mean assuming the implementer actually wrote correct code. This applies to any shared method today as well.This ("ok") can only be achieved if the "implementor" (the "expert") writes every function self-contained, at which point sharing something from user code becomes a non-issue (i.e. it becomes unnecessary). But that's not a very useful API. As soon as you have more than one function operating on the same data, the onus is on the user (the caller) to call those functions in correct order, or, more generally, without invalidating the state of shared data.
Oct 21 2018
On Sunday, 21 October 2018 at 13:24:49 UTC, Stanislav Blinov wrote:On Sunday, 21 October 2018 at 11:25:16 UTC, aliak wrote:The onus is *always* on the user to write function calls in the correct order, multi-threading or not. We expect programmers to be able to figure out why this doesn't print 'Hello world!': void main() { import std.stdio; string hello; writeln(hello); hello = "Hello world!"; } We also expect writeln to be written in such a way that it doesn't corrupt random data or cause life-threatening situations just because hello was uninitialized upon calling writeln, and assigned afterwards. We should expect the same of multi-threaded programs. This places the onus of writing thread-safe code on the writer of the multi-threaded equivalent of writeln and string.opAssign. Only this way can the user of the library write code and not expect things to blow up in their face. -- SimenWhen I say ok, I mean assuming the implementer actually wrote correct code. This applies to any shared method today as well.This ("ok") can only be achieved if the "implementor" (the "expert") writes every function self-contained, at which point sharing something from user code becomes a non-issue (i.e. it becomes unnecessary). But that's not a very useful API. As soon as you have more than one function operating on the same data, the onus is on the user (the caller) to call those functions in correct order, or, more generally, without invalidating the state of shared data.
Oct 21 2018
On Saturday, 20 October 2018 at 09:04:17 UTC, Walter Bright wrote:On 10/19/2018 11:18 PM, Manu wrote:Quoting Wikipedia:The reason I ask is because, by my definition, if you have: int* a; shared(int)* b = a; While you have 2 numbers that address the same data, it is not actually aliased because only `a` can access it.They are aliased,two pointers A and B which have the same value, then the name A[0] aliases the name B[0]. In this case we say the pointers A and B alias each other. Note that the concept of pointer aliasing is not very well-defined – two pointers A and B may or may not alias each other, depending on what operations are performed in the function using A and B.In this case given the above: `a[0]` does not alias `b[0]` because `b[0]` is ill defined under Manu's proposal, because the memory referenced by `a` is not reachable through `b` because you can't read or write through `b`.by code that believes it is unsharedyou cannot ` safe`ly modify the memory through `b`, `a`'s view of the memory is unchanged in safe code.and, code that believes it is shared.you cannot have non-atomic access though `b`, `b` has no safe view of the memory, unless it is atomic (which by definition is synchronised).This is not going to work.Aú contraire.
Oct 20 2018
On Saturday, 20 October 2018 at 16:48:05 UTC, Nicholas Wilson wrote:On Saturday, 20 October 2018 at 09:04:17 UTC, Walter Bright wrote:And that's already a bug, because the language can't enforce threadsafe access through `a`, regardless of presence of `b`. Only the programmer can.On 10/19/2018 11:18 PM, Manu wrote:Quoting Wikipedia:The reason I ask is because, by my definition, if you have: int* a; shared(int)* b = a; While you have 2 numbers that address the same data, it is not actually aliased because only `a` can access it.They are aliased,two pointers A and B which have the same value, then the name A[0] aliases the name B[0]. In this case we say the pointers A and B alias each other. Note that the concept of pointer aliasing is not very well-defined – two pointers A and B may or may not alias each other, depending on what operations are performed in the function using A and B.In this case given the above: `a[0]` does not alias `b[0]` because `b[0]` is ill defined under Manu's proposal, because the memory referenced by `a` is not reachable through `b` because you can't read or write through `b`.by code that believes it is unsharedyou cannot ` safe`ly modify the memory through `b`, `a`'s view of the memory is unchanged in safe code.Synchronized with what? You still have `a`, which isn't `shared` and doesn't require any atomic access or synchronization. At this point it doesn't matter if it's an int or a struct. As soon as you share `a`, you can't just pretend that reading or writing `a` is safe. Encapsulate it all you want, safety only remains a contract of convention, the language can't enforce it.and, code that believes it is shared.you cannot have non-atomic access though `b`, `b` has no safe view of the memory, unless it is atomic (which by definition is synchronised).
Oct 20 2018
On Saturday, 20 October 2018 at 17:06:22 UTC, Stanislav Blinov wrote:On Saturday, 20 October 2018 at 16:48:05 UTC, Nicholas Wilson wrote:access through `a` is through the owned reference threadsafety through a does't mean anything, all _other_ access must ensure that the are ordered correctly.On Saturday, 20 October 2018 at 09:04:17 UTC, Walter Bright wrote:And that's already a bug, because the language can't enforce threadsafe access through `a`, regardless of presence of `b`. Only the programmer can.by code that believes it is unsharedyou cannot ` safe`ly modify the memory through `b`, `a`'s view of the memory is unchanged in safe code.Synchronized w.r.t any writes to that memory, e.g. from `a`.Synchronized with what? You still have `a`, which isn't `shared` and doesn't require any atomic access or synchronization.and, code that believes it is shared.you cannot have non-atomic access though `b`, `b` has no safe view of the memory, unless it is atomic (which by definition is synchronised).At this point it doesn't matter if it's an int or a struct.Yes.As soon as you share `a`, you can't just pretend that reading or writing `a` is safe.You can if no-one else writes to it, which is the whole point of Manu's proposal. Perhaps it should be const shared instead of shared but still.
Oct 20 2018
On 10/20/2018 11:08 AM, Nicholas Wilson wrote:You can if no-one else writes to it, which is the whole point of Manu's proposal. Perhaps it should be const shared instead of shared but still.There is no purpose whatsoever to data that can be neither read nor written. Shared data is only useful if, at some point, it is read/written, presumably by casting it to unshared in trusted code. As soon as that is done, you've got a data race with the other existing unshared aliases.
Oct 21 2018
On Sunday, 21 October 2018 at 09:58:18 UTC, Walter Bright wrote:On 10/20/2018 11:08 AM, Nicholas Wilson wrote:Just a thought: if a hard requirement is made on `shared` data to be non-copyable, a safe conversion could be guaranteed. But it can't be implicit either: shared(T) share(T)(T value) if (!is(T == shared) && !isCopyable!T) { shared(T) result = move(value); return result; } struct ShareableData { disable <postblit and/or copy ctor>; // Generated by compiler in presence of `shared` members and/or `shared` methods /* ... */ } void sendToThread(T)(shared T* ptr) safe; void usage() safe { int x; sendToThread(&x); // Error: 'x' is not shared shared y = x; // Ok sendToThread(&y); // Ok ShareableData data; sendToThread(&data); // Error: 'data' is not shared auto p = &data; sendToThread(p); // Error: *p is not shared auto sharedData = share(move(data)); sendToThread(&sharedData); // Ok auto yCopy = y; // Error: cannot copy 'shared' y auto dataCopy = sharedData; // Error: cannot copy 'shared' sharedData ShareableData otherData; sendToThread(cast(shared(ShareableData)*) &otherData); // Error non- safe cast in safe code } And again, we're back to 'once it's shared, it can't be safe-ly unshared', which ruins the distinction between owned and shared references, which is one of the nicer properties that Manu's proposal seems to want to achieve :(You can if no-one else writes to it, which is the whole point of Manu's proposal. Perhaps it should be const shared instead of shared but still.There is no purpose whatsoever to data that can be neither read nor written. Shared data is only useful if, at some point, it is read/written, presumably by casting it to unshared in trusted code. As soon as that is done, you've got a data race with the other existing unshared aliases.
Oct 21 2018
On Sunday, 21 October 2018 at 09:58:18 UTC, Walter Bright wrote:On 10/20/2018 11:08 AM, Nicholas Wilson wrote:No, because every part of the public interface has to work together to ensure thread-safety. This code is invalid (but compiles) under MP: module A; struct S { private int n; void foo() safe { n--; // Not thread-safe } void bar() shared trusted { atomicOp!"++"(n.assumeUnshared); } } module B; import A; void passToOtherThread(shared(S)*); // Calls S.bar() void main() { S* s = new S(); passToOtherThread(s); s.foo(); } The reason: foo() breaks bar()s promise of thread-safety. This means that S does not provide a thread-safe interface. It would be nice if the compiler could statically notice that, but I don't see how that'd work. Now, for a thread-safe version: module A; struct S { int n; void foo() trusted { atomicOp!"--"(n); // Thread-safe } void bar() shared trusted { atomicOp!"++"(n.assumeUnshared); } } module B; import A; void passToOtherThread(shared(S)*); // Calls S.bar() void main() { S* s = new S(); passToOtherThread(s); s.foo(); } In this case, passToOtherThread is free to call S.bar as often as it may feel like, since atomic operations are used in every possible access to S.n. This is true even though one thread has unshared access and other threads have shared access. -- SimenYou can if no-one else writes to it, which is the whole point of Manu's proposal. Perhaps it should be const shared instead of shared but still.There is no purpose whatsoever to data that can be neither read nor written. Shared data is only useful if, at some point, it is read/written, presumably by casting it to unshared in trusted code. As soon as that is done, you've got a data race with the other existing unshared aliases.
Oct 21 2018
On Sunday, 21 October 2018 at 09:58:18 UTC, Walter Bright wrote:On 10/20/2018 11:08 AM, Nicholas Wilson wrote:Indeed but there is a subtle difference between that and Manu's proposal: access through the shared variable may not have non-atomic reads, not no reads.You can if no-one else writes to it, which is the whole point of Manu's proposal. Perhaps it should be const shared instead of shared but still.There is no purpose whatsoever to data that can be neither read nor written.Shared data is only useful if, at some point, it is read/written,Yespresumably by casting it to unshared in trusted code.That is one way to do it, others include atomics and other trusted primitivesAs soon as that is done, you've got a data race with the other existing unshared aliases.You're in trusted code, that is the whole point. The onus is on the programmer to make that correct, same with regular safe/ trusted system code.
Oct 21 2018
On 21.10.18 17:54, Nicholas Wilson wrote:Not all of the parties that participate in the data race are in trusted code. The point of trusted is modularity: you manually check trusted code according to some set of restrictions and then you are sure that there is no memory corruption. Note that you are not allowed to look at any of the safe code while checking your trusted code. You will only see an opaque interface to the safe code that you call and all you know is that all the safe code type checks according to safe rules. Note that there might be an arbitrary number of safe functions and methods that you do not see. Think about it this way: you first write all the trusted and system code, and some evil guy who does not like you comes in after you looks at your code and writes all the safe code. If there is any memory corruption, it will be your fault and you will face harsh consequences. Now, design the safe type checking rules. It won't be MP! Note that there may well be a good way to get the good properties of MP without breaking the type system, but MP itself is not good because it breaks safe.As soon as that is done, you've got a data race with the other existing unshared aliases.You're in trusted code, that is the whole point. The onus is on the programmer to make that correct, same with regular safe/ trusted system code.
Oct 21 2018
On Sun, Oct 21, 2018 at 12:00 PM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 21.10.18 17:54, Nicholas Wilson wrote:Show me. Nobody has been able to show that yet. I'd really like to know this.Not all of the parties that participate in the data race are in trusted code. The point of trusted is modularity: you manually check trusted code according to some set of restrictions and then you are sure that there is no memory corruption. Note that you are not allowed to look at any of the safe code while checking your trusted code. You will only see an opaque interface to the safe code that you call and all you know is that all the safe code type checks according to safe rules. Note that there might be an arbitrary number of safe functions and methods that you do not see. Think about it this way: you first write all the trusted and system code, and some evil guy who does not like you comes in after you looks at your code and writes all the safe code. If there is any memory corruption, it will be your fault and you will face harsh consequences. Now, design the safe type checking rules. It won't be MP! Note that there may well be a good way to get the good properties of MP without breaking the type system, but MP itself is not good because it breaks safe.As soon as that is done, you've got a data race with the other existing unshared aliases.You're in trusted code, that is the whole point. The onus is on the programmer to make that correct, same with regular safe/ trusted system code.
Oct 21 2018
On Sun, 21 Oct 2018 12:04:16 -0700, Manu wrote:On Sun, Oct 21, 2018 at 12:00 PM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:If we only used your proposal and only used safe code, we wouldn't have any data races, but that's only because we wouldn't have any shared data. We'd have shared *variables*, but they would not contain any data we could read or alter, and that's pretty much useless. To use your proposal, we need to cast data back from shared to unshared. When it's unshared, we need to make sure that exactly one thread has a reference to that data as unshared. And safe *should* help us with that. Currently, it helps because casting unshared to shared is not safe, because it makes it trivial to get multiple threads with unshared references to the same data. And that's when you're using shared as expected rather than doing something weird.Note that there may well be a good way to get the good properties of MP without breaking the type system, but MP itself is not good because it breaks safe.Show me. Nobody has been able to show that yet. I'd really like to know this.
Oct 21 2018
On Sunday, 21 October 2018 at 22:12:18 UTC, Neia Neutuladh wrote:On Sun, 21 Oct 2018 12:04:16 -0700, Manu wrote:Only of the trusted implementation is thread safe, which it _should_ be. This is the same caveat as non threaded - safe/ tusted code.On Sun, Oct 21, 2018 at 12:00 PM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:If we only used your proposal and only used safe code, we wouldn't have any data races,Note that there may well be a good way to get the good properties of MP without breaking the type system, but MP itself is not good because it breaks safe.Show me. Nobody has been able to show that yet. I'd really like to know this.but that's only because we wouldn't have any shared data. We'd have shared *variables*, but they would not contain any data we could readReads must be atomic.or alter, and that's pretty much useless. To use your proposal, we need to cast data back from shared to unshared.Yes but this is in the trusted implementation that forms the basis of your threadsafety.When it's unshared, we need to make sure that exactly one thread has a reference to that data as unshared.Nod.And safe *should* help us with that.Nod.Currently, it helps because casting unshared to shared is not safe,This remains the case, and should be done (enforced by the compiler) only in trusted/ system code as a basis for thread safe, safe code.because it makes it trivial to get multiple threads with unshared references to the same data.That is trusted or system code and therefore is the programmers responsibility.And that's when you're using shared as expected rather than doing something weird.That forms the basis of your thread safe stack. From there on, the basis that shared arguments to functions are treated safely in the presence of threading means that code that calls the trusted implementations is safe.
Oct 21 2018
On Sun, Oct 21, 2018 at 3:15 PM Neia Neutuladh via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Sun, 21 Oct 2018 12:04:16 -0700, Manu wrote:I've shown an implementation of Atomic(T) 3 times now... no response any time. Why is it being dismissed? Do I need to write it more times? This is a clear demonstration of how to build the foundation of the safe threadsafe stack.On Sun, Oct 21, 2018 at 12:00 PM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:If we only used your proposal and only used safe code, we wouldn't have any data races, but that's only because we wouldn't have any shared data. We'd have shared *variables*, but they would not contain any data we could read or alter, and that's pretty much useless.Note that there may well be a good way to get the good properties of MP without breaking the type system, but MP itself is not good because it breaks safe.Show me. Nobody has been able to show that yet. I'd really like to know this.To use your proposal, we need to cast data back from shared to unshared. When it's unshared, we need to make sure that exactly one thread has a reference to that data as unshared.No, you just need to make sure that access is atomic or synchronised in a proper way, and if you do cast away shared in some low-level trusted function, make sure that reference doesn't escape. You can do it, I have faith.And safe *should* help us with that.Totally.Currently, it helps because casting unshared to shared is not safe, because it makes it trivial to get multiple threads with unshared references to the same data.No no, that's a massive smell. That means anytime anyone wants to distribute something, they need to perform unsafe casts. That's not okay. Modeling shared-ness/unshared-ness is not *useful* in any way that I have been able to identify. Modelling what it means to be threadsafe is useful in every application I've ever written. 100% of my SMP code works with my proposal, and something close to 0% works with shared as it is today. (assuming we desire safe interaction, which we do, because threading is hard enough already!)And that's when you're using shared as expected rather than doing something weird.No, I *expect* to use shared in safe code, and not write any unsafe code ever. shared doesn't model a useful interaction now, not in any way. Today, access to shared data members are unrestricted and completely unsafe, passing data into something like a parallel-for requires unsafe casts.
Oct 21 2018
On Sun, 21 Oct 2018 17:35:38 -0700, Manu wrote:On Sun, Oct 21, 2018 at 3:15 PM Neia Neutuladh via Digitalmars-d <digitalmars-d puremagic.com> wrote:Yes, Atomic can be implemented here as trusted code, and maybe using safe compiler intrinsics for some situations. That's useful! But it's not *enough*. It would require a lot of work to refit existing code to using only atomic operations, and it would end up looking rather clunky a lot of the time. If you're doing anything complex with a complex object graph, you're going to have a terrible time. You need to copy that object graph (atomically), which is going to be expensive and provides non-trivial restrictions on what you can store. So I'm not keen on a world in which all multithreading is using atomic structs. Unless, when you were talking about Atomic, you actually meant a wrapper containing some sort of lock, allowing you to submit arbitrary delegates to mutate the data within in a serialized way, similar to Atila Neves's fearless library. Which really stretches the definition of "atomic".If we only used your proposal and only used safe code, we wouldn't have any data races, but that's only because we wouldn't have any shared data. We'd have shared *variables*, but they would not contain any data we could read or alter, and that's pretty much useless.I've shown an implementation of Atomic(T) 3 times now... no response any time. Why is it being dismissed? Do I need to write it more times? This is a clear demonstration of how to build the foundation of the safe threadsafe stack.Casting thread-local to shared makes it easy to cause errors, and that's why it's a massive smell. Making it silent doesn't eliminate the smell.Currently, it helps because casting unshared to shared is not safe, because it makes it trivial to get multiple threads with unshared references to the same data.No no, that's a massive smell. That means anytime anyone wants to distribute something, they need to perform unsafe casts. That's not okay.100% of my SMP code works with my proposal, and something close to 0% works with shared as it is today. (assuming we desire safe interaction, which we do, because threading is hard enough already!)You want un-shared things to implicitly cast to shared. You don't want to have to allocate anything as shared, and you do want to pass absolutely anything to any thread. You can write a trusted assumeShared function, analogous to assumeUnique, to accomplish that. It would be slightly more awkward than what you're proposing, but it accomplishes one of your two goals: convert non-shared things to shared things in safe code. The other goal in your proposal is for some code that currently compiles not to. So you should be able to write the code you want, albeit with an increased risk of bugs. Or you could write a template that wraps a shared thing and forbids field access and assignment.Yes, and that's bad and should be changed.And that's when you're using shared as expected rather than doing something weird.No, I *expect* to use shared in safe code, and not write any unsafe code ever. shared doesn't model a useful interaction now, not in any way. Today, access to shared data members are unrestricted and completely unsafepassing data into something like a parallel-for requires unsafe casts.Or allocating data as shared, which is the recommended way, because that makes absolutely certain that, from the start, no code has an un-shared copy of that data. No casts needed.
Oct 21 2018
On 21.10.18 21:04, Manu wrote:On Sun, Oct 21, 2018 at 12:00 PM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:I just did, but if you really need to, give me a non-trivial piece of correct multithreaded code that accesses some declared-unshared field from a shared method and I will show you how the evil guy would modify some safe code in it and introduce race conditions. It needs to be your code, as otherwise you will just claim again that it is me who wrote bad trusted code.On 21.10.18 17:54, Nicholas Wilson wrote:Show me. Nobody has been able to show that yet. I'd really like to know this.Not all of the parties that participate in the data race are in trusted code. The point of trusted is modularity: you manually check trusted code according to some set of restrictions and then you are sure that there is no memory corruption. Note that you are not allowed to look at any of the safe code while checking your trusted code. You will only see an opaque interface to the safe code that you call and all you know is that all the safe code type checks according to safe rules. Note that there might be an arbitrary number of safe functions and methods that you do not see. Think about it this way: you first write all the trusted and system code, and some evil guy who does not like you comes in after you looks at your code and writes all the safe code. If there is any memory corruption, it will be your fault and you will face harsh consequences. Now, design the safe type checking rules. It won't be MP! Note that there may well be a good way to get the good properties of MP without breaking the type system, but MP itself is not good because it breaks safe.As soon as that is done, you've got a data race with the other existing unshared aliases.You're in trusted code, that is the whole point. The onus is on the programmer to make that correct, same with regular safe/ trusted system code.
Oct 21 2018
On Monday, 22 October 2018 at 00:38:33 UTC, Timon Gehr wrote:I just did,Link please?
Oct 21 2018
On 22.10.18 02:46, Nicholas Wilson wrote:On Monday, 22 October 2018 at 00:38:33 UTC, Timon Gehr wrote:https://forum.dlang.org/post/pqii8k$11u3$1 digitalmars.comI just did,Link please?
Oct 21 2018
On Monday, 22 October 2018 at 00:55:00 UTC, Timon Gehr wrote:On 22.10.18 02:46, Nicholas Wilson wrote:That contains no code.On Monday, 22 October 2018 at 00:38:33 UTC, Timon Gehr wrote:https://forum.dlang.org/post/pqii8k$11u3$1 digitalmars.comI just did,Link please?Not all of the parties that participate in the data race are in trusted code.There are two cases of trusted code, bad and good. Bad trusted code can indeed be misused by safe code to corrupt memory. Good trusted code cannot. What part of the proposal breaks the type system? The implicit conversion to shared implies that the passed to function is safe iff all the functions it calls are safe only call function with that parameter to other shared safe functions _OR_ they are trusted.The point of trusted is modularity: you manually check trusted code according to some set of restrictions and then you are sure that there is no memory corruption.Yes. And?
Oct 21 2018
On Sun, Oct 21, 2018 at 5:40 PM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 21.10.18 21:04, Manu wrote:There's no code there... just a presumption that the person who wrote the trusted code did not deliver the promise they made.On Sun, Oct 21, 2018 at 12:00 PM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:I just did,On 21.10.18 17:54, Nicholas Wilson wrote:Show me. Nobody has been able to show that yet. I'd really like to know this.Not all of the parties that participate in the data race are in trusted code. The point of trusted is modularity: you manually check trusted code according to some set of restrictions and then you are sure that there is no memory corruption. Note that you are not allowed to look at any of the safe code while checking your trusted code. You will only see an opaque interface to the safe code that you call and all you know is that all the safe code type checks according to safe rules. Note that there might be an arbitrary number of safe functions and methods that you do not see. Think about it this way: you first write all the trusted and system code, and some evil guy who does not like you comes in after you looks at your code and writes all the safe code. If there is any memory corruption, it will be your fault and you will face harsh consequences. Now, design the safe type checking rules. It won't be MP! Note that there may well be a good way to get the good properties of MP without breaking the type system, but MP itself is not good because it breaks safe.As soon as that is done, you've got a data race with the other existing unshared aliases.You're in trusted code, that is the whole point. The onus is on the programmer to make that correct, same with regular safe/ trusted system code.but if you really need to, give me a non-trivial piece of> correct multithreaded code that accesses some declared-unshared field from a shared method and I will show you how the evil guy would modify some safe code in it and introduce race conditions. It needs to be your code, as otherwise you will just claim again that it is me who wrote bad trusted code.You can pick on any of my prior code fragments. They've all been ignored so far.
Oct 21 2018
On 22.10.18 02:54, Manu wrote:On Sun, Oct 21, 2018 at 5:40 PM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:Yes, because there is no way to write trusted code that holds its promise while actually doing something interesting in multiple threads if safe code can implicitly convert from unshared to shared.On 21.10.18 21:04, Manu wrote:There's no code there... just a presumption that the person who wrote the trusted code did not deliver the promise they made. ...On Sun, Oct 21, 2018 at 12:00 PM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:I just did,On 21.10.18 17:54, Nicholas Wilson wrote:Show me. Nobody has been able to show that yet. I'd really like to know this.Not all of the parties that participate in the data race are in trusted code. The point of trusted is modularity: you manually check trusted code according to some set of restrictions and then you are sure that there is no memory corruption. Note that you are not allowed to look at any of the safe code while checking your trusted code. You will only see an opaque interface to the safe code that you call and all you know is that all the safe code type checks according to safe rules. Note that there might be an arbitrary number of safe functions and methods that you do not see. Think about it this way: you first write all the trusted and system code, and some evil guy who does not like you comes in after you looks at your code and writes all the safe code. If there is any memory corruption, it will be your fault and you will face harsh consequences. Now, design the safe type checking rules. It won't be MP! Note that there may well be a good way to get the good properties of MP without breaking the type system, but MP itself is not good because it breaks safe.As soon as that is done, you've got a data race with the other existing unshared aliases.You're in trusted code, that is the whole point. The onus is on the programmer to make that correct, same with regular safe/ trusted system code.I don't want "code fragments". Show me the real code. I manually browsed through posts now (thanks a lot) and found this implementation: struct Atomic(T){ void opUnary(string op : "++")() shared { atomicIncrement(&val); } private T val; } This is system code. There is no safe or trusted here, so I am ignoring it. Then I browsed some more, because I had nothing better to do, and I found this. I completed it so that it is actually compilable, except for the unsafe implicit conversion. Please read this code, and then carefully read the comments below it before you respond. I will totally ignore any of your answers that arrive in the next two hours. --- module borked; void atomicIncrement(int* p) system{ import core.atomic; atomicOp!("+=",int,int)(*cast(shared(int)*)p,1); } struct Atomic(T){ private T val; void opUnary(string op : "++")() shared trusted { atomicIncrement(cast(T*)&val); } } void main() safe{ Atomic!int i; auto a=&[i][0];// was: Atomic!int* a = &i; import std.concurrency; spawn((shared(Atomic!int)* a){ ++*a; }, a); ++i.val; // race } --- Oh no! The author of the trusted function (i.e. you) did not deliver on the promise they made! Now, before you go and tell me that I am stupid because I wrote bad code, consider the following: - It is perfectly safe to access private members from the same module. - You may not blame the my safe main function for the problem. It is safe, so it cannot be blamed for UB. Any UB is the result of a bad trusted function, a compiler bug, or hardware failure. - The only trusted function in this module was written by you. You said that there is a third implementation somewhere. If that one actually works, I apologize and ask you to please paste it again in this subthread.but if you really need to, give me a non-trivial piece of> correct multithreaded code that accesses some declared-unshared field from a shared method and I will show you how the evil guy would modify some safe code in it and introduce race conditions. It needs to be your code, as otherwise you will just claim again that it is me who wrote bad trusted code.You can pick on any of my prior code fragments. They've all been ignored so far.
Oct 22 2018
On Monday, 22 October 2018 at 10:26:14 UTC, Timon Gehr wrote:--- module borked; void atomicIncrement(int* p) system{ import core.atomic; atomicOp!("+=",int,int)(*cast(shared(int)*)p,1); } struct Atomic(T){ private T val; void opUnary(string op : "++")() shared trusted { atomicIncrement(cast(T*)&val); } } void main() safe{ Atomic!int i; auto a=&[i][0];// was: Atomic!int* a = &i; import std.concurrency; spawn((shared(Atomic!int)* a){ ++*a; }, a); ++i.val; // race } --- Oh no! The author of the trusted function (i.e. you) did not deliver on the promise they made!hi, if you change the private val in Atomic to be “private shared T val”, is the situation the same?
Oct 22 2018
On 22.10.18 14:39, Aliak wrote:On Monday, 22 October 2018 at 10:26:14 UTC, Timon Gehr wrote:It's a bit different, because then there is no implicit unshared->shared conversion happening, and this discussion is only about that. However, without further restrictions, you can probably construct cases where a safe function in one module escapes a private shared(T)* member to somewhere else that expects a different synchronization strategy. Therefore, even if we agree that unshared->shared conversion cannot be implicit in safe code, the 'shared' design is not complete, but it would be a good first step to agree that this cannot happen, such that we can then move on to harder issues. E.g. probably it would be good to have something like trusted data that cannot be manipulated from safe code, such that trusted functions can rely on some invariants.--- module borked; void atomicIncrement(int* p) system{ import core.atomic; atomicOp!("+=",int,int)(*cast(shared(int)*)p,1); } struct Atomic(T){ private T val; void opUnary(string op : "++")() shared trusted { atomicIncrement(cast(T*)&val); } } void main() safe{ auto a=new Atomic!int; import std.concurrency; spawn((shared(Atomic!int)* a){ ++*a; }, a); ++a.val; // race } --- Oh no! The author of the trusted function (i.e. you) did not deliver on the promise they made!hi, if you change the private val in Atomic to be “private shared T val”, is the situation the same?
Oct 22 2018
On 22.10.18 12:26, Timon Gehr wrote:--- module borked; void atomicIncrement(int* p) system{ import core.atomic; atomicOp!("+=",int,int)(*cast(shared(int)*)p,1); } struct Atomic(T){ private T val; void opUnary(string op : "++")() shared trusted { atomicIncrement(cast(T*)&val); } } void main() safe{ Atomic!int i; auto a=&[i][0];// was: Atomic!int* a = &i; import std.concurrency; spawn((shared(Atomic!int)* a){ ++*a; }, a); ++i.val; // race } ---Obviously, this should have been: --- module borked; void atomicIncrement(int*p) system{ import core.atomic; atomicOp!"+="(*cast(shared(int)*)p,1); } struct Atomic(T){ private T val; void opUnary(string op:"++")()shared trusted{ atomicIncrement(cast(T*)&val); } } void main() safe{ auto a=new Atomic!int; import std.concurrency; spawn((shared(Atomic!int)* a){ ++*a; }, a); ++a.val; // race } --- (I was short on time and had to fix Manu's code because it was not actually compilable.)
Oct 22 2018
On Mon, Oct 22, 2018 at 6:00 AM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 22.10.18 12:26, Timon Gehr wrote:Nitpick; atomicOp does not receive a shared arg under my proposal, it's not a threadsafe function by definition as discussed a few times.--- module borked; void atomicIncrement(int* p) system{ import core.atomic; atomicOp!("+=",int,int)(*cast(shared(int)*)p,1); } struct Atomic(T){ private T val; void opUnary(string op : "++")() shared trusted { atomicIncrement(cast(T*)&val); } } void main() safe{ Atomic!int i; auto a=&[i][0];// was: Atomic!int* a = &i; import std.concurrency; spawn((shared(Atomic!int)* a){ ++*a; }, a); ++i.val; // race } ---Obviously, this should have been: --- module borked; void atomicIncrement(int*p) system{ import core.atomic; atomicOp!"+="(*cast(shared(int)*)p,1); } struct Atomic(T){ private T val; void opUnary(string op:"++")()shared trusted{ atomicIncrement(cast(T*)&val); } } void main() safe{ auto a=new Atomic!int; import std.concurrency; spawn((shared(Atomic!int)* a){ ++*a; }, a); ++a.val; // race } --- (I was short on time and had to fix Manu's code because it was not actually compilable.)
Oct 22 2018
On Monday, 22 October 2018 at 10:26:14 UTC, Timon Gehr wrote:module borked; void atomicIncrement(int* p) system{ import core.atomic; atomicOp!("+=",int,int)(*cast(shared(int)*)p,1); } struct Atomic(T){ private T val; void opUnary(string op : "++")() shared trusted { atomicIncrement(cast(T*)&val); } } void main() safe{ Atomic!int i; auto a=&[i][0];// was: Atomic!int* a = &i; import std.concurrency; spawn((shared(Atomic!int)* a){ ++*a; }, a); ++i.val; // race } --- Oh no! The author of the trusted function (i.e. you) did not deliver on the promise they made! Now, before you go and tell me that I am stupid because I wrote bad code, consider the following: - It is perfectly safe to access private members from the same module.Yes, so you need to place the trusted code in a separate module. The trusted code will be very rare (Atomic!T, lockfree queue, mutex, semaphore...). This will be in the standard library, possibly some other library. You should not be writing your own implementations of these. You should not be writing trusted code without very good reason. You should be using safe functions all over your code if at all possible.- You may not blame the my safe main function for the problem. It is safe, so it cannot be blamed for UB. Any UB is the result of a bad trusted function, a compiler bug, or hardware failure.No, we blame the fact you have not blocked off non-thread-safe access to Atomic!T.val. What you have made is this: https://i.imgur.com/PnKMigl.jpg The piece of code to blame is the trusted function - you can't trust it, since non-thread-safe access to Atomic!T.val has not been blocked off. As long as anyone can extend its interface, Atomic!T can't be thread-safe. I'll quote myself:For clarity: the interface of a type is any method, function, delegate or otherwise that may affect its internals. That means any free function in the same module, and any non-private members.I've actually missed some possibilities there - member functions of other types in the same module must also count as part of the interface. Because of this wide net, modules that implement thread-safe types with shared methods should be short and sweet.- The only trusted function in this module was written by you. You said that there is a third implementation somewhere. If that one actually works, I apologize and ask you to please paste it again in this subthread.Here's the correct version: module atomic; void atomicIncrement(int* p) system { import core.atomic; atomicOp!("+=",int,int)(*cast(shared(int)*)p,1); } struct Atomic(T) { // Should probably mark this shared for extra safety, // but it's not strictly necessary private T val; void opUnary(string op : "++")() shared trusted { atomicIncrement(cast(T*)&val); } } --------- module unborked; import atomic; void main() safe { auto a = new Atomic!int; import std.concurrency; spawn((shared(Atomic!int)* a){ ++*a; }, a); //++i.val; // Cannot access private member } Once more, Joe Average Programmer should not be writing the trusted code in Atomic!T.opUnary - he should be using libraries written by people who have studied the exact issues that make multithreading hard. -- Simen
Oct 22 2018
On 22.10.18 15:26, Simen Kjærås wrote:Here's the correct version: module atomic; void atomicIncrement(int* p) system { import core.atomic; atomicOp!("+=",int,int)(*cast(shared(int)*)p,1); } struct Atomic(T) { // Should probably mark this shared for extra safety, // but it's not strictly necessary private T val; void opUnary(string op : "++")() shared trusted { atomicIncrement(cast(T*)&val); } } --------- module unborked; import atomic; void main() safe { auto a = new Atomic!int; import std.concurrency; spawn((shared(Atomic!int)* a){ ++*a; }, a); //++i.val; // Cannot access private member } Once more, Joe Average Programmer should not be writing the trusted code in Atomic!T.opUnary - he should be using libraries written by people who have studied the exact issues that make multithreading hard. -- Simenmodule reborked; import atomic; void main() safe{ auto a=new Atomic!int; import std.concurrency; spawn((shared(Atomic!int)* a){ ++*a; }, a); ++a.tupleof[0]; }
Oct 22 2018
On Monday, 22 October 2018 at 13:40:39 UTC, Timon Gehr wrote:module reborked; import atomic; void main() safe{ auto a=new Atomic!int; import std.concurrency; spawn((shared(Atomic!int)* a){ ++*a; }, a); ++a.tupleof[0]; }Finally! Proof that MP is impossible. On the other hand, why the hell is that safe? It breaks all sorts of guarantees about safety. At a minimum, that should be un- safe. Filed in bugzilla: https://issues.dlang.org/show_bug.cgi?id=19326 -- Simen
Oct 22 2018
On 22.10.18 16:09, Simen Kjærås wrote:On Monday, 22 October 2018 at 13:40:39 UTC, Timon Gehr wrote:Even if this is changed (and it probably should be), it does not fix the case where the safe function is in the same module. I don't think it is desirable to change the definition of trusted such that you need to check the entire module if it contains a single trusted function. If I can break safety of some (previously correct) code by editing only safe code, then that's a significant blow to safe. I think we need a general way to protect data from being manipulated in safe code in any way, same module or not.module reborked; import atomic; void main() safe{ auto a=new Atomic!int; import std.concurrency; spawn((shared(Atomic!int)* a){ ++*a; }, a); ++a.tupleof[0]; }Finally! Proof that MP is impossible. On the other hand, why the hell is that safe? It breaks all sorts of guarantees about safety. At a minimum, that should be un- safe. Filed in bugzilla: https://issues.dlang.org/show_bug.cgi?id=19326 -- Simen
Oct 22 2018
On Monday, 22 October 2018 at 14:31:28 UTC, Timon Gehr wrote:On 22.10.18 16:09, Simen Kjærås wrote:What do you mean by 'previously correct'? struct Array(T) { safe: private int* ptr; private int length; disable this(); this(int n) trusted { ptr = new int[n].ptr; length = n; foreach (ref e; ptr[0..length]) e = 123; } trusted ref int get(int idx) { assert(idx < length); return ptr[idx]; } } unittest { auto s = Array!int(1); assert(s.get(0) == 123); } Is this correct code? What if I add this: safe void bork(T)(ref Array!T s) { s.length *= 2; } unittest { auto s = Array!int(1); bork(s); assert(s.get(1) == 123); // Out of bounds! } -- SimenOn Monday, 22 October 2018 at 13:40:39 UTC, Timon Gehr wrote:Even if this is changed (and it probably should be), it does not fix the case where the safe function is in the same module. I don't think it is desirable to change the definition of trusted such that you need to check the entire module if it contains a single trusted function. If I can break safety of some (previously correct) code by editing only safe code, then that's a significant blow to safe. I think we need a general way to protect data from being manipulated in safe code in any way, same module or not.module reborked; import atomic; void main() safe{ auto a=new Atomic!int; import std.concurrency; spawn((shared(Atomic!int)* a){ ++*a; }, a); ++a.tupleof[0]; }Finally! Proof that MP is impossible. On the other hand, why the hell is that safe? It breaks all sorts of guarantees about safety. At a minimum, that should be un- safe. Filed in bugzilla: https://issues.dlang.org/show_bug.cgi?id=19326 -- Simen
Oct 22 2018
On Mon, Oct 22, 2018 at 7:35 AM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 22.10.18 16:09, Simen Kjærås wrote:I'm all ears.On Monday, 22 October 2018 at 13:40:39 UTC, Timon Gehr wrote:Even if this is changed (and it probably should be), it does not fix the case where the safe function is in the same module. I don't think it is desirable to change the definition of trusted such that you need to check the entire module if it contains a single trusted function. If I can break safety of some (previously correct) code by editing only safe code, then that's a significant blow to safe. I think we need a general way to protect data from being manipulated in safe code in any way, same module or not.module reborked; import atomic; void main() safe{ auto a=new Atomic!int; import std.concurrency; spawn((shared(Atomic!int)* a){ ++*a; }, a); ++a.tupleof[0]; }Finally! Proof that MP is impossible. On the other hand, why the hell is that safe? It breaks all sorts of guarantees about safety. At a minimum, that should be un- safe. Filed in bugzilla: https://issues.dlang.org/show_bug.cgi?id=19326 -- Simen
Oct 22 2018
On 10/22/2018 7:31 AM, Timon Gehr wrote:Even if this is changed (and it probably should be), it does not fix the case where the safe function is in the same module. I don't think it is desirable to change the definition of trusted such that you need to check the entire module if it contains a single trusted function. If I can break safety of some (previously correct) code by editing only safe code, then that's a significant blow to safe. I think we need a general way to protect data from being manipulated in safe code in any way, same module or not.One possible workaround is to use PIMPL. Another is to put the trusted privates in a separate module. The rationale behind module access to privates is: 1. avoid the whole ugly C++ "friend" complexity 2. presumably someone with privileges to edit a module can be trusted to behave 3. the module is the level of encapsulation
Oct 22 2018
On Monday, 22 October 2018 at 23:39:37 UTC, Walter Bright wrote:On 10/22/2018 7:31 AM, Timon Gehr wrote:Quick question, if a class import a module in its scope, does it carry its own copy when creating multiple objects with it? -Alex[...]One possible workaround is to use PIMPL. Another is to put the trusted privates in a separate module. The rationale behind module access to privates is: 1. avoid the whole ugly C++ "friend" complexity 2. presumably someone with privileges to edit a module can be trusted to behave 3. the module is the level of encapsulation
Oct 22 2018
On Tue, 23 Oct 2018 00:08:04 +0000, 12345swordy wrote:Quick question, if a class import a module in its scope, does it carry its own copy when creating multiple objects with it?Importing a module doesn't make a copy of any code. Each module contains only its own code. Importing modules makes it so you can access their code, nothing more. Instances of a class don't contain private copies of their own code. There's only one copy per executable. So even if importing a module made a copy of the code, you'd still only have one copy per type, not per object.
Oct 22 2018
On Tuesday, 23 October 2018 at 01:21:32 UTC, Neia Neutuladh wrote:On Tue, 23 Oct 2018 00:08:04 +0000, 12345swordy wrote:Huh, that what I figured. Though it not easy to test it as you literally have to create two files for this.Quick question, if a class import a module in its scope, does it carry its own copy when creating multiple objects with it?Importing a module doesn't make a copy of any code. Each module contains only its own code. Importing modules makes it so you can access their code, nothing more. Instances of a class don't contain private copies of their own code. There's only one copy per executable. So even if importing a module made a copy of the code, you'd still only have one copy per type, not per object.
Oct 22 2018
On Mon, Oct 22, 2018 at 7:10 AM Simen Kjærås via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Monday, 22 October 2018 at 13:40:39 UTC, Timon Gehr wrote:Yeah, that's shockingly dangerous for all sorts of reasons! I mean, is this really an argument to destroy my proposal, or are you just destroying safe in general?module reborked; import atomic; void main() safe{ auto a=new Atomic!int; import std.concurrency; spawn((shared(Atomic!int)* a){ ++*a; }, a); ++a.tupleof[0]; }Finally! Proof that MP is impossible. On the other hand, why the hell is that safe? It breaks all sorts of guarantees about safety. At a minimum, that should be un- safe. Filed in bugzilla: https://issues.dlang.org/show_bug.cgi?id=19326
Oct 22 2018
On 22.10.18 21:29, Manu wrote:On Mon, Oct 22, 2018 at 7:10 AM Simen Kjærås via Digitalmars-d <digitalmars-d puremagic.com> wrote:It was a way to satisfy Simen's (imho, arbitrary) constraint that the safe code should be in a different module.On Monday, 22 October 2018 at 13:40:39 UTC, Timon Gehr wrote:Yeah, that's shockingly dangerous for all sorts of reasons! I mean, is this really an argument to destroy my proposal,module reborked; import atomic; void main() safe{ auto a=new Atomic!int; import std.concurrency; spawn((shared(Atomic!int)* a){ ++*a; }, a); ++a.tupleof[0]; }Finally! Proof that MP is impossible. On the other hand, why the hell is that safe? It breaks all sorts of guarantees about safety. At a minimum, that should be un- safe. Filed in bugzilla: https://issues.dlang.org/show_bug.cgi?id=19326or are you just destroying safe in general?It is likely that there is trusted code in the wild that is currently broken because of the assumption that private data cannot be modified by untrusted actors. I think if we can have e.g. trusted data that cannot be manipulated at all from safe code (including taking addresses), a lot more is possible. shared on variables/fields could then imply trusted. It is then however still not clear that it makes sense to allow implicit conversion from unshared to shared. It may still be error prone, or even impossible to realize. For example, it could hypothetically be the case that each processor has its own address space and there is additionally some shared address space, in which case allocation would differ for data that is shared and data that is unshared. So I would again like to ask: why can't classes that want to be able to have their references implicitly converted to shared and then be sent to other threads not just make all members 'shared'? Under your proposal, they can never know that they have not already been implicitly converted to 'shared' anyway, so any unshared code already needs to take into account the possibility that there are other, concurrent accesses.
Oct 22 2018
On 10/22/2018 6:40 AM, Timon Gehr wrote:++a.tupleof[0];Yes, tupleof is known to break through the private access barrier. This is an issue, not sure if there's a bugzilla for it.
Oct 22 2018
On Mon, Oct 22, 2018 at 3:30 AM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 22.10.18 02:54, Manu wrote:How do my examples prior fail to hold their promise? struct S { private int x; void method() shared trusted { /* safely manipulate x */ } } How can you not trust that function? How can a 3rd party invalidate that functions promise? `x` is inaccessible. S can be thread-local or shared as much as you like, and it's safe, and I don't know how a 3rd party could safely undermine that?On Sun, Oct 21, 2018 at 5:40 PM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:Yes, because there is no way to write trusted code that holds its promise while actually doing something interesting in multiple threads if safe code can implicitly convert from unshared to shared.On 21.10.18 21:04, Manu wrote:There's no code there... just a presumption that the person who wrote the trusted code did not deliver the promise they made. ...On Sun, Oct 21, 2018 at 12:00 PM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:I just did,On 21.10.18 17:54, Nicholas Wilson wrote:Show me. Nobody has been able to show that yet. I'd really like to know this.Not all of the parties that participate in the data race are in trusted code. The point of trusted is modularity: you manually check trusted code according to some set of restrictions and then you are sure that there is no memory corruption. Note that you are not allowed to look at any of the safe code while checking your trusted code. You will only see an opaque interface to the safe code that you call and all you know is that all the safe code type checks according to safe rules. Note that there might be an arbitrary number of safe functions and methods that you do not see. Think about it this way: you first write all the trusted and system code, and some evil guy who does not like you comes in after you looks at your code and writes all the safe code. If there is any memory corruption, it will be your fault and you will face harsh consequences. Now, design the safe type checking rules. It won't be MP! Note that there may well be a good way to get the good properties of MP without breaking the type system, but MP itself is not good because it breaks safe.As soon as that is done, you've got a data race with the other existing unshared aliases.You're in trusted code, that is the whole point. The onus is on the programmer to make that correct, same with regular safe/ trusted system code.Last time I checked, main does not go in core.atomic. I've never added any code to core.atomic. These things wouldn't live in peoples code. I understand that my proposal relies on trusting a very small number of low-level implementations at the bottom of the stack... but they're strongly encapsulated, live in libraries, and if there existed a world where you COULD describe threadsafe interaction with shared; you would. trusted user code, especially where it related to `shared` would be terrifying, and you wouldn't do it. I understand that the situation you present is technically possible, but why would it happen in reality? I find the proposition of a safe threading stack to far far outweight that risk. It's also possible that tech may be improved to assist with this particular problem, I haven't tried to address it; I'm interested in if the rules are sound. One suggestion was to make `private int val` shared, then external functions have no access... that helps mitigate this particular mistake. It's back to this 1:many thing. There's one place you can possibly make this mistake (and it's probably maintained by an expert author), and it should be well encapsulated in a core lib; whereas the current `shared` requires that _end-users_ do unsafe casts all the time among user code, and they're almost certainly not experts. That's rigged totally backwards. The values are all wrong. If my scheme is sound above this issue, then it's reasonable to focus on ideas to reduce the odds of mistake for the one implementer of the low-level tool.I don't want "code fragments". Show me the real code. I manually browsed through posts now (thanks a lot) and found this implementation: struct Atomic(T){ void opUnary(string op : "++")() shared { atomicIncrement(&val); } private T val; } This is system code. There is no safe or trusted here, so I am ignoring it. Then I browsed some more, because I had nothing better to do, and I found this. I completed it so that it is actually compilable, except for the unsafe implicit conversion. Please read this code, and then carefully read the comments below it before you respond. I will totally ignore any of your answers that arrive in the next two hours. --- module borked; void atomicIncrement(int* p) system{ import core.atomic; atomicOp!("+=",int,int)(*cast(shared(int)*)p,1); } struct Atomic(T){ private T val; void opUnary(string op : "++")() shared trusted { atomicIncrement(cast(T*)&val); } } void main() safe{ Atomic!int i; auto a=&[i][0];// was: Atomic!int* a = &i; import std.concurrency; spawn((shared(Atomic!int)* a){ ++*a; }, a); ++i.val; // race } --- Oh no! The author of the trusted function (i.e. you) did not deliver on the promise they made! Now, before you go and tell me that I am stupid because I wrote bad code, consider the following: - It is perfectly safe to access private members from the same module. - You may not blame the my safe main function for the problem. It is safe, so it cannot be blamed for UB. Any UB is the result of a bad trusted function, a compiler bug, or hardware failure. - The only trusted function in this module was written by you. You said that there is a third implementation somewhere. If that one actually works, I apologize and ask you to please paste it again in this subthread.but if you really need to, give me a non-trivial piece of> correct multithreaded code that accesses some declared-unshared field from a shared method and I will show you how the evil guy would modify some safe code in it and introduce race conditions. It needs to be your code, as otherwise you will just claim again that it is me who wrote bad trusted code.You can pick on any of my prior code fragments. They've all been ignored so far.
Oct 22 2018
On 10/21/2018 11:58 AM, Timon Gehr wrote:[...]Thank you, Timon, for a nice explanation of what I was trying to express.
Oct 22 2018
On Mon, Oct 22, 2018 at 12:55 AM Walter Bright via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/21/2018 11:58 AM, Timon Gehr wrote:You removed whatever comment you're referring to. I don't understand any of Timon's posts in this thread at all, which is unusual because he's usually pretty clear.[...]Thank you, Timon, for a nice explanation of what I was trying to express.
Oct 22 2018
On 10/22/2018 1:42 AM, Manu wrote:You removed whatever comment you're referring to.If your newsreader cannot find the antecedent, you badly need to use a better one. Thunderbird handles this rather well, there's no reason to use an inferior one. Or just click the <- button: https://digitalmars.com/d/archives/digitalmars/D/shared_-_i_need_it_to_be_useful_320165.html#N320607I don't understand any of Timon's posts in this thread at all, which is unusual because he's usually pretty clear.I suspect Timon is equally frustrated at not getting his point across.
Oct 22 2018
On Sun, Oct 21, 2018 at 3:00 AM Walter Bright via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/20/2018 11:08 AM, Nicholas Wilson wrote:There is no primitive type with implicit threadsafety... nor need there be. Shared data simply can not be read or written in any threadsafe manner. This is a rock-solid reality, and the type system needs to reflect that reality as a fundamental premise. Only from there can we start to define meaningful threadsafety. All threadsafe interactions with anything involve calling functions. It's completely reasonable to make `shared` inhibit all read and write access to data. We can only call shared methods, because only real-code can implement threadsafety.You can if no-one else writes to it, which is the whole point of Manu's proposal. Perhaps it should be const shared instead of shared but still.There is no purpose whatsoever to data that can be neither read nor written.Shared data is only useful if, at some point, it is read/written, presumably by casting it to unshared in trusted code. As soon as that is done, you've got a data race with the other existing unshared aliases.If such a race is possible, then the trusted function is not threadsafe, so it is not trusted by definition. You wrote a bad trusted function, and you should feel bad. The simplest way to guarantee that no unsafe access is possible is to use encapsulation to assure no unregulated access exists.
Oct 21 2018
On 21.10.18 20:46, Manu wrote:I wonder where this "each piece of code is maintained by only one person and furthermore this is the only person that will suffer if the code has bugs" mentality comes from. It is very popular as well as obviously nonsense.Shared data is only useful if, at some point, it is read/written, presumably by casting it to unshared in trusted code. As soon as that is done, you've got a data race with the other existing unshared aliases.If such a race is possible, then the trusted function is not threadsafe, so it is not trusted by definition. You wrote a bad trusted function, and you should feel bad. ...The simplest way to guarantee that no unsafe access is possible is to use encapsulation to assure no unregulated access exists.This only works if untrusted programmers (i.e. programmers who are only allowed to write/modify safe code) are not allowed to change your class. I.e. it does not work.
Oct 21 2018
On Monday, 22 October 2018 at 00:32:35 UTC, Timon Gehr wrote:This only works if untrusted programmers (i.e. programmers who are only allowed to write/modify safe code) are not allowed to change your class. I.e. it does not work.This is the basis of the current safe/ trusted/ system model. Are you saying it is useless?
Oct 21 2018
On Sun, Oct 21, 2018 at 5:35 PM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 21.10.18 20:46, Manu wrote:Have you ever cracked open std::map and 'fixed' it because you thought it was bad? Of course not. Same applies here. Nobody 'fixes' core.atomic.Atomic without understanding what they're doing. You seem to be stuck on the detail whether you can trust the trusted author though... that is a reasonable point of debate, but it's a slightly separate topic. I am confident that the number of trusted functions required to found a useful stack are low, probably countable with fingers, and always in a library. If we can put aside that point of debate just for now; whether you feel the trusted author can be trusted, assuming that they can, does the model work? Can you break the model as I have presented it? If not; if the model is sound, then we can begin the discussion you're alluding to and talk about opportunities to improve on static guarantees for trusted authors, or ways to communicate their responsibility clearly, and patterns to assure success.I wonder where this "each piece of code is maintained by only one person and furthermore this is the only person that will suffer if the code has bugs" mentality comes from. It is very popular as well as obviously nonsense.Shared data is only useful if, at some point, it is read/written, presumably by casting it to unshared in trusted code. As soon as that is done, you've got a data race with the other existing unshared aliases.If such a race is possible, then the trusted function is not threadsafe, so it is not trusted by definition. You wrote a bad trusted function, and you should feel bad. ...The simplest way to guarantee that no unsafe access is possible is to use encapsulation to assure no unregulated access exists.This only works if untrusted programmers (i.e. programmers who are only allowed to write/modify safe code) are not allowed to change your class. I.e. it does not work.
Oct 21 2018
On 22.10.18 02:45, Manu wrote:On Sun, Oct 21, 2018 at 5:35 PM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:You are not proposing to let core.atomic.Atomic convert to shared implicitly, you are proposing to do that for all classes.On 21.10.18 20:46, Manu wrote:Have you ever cracked open std::map and 'fixed' it because you thought it was bad? Of course not. Same applies here. Nobody 'fixes' core.atomic.Atomic without understanding what they're doing.I wonder where this "each piece of code is maintained by only one person and furthermore this is the only person that will suffer if the code has bugs" mentality comes from. It is very popular as well as obviously nonsense.Shared data is only useful if, at some point, it is read/written, presumably by casting it to unshared in trusted code. As soon as that is done, you've got a data race with the other existing unshared aliases.If such a race is possible, then the trusted function is not threadsafe, so it is not trusted by definition. You wrote a bad trusted function, and you should feel bad. ...The simplest way to guarantee that no unsafe access is possible is to use encapsulation to assure no unregulated access exists.This only works if untrusted programmers (i.e. programmers who are only allowed to write/modify safe code) are not allowed to change your class. I.e. it does not work.You seem to be stuck on the detail whether you can trust the trusted author though...Again: the safe author is the problem.
Oct 21 2018
On Sun, Oct 21, 2018 at 5:55 PM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 22.10.18 02:45, Manu wrote:You can always implicitly convert to shared. Where did I ever say anything like that? I'm sure I've never said this. How do these transformations of what I've said keep happening?On Sun, Oct 21, 2018 at 5:35 PM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:You are not proposing to let core.atomic.Atomic convert to shared implicitly, you are proposing to do that for all classes.On 21.10.18 20:46, Manu wrote:Have you ever cracked open std::map and 'fixed' it because you thought it was bad? Of course not. Same applies here. Nobody 'fixes' core.atomic.Atomic without understanding what they're doing.I wonder where this "each piece of code is maintained by only one person and furthermore this is the only person that will suffer if the code has bugs" mentality comes from. It is very popular as well as obviously nonsense.Shared data is only useful if, at some point, it is read/written, presumably by casting it to unshared in trusted code. As soon as that is done, you've got a data race with the other existing unshared aliases.If such a race is possible, then the trusted function is not threadsafe, so it is not trusted by definition. You wrote a bad trusted function, and you should feel bad. ...The simplest way to guarantee that no unsafe access is possible is to use encapsulation to assure no unregulated access exists.This only works if untrusted programmers (i.e. programmers who are only allowed to write/modify safe code) are not allowed to change your class. I.e. it does not work.I don't follow. The safe author is incapable of doing threadsafety violation. They can only combine threadsafe functions. They can certainly produce a program that doesn't work, and they are capable of ordering issues, but that's not the same as data-race related crash bugs.You seem to be stuck on the detail whether you can trust the trusted author though...Again: the safe author is the problem.
Oct 21 2018
On 22.10.18 03:01, Manu wrote:On Sun, Oct 21, 2018 at 5:55 PM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:(Also, yes, some people do that because std::map does not provide an interface to augment the binary search tree.)On 22.10.18 02:45, Manu wrote:On Sun, Oct 21, 2018 at 5:35 PM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 21.10.18 20:46, Manu wrote:Have you ever cracked open std::map and 'fixed' it because you thought it was bad?I wonder where this "each piece of code is maintained by only one person and furthermore this is the only person that will suffer if the code has bugs" mentality comes from. It is very popular as well as obviously nonsense.Shared data is only useful if, at some point, it is read/written, presumably by casting it to unshared in trusted code. As soon as that is done, you've got a data race with the other existing unshared aliases.If such a race is possible, then the trusted function is not threadsafe, so it is not trusted by definition. You wrote a bad trusted function, and you should feel bad. ...The simplest way to guarantee that no unsafe access is possible is to use encapsulation to assure no unregulated access exists.This only works if untrusted programmers (i.e. programmers who are only allowed to write/modify safe code) are not allowed to change your class. I.e. it does not work.Yes, exactly what I said.You can always implicitly convert to shared.Of course not. Same applies here. Nobody 'fixes' core.atomic.Atomic without understanding what they're doing.You are not proposing to let core.atomic.Atomic convert to shared implicitly, you are proposing to do that for all classes.Where did I ever say anything like that? I'm sure I've never said this.??? I said that you are proposing to allow implicit conversions to shared for all classes, not only core.atomic.Atomic, and the last time you said it was the previous sentence of the same post.How do these transformations of what I've said keep happening? ...You literally said that nobody changes core.atomic.Atomic. Anyway, even if I bought that safe somehow should not be checked within druntime (I don't), bringing up this example does not make for a coherent argument why implicit conversion to shared should be allowed for all classes.They are capable of doing so as soon as you provide them a trusted function that treats data as shared that they can access as unshared.I don't follow. The safe author is incapable of doing threadsafety violation.You seem to be stuck on the detail whether you can trust the trusted author though...Again: the safe author is the problem.They can only combine threadsafe functions. They can certainly produce a program that doesn't work, and they are capable of ordering issues, but that's not the same as data-race related crash bugs.Accessing private members of aggregates in the same module is safe. tupleof is safe too.
Oct 22 2018
On Mon, Oct 22, 2018 at 6:40 AM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 22.10.18 03:01, Manu wrote:Sorry, I read the "not proposing to let" as something like "proposing to not let". So let me re-respond. Yes, I propose implicit conversion to shared. That's what makes it usable. Basically any argument to a fork/join, parallel-for, map/reduce, etc... they all require this transition. Our software is almost exclusively made up of those processes. Almost every type we have transits to shared contexts (with a restricted threadsafe API), and almost nothing we have could be allocated shared to the exclusion of thread-local access (like Atila likes to suggest). I don't know how to make use of the mutually-exclusive design of the current model in any code I've ever written. I've been writing SMP code since the xbox360 alpha-kit landed on my desk in 2006. As we've matured, we've seen mutexes disappear completely. Even discreet worker threads which used to use semaphores to signal new work arrival are dispappearing as it's becoming impossible to find enough distinct kinds of work for a discreet worker threads to do that keeps all cores busy. We break the work into tasks, build a DAG of the execution schedule with respect to data access dependencies, and parallel-for/reduce within tasks that operate over large volumes of data. We used to hide data sharing detail behind walls, but it moves into user-facing code as parallel-for appears, and that invokes the necessity to be able to express what is threadsafe at the type system level. I can assure, in our architecture at least, I am not aware of any case where a user would write a trusted function under my proposal. We could run our stack safe.Where did I ever say anything like that? I'm sure I've never said this.??? I said that you are proposing to allow implicit conversions to shared for all classes, not only core.atomic.Atomic, and the last time you said it was the previous sentence of the same post.That function is not trusted by definition.They are capable of doing so as soon as you provide them a trusted function that treats data as shared that they can access as unshared.I don't follow. The safe author is incapable of doing threadsafety violation.You seem to be stuck on the detail whether you can trust the trusted author though...Again: the safe author is the problem.I don't see any situation where any user would write code in the same module as core.atomic, or core.mutex, or wherever these couple of functions live.They can only combine threadsafe functions. They can certainly produce a program that doesn't work, and they are capable of ordering issues, but that's not the same as data-race related crash bugs.Accessing private members of aggregates in the same module is safe. tupleof is safe too.
Oct 22 2018
On Sat, Oct 20, 2018 at 10:10 AM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Saturday, 20 October 2018 at 16:48:05 UTC, Nicholas Wilson wrote:`b` can't read or write `a`... accessing `a` is absolutely safe. Someone must do something unsafe to undermine your threadsafety... and if you write unsafe code and don't know what you're doing, there's nothing that can help you. Today, every interaction with shared is unsafe. Creating a safe interaction with shared will lead to people not doing unsafe things at every step.On Saturday, 20 October 2018 at 09:04:17 UTC, Walter Bright wrote:And that's already a bug, because the language can't enforce threadsafe access through `a`, regardless of presence of `b`. Only the programmer can.On 10/19/2018 11:18 PM, Manu wrote:Quoting Wikipedia:The reason I ask is because, by my definition, if you have: int* a; shared(int)* b = a; While you have 2 numbers that address the same data, it is not actually aliased because only `a` can access it.They are aliased,two pointers A and B which have the same value, then the name A[0] aliases the name B[0]. In this case we say the pointers A and B alias each other. Note that the concept of pointer aliasing is not very well-defined – two pointers A and B may or may not alias each other, depending on what operations are performed in the function using A and B.In this case given the above: `a[0]` does not alias `b[0]` because `b[0]` is ill defined under Manu's proposal, because the memory referenced by `a` is not reachable through `b` because you can't read or write through `b`.by code that believes it is unsharedyou cannot ` safe`ly modify the memory through `b`, `a`'s view of the memory is unchanged in safe code.Synchronized with what? You still have `a`, which isn't `shared` and doesn't require any atomic access or synchronization. At this point it doesn't matter if it's an int or a struct. As soon as you share `a`, you can't just pretend that reading or writing `a` is safe.and, code that believes it is shared.you cannot have non-atomic access though `b`, `b` has no safe view of the memory, unless it is atomic (which by definition is synchronised).Encapsulate it all you want, safety only remains a contract of convention, the language can't enforce it.You're talking about trusted code again. You're fixated on unsafe interactions... my proposal is about SAFE interactions. I'm trying to obliterate unsafe interactions with shared.module expertcode; safe: struct FileHandle { safe: void[] read(void[] storage) shared; void[] write(const(void)[] buffer) shared; } FileHandle openFile(string path); // only the owner can close void closeFile(ref FileHandle); void shareWithThreads(shared FileHandle*); // i.e. generate a number of jobs in some queue void waitForThreads(); // waits until all processing is done module usercode; import expertcode; void processHugeFile(string path) { FileHandle file = openFile(path); shareWithThreads(&file); // implicit cast waitForThreads(); file.closeFile(); }This is a very strange program... I'm dubious it is in fact "expertcode"... but let's look into it. File handle seems to have just 2 methods... and they are both threadsafe. Open and Close are free-functions. Close does not promise threadsafety itself (but of course, it doesn't violate read/write's promise, or the program is invalid). I expect the only possible way to achieve this is by an internal mutex to make sure read/write/close calls are serialised. read and write will appropriately check their file-open state each time they perform their actions. What read/write do in the case of being called on a closed file... anyones guess? I'm gonna say they do no-op... they return a null pointer to indicate the error state. Looking at the meat of the program; you open a file, and distribute it to do accesses (I presume?).... Naturally, this is a really weird thing to do, because even if the API is threadsafe such that it doesn't crash and reads/writes are serialised, the sequencing of reads/writes will be random, so I don't believe any sane person (let alone an expert) would write this program... but moving on. Then you wait for them to finish, and close the file. Fine. You have a file with randomly interleaved data... for whatever reason.Per your proposal, everything in 'expertcode' can be written safe, i.e. not violating any of the points that safe forbids, or doing so only in a trusted manner. As far as the language is concerned, this would mean that processHugeFile can be safe as well.This program does appear to be safe (assuming that the implementations aren't invalid), but a very strange program nonetheless.Remove the call to `waitForThreads()` (assume user just forgot that, i.e. the "accident"). Nothing would change for the compiler: all calls remain safe.Yup.And yet, if we're lucky, we get a consistent instacrash. If we're unlucky, we get memory corruption, or an unsolicited write to another currently open file, either of which can go unnoticed for some time.Woah! Now this is way off-piste.. Why would get a crash? Why would get memory corruption? None of those things make sense. So, you call closeFile immediately and read/write start returning null. I'm going to assume that `shareWithThreads()` was implemented by an 'expert' who checked the function results for errors. It was detected that the reads/write failed, and an error "failed to read file" was emit, then the function returned promptly. The uncertainty of what happens in this program is however `shareWithThreads()` handles read/write emitting an error.Of course the program becomes invalid if you do that, there's no question about it, this goes for all buggy code.In this case, I wouldn't say the program becomes 'invalid'; it is valid for filesystem functions to return error states and you should handle them. In this case, read/write must return some "file not open" state, and it should be handled properly. This problem has nothing to do with threadsafety. It's a logic issue related to threading, but that's got nothing to do with this.The problem is, definition of "valid" lies beyond the type system: it's an agreement between different parts of code, i.e. between expert programmers who wrote FileHandle et al., and users who write processHugeFile(). The main issue is that certain *runtime* conditions can still violate safe-ty.Perhaps you don't understand what safe-ty means? It's a compiler assertion that the code is memory-safe. It's not a magic attribute that tells you that your program is right. Runtime conditions being in a valid state is a high-level problem for the program, and doesn't interacts with threadsafety in any fundamental way, and not in any way that safe has anything to do with. You're just describing normal high-level multi-threading logic problems. `shared` does not and can not help you with that; you need to look to libraries that offer threading support frameworks for that. It can help you not write code that does invalid access to memory and crash. That's the extent of its charter. If a `shared` API is designed well, it can also offer strong implicit advice about how to correctly interact with API's. The compiler will coerce you to do the right things with error messages.Your proposal makes the language more strict wrt. to writing safe 'expertmodule', thanks to disallowing reads and writes through `shared`, which is great. However the implicit conversion to `shared` doesn't in any way improve the situation as far as user code is concerned, unless I'm still missing something.It does, it eliminates unsafe user interactions. It must be that way to be safe. There were no casts above, it's great! And your program is safe! (although it's wrong) FWIW, I doubt anybody in their right mind would attempt to write a threadsafe filesystem API this way. Any such API would be structured COMPLETELY differently; it would likely have one `shared` method that would accept requests for deferred fulfillment, and handle unique objects associated with each request.
Oct 20 2018
On Sunday, 21 October 2018 at 05:47:14 UTC, Manu wrote:On Sat, Oct 20, 2018 at 10:10 AM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:Synchronized with what? You still have `a`, which isn't `shared` and doesn't require any atomic access or synchronization. At this point it doesn't matter if it's an int or a struct. As soon as you share `a`, you can't just pretend that reading or writing `a` is safe.`b` can't read or write `a`... accessing `a` is absolutely safe.It's not, with or without your proposal. The purpose of sharing `a` into `b` is to allow someone to access `*a` in a threadsafe way (but un- safe, as it *will* require casting away `shared` from `b`). That is what's making keeping an unshared reference `a` un- safe: whoever accesses `*a` in their trusted implementations via `*b` can't know that `*a` is being ( safe-ly!) accessed in a non-threadsafe way at the same time.Someone must do something unsafe to undermine your threadsafety... and if you write unsafe code and don't know what you're doing, there's nothing that can help you.Ergo, it follows that anyone that is making an implicit cast from mutable to shared better know what they're doing, which mere mortal users (not "experts") might not. I.e. it's a way to giving a loaded gun to someone who never held a weapon before.Today, every interaction with shared is unsafe.Nod.Creating a safe interaction with shared will lead to people not doing unsafe things at every step.Triple nod.I know... Manu, I *know* what you're trying to do. We (me, Atila, Timon, Walter...) are not opposing your goals, we're pointing out the weakest spot of your proposal, which, it would seem, would require more changes to the language than just disallowing reading/writing `shared` members.Encapsulate it all you want, safety only remains a contract of convention, the language can't enforce it.You're talking about trusted code again. You're fixated on unsafe interactions... my proposal is about SAFE interactions. I'm trying to obliterate unsafe interactions with shared.Why? That's literally the purpose of being able to `share`: you create/acquire a resource, share it, but keep a non-`shared` reference to yourself. If that's not required, you'd just create the data `shared` to begin with.module expertcode; safe: struct FileHandle { safe: void[] read(void[] storage) shared; void[] write(const(void)[] buffer) shared; } FileHandle openFile(string path); // only the owner can close void closeFile(ref FileHandle); void shareWithThreads(shared FileHandle*); // i.e. generate a number of jobs in some queue void waitForThreads(); // waits until all processing is done module usercode; import expertcode; void processHugeFile(string path) { FileHandle file = openFile(path); shareWithThreads(&file); // implicit cast waitForThreads(); file.closeFile(); }This is a very strange program...I'm dubious it is in fact "expertcode"... but let's look into it.You're fixating on it being file now. I give an abstract example, you dismiss it as contrived, I give a concrete one, you want to dismiss it as "strange". Heh, replace 'FileHandle' with 'BackBuffer', 'openFile' with 'acquireBackBuffer', 'shareWithThreads' with 'generateDrawCommands', 'waitForThreads' with 'gatherCommandsAndDraw', 'closeFile' with 'postProcessAndPresent' ;)File handle seems to have just 2 methods... and they are both threadsafe. Open and Close are free-functions.It doesn't matter if they're free functions or not. What matters is signature: they're taking non-`shared` (i.e. 'owned') reference. Methods are free functions in disguise.Close does not promise threadsafety itself (but of course, it doesn't violate read/write's promise, or the program is invalid).Yep, and that's the issue. It SHALL NOT violate threadsafety, but it can't promise such in any way :(I expect the only possible way to achieve this is by an internal mutex to make sure read/write/close calls are serialised.With that particular interface, yes.read and write will appropriately check their file-open state each time they perform their actions.Why? The only purpose of giving someone a `shared` reference is to give a reference to an open file. `shared` references can't do anything with the file but read and write, they would expect to be able to do so.What read/write do in the case of being called on a closed file... anyones guess? I'm gonna say they do no-op... they return a null pointer to indicate the error state. Looking at the meat of the program; you open a file, and distribute it to do accesses (I presume?)....Naturally, this is a really weird thing to do, because even if the API is threadsafe such that it doesn't crash and reads/writes are serialised, the sequencing of reads/writes will be random, so I don't believe any sane person (let alone an expert) would write this program... but moving on.Um, that's literally what std.stdio does, for writes at least, except it doesn't advertise `File` as `shared`. That's how we get interleaved, but not corrupted, output even when writing from multiple threads. Now, that's not *universally* useful, but nonetheless that's a valid use case.Then you wait for them to finish, and close the file. Fine. You have a file with randomly interleaved data... for whatever reason.Or I have command lists, or images loaded in background...This program does appear to be safe (assuming that the implementations aren't invalid), but a very strange program nonetheless.Remove the call to `waitForThreads()` (assume user just forgot that, i.e. the "accident"). Nothing would change for the compiler: all calls remain safe.Yup.And yet, if we're lucky, we get a consistent instacrash. If we're unlucky, we get memory corruption, or an unsolicited write to another currently open file, either of which can go unnoticed for some time.Woah! Now this is way off-piste.. Why would get a crash? Why would get memory corruption? None of those things make sense.Because the whole reason to have `shared` is to avoid the extraneous checks that you mentioned above, and only write actual useful code (i.e. lock-write-unlock, or read-put-to-queue-repeat, or whatever), not busy-work (testing if the file is open on every call). If you have a `shared` reference, it better be to existing data. If it isn't, the program is invalid already: you've shared something that doesn't "exist" (good for marketing, not so good for multithreading). That's why having `shared` and un-`shared` references to the same data simultaneously is not safe: you can't guarantee in any way that the owning thread doesn't invalidate the data through it's non-`shared` reference while you're doing your threadsafe `shared` work; you can only "promise" that by convention (documentation).So, you call closeFile immediately and read/write start returning null.And I have partially-read or partially-written data. Or Maybe I call closeFile(), main thread continues and opens another file, which gives the same file descriptor, `shared` references to FileHandle which the user forgot to wait on continue to work oblivious to the fact that it's a different file now. It's a horrible, but still safe, implementation of FileHandle, yes, but the caller (user) doesn't know that, and can't know that just from the interface. The only advice against that is "don't do that", but that's irrespective of your proposal.I'm going to assume that `shareWithThreads()` was implemented by an 'expert' who checked the function results for errors. It was detected that the reads/write failed, and an error "failed to read file" was emit, then the function returned promptly. The uncertainty of what happens in this program is however `shareWithThreads()` handles read/write emitting an error.But you can only find out about these errors in `waitForThreads`, the very call that the user "forgot" to make!There's no question about it, it *is* a logic error. The point is, it's a logic error that ultimately can lead to UB despite being safe. Just like this is: https://issues.dlang.org/show_bug.cgi?id=19316.In this case, I wouldn't say the program becomes 'invalid'; it is valid for filesystem functions to return error states and you should handle them. In this case, read/write must return some "file not open" state, and it should be handled properly. This problem has nothing to do with threadsafety. It's a logic issue related to threading, but that's got nothing to do with this.Of course the program becomes invalid if you do that, there'sno question about it, this goes for all buggy code.I know.The problem is, definition of "valid" lies beyond the type system: it's an agreement between different parts of code, i.e. between expert programmers who wrote FileHandle et al., and users who write processHugeFile(). The main issue is that certain *runtime* conditions can still violate safe-ty.Perhaps you don't understand what safe-ty means? It's a compiler assertion that the code is memory-safe. It's not a magic attribute that tells you that your program is right.Runtime conditions being in a valid state is a high-level problem for the program, and doesn't interacts with threadsafety in any fundamental way, and not in any way that safe has anything to do with.Yep.You're just describing normal high-level multi-threading logic problems. `shared` does not and can not help you with that; you need to look to libraries that offer threading support frameworks for that. It can help you not write code that does invalid access to memory and crash. That's the extent of its charter.I understand that. So... it would seem that your proposal focuses more on safe than on threadsafety?If a `shared` API is designed well, it can also offer strong implicit advice about how to correctly interact with API's. The compiler will coerce you to do the right things with error messages.Your proposal makes the language more strict wrt. to writing safe 'expertmodule', thanks to disallowing reads and writes through `shared`, which is great. However the implicit conversion to `shared` doesn't in any way improve the situation as far as user code is concerned, unless I'm still missing something.It does, it eliminates unsafe user interactions. It must be that way to be safe. There were no casts above, it's great! And your program is safe! (although it's wrong)It's safe, but it's wrong because it's not threadsafe. Yay! :DFWIW, I doubt anybody in their right mind would attempt to write a threadsafe filesystem API this way.std.stdio ;) (yes, I know there's no `shared` there, but that's what it does).Any such API would be structured COMPLETELY differently; it would likely have one `shared` method that would accept requests for deferred fulfillment, and handle unique objects associated with each request.Perhaps. How would the user know that?
Oct 21 2018
On Sunday, 21 October 2018 at 12:45:43 UTC, Stanislav Blinov wrote:On Sunday, 21 October 2018 at 05:47:14 UTC, Manu wrote:Then someone has not done their job. Since the pieces of code that will actually use the un- safe building blocks at the bottom are few and far between, it is reasonable to assume that an expert will be writing this code, and that such code be placed in a separate module where all access to the shared type is controlled. It seems you expect regular users to have calls to atomicOp!"++" scattered all over their code. I find this an unreasonable expectation, and fully agree that this will lead to problems.On Sat, Oct 20, 2018 at 10:10 AM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:Synchronized with what? You still have `a`, which isn't `shared` and doesn't require any atomic access or synchronization. At this point it doesn't matter if it's an int or a struct. As soon as you share `a`, you can't just pretend that reading or writing `a` is safe.`b` can't read or write `a`... accessing `a` is absolutely safe.It's not, with or without your proposal. The purpose of sharing `a` into `b` is to allow someone to access `*a` in a threadsafe way (but un- safe, as it *will* require casting away `shared` from `b`). That is what's making keeping an unshared reference `a` un- safe: whoever accesses `*a` in their trusted implementations via `*b` can't know that `*a` is being ( safe-ly!) accessed in a non-threadsafe way at the same time.No.Someone must do something unsafe to undermine your threadsafety... and if you write unsafe code and don't know what you're doing, there's nothing that can help you.Ergo, it follows that anyone that is making an implicit cast from mutable to shared better know what they're doing, which mere mortal users (not "experts") might not. I.e. it's a way to giving a loaded gun to someone who never held a weapon before.Can you demonstrate any system that can promise something like that? (apart from all-immutable)Close does not promise threadsafety itself (but of course, it doesn't violate read/write's promise, or the program is invalid).Yep, and that's the issue. It SHALL NOT violate threadsafety, but it can't promise such in any way :(Because otherwise it's not thread-safe. Exactly as you point out, the owner could call closeFile before some other thread was finished writing. If the implementer of FileHandle fails to take this into account, then no, it's not thread-safe.read and write will appropriately check their file-open state each time they perform their actions.Why? The only purpose of giving someone a `shared` reference is to give a reference to an open file. `shared` references can't do anything with the file but read and write, they would expect to be able to do so.Of course not. You can throw exceptions, you could add a destructor that reports on these errors, you could set an error flag somewhere and check that every now and then. The fact that you've managed to write a horribly broken API under MP and can't see a way to do better inside that system does not necessarily mean the problem is with MP. -- SimenI'm going to assume that `shareWithThreads()` was implemented by an 'expert' who checked the function results for errors. It was detected that the reads/write failed, and an error "failed to read file" was emit, then the function returned promptly. The uncertainty of what happens in this program is however `shareWithThreads()` handles read/write emitting an error.But you can only find out about these errors in `waitForThreads`, the very call that the user "forgot" to make!
Oct 21 2018
On Sun, Oct 21, 2018 at 5:50 AM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Sunday, 21 October 2018 at 05:47:14 UTC, Manu wrote:No, it is to assure that you write correct not-broken code.And yet, if we're lucky, we get a consistent instacrash. If we're unlucky, we get memory corruption, or an unsolicited write to another currently open file, either of which can go unnoticed for some time.Woah! Now this is way off-piste.. Why would get a crash? Why would get memory corruption? None of those things make sense.Because the whole reason to have `shared` is to avoid the extraneous checks that you mentioned above,and only write actual useful code (i.e. lock-write-unlock, or read-put-to-queue-repeat, or whatever), not busy-work (testing if the file is open on every call).`shared` is no comment on performance. You have written a slow locking API. If you care about perf, you would write a completely different API that favours perf. This not impossible, nor even particularly hard.If you have a `shared` reference, it better be to existing data.I mean, if I dereference a pointer, it had better not be null!If it isn't, the program is invalid already: you've shared something that doesn't "exist" (good for marketing, not so good for multithreading).I mean, if I dereference a pointer, it had better not be null!That's why having `shared` and un-`shared` references to the same data simultaneously is not safe: you can't guarantee in any way that the owning thread doesn't invalidate the data through it's non-`shared` reference while you're doing your threadsafe `shared` work; you can only "promise" that by convention (documentation).The owning thread is not a special actor. Your reasoning is wonky here. Lets say there is no owning instance, only a collection of shared instances... any one of them can theoretically do an operation that interferes with the others. That's issues for general threading, and no design for `shared` can (or should) interact with that problem. That's a problem for architecture.I expect you flushed before killing the file.So, you call closeFile immediately and read/write start returning null.And I have partially-read or partially-written data.Or Maybe I call closeFile(), main thread continues and opens another file, which gives the same file descriptor, `shared` references to FileHandle which the user forgot to wait on continue to work oblivious to the fact that it's a different file now.It's wild to suggest that ANY design for `shared` should somehow deal with the OS recycling a file handle... And it's still not an un- safe crash! It's just a program with a bug.It's a horrible, but still safe, implementation of FileHandle, yes, but the caller (user) doesn't know that, and can't know that just from the interface. The only advice against that is "don't do that", but that's irrespective of your proposal.No proposal can (or should) address these issues. You're concerned with an issue that is sooooo far left-field at this stage. Programming languages don't validate that you wrote a working bug-free program....what?I'm going to assume that `shareWithThreads()` was implemented by an 'expert' who checked the function results for errors. It was detected that the reads/write failed, and an error "failed to read file" was emit, then the function returned promptly. The uncertainty of what happens in this program is however `shareWithThreads()` handles read/write emitting an error.But you can only find out about these errors in `waitForThreads`, the very call that the user "forgot" to make![ ... snip ... ]You have to concede defeat at this point. Destroy my proposal with another legitimately faulty program.I understand that. So... it would seem that your proposal focuses more on safe than on threadsafety?I am trying to achieve safe-ty _with respect to threadsafety_. 'threadsafety' with respect to "proper program operation" is a job for programmers, and program architecture. No language attribute can make your program right.
Oct 21 2018
On Sunday, 21 October 2018 at 19:22:45 UTC, Manu wrote:On Sun, Oct 21, 2018 at 5:50 AM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:You can do that without `shared`.Because the whole reason to have `shared` is to avoid the extraneous checks that you mentioned above,No, it is to assure that you write correct not-broken code.You're conflating your assumptions about the code with the topic of this discussion. I can write a million examples and you'll still find a million reasons to talk about how they're incorrectly implemented, instead of focusing on merits or disadvantages of your proposal with the code given as is.and only write actual useful code (i.e. lock-write-unlock, or read-put-to-queue-repeat, or whatever), not busy-work (testing if the file is open on every call).`shared` is no comment on performance. You have written a slow locking API. If you care about perf, you would write a completely different API that favours perf. This not impossible, nor even particularly hard.Why would you share a null pointer?If you have a `shared` reference, it better be to existing data.I mean, if I dereference a pointer, it had better not be null!Why have it then at all? If it's not a "special" actor, just make all shared data `shared`. But your proposal specifically targets the conversion, suggesting you *do* need a special actor.That's why having `shared` and un-`shared` references to the same data simultaneously is not safe: you can't guarantee in any way that the owning thread doesn't invalidate the data through it's non-`shared` reference while you're doing your threadsafe `shared` work; you can only "promise" that by convention (documentation).The owning thread is not a special actor. Your reasoning is wonky here.So? The threads still weren't done yet.And I have partially-read or partially-written data.I expect you flushed before killing the file.Or Maybe I call closeFile(), main thread continues and opens another file, which gives the same file descriptor, `shared` references to FileHandle which the user forgot to wait on continue to work oblivious to the fact that it's a different file now.It's wild to suggest that ANY design for `shared` should somehow deal with the OS recycling a file handle...I'm not suggesting that at all, you've completely misrepresenting what I'm saying by splitting a quote.And it's still not an un- safe crash! It's just a program with a bug.Ok, if you say that sticking safe on code that can partially piece together data from unrelated sources is fine, then sure.I'm going to assume that `shareWithThreads()` was implemented by an 'expert' who checked the function results for errors...Please suggest another way of handling errors reported by other threads. More `shared` state?But you can only find out about these errors in `waitForThreads`, the very call that the user "forgot" to make!...what?I agree. No matter how hard I try or how many times I ask you to demonstrate, I still fail to see the value in assuming safe implicitly conversion of mutable data to shared. Instead of defending your proposal, you chose to attack the opposition. You've defeated me, flawless victory.[ ... snip ... ]You have to concede defeat at this point.Destroy my proposal with another legitimately faulty program.Is there a point? I post code, you: "nah, that's wrong". Steven posts code, you: "nah, that's wrong". Timon posts code, you: "nah, that's wrong". Walter posts code, you: "nah, that's wrong"... What's right then?
Oct 21 2018
On Sat, 20 Oct 2018 22:47:14 -0700, Manu wrote:Looking at the meat of the program; you open a file, and distribute it to do accesses (I presume?).... Naturally, this is a really weird thing to do, because even if the API is threadsafe such that it doesn't crash and reads/writes are serialised, the sequencing of reads/writes will be random, so I don't believe any sane person (let alone an expert) would write this program... but moving on. Then you wait for them to finish, and close the file. Fine. You have a file with randomly interleaved data... for whatever reason.I'd expect almost every nontrivial multithreaded program to do this. It's called a log file. You don't need to read data pointed at by a log file, but you do need to read the FILE* or the file descriptor. Database-like things using a journal file might need a shared file for both reading and writing.So, you call closeFile immediately and read/write start returning null.You start threads. You give them access to the log file. You wait for the threads to exit. Then you close the file.
Oct 21 2018
On Sat, Oct 20, 2018 at 2:05 AM Walter Bright via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/19/2018 11:18 PM, Manu wrote:This situation could only occur if you do unsafe code badly. Unlike today, where you must do unsafe code to do any interaction of any kind, you will be able to do fully-safe interaction with the stack of tooling. In that world, any unsafe code and *particularly* where it interacts with shared will be an obnoxious and scary code smell. If it was possible to interact with shared safely, it would be blindingly suspicious when people are likely to be shooting themselves in the foot. The situation you describe here is *exactly* what we have right now, and I'm trying to prevent that.The reason I ask is because, by my definition, if you have: int* a; shared(int)* b = a; While you have 2 numbers that address the same data, it is not actually aliased because only `a` can access it.They are aliased, by code that believes it is unshared, and code that believes it is shared.This is not going to workThis is an unfair dismissal. Have you tried it? I have. Write me the rules in a patch that I can take for a drive and demonstrate what the stack looks like.I'm not sure you've understood the proposal. This is the reason for the implicit conversion. It provides safe transition. That's why I'm so insistent on it.Exclusively distinguishing shared and unshared data is not an interesting distinction if shared data has no access.Somehow, you still have to find a way to give the shared path access, through a gate or a cast or a lock or whatever.And then it breaks, because two different threads are accessing the same data each thinking that data is not shared.This can only occur if you deliberately violate your safety, and then mess up. This is *exactly* the interaction prescribed to shared today. This is what I'm fixing by making a fully safe path! I think you demonstrate here that you haven't understood the reason, or the semantics of my proposal. I'm not sure how to clarify it, what can I give you?
Oct 20 2018
On 10/20/2018 11:24 AM, Manu wrote:This is an unfair dismissal.It has nothing at all to do with fairness. It is about what the type system guarantees in safe code. To repeat, the current type system guarantees in safe code that T* and shared(T)* do not point to the same memory location. Does your proposal maintain that or not? It's a binary question.I'm not sure you've understood the proposal. This is the reason for the implicit conversion. It provides safe transition.I don't see any way to make an implicit T* to shared(T)* safe, or vice versa. The T* code can create more aliases that the conversion doesn't know about, and the shared(T)* code can hand out aliases to other threads. So it all falls to pieces. Using a 'scope' qualifier won't work, because 'scope' isn't transitive, while shared is, i.e. U** and shared(U*)*.I'm not sure how to clarify it, what can I give you?Write a piece of code that does such an implicit conversion that you argue is safe. Make the code as small as possible. Your example:int* a; shared(int)* b = a;This is not safe. ---- Manu's Proposal --- safe: int i; int* a = &i; StartNewThread(a); // Compiles! Coder has no idea! ... in the new thread ... void StartOfNewThread(shared(int)* b) { ... we have two threads accessing 'i', one thinks it is shared, the other unshared, and StartOfNewThread() has no idea and anyone writing code for StartOfNewThread() has no way to know anything is wrong ... lockedIncrement(b); // Data Race! } --- Current D --- safe: int i; int* a = &i; StartNewThread(a); // Danger, Will Robinson! Does Not Compile! StartNewThread(cast(shared(int)*) a) // Danger, Will Robinson! // Unsafe Cast! Does Not Compile! --- Your proposal means that the person writing the lockedIncrement(), which is a perfectly reasonable thing to do, simply cannot write it in a way that has a safe interface, because the person writing the lockedIncrement() library function has no way to know that the data it receives is actually unshared data. I.e. trusted code is obliged to proved a safe interface. Your proposal makes that impossible because the compiler would allow unshared data to be implicitly typed as shared.
Oct 21 2018
On Sunday, 21 October 2018 at 09:50:09 UTC, Walter Bright wrote:On 10/20/2018 11:24 AM, Manu wrote:No. Instead, it proposes something more useful: once cast to shared(T)*, only thread-safe operations may be performed on it.This is an unfair dismissal.It has nothing at all to do with fairness. It is about what the type system guarantees in safe code. To repeat, the current type system guarantees in safe code that T* and shared(T)* do not point to the same memory location. Does your proposal maintain that or not? It's a binary question.Under MP, this is perfectly safe - you can do nothing with a shared(int)*, except call un- safe, non-thread-safe functions on it, which will *fail to compile* under safe.int* a; shared(int)* b = a;This is not safe.---- Manu's Proposal --- safe: int i; int* a = &i; StartNewThread(a); // Compiles! Coder has no idea! ... in the new thread ... void StartOfNewThread(shared(int)* b) { ... we have two threads accessing 'i', one thinks it is shared, the other unshared, and StartOfNewThread() has no idea and anyone writing code for StartOfNewThread() has no way to know anything is wrong ... lockedIncrement(b); // Data Race! }Someone's messed up if they've marked lockedIncrement safe - under MP, it shouldn't be. lockedIncrement is a very low-level piece of functionality, and should be system. It also shouldn't take a shared(int)*, but a int*, forcing an unsafe cast and making it obvious the code is un safe. -- Simen
Oct 21 2018
On Sun., 21 Oct. 2018, 2:55 am Walter Bright via Digitalmars-d, <digitalmars-d puremagic.com> wrote:On 10/20/2018 11:24 AM, Manu wrote:By the definition Nick pulled from Wikipedia and posted for you a few posts back, yes, my proposal satisfies Wikipedia's definition of no aliasing. I understand that property is critical, and I have carefully designed for it.This is an unfair dismissal.It has nothing at all to do with fairness. It is about what the type system guarantees in safe code. To repeat, the current type system guarantees in safe code that T* and shared(T)* do not point to the same memory location. Does your proposal maintain that or not? It's a binary question.T* can't make additional T* aliases on other threads; there can only be one thread with T*. shared(T)* can not make a T*. shared(T)* has no read or write access, so it's not an alias of T* by Wikipedia's definition. Only threadsafe functions can do anything to T. The leap of faith is; some trusted utility functions at the bottom of the shared stack makes a promise that it is threadsafe, and must deliver that promise. I don't think this is unreasonable; this is the nature of trusted functions, they make a promise, and they must keep it. If the trusted function does not lie, then the chain of trust holds upwards through the stack. The are very few such trusted functions in practise. Like, similar to the number of digits you have.I'm not sure you've understood the proposal. This is the reason for the implicit conversion. It provides safe transition.I don't see any way to make an implicit T* to shared(T)* safe, or vice versa. The T* code can create more aliases that the conversion doesn't know about, and the shared(T)* code can hand out aliases to other threads. So it all falls to pieces.Using a 'scope' qualifier won't work, because 'scope' isn't transitive, while shared is, i.e. U** and shared(U*)*.I don't think I depend on scope in any way. That was an earlier revision of thinking in an older thread.> I'm not sure how to clarify it, what can I give you? Write a piece of code that does such an implicit conversion that you argue is safe. Make the code as small as possible. Your example: > int* a; > shared(int)* b = a; This is not safe. ---- Manu's Proposal --- safe: int i; int* a = &i; StartNewThread(a); // Compiles! Coder has no idea! ... in the new thread ... void StartOfNewThread(shared(int)* b) { ... we have two threads accessing 'i', one thinks it is shared, the other unshared, and StartOfNewThread() has no idea and anyone writing code for StartOfNewThread() has no way to know anything is wrong ... lockedIncrement(b); // Data Race! }This program doesn't compile. You receive an error because it is not safe. The function is `lockedIncrement(int*)`. It can't receive a shared argument; the function is not threadsafe by my definition. You have written a program that produces the expected error that alerts you that you have tried to do un- safe and make a race. Stanislav produced this same program, and I responded with the correct program a few posts back. I'll repeat it here; the safe program to model this interaction is: safe: // function is NOT threadsafe by my definition, can not be called on shared arguments void atomicIncrement(int*); struct Atomic(T) { // encapsulare the unsafe data so it's inaccessible by any unsafe means private T val; // perform the unsafe cast in a trusted function // we are able to assure a valid calling context by encapsulating the data above void opUnary(string op : "++")() shared trusted { atomicIncrement(cast(T*)&val); } } Atomic!int i; Atomic!int* a = &i; StartNewThread(a); // Compiles, of course! ++i; // no race ... in the new thread ... void StartOfNewThread(shared(Atomic!int)* b) { ... we have two threads accessing 'i', one has thread-local access, this one has a restricted shared access. here, we have a shared instance, so we can only access `b` via threadsafe functions. as such, we can manipulate `b` without fear. ++i; // no race! }Your proposal means that the person writing the lockedIncrement(), which is a perfectly reasonable thing to do, simply cannot write it in a way that has a safe interfaceCorrect, the rules of my proposal apply to lockedIncrement(). They apply to `shared` generally. lockedIncrement() is not a threadsafe function. You can't call it on a shared instance, because `int`s API (ie, all intrinsic operations) are not threadsafe. lockedIncrement() can't promise threadsafe access to `shared(int)*`, so the argument is not shared. Your program made the correct compile error about doing unsafety, but the location of the compile error is different under my proposal; complexity is worn by the shared library author, rather than every calling user ever. I think my proposal places the complexity in the right location. `shared` is intrinsically dangerous; it's not reasonable to ask every user that ever calls a shared API to write unsafe code when when calling. That's just plain bad design.because the person writing the lockedIncrement() library function has no way to know that the data it receives is actually unshared data.The author of `shared` tooling must assure a valid context such that its threadsafety promises are true. Atomic(T) does that with a private member. The safe way to interact with atomics is to use the Atomic utility type I showed above. That is one such trusted tool that I talk about as being "at the bottom of the stack". It is probably joined by a mutex/semaphore, and some containers/queues. That is probably all the things, and other things would be safe compositions of those tools.I.e. trusted code is obliged to proved a safe interface. Your proposal makes that impossible because the compiler would allow unshared data to be implicitly typed as shared.What? No. Please, try and understand my proposal...
Oct 21 2018
I'd like to add that if the compiler can prove that a T* points to a unique T, then it can be implicitly cast to shared(T)*. And it does so, like the result of .dup can be so converted.
Oct 21 2018
On Sunday, 21 October 2018 at 18:45:15 UTC, Walter Bright wrote:I'd like to add that if the compiler can prove that a T* points to a unique T, then it can be implicitly cast to shared(T)*. And it does so, like the result of .dup can be so converted.This can be achieved by using the unique struct and enforce the uniqueness at compile time. https://github.com/dlang/phobos/blob/master/std/typecons.d#L130
Oct 21 2018
On Sunday, 21 October 2018 at 09:50:09 UTC, Walter Bright wrote:---- Manu's Proposal --- safe: int i; int* a = &i; StartNewThread(a); // Compiles! Coder has no idea! ... in the new thread ... void StartOfNewThread(shared(int)* b) { ... we have two threads accessing 'i', one thinks it is shared, the other unshared, and StartOfNewThread() has no idea and anyone writing code for StartOfNewThread() has no way to know anything is wrong ... lockedIncrement(b); // Data Race!No, does not compile, lockedIncrement takes an int* Error cannot convert shared(int)* to int*Your proposal means that the person writing the lockedIncrement(), which is a perfectly reasonable thing to do,Indeed.simply cannot write it in a way that has a safe interface, because the person writing the lockedIncrement() library function has no way to know that the data it receives is actually unshared data.It does, it takes an int* which is not implicitly convertible to given an shared(int)*I.e. trusted code is obliged to proved a safe interface.Yes.Your proposal makes that impossible because the compiler would allow unshared data to be implicitly typed as shared.Yes, but not the other way around.
Oct 21 2018
On Sunday, 21 October 2018 at 19:07:37 UTC, Nicholas Wilson wrote:On Sunday, 21 October 2018 at 09:50:09 UTC, Walter Bright wrote:Whoops that should readYour proposal makes that impossible because the compiler would allow unshared data to be implicitly typed as shared.Yes, but not the other way around.NoYour proposal makes that impossibleYes, but the problem you describe is arises from implicit conversion in the other direction, which is not part of the proposal.because the compiler would allow unshared data to be implicitly typed as shared.
Oct 21 2018
On 10/21/2018 12:20 PM, Nicholas Wilson wrote:Yes, but the problem you describe is arises from implicit conversion in the other direction, which is not part of the proposal.It's Manu's example.
Oct 21 2018
On 10/21/2018 2:08 PM, Walter Bright wrote:On 10/21/2018 12:20 PM, Nicholas Wilson wrote:Then I don't know what the proposal is. Pieces of it appear to be scattered over numerous posts, mixed in with other text, opinions, and handwavy stuff. There's nothing to point to that is "the proposal". I suggest you and Manu write up a proper proposal. Something that is complete, has nothing else in it, has a rationale, illuminating examples, and explains why alternatives are inferior. For examples of how to do it: https://github.com/dlang/DIPs/tree/master/DIPs Trying to rewrite the semantics of shared is not a simple task, doing multithreading correctly is a minefield of "OOPS! I didn't think of that!" and if anything cries out for a DIP, your and Manu's proposal does.Yes, but the problem you describe is arises from implicit conversion in the other direction, which is not part of the proposal.It's Manu's example.
Oct 21 2018
On Sunday, 21 October 2018 at 21:32:14 UTC, Walter Bright wrote:On 10/21/2018 2:08 PM, Walter Bright wrote:The proposal is: Implicit conversion _to_ shared, e.g. passing it to a thread entry point, and not implicit conversion _from_ shared (just like implicit const conversions). Shared disables reads and writes Your confusion results from the use of atomic add which, in Manu's examples had a different signature than before.On 10/21/2018 12:20 PM, Nicholas Wilson wrote:Then I don't know what the proposal is. Pieces of it appear to be scattered over numerous posts, mixed in with other text, opinions, and handwavy stuff. There's nothing to point to that is "the proposal".Yes, but the problem you describe is arises from implicit conversion in the other direction, which is not part of the proposal.It's Manu's example.I suggest you and Manu write up a proper proposal. Something that is complete, has nothing else in it, has a rationale, illuminating examples, and explains why alternatives are inferior.We will eventually. This started as a "please point out any problems with this" and has probably outlived that phase.Trying to rewrite the semantics of shared is not a simple task,Not as much as trying to explain it! Having talked to Manu in person it is much easier to understand.doing multithreading correctly is a minefield of "OOPS! I didn't think of that!"The above case in point, this is about assuming your implementation of thread safe primitives are thread safe ( trusted) that you use it correctly ( safe).
Oct 21 2018
On 10/21/2018 4:12 PM, Nicholas Wilson wrote:On Sunday, 21 October 2018 at 21:32:14 UTC, Walter Bright wrote:That's what I was referring to, and Manu's example. It doesn't work, as I pointed out.On 10/21/2018 2:08 PM, Walter Bright wrote:The proposal is: Implicit conversion _to_ shared, e.g. passing it to a thread entry point, and not implicit conversion _from_ shared (just like implicit const conversions).On 10/21/2018 12:20 PM, Nicholas Wilson wrote:Then I don't know what the proposal is. Pieces of it appear to be scattered over numerous posts, mixed in with other text, opinions, and handwavy stuff. There's nothing to point to that is "the proposal".Yes, but the problem you describe is arises from implicit conversion in the other direction, which is not part of the proposal.It's Manu's example.You'll need to address the issues raised here in the DIP.I suggest you and Manu write up a proper proposal. Something that is complete, has nothing else in it, has a rationale, illuminating examples, and explains why alternatives are inferior.We will eventually. This started as a "please point out any problems with this" and has probably outlived that phase.
Oct 21 2018
On Monday, 22 October 2018 at 00:46:04 UTC, Walter Bright wrote:That's what I was referring to, and Manu's example. It doesn't work, as I pointed out.I'm pretty sure it does, but please repeat it.That is a given. You would do well to heed it for your own DIPs.We will eventually. This started as a "please point out any problems with this" and has probably outlived that phase.You'll need to address the issues raised here in the DIP.
Oct 21 2018
On Sun, Oct 21, 2018 at 5:50 PM Walter Bright via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/21/2018 4:12 PM, Nicholas Wilson wrote:Would you please respond to my messages, and specifically, respond to the code that I presented to you in response to your broken example. Or any of my earlier fragments throughout this thread. I've shared quite a few, and so far, nobody has ever produced a criticism of any of my fragments. They've just been skipped over. But the one aimed directly at your own most recent sample program addresses your program directly.On Sunday, 21 October 2018 at 21:32:14 UTC, Walter Bright wrote:That's what I was referring to, and Manu's example. It doesn't work, as I pointed out.On 10/21/2018 2:08 PM, Walter Bright wrote:The proposal is: Implicit conversion _to_ shared, e.g. passing it to a thread entry point, and not implicit conversion _from_ shared (just like implicit const conversions).On 10/21/2018 12:20 PM, Nicholas Wilson wrote:Then I don't know what the proposal is. Pieces of it appear to be scattered over numerous posts, mixed in with other text, opinions, and handwavy stuff. There's nothing to point to that is "the proposal".Yes, but the problem you describe is arises from implicit conversion in the other direction, which is not part of the proposal.It's Manu's example.You'll need to address the issues raised here in the DIP.I suggest you and Manu write up a proper proposal. Something that is complete, has nothing else in it, has a rationale, illuminating examples, and explains why alternatives are inferior.We will eventually. This started as a "please point out any problems with this" and has probably outlived that phase.
Oct 21 2018
On 10/21/2018 5:54 PM, Manu wrote:Would you please respond to my messages, and specifically, respond to the code that I presented to you in response to your broken example. Or any of my earlier fragments throughout this thread. I've shared quite a few, and so far, nobody has ever produced a criticism of any of my fragments. They've just been skipped over.That's just the problem. You've posted 62 messages so far in this thread, and then there's all the ones Nicholas posted. Trying to assemble the "earlier fragments throughout this thread" is not practical for readers, and the endless nature of this thread is ample evidence for it. The n.g. is a place to discuss a proposal, not the proposal itself. This change is definitely merits an actual proposal DIP, so that one is assured of seeing the complete proposal, rationale, examples, etc., in one document, as well as not being distracted by sidebars, thread drift, and mistakes. This document can evolve with corrections and clarifications from the discussion, and anyone can get up to speed quickly by just reading the latest version of it.But the one aimed directly at your own most recent sample program addresses your program directly.My most recent sample program was a direct criticism of one of your fragments, so please don't say "nobody has ever ...". I do understand your frustration at finding it hard to get your point across, but the problem at least for me is trying to mine it from nuggets scattered across 62 posts. Mine it, refine it, cast it into an ingot, then present it as a DIP.
Oct 22 2018
On Mon, Oct 22, 2018 at 12:50 AM Walter Bright via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/21/2018 5:54 PM, Manu wrote:I sent it twice... again just a short while ago right before this one... but you responded to this one and not that one O_oWould you please respond to my messages, and specifically, respond to the code that I presented to you in response to your broken example. Or any of my earlier fragments throughout this thread. I've shared quite a few, and so far, nobody has ever produced a criticism of any of my fragments. They've just been skipped over.That's just the problem. You've posted 62 messages so far in this thread, and then there's all the ones Nicholas posted.Trying to assemble the "earlier fragments throughout this thread" is not practical for readers, and the endless nature of this thread is ample evidence for it. The n.g. is a place to discuss a proposal, not the proposal itself. This change is definitely merits an actual proposal DIP, so that one is assured of seeing the complete proposal, rationale, examples, etc., in one document, as well as not being distracted by sidebars, thread drift, and mistakes. This document can evolve with corrections and clarifications from the discussion, and anyone can get up to speed quickly by just reading the latest version of it.Okay, but I still want you to respond to my corrections of your program, which were in direct response to you... twice.> But the one aimed directly at your own most recent sample program > addresses your program directly. My most recent sample program was a direct criticism of one of your fragments, so please don't say "nobody has ever ...". I do understand your frustration at finding it hard to get your point across, but the problem at least for me is trying to mine it from nuggets scattered across 62 posts. Mine it, refine it, cast it into an ingot, then present it as a DIP.I posted it, twice... 2 messages, back to back, and you're responding to this one, and not that one. I'll post it again...
Oct 22 2018
On 10/22/2018 1:34 AM, Manu wrote:I posted it, twice... 2 messages, back to back, and you're responding to this one, and not that one. I'll post it again...Posting it over and over is illustrative of the failure of posting proposal documents to the n.g. instead of posting it as a DIP which can be referred to: 1. nobody knows which of your 70 messages are the ones with the proposal in it 2. with multiple posts of the proposal, nobody knows which one is the most up-to-date one Doing it this way does not work. Continuing to repost it is a waste of your time. Post it as a DIP and link to it.
Oct 22 2018
On 22/10/2018 10:28 PM, Walter Bright wrote:On 10/22/2018 1:34 AM, Manu wrote:As I've said previously, it doesn't need to be a good DIP or anywhere near complete. It just needs code examples comparing current and proposed behavior with some text about semantics changes.I posted it, twice... 2 messages, back to back, and you're responding to this one, and not that one. I'll post it again...Posting it over and over is illustrative of the failure of posting proposal documents to the n.g. instead of posting it as a DIP which can be referred to: 1. nobody knows which of your 70 messages are the ones with the proposal in it 2. with multiple posts of the proposal, nobody knows which one is the most up-to-date one Doing it this way does not work. Continuing to repost it is a waste of your time. Post it as a DIP and link to it.
Oct 22 2018
On 10/22/2018 2:31 AM, rikki cattermole wrote:As I've said previously, it doesn't need to be a good DIP or anywhere near complete. It just needs code examples comparing current and proposed behavior with some text about semantics changes.That's right, and it can be evolved so everyone involved can easily see what the latest is, and not get mired in and distracted by resolved issues.
Oct 22 2018
On Mon, Oct 22, 2018 at 2:30 AM Walter Bright via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/22/2018 1:34 AM, Manu wrote:It hasn't changed. Not one single bit. I haven't changed a single detail in this thread.I posted it, twice... 2 messages, back to back, and you're responding to this one, and not that one. I'll post it again...Posting it over and over is illustrative of the failure of posting proposal documents to the n.g. instead of posting it as a DIP which can be referred to: 1. nobody knows which of your 70 messages are the ones with the proposal in it 2. with multiple posts of the proposal, nobody knows which one is the most up-to-date oneDoing it this way does not work. Continuing to repost it is a waste of your time. Post it as a DIP and link to it.And you STILL ignored my post >_< Please look at the proper code that implements your broken example. I know you've seen it now.
Oct 22 2018
On 10/22/2018 2:44 AM, Manu wrote:It hasn't changed. Not one single bit. I haven't changed a single detail in this thread.A reader would not know that. Reposting the same thing over and over in a thread is not how threaded discussions work. You've indicated previously you're not even using a threaded newsreader.And you STILL ignored my post >_<I'm going to until you post a DIP. The way this discussion is going is incredibly inefficient of your time and mine, and everyone else's who tries to engage constructively. It's exactly why we have a DIP process. It's really not hard. You can even use Markdown to make it look much nicer. https://github.com/dlang/DIPs https://github.com/dlang/DIPs/blob/master/PROCEDURE.md
Oct 22 2018
On Sun, Oct 21, 2018 at 2:35 PM Walter Bright via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/21/2018 2:08 PM, Walter Bright wrote:No no, they're repeated, not scattered, because I seem to have to keep repeating it over and over, because nobody is reading the text, or perhaps imaging there is a lot more text than there is.On 10/21/2018 12:20 PM, Nicholas Wilson wrote:Then I don't know what the proposal is. Pieces of it appear to be scattered over numerous posts, mixed in with other text,Yes, but the problem you describe is arises from implicit conversion in the other direction, which is not part of the proposal.It's Manu's example.opinions, and handwavy stuff.You mean like every post in opposition which disregards the rules and baselessly asserts it's a terrible idea? :/There's nothing to point to that is "the proposal".You can go back to the OP, not a single detail is changed at any point, but I've repeated it a whole bunch of times (including in direct response to your last post) and the summary has become more concise, but not different. 1. Shared has no read or write access to data 2. Functions with shared arguments are threadsafe with respect to those arguments a. This is a commitment that must be true in _absolute terms_ (there exists discussion about the ways that neighbours must not undermine this promise) b. There are numerous examples demonstrating how to configure this (TL;DR: use encapsulation, and trusted at the bottom of the stack) If you can find a legitimate example where those rules don't hold, I want to see it. But every example so far has been based on a faulty premise where those 2 simple rules were not actually applied. I responded to your faulty program directly with the correct program, and you haven't acknowledged it. Did you see it?I suggest you and Manu write up a proper proposal. Something that is complete, has nothing else in it, has a rationale, illuminating examples, and explains why alternatives are inferior.I have written this program a couple of times, including in direct response to your last sample program. You seem to have dismissed it... where is your response to that program, or my last entire post?For examples of how to do it: https://github.com/dlang/DIPs/tree/master/DIPs Trying to rewrite the semantics of shared is not a simple task, doing multithreading correctly is a minefield of "OOPS! I didn't think of that!" and if anything cries out for a DIP, your and Manu's proposal does.Yes, I agree it's DIP worthy. But given the almost nothing but overt hostility I've received here, why on earth would I waste my time writing a DIP? I put months into my other DIP which sits gathering dust... if this thread inspired any confidence that it would be well-received I would make the effort, but the critical reception we've seen here is... a bit strange. It's a 2-point proposal, the rules are **SO SIMPLE**, which is why I love it. How it can be misunderstood is something I'm having trouble understanding, and I don't know how to make it any clearer than I already have; numerous times over, including in my last reply to you, which you have ignored and dismissed it seems. Please go back and read my response to your last program.
Oct 21 2018
On Monday, 22 October 2018 at 00:22:19 UTC, Manu wrote:On Sun, Oct 21, 2018 at 2:35 PM Walter Bright via Digitalmars-d <digitalmars-d puremagic.com> wrote:I told you this is what happens with forum posts 4 days ago, yet you didn't listen: https://forum.dlang.org/post/fokdcnzircoiuhrhzmgv forum.dlang.orgOn 10/21/2018 2:08 PM, Walter Bright wrote:No no, they're repeated, not scattered, because I seem to have to keep repeating it over and over, because nobody is reading the text, or perhaps imaging there is a lot more text than there is.On 10/21/2018 12:20 PM, Nicholas Wilson wrote:Then I don't know what the proposal is. Pieces of it appear to be scattered over numerous posts, mixed in with other text,Yes, but the problem you describe is arises from implicit conversion in the other direction, which is not part of the proposal.It's Manu's example.Put it all together in a 2-3 page proposal elsewhere, so he doesn't have to hunt everything out in a blizzard of forum posts.opinions, and handwavy stuff.You mean like every post in opposition which disregards the rules and baselessly asserts it's a terrible idea? :/There's nothing to point to that is "the proposal".You can go back to the OP, not a single detail is changed at any point, but I've repeated it a whole bunch of times (including in direct response to your last post) and the summary has become more concise, but not different. 1. Shared has no read or write access to data 2. Functions with shared arguments are threadsafe with respect to those arguments a. This is a commitment that must be true in _absolute terms_ (there exists discussion about the ways that neighbours must not undermine this promise) b. There are numerous examples demonstrating how to configure this (TL;DR: use encapsulation, and trusted at the bottom of the stack) If you can find a legitimate example where those rules don't hold, I want to see it. But every example so far has been based on a faulty premise where those 2 simple rules were not actually applied.I responded to your faulty program directly with the correct program, and you haven't acknowledged it. Did you see it?He did not say to write a full DIP, just a proposal, so he knows exactly what you mean, just as I said. It will require a DIP eventually, but he didn't ask you to write one now.I suggest you and Manu write up a proper proposal. Something that is complete, has nothing else in it, has a rationale, illuminating examples, and explains why alternatives are inferior.I have written this program a couple of times, including in direct response to your last sample program. You seem to have dismissed it... where is your response to that program, or my last entire post?For examples of how to do it: https://github.com/dlang/DIPs/tree/master/DIPs Trying to rewrite the semantics of shared is not a simple task, doing multithreading correctly is a minefield of "OOPS! I didn't think of that!" and if anything cries out for a DIP, your and Manu's proposal does.Yes, I agree it's DIP worthy. But given the almost nothing but overt hostility I've received here, why on earth would I waste my time writing a DIP? I put months into my other DIP which sits gathering dust... if this thread inspired any confidence that it would be well-received I would make the effort, but the critical reception we've seen here is... a bit strange. It's a 2-point proposal, the rules are **SO SIMPLE**, which is why I love it. How it can be misunderstood is something I'm having trouble understanding, and I don't know how to make it any clearer than I already have; numerous times over, including in my last reply to you, which you have ignored and dismissed it seems. Please go back and read my response to your last program.
Oct 21 2018
On Monday, 22 October 2018 at 00:22:19 UTC, Manu wrote:No no, they're repeated, not scattered, because I seem to have to keep repeating it over and over, because nobody is reading the text, or perhaps imaging there is a lot more text than there is. ... You mean like every post in opposition which disregards the rules and baselessly asserts it's a terrible idea? :/ ... I responded to your faulty program directly with the correct program, and you haven't acknowledged it. Did you see it?Right back at you. Quote: I think this is a typical sort of construction: struct ThreadsafeQueue(T) { void QueueItem(T*) shared; T* UnqueueItem() shared; } struct SpecialWorkList { struct Job { ... } void MakeJob(int x, float y, string z) shared // <- any thread may produce a job { Job* job = new Job; // <- this is thread-local PopulateJob(job, x, y, z); // <- preparation of a job might be complex, and worthy of the SpecialWorkList implementation jobList.QueueItem(job); // <- QueueItem encapsulates thread-safety, no need for blunt casts } void Flush() // <- not shared, thread-local consumer { Job* job; while (job = jobList.UnqueueItem()) // <- it's obviously safe for a thread-local to call UnqueueItem even though the implementation is threadsafe { // thread-local dispatch of work... // perhaps rendering, perhaps deferred destruction, perhaps deferred resource creation... whatever! } } void GetSpecialSystemState() // <- this has NOTHING to do with the threadsafe part of SpecialWorkList { return os.functionThatChecksSystemState(); } // there may be any number of utility functions that don't interact with jobList. private: void PopulateJob(ref Job job, ...) { // expensive function; not thread-safe, and doesn't have any interaction with threading. } ThreadsafeQueue!Job jobList; } This isn't an amazing example, but it's typical of a thing that's mostly thread-local, and only a small controlled part of it's functionality is thread-safe. The thread-local method Flush() also deals with thread-safety internally... because it flushes a thread-safe queue. All thread-safety concerns are composed by a utility object, so there's no need for locks, magic, or casts here. EndQuote; The above: 1) Will not compile, not currently, not under your proposal (presumably you forgot in frustration to cast before calling PopulateJob?..) 2) Does not in any way demonstrate a practical safe application of an implicit conversion. As I wrote in the original response to that code, with that particular code it seems more like you just need forwarding methods that call `shared` methods under the hood (i.e. MakeJob), and it'd be "nice" if you didn't have to write those and could just call `shared` MakeJob on an un-`shared` reference directly. But these are all assumptions without seeing the actual usage. Please just stop acting like everyone here is opposing *you*. All you're doing is dismissing everyone with a "nuh-huh, you no understand, you bad". If it was just me, fine, it would mean I'm dumb and not worthy of this discussion. But this isn't the case, which means *you are not getting your point across*. And yet instead of trying to fix that, you're getting all snarky.
Oct 22 2018
On Mon, Oct 22, 2018 at 4:50 AM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Monday, 22 October 2018 at 00:22:19 UTC, Manu wrote:I did correct that line (along with an apology) on my very next post; it would probably be a member of Job... or any manner of other code. That is the least interesting line in the programNo no, they're repeated, not scattered, because I seem to have to keep repeating it over and over, because nobody is reading the text, or perhaps imaging there is a lot more text than there is. ... You mean like every post in opposition which disregards the rules and baselessly asserts it's a terrible idea? :/ ... I responded to your faulty program directly with the correct program, and you haven't acknowledged it. Did you see it?Right back at you. Quote: I think this is a typical sort of construction: struct ThreadsafeQueue(T) { void QueueItem(T*) shared; T* UnqueueItem() shared; } struct SpecialWorkList { struct Job { ... } void MakeJob(int x, float y, string z) shared // <- any thread may produce a job { Job* job = new Job; // <- this is thread-local PopulateJob(job, x, y, z); // <- preparation of a job might be complex, and worthy of the SpecialWorkList implementation jobList.QueueItem(job); // <- QueueItem encapsulates thread-safety, no need for blunt casts } void Flush() // <- not shared, thread-local consumer { Job* job; while (job = jobList.UnqueueItem()) // <- it's obviously safe for a thread-local to call UnqueueItem even though the implementation is threadsafe { // thread-local dispatch of work... // perhaps rendering, perhaps deferred destruction, perhaps deferred resource creation... whatever! } } void GetSpecialSystemState() // <- this has NOTHING to do with the threadsafe part of SpecialWorkList { return os.functionThatChecksSystemState(); } // there may be any number of utility functions that don't interact with jobList. private: void PopulateJob(ref Job job, ...) { // expensive function; not thread-safe, and doesn't have any interaction with threading. } ThreadsafeQueue!Job jobList; } This isn't an amazing example, but it's typical of a thing that's mostly thread-local, and only a small controlled part of it's functionality is thread-safe. The thread-local method Flush() also deals with thread-safety internally... because it flushes a thread-safe queue. All thread-safety concerns are composed by a utility object, so there's no need for locks, magic, or casts here. EndQuote; The above: 1) Will not compile, not currently, not under your proposal (presumably you forgot in frustration to cast before calling PopulateJob?..)2) Does not in any way demonstrate a practical safe application of an implicit conversion. As I wrote in the original response to that code, with that particular code it seems more like you just need forwarding methods that call `shared` methods under the hood (i.e. MakeJob), and it'd be "nice" if you didn't have to write those and could just call `shared` MakeJob on an un-`shared` reference directly. But these are all assumptions without seeing the actual usage. Please just stop acting like everyone here is opposing *you*.You're right, it's mostly you.All you're doing is dismissing everyone with a "nuh-huh, you no understand, you bad". If it was just me, fine, it would mean I'm dumb and not worthy of this discussion. But this isn't the case, which means *you are not getting your point across*. And yet instead of trying to fix that, you're getting all snarky.I mean, it's fair, but it's pretty bloody hypocritical coming from you! I think it's fair to point out that your high-frequency, persistent, least, until I told you to GF) is the primary reason I'm frustrated here. You can own part responsibility for my emotion.
Oct 22 2018
On Monday, 22 October 2018 at 00:22:19 UTC, Manu wrote:On Sun, Oct 21, 2018 at 2:35 PM Walter Bright via Digitalmars-d <digitalmars-d puremagic.com> wrote:I've read every post on this thread and I also have the feeling that it's scattered. At the very least, I'm 90% confident I don't understand what it is you're proposing. Trust me, I'm trying. I believe that you have a proposal which you believe results in safe multithreaded code. I don't understand how what I've read so far would accomplish that. I'm conviced that shared data shouldn't be allowed to be written to, but I haven't yet been convinced of anything else. I don't see how it's possible that implicit conversion from non-shared to shared can work at all. Yes, I know that in the proposal putting `shared` on anything makes it useless, but *somehow* that data gets to be used, even if it's by a trusted function that casts away shared. At that point, nothing you do thread-safely to the shared data matters if you obtained the shared data from an implicit conversion. There may be many many aliases to it before it was converted, all of them able to write to that memory location believing it's not shared. And it would be safe (but definitely not thread-safe) to do so! This has been explained a few times, by multiple people. I haven't seen anyone addressing this yet (it's possible it got lost in a sea of text). I don't even understand why it is you want to cast anything to shared anyway - that'd always be a code smell if I saw it during code review. If it's shared, type it as such. Or better yet, if you can just use immutable. I understand the frustration of not getting your point across. I would like to kindly point out that, if the replies have gotten to multiple dozen pages and several well-meaning people still don't get it, then the proposal probably isn't as simple as you believe it to be.Then I don't know what the proposal is. Pieces of it appear to be scattered over numerous posts, mixed in with other text,No no, they're repeated, not scattered, because I seem to have to keep repeating it over and over, because nobody is reading the text, or perhaps imaging there is a lot more text than there is.
Oct 22 2018
On Monday, 22 October 2018 at 00:22:19 UTC, Manu wrote:On Sun, Oct 21, 2018 at 2:35 PM Walter Bright via Digitalmars-d <digitalmars-d puremagic.com> wrote:I can go look at the original post - and I have - but while it may strictly speaking contain all the information I need, the amount of time and brain power it would take for me to comprehend the consequences is pretty large. I assume you have done a lot of that work already and could save everyone a lot of time by putting together a quick document that covers some of that, with good examples. I'm not going to read {1,2,3}00 messages full of irritated bidirectional miscommunication to try and understand this unless I really have to, and I assume others feel similarly.On 10/21/2018 2:08 PM, Walter Bright wrote:No no, they're repeated, not scattered, because I seem to have to keep repeating it over and over, because nobody is reading the text, or perhaps imaging there is a lot more text than there is.On 10/21/2018 12:20 PM, Nicholas Wilson wrote:Then I don't know what the proposal is. Pieces of it appear to be scattered over numerous posts, mixed in with other text,Yes, but the problem you describe is arises from implicit conversion in the other direction, which is not part of the proposal.It's Manu's example.
Oct 22 2018
On Sun, Oct 21, 2018 at 11:31 AM Manu <turkeyman gmail.com> wrote:On Sun., 21 Oct. 2018, 2:55 am Walter Bright via Digitalmars-d, <digitalmars-d puremagic.com> wrote:Did you read this email? It seems you didn't read this email... Please read it.On 10/20/2018 11:24 AM, Manu wrote:By the definition Nick pulled from Wikipedia and posted for you a few posts back, yes, my proposal satisfies Wikipedia's definition of no aliasing. I understand that property is critical, and I have carefully designed for it.This is an unfair dismissal.It has nothing at all to do with fairness. It is about what the type system guarantees in safe code. To repeat, the current type system guarantees in safe code that T* and shared(T)* do not point to the same memory location. Does your proposal maintain that or not? It's a binary question.T* can't make additional T* aliases on other threads; there can only be one thread with T*. shared(T)* can not make a T*. shared(T)* has no read or write access, so it's not an alias of T* by Wikipedia's definition. Only threadsafe functions can do anything to T. The leap of faith is; some trusted utility functions at the bottom of the shared stack makes a promise that it is threadsafe, and must deliver that promise. I don't think this is unreasonable; this is the nature of trusted functions, they make a promise, and they must keep it. If the trusted function does not lie, then the chain of trust holds upwards through the stack. The are very few such trusted functions in practise. Like, similar to the number of digits you have.I'm not sure you've understood the proposal. This is the reason for the implicit conversion. It provides safe transition.I don't see any way to make an implicit T* to shared(T)* safe, or vice versa. The T* code can create more aliases that the conversion doesn't know about, and the shared(T)* code can hand out aliases to other threads. So it all falls to pieces.Using a 'scope' qualifier won't work, because 'scope' isn't transitive, while shared is, i.e. U** and shared(U*)*.I don't think I depend on scope in any way. That was an earlier revision of thinking in an older thread.> I'm not sure how to clarify it, what can I give you? Write a piece of code that does such an implicit conversion that you argue is safe. Make the code as small as possible. Your example: > int* a; > shared(int)* b = a; This is not safe. ---- Manu's Proposal --- safe: int i; int* a = &i; StartNewThread(a); // Compiles! Coder has no idea! ... in the new thread ... void StartOfNewThread(shared(int)* b) { ... we have two threads accessing 'i', one thinks it is shared, the other unshared, and StartOfNewThread() has no idea and anyone writing code for StartOfNewThread() has no way to know anything is wrong ... lockedIncrement(b); // Data Race! }This program doesn't compile. You receive an error because it is not safe. The function is `lockedIncrement(int*)`. It can't receive a shared argument; the function is not threadsafe by my definition. You have written a program that produces the expected error that alerts you that you have tried to do un- safe and make a race. Stanislav produced this same program, and I responded with the correct program a few posts back. I'll repeat it here; the safe program to model this interaction is: safe: // function is NOT threadsafe by my definition, can not be called on shared arguments void atomicIncrement(int*); struct Atomic(T) { // encapsulare the unsafe data so it's inaccessible by any unsafe means private T val; // perform the unsafe cast in a trusted function // we are able to assure a valid calling context by encapsulating the data above void opUnary(string op : "++")() shared trusted { atomicIncrement(cast(T*)&val); } } Atomic!int i; Atomic!int* a = &i; StartNewThread(a); // Compiles, of course! ++i; // no race ... in the new thread ... void StartOfNewThread(shared(Atomic!int)* b) { ... we have two threads accessing 'i', one has thread-local access, this one has a restricted shared access. here, we have a shared instance, so we can only access `b` via threadsafe functions. as such, we can manipulate `b` without fear. ++i; // no race! }Your proposal means that the person writing the lockedIncrement(), which is a perfectly reasonable thing to do, simply cannot write it in a way that has a safe interfaceCorrect, the rules of my proposal apply to lockedIncrement(). They apply to `shared` generally. lockedIncrement() is not a threadsafe function. You can't call it on a shared instance, because `int`s API (ie, all intrinsic operations) are not threadsafe. lockedIncrement() can't promise threadsafe access to `shared(int)*`, so the argument is not shared. Your program made the correct compile error about doing unsafety, but the location of the compile error is different under my proposal; complexity is worn by the shared library author, rather than every calling user ever. I think my proposal places the complexity in the right location. `shared` is intrinsically dangerous; it's not reasonable to ask every user that ever calls a shared API to write unsafe code when when calling. That's just plain bad design.because the person writing the lockedIncrement() library function has no way to know that the data it receives is actually unshared data.The author of `shared` tooling must assure a valid context such that its threadsafety promises are true. Atomic(T) does that with a private member. The safe way to interact with atomics is to use the Atomic utility type I showed above. That is one such trusted tool that I talk about as being "at the bottom of the stack". It is probably joined by a mutex/semaphore, and some containers/queues. That is probably all the things, and other things would be safe compositions of those tools.I.e. trusted code is obliged to proved a safe interface. Your proposal makes that impossible because the compiler would allow unshared data to be implicitly typed as shared.What? No. Please, try and understand my proposal...
Oct 21 2018
Third time's the charm.... maybe? --------------------- repeated, 3rd time ------------------------ On Sun., 21 Oct. 2018, 2:55 am Walter Bright via Digitalmars-d, <digitalmars-d puremagic.com> wrote:On 10/20/2018 11:24 AM, Manu wrote:By the definition Nick pulled from Wikipedia and posted for you a few posts back, yes, my proposal satisfies Wikipedia's definition of no aliasing. I understand that property is critical, and I have carefully designed for it.This is an unfair dismissal.It has nothing at all to do with fairness. It is about what the type system guarantees in safe code. To repeat, the current type system guarantees in safe code that T* and shared(T)* do not point to the same memory location. Does your proposal maintain that or not? It's a binary question.T* can't make additional T* aliases on other threads; there can only be one thread with T*. shared(T)* can not make a T*. shared(T)* has no read or write access, so it's not an alias of T* by Wikipedia's definition. Only threadsafe functions can do anything to T. The leap of faith is; some trusted utility functions at the bottom of the shared stack makes a promise that it is threadsafe, and must deliver that promise. I don't think this is unreasonable; this is the nature of trusted functions, they make a promise, and they must keep it. If the trusted function does not lie, then the chain of trust holds upwards through the stack. The are very few such trusted functions in practise. Like, similar to the number of digits you have.I'm not sure you've understood the proposal. This is the reason for the implicit conversion. It provides safe transition.I don't see any way to make an implicit T* to shared(T)* safe, or vice versa. The T* code can create more aliases that the conversion doesn't know about, and the shared(T)* code can hand out aliases to other threads. So it all falls to pieces.Using a 'scope' qualifier won't work, because 'scope' isn't transitive, while shared is, i.e. U** and shared(U*)*.I don't think I depend on scope in any way. That was an earlier revision of thinking in an older thread.> I'm not sure how to clarify it, what can I give you? Write a piece of code that does such an implicit conversion that you argue is safe. Make the code as small as possible. Your example: > int* a; > shared(int)* b = a; This is not safe. ---- Manu's Proposal --- safe: int i; int* a = &i; StartNewThread(a); // Compiles! Coder has no idea! ... in the new thread ... void StartOfNewThread(shared(int)* b) { ... we have two threads accessing 'i', one thinks it is shared, the other unshared, and StartOfNewThread() has no idea and anyone writing code for StartOfNewThread() has no way to know anything is wrong ... lockedIncrement(b); // Data Race! }This program doesn't compile. You receive an error because it is not safe. The function is `lockedIncrement(int*)`. It can't receive a shared argument; the function is not threadsafe by my definition. You have written a program that produces the expected error that alerts you that you have tried to do un- safe and make a race. Stanislav produced this same program, and I responded with the correct program a few posts back. I'll repeat it here; the safe program to model this interaction is: safe: // function is NOT threadsafe by my definition, can not be called on shared arguments void atomicIncrement(int*); struct Atomic(T) { // encapsulare the unsafe data so it's inaccessible by any unsafe means private T val; // perform the unsafe cast in a trusted function // we are able to assure a valid calling context by encapsulating the data above void opUnary(string op : "++")() shared trusted { atomicIncrement(cast(T*)&val); } } Atomic!int i; Atomic!int* a = &i; StartNewThread(a); // Compiles, of course! ++i; // no race ... in the new thread ... void StartOfNewThread(shared(Atomic!int)* b) { //... we have two threads accessing 'i', one has thread-local access, this one has a restricted shared access. // here, we have a shared instance, so we can only access `b` via threadsafe functions. // as such, we can manipulate `b` without fear. ++i; // no race! }Your proposal means that the person writing the lockedIncrement(), which is a perfectly reasonable thing to do, simply cannot write it in a way that has a safe interfaceCorrect, the rules of my proposal apply to lockedIncrement(). They apply to `shared` generally. lockedIncrement() is not a threadsafe function. You can't call it on a shared instance, because `int`s API (ie, all intrinsic operations) are not threadsafe. lockedIncrement() can't promise threadsafe access to `shared(int)*`, so the argument is not shared. Your program made the correct compile error about doing unsafety, but the location of the compile error is different under my proposal; complexity is worn by the shared library author, rather than every calling user ever. I think my proposal places the complexity in the right location. `shared` is intrinsically dangerous; it's not reasonable to ask every user that ever calls a shared API to write unsafe code when when calling. That's just plain bad design.because the person writing the lockedIncrement() library function has no way to know that the data it receives is actually unshared data.The author of `shared` tooling must assure a valid context such that its threadsafety promises are true. Atomic(T) does that with a private member. The safe way to interact with atomics is to use the Atomic utility type I showed above. That is one such trusted tool that I talk about as being "at the bottom of the stack". It is probably joined by a mutex/semaphore, and some containers/queues. That is probably all the things, and other things would be safe compositions of those tools.I.e. trusted code is obliged to proved a safe interface. Your proposal makes that impossible because the compiler would allow unshared data to be implicitly typed as shared.What? No. Please, try and understand my proposal...
Oct 22 2018
On Wednesday, 17 October 2018 at 05:40:41 UTC, Walter Bright wrote:When Andrei and I came up with the rules for: mutable const shared const shared immutable and which can be implicitly converted to what, so far nobody has found a fault in those rules...Here's one: shared -> const shared shouldn't be allowed. Mutable aliasing in single-threaded code is bad enough as it is.
Oct 17 2018
On Wednesday, 17 October 2018 at 07:24:13 UTC, Stanislav Blinov wrote:On Wednesday, 17 October 2018 at 05:40:41 UTC, Walter Bright wrote:Could you explain that a bit more? I don't understand it, mutable -> const is half the reason const exists. I was thinking that mutable -> shared const as apposed to mutable -> shared would get around the issues that Timon posted.When Andrei and I came up with the rules for: mutable const shared const shared immutable and which can be implicitly converted to what, so far nobody has found a fault in those rules...Here's one: shared -> const shared shouldn't be allowed. Mutable aliasing in single-threaded code is bad enough as it is.
Oct 17 2018
On 17.10.2018 16:14, Nicholas Wilson wrote:I was thinking that mutable -> shared const as apposed to mutable -> shared would get around the issues that Timon posted.Unfortunately not. For example, the thread with the mutable reference is not obliged to actually make the changes that are performed on that reference visible to other threads.
Oct 17 2018
On Wednesday, 17 October 2018 at 14:26:43 UTC, Timon Gehr wrote:On 17.10.2018 16:14, Nicholas Wilson wrote:Yes, but that is covered by not being able to read non-atomically from a shared reference.I was thinking that mutable -> shared const as apposed to mutable -> shared would get around the issues that Timon posted.Unfortunately not. For example, the thread with the mutable reference is not obliged to actually make the changes that are performed on that reference visible to other threads.
Oct 17 2018
On 10/17/18 10:33 AM, Nicholas Wilson wrote:On Wednesday, 17 October 2018 at 14:26:43 UTC, Timon Gehr wrote:All sides must participate in synchronization for it to make sense. The mutable side has no obligation to use atomics. It can use ++data, and race conditions will happen. -SteveOn 17.10.2018 16:14, Nicholas Wilson wrote:Yes, but that is covered by not being able to read non-atomically from a shared reference.I was thinking that mutable -> shared const as apposed to mutable -> shared would get around the issues that Timon posted.Unfortunately not. For example, the thread with the mutable reference is not obliged to actually make the changes that are performed on that reference visible to other threads.
Oct 17 2018
On Wednesday, 17 October 2018 at 14:14:56 UTC, Nicholas Wilson wrote:On Wednesday, 17 October 2018 at 07:24:13 UTC, Stanislav Blinov wrote:Yes, but `shared` is not `const`. This 'might change' rule is poisonous for shared data: you essentially create extra work for the CPU for very little gain. There's absolutely no reason to share a read-only half-constant anyway. Either give immutable, or just flat out copy.On Wednesday, 17 October 2018 at 05:40:41 UTC, Walter Bright wrote:Could you explain that a bit more? I don't understand it, mutable -> const is half the reason const exists.When Andrei and I came up with the rules for: mutable const shared const shared immutable and which can be implicitly converted to what, so far nobody has found a fault in those rules...Here's one: shared -> const shared shouldn't be allowed. Mutable aliasing in single-threaded code is bad enough as it is.I was thinking that mutable -> shared const as apposed to mutable -> shared would get around the issues that Timon posted.It wouldn't, as Timon already explained. Writes to not-`shared` mutables might just not be propagated beyond the core. Wouldn't happen on amd64, of course, but still. It's not about atomicity of reads, it just depends on architecture. On some systems you have to explicitly synchronize memory.
Oct 17 2018
On Wednesday, 17 October 2018 at 05:40:41 UTC, Walter Bright wrote:On 10/15/2018 11:46 AM, Manu wrote:Isn't that also true for isolated data (data that only allows one alias)?[...]Shared has one incredibly valuable feature - it allows you, the programmer, to identify data that can be accessed by multiple threads. There are so many ways that data can be shared, the only way to comprehend what is going on is to build a wall around shared data. (The exception to this is immutable data. Immutable data does not need synchronization, so there is no need to distinguish between shared and unshared immutable data.) [snip]
Oct 17 2018
On 10/17/2018 4:29 AM, jmh530 wrote:Isn't that also true for isolated data (data that only allows one alias)?That's colloquially called "unique" data. And yes, it is also true for that. That's why casting the return value of malloc() to 'shared' is safe. It's just that the language has no way to semantically identify unique data with its current type system.
Oct 19 2018
On 19/10/2018 9:02 PM, Walter Bright wrote:On 10/17/2018 4:29 AM, jmh530 wrote:Actually we kind of have a way to do this (I think). Scope. It can't cross the thread boundary and it has a pretty clear owner as far as the stack is concerned. Given a bit of time and empowering of scope, we could do a scopeonly type attribute for functions.Isn't that also true for isolated data (data that only allows one alias)?That's colloquially called "unique" data. And yes, it is also true for that. That's why casting the return value of malloc() to 'shared' is safe. It's just that the language has no way to semantically identify unique data with its current type system.
Oct 19 2018
I don't see any problem with this proposal as long as these points hold: - Shared <-> Unshared is never implicit, either requiring an explicit cast (both ways) or having a language support which allows the conversion gracefully. - Shared methods are called by compiler if the type is shared or if there is no unshared equivalent. - Programmer needs to guarantee that shared -> unshared cast/conversion is thread-safe by hand; such as acquiring a lock, atomic operations... - Programmer needs to guarantee that when unshared -> shared cast/conversion happens, data is not accessed through unshared reference during the lifetime of shared reference(s). Effectively this means a data needs to be treated as shared everywhere at the same time otherwise all things fall apart.
Oct 17 2018
On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:1. shared should behave exactly like const, except in addition to inhibiting write access, it also inhibits read access.How is this significantly different from now? ----------------- shared int i; ++i; Error: read-modify-write operations are not allowed for shared variables. Use core.atomic.atomicOp!"+="(i, 1) instead. ----------------- There's not much one can do to modify a shared value as it is.Current situation where you can arbitrarily access shared members undermines any value it has.Unless I'm missing something, I can't arbitrarily do anything with shared members right now.Shared must assure you don't access members unsafely, and the only way to do that with respect to data members, is to inhibit access completely.Or use atomic operations.Assuming this world... how do you use shared?https://github.com/atilaneves/fearless or https://dlang.org/phobos/core_atomic.htmlFrom there, it opens up another critical opportunity; T* -> shared(T)* promotion.I don't think that works. See below.If you write a lock-free queue for instance, and all the methods are `shared` (ie, threadsafe), then under the current rules, you can't interact with the object when it's not shared, and that's fairly useless.Not really: ----------- struct FancyQueue(E) { // ... void pushBack(this T)(E element) { static if(is(T == shared)) { // lock mutex or whatever is needed auto safeThis = () trusted { return cast(FancyQueue) this; }(); } else { // no need to lock anything alias safeThis = this; } // profit } } ----------- Usable if shared or not.Assuming the rules above: "can't read or write to members", and the understanding that `shared` methods are expected to have threadsafe implementations (because that's the whole point), what are the risks from allowing T* -> shared(T)* conversion?int i; tid.send(&i); ++i; // oops, data raceAll the risks that I think have been identified previously assume that you can arbitrarily modify the data.Do you have any examples of arbitrarily modifying shared data? I can't think of any.That's insanity... assume we fix that... I think the promotion actually becomes safe now...?I don't think so, no.
Oct 18 2018
On 10/18/18 1:17 PM, Atila Neves wrote:On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:i = i + 1; // OK(!) -Steve1. shared should behave exactly like const, except in addition to inhibiting write access, it also inhibits read access.How is this significantly different from now? ----------------- shared int i; ++i;
Oct 18 2018
On Thursday, 18 October 2018 at 17:43:40 UTC, Steven Schveighoffer wrote:On 10/18/18 1:17 PM, Atila Neves wrote:Sigh... I'm sure there's a practical reason for it, but still.On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:i = i + 1; // OK(!) -Steve1. shared should behave exactly like const, except in addition to inhibiting write access, it also inhibits read access.How is this significantly different from now? ----------------- shared int i; ++i;
Oct 18 2018
On Thursday, 18 October 2018 at 17:17:37 UTC, Atila Neves wrote:On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:i = 1; int x = i; shared int y = i; And so on. The compiler needs to forbid this.1. shared should behave exactly like const, except in addition to inhibiting write access, it also inhibits read access.How is this significantly different from now? ----------------- shared int i; ++i; Error: read-modify-write operations are not allowed for shared variables. Use core.atomic.atomicOp!"+="(i, 1) instead. ----------------- There's not much one can do to modify a shared value as it is.Unless I'm missing something, I can't arbitrarily do anything with shared members right now.Except arbitrarily read and write them :)From there, it opens up another critical opportunity; T* -> shared(T)* promotion.I don't think that works. See below.Welcome to the club.Doesn't work. No matter what you show Manu or Simen here they think it's just a bad contrived example. You can't sway them by the fact that the compiler currently *prevents* this from happening.Assuming the rules above: "can't read or write to members", and the understanding that `shared` methods are expected to have threadsafe implementations (because that's the whole point), what are the risks from allowing T* -> shared(T)* conversion?int i; tid.send(&i); ++i; // oops, data raceAll the risks that I think have been identified previously assume that you can arbitrarily modify the data.Do you have any examples of arbitrarily modifying shared data? I can't think of any.See near the beginning of this post ;)+100500.That's insanity... assume we fix that... I think the promotion actually becomes safe now...?I don't think so, no.
Oct 18 2018
On 10/18/18 1:47 PM, Stanislav Blinov wrote:On Thursday, 18 October 2018 at 17:17:37 UTC, Atila Neves wrote:This should be fine, y is not shared when being created. However, this still is allowed, and shouldn't be: y = 5; -SteveOn Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:i = 1; int x = i; shared int y = i;1. shared should behave exactly like const, except in addition to inhibiting write access, it also inhibits read access.How is this significantly different from now? ----------------- shared int i; ++i; Error: read-modify-write operations are not allowed for shared variables. Use core.atomic.atomicOp!"+="(i, 1) instead. ----------------- There's not much one can do to modify a shared value as it is.
Oct 18 2018
On Thursday, 18 October 2018 at 18:26:27 UTC, Steven Schveighoffer wrote:On 10/18/18 1:47 PM, Stanislav Blinov wrote:'y' isn't, but 'i' is. It's fine on amd64, but that's incidental.On Thursday, 18 October 2018 at 17:17:37 UTC, Atila Neves wrote:This should be fine, y is not shared when being created.On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:i = 1; int x = i; shared int y = i;1. shared should behave exactly like const, except in addition to inhibiting write access, it also inhibits read access.How is this significantly different from now? ----------------- shared int i; ++i; Error: read-modify-write operations are not allowed for shared variables. Use core.atomic.atomicOp!"+="(i, 1) instead. ----------------- There's not much one can do to modify a shared value as it is.
Oct 18 2018
On 10/18/18 2:42 PM, Stanislav Blinov wrote:On Thursday, 18 October 2018 at 18:26:27 UTC, Steven Schveighoffer wrote:OH, I didn't even notice that `i` didn't have a type, so it was a continuation of the original example! I read it as declaring y as shared and assigning it to a thread-local (which it isn't actually). My bad. -SteveOn 10/18/18 1:47 PM, Stanislav Blinov wrote:'y' isn't, but 'i' is. It's fine on amd64, but that's incidental.On Thursday, 18 October 2018 at 17:17:37 UTC, Atila Neves wrote:This should be fine, y is not shared when being created.On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:i = 1; int x = i; shared int y = i;1. shared should behave exactly like const, except in addition to inhibiting write access, it also inhibits read access.How is this significantly different from now? ----------------- shared int i; ++i; Error: read-modify-write operations are not allowed for shared variables. Use core.atomic.atomicOp!"+="(i, 1) instead. ----------------- There's not much one can do to modify a shared value as it is.
Oct 18 2018
On 18.10.18 20:26, Steven Schveighoffer wrote:I'm pretty sure you will have to allow operations on shared local variables. Otherwise, how are you ever going to use a shared(C)? You can't even call a shared method on it because it involves reading the reference.i = 1; int x = i; shared int y = i;This should be fine, y is not shared when being created. However, this still is allowed, and shouldn't be: y = 5; -Steve
Oct 18 2018
On Thursday, 18 October 2018 at 23:47:56 UTC, Timon Gehr wrote:I'm pretty sure you will have to allow operations on shared local variables. Otherwise, how are you ever going to use a shared(C)? You can't even call a shared method on it because it involves reading the reference.Because you can't really "share" C (e.g. by value). You share a C*, or, rather a shared(C)*. The pointer itself, which you own, isn't shared at all, and shouldn't be: it's your own reference to shared data. You can read and write that pointer all you want. What you must not be able to do is read and write *c. Although, when it's a global?.. I'm not sure. We can have the compiler always generate a by-reference access, i.e. make that part of the language spec. Because full-on read of C.sizeof can't be statically proven thread-safe generically anyway (that's why generated copying and assignment don't make any sense for `shared`).
Oct 18 2018
On 19.10.18 02:29, Stanislav Blinov wrote:On Thursday, 18 October 2018 at 23:47:56 UTC, Timon Gehr wrote:(Here, I intended C to be a class, if that was unclear.)I'm pretty sure you will have to allow operations on shared local variables. Otherwise, how are you ever going to use a shared(C)? You can't even call a shared method on it because it involves reading the reference.Because you can't really "share" C (e.g. by value). You share a C*, or, rather a shared(C)*.The pointer itself, which you own, isn't shared at all, and shouldn't be: it's your own reference to shared data. You can read and write that pointer all you want. What you must not be able to do is read and write *c. ...Presumably you could have a local variable shared(C) c, then take its address &c and send it to a thread which will be terminated before the scope of the local variable ends. So, basically, the lack of tail-shared is an issue.
Oct 18 2018
On Friday, 19 October 2018 at 00:36:11 UTC, Timon Gehr wrote:On 19.10.18 02:29, Stanislav Blinov wrote:In that case, it's already a pointer, and the only real issue is the interaction with GC, which I mentioned before *needs* to be addressed. And that is only when C was allocated by GC.On Thursday, 18 October 2018 at 23:47:56 UTC, Timon Gehr wrote:(Here, I intended C to be a class, if that was unclear.)I'm pretty sure you will have to allow operations on shared local variables. Otherwise, how are you ever going to use a shared(C)? You can't even call a shared method on it because it involves reading the reference.Because you can't really "share" C (e.g. by value). You share a C*, or, rather a shared(C)*.I assume you mean *after*, because if that thread terminates before there's no problem.The pointer itself, which you own, isn't shared at all, and shouldn't be: it's your own reference to shared data. You can read and write that pointer all you want. What you must not be able to do is read and write *c. ...Presumably you could have a local variable shared(C) c, then take its address &c and send it to a thread which will be terminated before the scope of the local variable ends.So, basically, the lack of tail-shared is an issue.Well, not exactly. Irrespective of Manu's proposal, it's just inherent in D: sharing implies escaping, there's really no way around it. Provisions must be made in DIP1000 and in the language in general. However, that is a good point *against* implicit conversions, let alone safe ones.
Oct 18 2018
On Thursday, 18 October 2018 at 17:47:29 UTC, Stanislav Blinov wrote:On Thursday, 18 October 2018 at 17:17:37 UTC, Atila Neves wrote:Manu said clearly that the receiving thread won't be able to read or write the pointer. Because int or int* does not have threadsafe member functions. You can still disagree on the merits, but so far it has been demonstrated as a sound idea.On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:Doesn't work. No matter what you show Manu or Simen here they think it's just a bad contrived example. You can't sway them by the fact that the compiler currently *prevents* this from happening.Assuming the rules above: "can't read or write to members", and the understanding that `shared` methods are expected to have threadsafe implementations (because that's the whole point), what are the risks from allowing T* -> shared(T)* conversion?int i; tid.send(&i); ++i; // oops, data race
Oct 18 2018
On Thursday, 18 October 2018 at 19:04:58 UTC, Erik van Velzen wrote:On Thursday, 18 October 2018 at 17:47:29 UTC, Stanislav Blinov wrote:Yes it will, by casting `shared` away. *Just like* his proposed "wrap everything into" struct will. There's exactly no difference.On Thursday, 18 October 2018 at 17:17:37 UTC, Atila Neves wrote:Manu said clearly that the receiving thread won't be able to read or write the pointer.On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:Doesn't work. No matter what you show Manu or Simen here they think it's just a bad contrived example. You can't sway them by the fact that the compiler currently *prevents* this from happening.Assuming the rules above: "can't read or write to members", and the understanding that `shared` methods are expected to have threadsafe implementations (because that's the whole point), what are the risks from allowing T* -> shared(T)* conversion?int i; tid.send(&i); ++i; // oops, data raceBecause int or int* does not have threadsafe member functions.int doesn't have any member functions. Or it can have as many as you like per UFCS. Same goes for structs. Because "methods" are just free functions in disguise, so that whole distinction in Manu's proposal is a weaksauce convention at best.You can still disagree on the merits, but so far it has been demonstrated as a sound idea.No, it hasn't been.
Oct 18 2018
On Thursday, 18 October 2018 at 19:26:39 UTC, Stanislav Blinov wrote:On Thursday, 18 October 2018 at 19:04:58 UTC, Erik van Velzen wrote:Casting is inherently unsafe. Or at least, there's no threadsafe guarantee.On Thursday, 18 October 2018 at 17:47:29 UTC, Stanislav Blinov wrote:Yes it will, by casting `shared` away. *Just like* his proposed "wrap everything into" struct will. There's exactly no difference.Doesn't work. No matter what you show Manu or Simen here they think it's just a bad contrived example. You can't sway them by the fact that the compiler currently *prevents* this from happening.Manu said clearly that the receiving thread won't be able to read or write the pointer.I think you are missing the wider point. I can write thread-unsafe code *right now*, no casts required. Just put shared at the declaration. The proposal would actually give some guarantees.Because int or int* does not have threadsafe member functions.int doesn't have any member functions. Or it can have as many as you like per UFCS. Same goes for structs. Because "methods" are just free functions in disguise, so that whole distinction in Manu's proposal is a weaksauce convention at best.You can still disagree on the merits, but so far it has been demonstrated as a sound idea.No, it hasn't been.
Oct 18 2018
On Thursday, 18 October 2018 at 19:51:17 UTC, Erik van Velzen wrote:On Thursday, 18 October 2018 at 19:26:39 UTC, Stanislav BlinovManu said clearly that the receiving thread won't be able to read or write the pointer.Yes it will, by casting `shared` away. *Just like* his proposed "wrap everything into" struct will. There's exactly no difference.Casting is inherently unsafe. Or at least, there's no threadsafe guarantee.So? That's the only way to implement required low-level access, especially if we imagine that the part of Manu's proposal about disabling reads and writes on `shared` values is a given. It's the only way to implement Manu's Atomic!int, or at least operation it requires, for example.No, I think you are missing the wider point. You can write thread-unsafe code regardless of using `shared`, and regardless of it's implementation details. Allowing *implicit automatic promotion* of *mutable thread-local* data to shared will allow you to write even more thread-unsafe code, not less. The solid part of the proposal is about disabling reads and writes. The rest is pure convention: write structs instead of functions, and somehow (???) benefit from a totally unsafe implicit cast.I think you are missing the wider point. I can write thread-unsafe code *right now*, no casts required. Just put shared at the declaration. The proposal would actually give some guarantees.You can still disagree on the merits, but so far it has been demonstrated as a sound idea.No, it hasn't been.
Oct 18 2018
On Thu, Oct 18, 2018 at 1:10 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:On Thursday, 18 October 2018 at 19:51:17 UTC, Erik van Velzen wrote:trusted code exists, and it's the foundation of the safe stack. I think you're just trying to be obtuse at this point.On Thursday, 18 October 2018 at 19:26:39 UTC, Stanislav BlinovManu said clearly that the receiving thread won't be able to read or write the pointer.Yes it will, by casting `shared` away. *Just like* his proposed "wrap everything into" struct will. There's exactly no difference.Casting is inherently unsafe. Or at least, there's no threadsafe guarantee.So? That's the only way to implement required low-level access, especially if we imagine that the part of Manu's proposal about disabling reads and writes on `shared` values is a given. It's the only way to implement Manu's Atomic!int, or at least operation it requires, for example.
Oct 18 2018
Manu I'm also making a plea for you to write a document with your proposal which aggregates all relevant examples and objections. Then you can easily refer to it and we can introduce ppl to idea without reading a megathread.
Oct 18 2018
On Thu, Oct 18, 2018 at 2:40 PM Erik van Velzen via Digitalmars-d <digitalmars-d puremagic.com> wrote:Manu I'm also making a plea for you to write a document with your proposal which aggregates all relevant examples and objections. Then you can easily refer to it and we can introduce ppl to idea without reading a megathread.I can start on this. I wouldn't take the time before having confidence the whole effort was not to be rejected in principle. I'm still not sure on this. I think WRT Steven, we've understood each others position, and the only point of contention is on weighting a value-judgement. I'm confident in my weighting strategy, but I need to convince.
Oct 18 2018
Manu, Erik, Simen... In what world can a person consciously say "casting is unsafe", and yet at the same time claim that *implicit casting* is safe? What the actual F, guys?
Oct 18 2018
On Thursday, 18 October 2018 at 21:54:55 UTC, Stanislav Blinov wrote:Manu, Erik, Simen... In what world can a person consciously say "casting is unsafe", and yet at the same time claim that *implicit casting* is safe? What the actual F, guys?In a world where the implicit casting always ends in a place where anything you can do is safe. As we have said ad nauseam. -- Simen
Oct 18 2018
On Thu, Oct 18, 2018 at 2:55 PM Stanislav Blinov via Digitalmars-d <digitalmars-d puremagic.com> wrote:Manu, Erik, Simen... In what world can a person consciously say "casting is unsafe", and yet at the same time claim that *implicit casting* is safe? What the actual F, guys?Implicit casting exists in a world where the conversion is guaranteed to be safe, because rules are defined appropriately. Explicit casting exists in a world where those guarantees are not present, because no such rules. The 2 different strategies are 2 different worlds, one is my proposal, the other is more like what we have now. They are 2 different rule-sets. You are super-attached to some presumptions, and appear to refuse to analyse the proposal from the grounds it defines.
Oct 18 2018
On Thursday, 18 October 2018 at 22:09:02 UTC, Manu wrote:The 2 different strategies are 2 different worlds, one is my proposal, the other is more like what we have now. They are 2 different rule-sets. You are super-attached to some presumptions, and appear to refuse to analyse the proposal from the grounds it defines.Please see my exchange with Simen in case you're skipping my posts.
Oct 18 2018
On Thursday, 18 October 2018 at 20:07:54 UTC, Stanislav Blinov wrote:On Thursday, 18 October 2018 at 19:51:17 UTC, Erik van Velzen wrote:This may come as a surprise but I agree with this factually. It's just that the extra guarantee provided by disallowing implicit casting to shared is not so valuable to me. If you have an object which can be used in both a thread-safe and a thread-unsafe way that's a bug or code smell. Like the earlier example with localInc() and sharedInc(). And we can't guarantee the safety of trusted implementations anyways. If the extra guarantee is not valuable, might as well allow it.On Thursday, 18 October 2018 at 19:26:39 UTC, Stanislav BlinovManu said clearly that the receiving thread won't be able to read or write the pointer.Yes it will, by casting `shared` away. *Just like* his proposed "wrap everything into" struct will. There's exactly no difference.Casting is inherently unsafe. Or at least, there's no threadsafe guarantee.So? That's the only way to implement required low-level access, especially if we imagine that the part of Manu's proposal about disabling reads and writes on `shared` values is a given. It's the only way to implement Manu's Atomic!int, or at least operation it requires, for example.No, I think you are missing the wider point. You can write thread-unsafe code regardless of using `shared`, and regardless of its implementation details. Allowing *implicit automatic promotion* of *mutable thread-local* data to shared will allow you to write even more thread-unsafe code, not less.I think you are missing the wider point. I can write thread-unsafe code *right now*, no casts required. Just put shared at the declaration. The proposal would actually give some guarantees.You can still disagree on the merits, but so far it has been demonstrated as a sound idea.No, it hasn't been.The solid part of the proposal is about disabling reads and writes. The rest is pure convention: write structs instead of functions,"Writing structs" seems acknowledging how it's usually done. And then you put these in a library with generic concurrent data structures.and somehow (???) benefit from a totally unsafe implicit cast.(See first part)
Oct 18 2018
On 18.10.18 23:34, Erik van Velzen wrote:If you have an object which can be used in both a thread-safe and a thread-unsafe way that's a bug or code smell.Then why do you not just make all members shared? Because with Manu's proposal, as soon as you have a shared method, all members effectively become shared. It just seems pointless to type them as unshared anyway and then rely on convention within safe code to prevent unsafe accesses. Because, why? It just makes no sense. With the proposal I posted in the beginning, you would then not only get implicit conversion of class references to shared, but also back to unshared. I think the conflation of shared member functions and thread safe member functions is confusing. shared on a member function just means that the `this` reference is shared. The only use case for this is overloading on shared. The D approach to multithreading is that /all/ functions should be thread safe, but it is easier for some of them because they don't even need to access any shared state. It is therefore helpful if the type system cleanly separates shared from unshared state.
Oct 18 2018
On Friday, 19 October 2018 at 00:29:01 UTC, Timon Gehr wrote:On 18.10.18 23:34, Erik van Velzen wrote:Let's assume you have something like this: module foo; private shared int sharedState; struct Accessor { int flags; void addUser(this T)() { static if (is(T == shared)) sharedState.atomicInc(); // unconditionally increment when it's a shared reference else { // owner may optimize shared access based on it's own state if (!(flags & SKIP_LOCKS)) sharedState.atomicInc(); } } void removeUser(this T)() { static if (is(T == shared)) sharedState.atomicDec(); else { if (!(flags & SKIP_LOCKS)) sharedState.atomicDec(); } } void setFlags(int f) { flags = f; } } The 'Accessor' doesn't really hold any shared state, but it accesses a shared module global. Now, the *owner* (e.g. code that instantiated a local Accessor) may use non-`shared` interface to track additional state and make decisions whether or not access the global. A concrete example would be e.g. an I/O lock, where if you know you don't have any threads other than main, you can skip syscalls locking/unlocking the handle.If you have an object which can be used in both a thread-safe and a thread-unsafe way that's a bug or code smell.Then why do you not just make all members shared? Because with Manu's proposal, as soon as you have a shared method, all members effectively become shared. It just seems pointless to type them as unshared anyway and then rely on convention within safe code to prevent unsafe accesses. Because, why? It just makes no sense.With the proposal I posted in the beginning, you would then not only get implicit conversion of class references to shared, but also back to unshared.Is it in your first posts in this thread? I must've skipped that.I think the conflation of shared member functions and thread safe member functions is confusing. shared on a member function just means that the `this` reference is shared.Nod.The only use case for this is overloading on shared. The D approach to multithreading is that /all/ functions should be thread safe, but it is easier for some of them because they don't even need to access any shared state. It is therefore helpful if the type system cleanly separates shared from unshared state.Nod nod.
Oct 18 2018
On Thu, Oct 18, 2018 at 5:30 PM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 18.10.18 23:34, Erik van Velzen wrote:No they don't, only facets that overlap with the shared method. I tried to present an example before: struct Threadsafe { int x; Atomic!int y; void foo() shared { ++y; } // <- shared interaction only affects 'y' void bar() { ++x; ++y; } // <- not threadsafe, but does not violate foo's commitment; only interaction with 'y' has any commitment associated with it void unrelated() { ++x; } // <- no responsibilities are transposed here, you can continue to do whatever you like throughout the class where 'y' is not concerned } In practise, and in my direct experience, classes tend to have exactly one 'y', and either zero (pure utility), or many such 'x' members. Threadsafe API interacts with 'y', and the rest is just normal thread-local methods which interact with all members thread-locally, and may also interact with 'y' while not violating any threadsafety commitments.If you have an object which can be used in both a thread-safe and a thread-unsafe way that's a bug or code smell.Then why do you not just make all members shared? Because with Manu's proposal, as soon as you have a shared method, all members effectively become shared.It just seems pointless to type them as unshared anyway and then rely on convention within safe code to prevent unsafe accesses. Because, why? It just makes no sense.The pattern, is almost 100% of cses is: owner does all sorts of stuff... threadsafe interactions with things happen in high-frequency worker things. Parallel-for type workloads happen in bursts, the rest of the program works like normal. Parallel for workloads are restricted to the threadsafe API.With the proposal I posted in the beginning, you would then not only get implicit conversion of class references to shared, but also back to unshared.I'm not sure how your proposal (which felt way more complicated to me) improved on my design. Can you clarify how my very simple design is ineffective?I think the conflation of shared member functions and thread safe member functions is confusing.What else can it possibly mean to be a shared method? If it's not threadsafe, it's an insta-crash...shared on a member function just means that the `this` reference is shared.And by extension, you can only interact with other threadsafe methods.The only use case for this is overloading on shared.No, it's for separating threadsafe methods from those that aren't. Just like const methods separate mutating methods from those that don't.The D approach to multithreading is that /all/ functions should be thread safe, but it is easier for some of them because they don't even need to access any shared state. It is therefore helpful if the type system cleanly separates shared from unshared state.This is patently untrue. I can't implement any interesting shared architecture with the design as is today. I am also completely free to violate any sense of threadsafety with no restrictions of any kind. Write a shared method, access any members, call it from shared instances, watch the fire burn. I don't understand how you can claim that D's design says that all functions are threadsafe, it's so plainly far from the truth...?
Oct 18 2018
On 10/18/18 9:09 PM, Manu wrote:On Thu, Oct 18, 2018 at 5:30 PM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:I promised I wouldn't respond, I'm going to break that (obviously). But that's because after reading this description I ACTUALLY understand what you are looking for. I'm going to write a fuller post later, but I can't right now. But the critical thing here is, you want a system where you can divvy up a type into pieces you share and pieces you don't. But then you *don't* want to have to share only the shared pieces. You want to share the whole thing and be sure that it can't access your unshared pieces. This critical requirement makes things a bit more interesting. For the record, the most difficult thing to reaching this understanding was that whenever I proposed anything, your answer was something like 'I just can't work with that', and when I asked why, you said 'because it's useless', etc. Fully explaining this point is very key to understanding your thinking. To be continued... -SteveOn 18.10.18 23:34, Erik van Velzen wrote:No they don't, only facets that overlap with the shared method. I tried to present an example before: struct Threadsafe { int x; Atomic!int y; void foo() shared { ++y; } // <- shared interaction only affects 'y' void bar() { ++x; ++y; } // <- not threadsafe, but does not violate foo's commitment; only interaction with 'y' has any commitment associated with it void unrelated() { ++x; } // <- no responsibilities are transposed here, you can continue to do whatever you like throughout the class where 'y' is not concerned } In practise, and in my direct experience, classes tend to have exactly one 'y', and either zero (pure utility), or many such 'x' members. Threadsafe API interacts with 'y', and the rest is just normal thread-local methods which interact with all members thread-locally, and may also interact with 'y' while not violating any threadsafety commitments.If you have an object which can be used in both a thread-safe and a thread-unsafe way that's a bug or code smell.Then why do you not just make all members shared? Because with Manu's proposal, as soon as you have a shared method, all members effectively become shared.
Oct 19 2018
On Fri, Oct 19, 2018 at 9:45 AM Steven Schveighoffer via Digitalmars-d <digitalmars-d puremagic.com> wrote:On 10/18/18 9:09 PM, Manu wrote:I'm glad that there's movement here... but I'm still not 100% convinced you understood me; perhaps getting close though. I only say that because your description above has a whole lot more words and complexity than is required to express my proposal. If you perceive that complexity in structural terms, then I am still not clearly understood.On Thu, Oct 18, 2018 at 5:30 PM Timon Gehr via Digitalmars-d <digitalmars-d puremagic.com> wrote:I promised I wouldn't respond, I'm going to break that (obviously). But that's because after reading this description I ACTUALLY understand what you are looking for. I'm going to write a fuller post later, but I can't right now. But the critical thing here is, you want a system where you can divvy up a type into pieces you share and pieces you don't. But then you *don't* want to have to share only the shared pieces. You want to share the whole thing and be sure that it can't access your unshared pieces. This critical requirement makes things a bit more interesting. For the record, the most difficult thing to reaching this understanding was that whenever I proposed anything, your answer was something like 'I just can't work with that', and when I asked why, you said 'because it's useless', etc. Fully explaining this point is very key to understanding your thinking. To be continued...On 18.10.18 23:34, Erik van Velzen wrote:No they don't, only facets that overlap with the shared method. I tried to present an example before: struct Threadsafe { int x; Atomic!int y; void foo() shared { ++y; } // <- shared interaction only affects 'y' void bar() { ++x; ++y; } // <- not threadsafe, but does not violate foo's commitment; only interaction with 'y' has any commitment associated with it void unrelated() { ++x; } // <- no responsibilities are transposed here, you can continue to do whatever you like throughout the class where 'y' is not concerned } In practise, and in my direct experience, classes tend to have exactly one 'y', and either zero (pure utility), or many such 'x' members. Threadsafe API interacts with 'y', and the rest is just normal thread-local methods which interact with all members thread-locally, and may also interact with 'y' while not violating any threadsafety commitments.If you have an object which can be used in both a thread-safe and a thread-unsafe way that's a bug or code smell.Then why do you not just make all members shared? Because with Manu's proposal, as soon as you have a shared method, all members effectively become shared."divvy up a type into pieces"This is an odd mental model of what I'm saying, and I can't sympathise with those words, but if they work for you, and we agree on the semantics, then sure... If you write an object with some const methods and some non-const methods, then take a const instance of the object... you can only call the const methods. Have you 'divvied up the type' into a const portion and a non-const portion? If the answer is yes, then I can accept your description. I would talk in terms of restriction: An object has 4 functions, 2 are mutable, 2 are const... you apply const to the type and you are *restricted* to only calling the 2 const functions. An object has 4 functions, 2 are unsahred, 2 are shared... you apply shared to the type and you are *restricted* to only calling the 2 shared (threadsafe) functions. I haven't 'broken the type up', I'm just restricting what you can do to it from within a particular context. In the const context, you can't mutate it. In the shared context, you can't do un-threadsafe to it, and the guarantee of that is embedded in the rules: 1. shared data can not be read or written 2. shared methods must be threadsafe a. this may require that they be trusted at the low-level, like the methods of `Atomic(T)` b. no other method may violate the shared method's promise, otherwise it does not actually deliver its promise i. I have extensive experience with this and it's just not a problem in practise, but compiler technology to assist would be welcome! ii. This does NOT mean the not-shared methods are somehow shared; they just need to be careful when interacting with some (usually small) subset of members This design implies strong encapsulation, but that's a naturally occurring tendency implementing anything that's threadsafe. As a helpful best-practise to assure that non-shared methods don't undermine a shared method's commitment; prefer to interact with volatile members via accessors or properties that are themselves shared (can be private if you like). You will find that it's not actually hard to deliver on the object's commitment. If you write tooling that is at the level one-up from the bottom of the stack or higher, it should be unusual that you ever need to write a trusted method, in which case delivering on *your* shared methods promise is implicit.
Oct 19 2018
On Thursday, 18 October 2018 at 19:04:58 UTC, Erik van Velzen wrote:On Thursday, 18 October 2018 at 17:47:29 UTC, Stanislav Blinov wrote:Not directly - but obviously there must be *some* way to using it, in this case since it's an int with one of the core.atomic functions. At that point the spawned thread will be accessing it correctly, but the parent thread can modify the int in a non-atomic fashion.On Thursday, 18 October 2018 at 17:17:37 UTC, Atila Neves wrote:Manu said clearly that the receiving thread won't be able to read or write the pointer.On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:Doesn't work. No matter what you show Manu or Simen here they think it's just a bad contrived example. You can't sway them by the fact that the compiler currently *prevents* this from happening.Assuming the rules above: "can't read or write to members", and the understanding that `shared` methods are expected to have threadsafe implementations (because that's the whole point), what are the risks from allowing T* -> shared(T)* conversion?int i; tid.send(&i); ++i; // oops, data raceBecause int or int* does not have threadsafe member functions.https://dlang.org/phobos/core_atomic.html
Oct 19 2018
On Friday, 19 October 2018 at 18:00:47 UTC, Atila Neves wrote:Atomic and thread-safe are two very different concepts. Thread-safe is more of an ecosystem thing - if there are ways to do non-thread-safe operations on something, atomic operations are not enough to make things thread-safe. This is what core.atomic does: https://i.imgur.com/PnKMigl.jpg If you close the other openings, atomics are fantastic building blocks to making something thread-safe, but on their own they are not enough. -- SimenBecause int or int* does not have threadsafe member functions.https://dlang.org/phobos/core_atomic.html
Oct 19 2018
On Thursday, 18 October 2018 at 17:17:37 UTC, Atila Neves wrote:[snip]I had posted your library before to no response... I had two questions, if you'll indulge me. The first is perhaps more wrt automem. I noticed that I couldn't use automem's Unique with safe currently. Is there any way to make it safe, perhaps with dip1000? Second, Rust's borrow checker is that you can only have one mutable borrow. This is kind of like Exclusive, but it does it at compile-time, rather than with GC/RC. Is this something that can be incorporated into fearless?Assuming this world... how do you use shared?https://github.com/atilaneves/fearless
Oct 18 2018
On Thursday, 18 October 2018 at 21:24:53 UTC, jmh530 wrote:On Thursday, 18 October 2018 at 17:17:37 UTC, Atila Neves wrote:Yeah, I punted on making anything there safe. I have to go back and fix it. At least I wrote Vector from scratch to be safe.[snip]I had posted your library before to no response... I had two questions, if you'll indulge me. The first is perhaps more wrt automem. I noticed that I couldn't use automem's Unique with safe currently. Is there any way to make it safe, perhaps with dip1000?Assuming this world... how do you use shared?https://github.com/atilaneves/fearlessSecond, Rust's borrow checker is that you can only have one mutable borrow. This is kind of like Exclusive, but it does it at compile-time, rather than with GC/RC. Is this something that can be incorporated into fearless?Well, Rust's version of Exclusive is Mutex, and that's pretty much always used with Arc. The closest we have to a borrow checker is DIP1000 and that's what fearless relies on.
Oct 19 2018
On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:Okay, so I've been thinking on this for a while... I think I have a pretty good feel for how shared is meant to be. [...]I don't understand how you can safely have simultaneously shared methods that can modify data and unshared references that can modify data. struct S { int* x; void incX() shared { ... } } auto x = new int; auto s = shared S(x); Now I have two references to x, one shared, one unshared, both can be written to. What am I missing?
Oct 22 2018
On Monday, 22 October 2018 at 21:40:23 UTC, John Colvin wrote:On Monday, 15 October 2018 at 18:46:45 UTC, Manu wrote:S's constructor is not written in such a way as to disallow unsafe, mutable sharing of S.x. S.x is not private, and there's no indication that any attempt has been made to block off other avenues of access to S.x (other code in the same module could access it). S.incX is not safe. I don't know for sure what's inside S.incX, but let's assume it's this: void incX() shared { atomicOp!"++"(x); } This will fail to compile under MP, since atomicOp would take a ref int, not a ref shared int as it does today (since no implicit conversion from shared to unshared is allowed). To make it compile, you will need to explicitly cast it to unshared. Since incX is not safe, you can do this without the compiler complaining. (this is why you should be writing all the code that you can, as safe) If we assume you perform no unsafe casts, then I have no idea what S.incX would be doing - int* can have no thread-safe, trusted methods, so you can do absolutely nothing with S.x inside S's shared, safe methods. Most importantly though, why the hell are you trying to write such low-level code? The code you should be writing is this: struct S { Atomic!int x; void incX() safe shared { x++; } } Because Atomic!T has been written by an expert to be thread-safe. This is why we've been repeating, over and over, again and again, that Joe Average Programmer should not be writing the foundational building blocks of MP. -- SimenOkay, so I've been thinking on this for a while... I think I have a pretty good feel for how shared is meant to be. [...]I don't understand how you can safely have simultaneously shared methods that can modify data and unshared references that can modify data. struct S { int* x; void incX() shared { ... } } auto x = new int; auto s = shared S(x); Now I have two references to x, one shared, one unshared, both can be written to. What am I missing?
Oct 23 2018
On Tuesday, 23 October 2018 at 07:33:14 UTC, Simen Kjærås wrote:On Monday, 22 October 2018 at 21:40:23 UTC, John Colvin wrote:I don't think this direction of me providing examples and you examining them is going to work, as I clearly don't (or at least I hope I don't currently) understand the proposal. Is there an example something useful that would work and have an safe API under this proposal? Something that includes the implicit cast from T* to shared(T)* and some shared methods?[...]S's constructor is not written in such a way as to disallow unsafe, mutable sharing of S.x. S.x is not private, and there's no indication that any attempt has been made to block off other avenues of access to S.x (other code in the same module could access it). S.incX is not safe. I don't know for sure what's inside S.incX, but let's assume it's this: void incX() shared { atomicOp!"++"(x); } This will fail to compile under MP, since atomicOp would take a ref int, not a ref shared int as it does today (since no implicit conversion from shared to unshared is allowed). To make it compile, you will need to explicitly cast it to unshared. Since incX is not safe, you can do this without the compiler complaining. (this is why you should be writing all the code that you can, as safe) If we assume you perform no unsafe casts, then I have no idea what S.incX would be doing - int* can have no thread-safe, trusted methods, so you can do absolutely nothing with S.x inside S's shared, safe methods. Most importantly though, why the hell are you trying to write such low-level code? The code you should be writing is this: struct S { Atomic!int x; void incX() safe shared { x++; } } Because Atomic!T has been written by an expert to be thread-safe. This is why we've been repeating, over and over, again and again, that Joe Average Programmer should not be writing the foundational building blocks of MP. -- Simen
Oct 23 2018
On Mon, 15 Oct 2018 11:46:45 -0700, Manu wrote:Somehow I lost track of your reason behind the promotion. Why not just ask to be able to call shared methods on a thread-local variable? No implicit casting; you need to take positive action to share anything. And if there's a non-shared overload, that's still preferred. Would that be enough? Or do you have use cases that wouldn't fit there?From there, it opens up another critical opportunity; T* -> shared(T)*promotion. Const would be useless without T* -> const(T)* promotion. Shared suffers a similar problem. If you write a lock-free queue for instance, and all the methods are `shared` (ie, threadsafe), then under the current rules, you can't interact with the object when it's not shared, and that's fairly useless.
Oct 22 2018
On Tuesday, 23 October 2018 at 01:26:57 UTC, Neia Neutuladh wrote:Somehow I lost track of your reason behind the promotion. Why not just ask to be able to call shared methods on a thread-local variable? No implicit casting; you need to take positive action to share anything. And if there's a non-shared overload, that's still preferred.Because these have the same signature: struct S { void method() shared; } void func(ref shared S zis); i.e. calling a shared method on a thread-local variable does require an implicit cast of the 'this' reference. Just like calling a 'const' method.
Oct 23 2018
On Tuesday, 23 October 2018 at 01:26:57 UTC, Neia Neutuladh wrote:On Mon, 15 Oct 2018 11:46:45 -0700, Manu wrote:This is easily achievable with template this.Somehow I lost track of your reason behind the promotion. Why not just ask to be able to call shared methods on a thread-local variable? No implicit casting; you need to take positive action to share anything. And if there's a non-shared overload, that's still preferred.From there, it opens up another critical opportunity; T* -> shared(T)*promotion. Const would be useless without T* -> const(T)* promotion. Shared suffers a similar problem. If you write a lock-free queue for instance, and all the methods are `shared` (ie, threadsafe), then under the current rules, you can't interact with the object when it's not shared, and that's fairly useless.
Oct 23 2018