In Chapel, a direct access to a sync variable in a RHS context, like mySync$; is interpreted as a .readFE() which can cause surprises (e.g., deadlocks) if the someone reading/modifying the code isn't aware that this is a sync variable. This is the reason that we label syncs with $ by convention鈥攖o alert the reader of the code to the fact that it isn't a plain old variable.
It's been pointed out that the fact that the language relies on a naming convention to communicate this is arguably problematic. Both the behavior and the naming convention were inherited from the Tera MTA / Cray XMT programming model, so there's precedent, but that doesn't necessarily make the ideas gold-plated.
This choice also leads to a few other irregularities. For example:
write(mySync$); and read(mySync$); are a bit odd in that it's not clear whether the argument should be evaluated as a more-or-less typical reference to a sync variable being passed to a procedure or whether they should get special treatment due to this I/O context (where issue #15891 goes into this in more detail).
In most (though not all) cases, var x = y; causes x to have the same type as y. However, if y is a sync int variable, say, then x is an int (whose value is obtained by doing a .readFE() on y, making it somewhat odd.
So this issue essentially asks whether we should consider eliminating the interpretation of direct accesses to sync variables as being .readFE/.writeEF and require methods to be called explicitly on them instead. The naming convention could be retained, but would not be as semantically necessary to understand a program because an unadorned sync variable wouldn't be legal in most contexts (other than passing them to routines expecting a sync variable argument?).
If some generic function wrote var x = someGenericArgument;, would it be a compilation error to call that generic function with a sync variable? In other words, sync variables are not copy-initializable?
Would this apply to single variables as well?
I do recall us having problems with generated functions without this support - for instance, if a class has a sync variable field, at some point the initializer was generated with a plain assignment and this meant that the generated initializer was basically useless and had to be overwritten by the user to get correct behavior. There's certainly ways to work around that issue (and perhaps those ways are currently implemented today, I'm not as up to date on the initializer code), so just throwing it out there as a consideration.
If some generic function wrote var x = someGenericArgument;, would it be a compilation error to call that generic function with a sync variable?
In a strict reading of this issue's proposal, yes. But we could consider doing otherwise in some cases such as initialization, which also might help with Lydia's concern (although, note that Lydia's case is a little different in that the sync field is the thing being initialized, not the thing being read as part of the initialization). All that said, there are a lot of things you could do in a generic routine that wouldn't necessarily make sense or result in correct code if you passed a sync or single in (such as write var x = someGenericArgument;, var y = someGenericArgument;
In other words, sync variables are not copy-initializable?
I think it depends on what you mean a type being copy-initializable. Recall that syncs are a bit odd in that var x = mySyncInt$; will infer x to be an int based on the result of what happens for a default read of mySyncInt$. So it might suggest that we can't copy initialize a type-inferred variable from a sync int RHS, but I don't think it has to mean we can't copy-initialize into a known sync int (which, again, is the case that Lydia's case seemed to be thinking about).
Would this apply to single variables as well?
I was thinking "yes" when I wrote this, but didn't think very hard about that case, so don't feel confident enough in that answer to mention it here. I generally think syncs and singles should be as similar as possible (apart from their obvious "write-once vs. many times" difference), though they arguably might want to behave differently w.r.t. the "what do I/O routines do with the type?" given that singles aren't meant to be re-assignable (?). I think we definitely should think about single as well, but given how much more rarely they're used, I'd like to get to a more comfortable place with sync first.
It's probably obvious, but the bad smell around syncs for me stems from:
$ when naming sync variables.var x = mySyncVar; assert(x.type == y.type); would fail for a sync without the user being able to make a similar type?So this issue is exploring one possible way to address these concerns.
So it might suggest that we can't copy initialize a type-inferred variable from a sync int RHS, but I don't think it has to mean we can't copy-initialize into a known sync int (which, again, is the case that Lydia's case seemed to be thinking about).
I think you are saying
var x: sync int;
var y: sync int = x;
that this is valid. But what does it do? Does it create a new sync variable storing the same value as x? Does it leave x empty? Given the problems described in this issue, if we went in the general direction, wouldn't it be better to write var y: sync int = x.readFF() or var y: sync int = x.readFE(); ?
In the OP's proposal, it wouldn't be valid, and you'd have to apply a .readXY() call to x. The excerpt you quoted was trying to say that if you found that to be a non-starter in the context of a variable initialization ("wait, sync variables are not copy initializable?!?" :) ), then we could consider relaxing it in that case. But yeah, it does lead to ambiguities, and these could be particularly confusing in a world where direct accesses to syncs no longer meant .readFE() by default. Though if one weren't coming from that historical perspective, it might be most natural to have y take on the identical state as x (same F/E state, same value), leaving x unmodified. I'm not necessarily advocating for that, but am open to it.
One could also go further and say that var y = x; would create a new sync variable whose state was identical to x which would make syncs less odd w.r.t. inferred type copy initialization while still disallowing the ability to have default reads/writes to them in more general expression contexts. Again, this isn't something that I'm advocating for or that was in the OP, but I think it could be considered (though probably only after we had a release that prevented such declarations by default since the meaning would change pretty dramatically).
I'm generally in favor of making sync variables not copy-initializeable for now and then considering allowing copies that clone the state at some point in the future. I think our copy elision is probably good enough now that having sync variables not be copy-initializeable at all is probably not that big of a deal, other than requiring the .readFE() calls as discussed above.
More broadly, are you saying you're general open to / favorable towards this proposal?
(in either case, I was imagining taking a next step of disabling the implicit accesses of sync variables, seeing the impact on codes, and taking stock of the situation based on what's learned there).
More broadly, are you saying you're general open to / favorable towards this proposal?
Yes
To clarify, are you suggesting requiring an explicit readFE() where today I can write var a = b$ + c$; ?
If so, maybe we can have uniform treatment with futures as well?
In a lunchtime conversation long time ago we were concerned about generic code that could be used interchangeably, say, with ints and sync ints. We were thinking about a way to do "readFE if it is a sync, just get the value if it is an int".
Requiring an explicit readFE()/readFF() makes sense to me, the biggest obstacle being legacy code and user mentality. Also, how much attention has this topic received from users in the past? This should give some indication of user impact of the current design or of changing it.
Speaking about using a sync variable as a statement-level expression, currently implying readFE(), we can also think about other variable types used as a statement-level expression to avoid split initialization. Do we want to change how it is done?
are you suggesting requiring an explicit readFE() where today I can write var a = b$ + c$; ?
Yes.
If so, maybe we can have uniform treatment with futures as well?
We don't have futures today, right? (or are you talking about the futures library?)
We were thinking about a way to do "readFE if it is a sync, just get the value if it is an int".
Yeah, I feel like we never cracked that puzzle, though. And I remain skeptical that it's reasonable to write (very interesting) generic code that works on both ints and sync ints given that you can read an int multiple times without problems, but not a sync int.
how much attention has this topic received from users in the past? This should give some indication of user impact of the current design or of changing it.
My sense is that most users haven't used syncs all that much, though I could be wrong. I think it's a feature they appreciate knowing is in the language, but then rarely use. I think there are still patterns where syncs are great (producer-consumer styles of computation, e.g.), but in other cases, it's interesting that we've gradually been replacing syncs with atomics over time.
we can also think about other variable types used as a statement-level expression to avoid split initialization. Do we want to change how it is done?
I'm not quite sure I understand what you're asking here. Are you saying, would we want to support other ways of forcing default initialization? I think Michael's got an issue on this, where my favorite contender is var x: int = init; as a means of saying "default initialize me" (in contrast to noinit, say).
Sounds like the issues I brought up do not affect the proposal here. In detail...
Futures - I was thinking about the future where futures are part of Chapel language. Maybe we will want readFE and "force computing the future's value" look the same.
"readFE if it is a sync, just get the value if it is an int"
I don't see anything special/promising here either.
User feedback - sounds like syncs/singles are off the beaten track. This makes it easier to make a change being proposed.
The force-default-init issue is #15769. If we implement one of its proposals, then we may want to outlaw variable as a statement-level expression altogether.
An interesting case that came up when I started prototyping this to see the impacts was
mySyncVar$ op= val;
Specifically, this is a case that, if written out with explicit access methods, would need to be written:
mySyncVar$.writeEF() = mySyncVar$.readFE() op val;
which is very explicit, yet verbose. It could be that's exactly what the doctor ordered. Or it could be a reason to support op= on sync vars as a shorthand. Or it might suggest moving such operators into an optional library (but, given recent discussion, would that mean that if any module used them, everyone would suddenly have access to them?)
Musing on this op= case a bit more, I think it makes sense to require the long-form version for now at least, for consistency and to avoid assumptions about what the starting and ending state are expected to be.
I'm in favor of deprecating direct access to sync vars. I'm definitely not in favor of having relatively subtle (and totally optional) lexical indicators mark semantics as dramatic as task suspend/resume due to full/empty on a sync var. It made some sense on MTA/XMT. I came late to that party so don't know for sure, but it seemed the notion there was that applications using full/empty semantics for correctness or performance would not be portable to other systems anyway, so the subtle lexical cue of a trailing '$' allowed invoking the special capabilities of the programming model while being minimally-invasive to the expression of the algorithm. (We in Chapel say that same thing about getting rid of MPI calls.) But that sort of implicit lightweight multithreading can't be performance-portable without architectural support, so Chapel programmers are better off using different algorithms than the more dataflow-ish style where MTA var$ full/empty really shone.
It's true this will end up producing some wordy situations such as the op= already mentioned, but I suspect op= with sync vars isn't terribly common anyway.
How do we feel about wordiness of access to syncs/singles vs. accesses to atomics? There have been some discussions, perhaps "off list", to allow dropping .read() / .write() for atomics.
Given that atomics are used more frequently than syncs, at least nowadays, this may not be a consideration.
It does seem ironic that I'm advocating for more wordy sync accesses while also wanting less wordy atomic accesses. My feeling is that the rationale for making syncs more wordy is that it can be super problematic when you accidentally read or write a sync unwittingly, for example causing program deadlock. And there are multiple modes for reading/writing sync, so while we've considered certain ones to be "most common", the intent may not always be obvious.
In contrast, atomics generally don't have those problems: direct reads/writes can't cause deadlock and they only have one read/write mode. So whenever I find myself saying myAtomic.read() I wonder why I can't just say myAtomic (and ditto for myAtomic.add(1) vs. myAtomic += 1;). As I understand it, C++ also provides a precedent for supporting more direct operations on atomics.
Most helpful comment
It does seem ironic that I'm advocating for more wordy sync accesses while also wanting less wordy atomic accesses. My feeling is that the rationale for making syncs more wordy is that it can be super problematic when you accidentally read or write a sync unwittingly, for example causing program deadlock. And there are multiple modes for reading/writing sync, so while we've considered certain ones to be "most common", the intent may not always be obvious.
In contrast, atomics generally don't have those problems: direct reads/writes can't cause deadlock and they only have one read/write mode. So whenever I find myself saying
myAtomic.read()I wonder why I can't just saymyAtomic(and ditto formyAtomic.add(1)vs.myAtomic += 1;). As I understand it, C++ also provides a precedent for supporting more direct operations on atomics.