Records and concurrency #3857
-
I'm experimenting with records, and I think that concurrency will cause some real confusion and bugs for people, when they start using records to hold their UI state. Here's some code:
|
Beta Was this translation helpful? Give feedback.
Replies: 15 comments 2 replies
-
I'm really not sure what you're saying will happen here, and what is expected? |
Beta Was this translation helpful? Give feedback.
-
The thing is, you're not just mutating a property. You're re-assigning I don't think records are at fault here specifically: any immutable object is going to have the same issue. Heck, multithreaded lockless manipulation of mutable objects is normally going to have the odd race condition hiding away. Also, no one should be manipulating UI state from a non-UI thread. Ever, regardless of records. Always marshal back to the UI thread before doing anything like that. WPF will (un)helpfully marshal INPC events to the UI thread for you most of the time (but you're still open to race conditions if you try), but won't do the same for many other things, including INCC. Most other UI frameworks will throw, or just silently go bad. (As an aside, you can use a |
Beta Was this translation helpful? Give feedback.
-
Thanks for the pointers, canton7, there are definitely things for me to look at there. Mostly, my feeling is that these situations are traps waiting for the unsuspecting. In particular, I don't see any value in allowing await inside a with block. The record reference is set at the beginning of the with, and the UI thread is free to change the record reference while the await is happening. That means that any changes that happen during the await are guaranteed to be lost. Consider a UI with these two events:
If you click the Slower button, and then click the Faster button, the end result is that the First property will be updated, but the faster change to the Second property will be lost. This is only because the await is inside the with block. If the compiler forced or encouraged me to move the await up, then no changes would be lost. |
Beta Was this translation helpful? Give feedback.
-
Yeah, I can see there being an argument for Not necessarily as a thread safety argument, but more in case one of the wither arguments re-assigns the record as a side effect. |
Beta Was this translation helpful? Give feedback.
-
Tagging @jcouv what are our rolls here for evaluation of the 'with' receiver and the assignment to the lvalue? I would expect to this to be similar to be how we do object initializers. In this case I could see though it's evaluating all the initializers first, then grabbing the receiver and doing the clone. Or I could see us grab the receiver first. This is possibly worth a quick ldm email |
Beta Was this translation helpful? Give feedback.
-
I see a ton of value in that. I would find it exceptionally unfortunate if you could not do that. There is an important discussion to be had though about evaluation order here. And, in general, you will always need to be careful if you're partially evaluating and assigning in async contexts |
Beta Was this translation helpful? Give feedback.
-
I'm sorry for speaking so absolutely. I certainly can't claim any authority, especially among this crowd. To further clarify my point:
is pretty clear and predictable.
has some very subtle things going on, and at the moment is somewhat dangerous. I hadn't even considered what happens if the called method changes the record reference, like @canton7 mentioned. That would move this conversation away from concurrency. since await isn't necessary.
|
Beta Was this translation helpful? Give feedback.
-
I don't really understand what records have to do with this though. As I mentioned above:
You are referencing a value in a certain location, hit also assigning over that location. This is always something you need to be careful around. |
Beta Was this translation helpful? Give feedback.
-
I would expect the same, it should be equivalent to |
Beta Was this translation helpful? Give feedback.
-
I also wouldn't expect this to be different from |
Beta Was this translation helpful? Give feedback.
-
On reflection, I would be very surprised if this code created record Rec
{
public string Member { get; set; }
}
class C
{
private string s = "first";
private Rec GetRec()
{
s = "second";
return new Rec();
}
public void M()
{
rec = GetRec() with { Member = a };
}
} |
Beta Was this translation helpful? Give feedback.
-
I worked up a slightly more real-world example of the await-less version of this issue. Nothing that the compiler is doing now is unexpected. It is all in-line with the way that everything else in C# behaves. It's just that the end result of all of this expected behavior is disappointing. I view the await-less version to be "irritating, but I should have programmed it better". The await version is much more serious, since I have no control over what state changes will be applied (and subsequently lost) while the await is going.
Results in
|
Beta Was this translation helpful? Give feedback.
-
This kind of problem is always possible when mutating shared state in multiple locations, regardless of threading. |
Beta Was this translation helpful? Give feedback.
-
I've been experimenting with F# and fabulous to see if I can get some of the same behavior there. The framework blocks the dumb thing I'm doing in two ways.
In addition, I can't find a way to await inside a |
Beta Was this translation helpful? Give feedback.
-
Wrapping this conversation up... Don't do what I did.
I'm still interested to know if Thank you all for the insight and clarification. |
Beta Was this translation helpful? Give feedback.
Wrapping this conversation up...
Don't do what I did.
await
in awith
block if you are certain that your record reference can't be changed elsewhere during the await. (Personally, I will round this to "Never useawait
in awith
block")I'm still interested to know if
await
inside awith
block is something that is possible in F#. It seems like a bad idea to release the UI t…