WhenAnyValue might not send latest value #3189
-
Describe the bug Steps To Reproduce
´´´ Environment
|
Beta Was this translation helpful? Give feedback.
Replies: 34 comments 31 replies
-
Isn't this just a standard race condition? Both threads are running in parallel and nothing is stopping the subscription from being setup after the Task has completed.
No initial value is "sent", you just Assert that found = false at the end, which is what you initialize it to. The subscription is never called if it is setup after the value is changed. |
Beta Was this translation helpful? Give feedback.
-
If the task completes before the subscription is setup, WhenAnyValue emits 2 and the test passes. |
Beta Was this translation helpful? Give feedback.
-
If I would modify the test and add .Do(TestContext.Write) before the .Where, I would expect the test to sometimes print 12, sometimes print just 2 and in a few occasions print 21, but in all cases I would expect the test to pass and never to print just 1 and fail. There is no race condition between assigning the values 1 and 2. |
Beta Was this translation helpful? Give feedback.
-
There's a race condition in your Code. The task will start immediately which may or may not be before the WhenAnyValue is registered. Try converting your task into a Func and making it into a function you call after the WhenAnyValue is registered and then await that. |
Beta Was this translation helpful? Give feedback.
-
@glennawatson I tested this code: public MainWindowViewModel()
{
var i = 1000;
do
{
i--;
WhenAnyShouldBeThreadSafe();
} while (i > 0);
}
public void WhenAnyShouldBeThreadSafe()
{
var anyReactiveObject = new SomeReactiveObject { Value = 1 };
Task.Run(() => anyReactiveObject.Value = 2);
anyReactiveObject.WhenAnyValue(x => x.Value)
.Subscribe(x =>
{
if (anyReactiveObject.Value is 1) Debug.WriteLine(anyReactiveObject.Value);
});
}
}
public class SomeReactiveObject : ReactiveObject
{
public int Value
{
get => value;
set => this.RaiseAndSetIfChanged(ref this.value, value);
}
private int value;
} In theory |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
@glennawatson Without That would mean Value = 1 should never show up in the observable / subscription (be sent to debug out), or am I missing something? |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
Ignoring all of the code above - from my understanding, just these 2 lines of code should never fire the subscription, correct? anyReactiveObject.Value = 1;
anyReactiveObject.WhenAnyValue(x => x.Value).Subscribe(x => Debug.WriteLine(anyReactiveObject.Value)); So value = 1 should never show up in the debug output. This code however will very seldomly show 1 in the debug output: anyReactiveObject.Value = 1;
Task.Run(() => anyReactiveObject.Value = 2);
anyReactiveObject.WhenAnyValue(x => x.Value).Subscribe(x => Debug.WriteLine(anyReactiveObject.Value)); I can't reason as to why 1 should ever show up, but it does. |
Beta Was this translation helpful? Give feedback.
-
You don't control when Task.Run runs, it's up to the task scheduler |
Beta Was this translation helpful? Give feedback.
-
I understand there is a race condition happening. In this race condition there should be two scenarios:
There should be no scenario 3:
But that 3rd scenario does indeed occur 5-10 times out of 1000. @RLittlesII Can you sanity check me here? |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
Scenario 3 can happen
|
Beta Was this translation helpful? Give feedback.
-
The fact you are using |
Beta Was this translation helpful? Give feedback.
-
Sorry guys, didn't know it emitted the initial value on setup, please forget I ever existed, lol I checked the docs here when I first saw this issue and it only mentions things like this for a while:
Funny thing is I've been using it for months now... |
Beta Was this translation helpful? Give feedback.
-
I know how await and Task.Run works. When await task returns I know that the task has completed, that is what is important here. I want the task and setting up the subscription to be run in parallel in this test since that is what is triggering the error. In my real application it is a completely different part of the code running on a different thread updating the value. This is just a very simple example of reproducing the behavior of a multi threaded application where my WhenAnyValue subscriptions does not trigger if the value is updated at the same time as the subscription is set up. |
Beta Was this translation helpful? Give feedback.
-
I see your problem now. The subscription is run on the thread that changed the value, not the one where it was setup. When the Task runs on a different thread then the bool is changed on that thread, and bool is inherently not thread safe, so back on the original thread it may or may not Assert to true at the end. If you make sure to change the bool on the original thread in the subscription then it should work as you expect. |
Beta Was this translation helpful? Give feedback.
-
That is not the issue. The problem is that the value 2 is never emitted. I can change the bool to an int, increment it with Interlocked.Increment in the subscription and add a memory barrier before my assert, it is still the same error. The lambda passed to subscribe is simply never executed. |
Beta Was this translation helpful? Give feedback.
-
I have no idea what you are talking about now. If the lamba didn't execute then why does this print to debug output? anyReactiveObject.WhenAnyValue(x => x.Value).Where(v => v == 2).Subscribe(_ => Debug.WriteLine("Hello World!")); |
Beta Was this translation helpful? Give feedback.
-
@erikm-g Have you considered using Synchronize to ensure that the observables from multiple threads are received correctly? |
Beta Was this translation helpful? Give feedback.
-
You may think there are cases where the lamba isn't being executed because the Each one of those cases is because bool isn't thread safe. Yes, when you |
Beta Was this translation helpful? Give feedback.
-
Also, in your original codebase initial values won't be emitted (the lamba won't be executed) if you use |
Beta Was this translation helpful? Give feedback.
-
We will be locking this thread soon but welcome to continue a help topic in the discussion area. Just due to the mass emails it produces |
Beta Was this translation helpful? Give feedback.
-
@RLittlesII I am not sure how Synchronize would help. There are no OnError or OnCompleted events and there is no issue if 2 OnNext are called at the same time. I tried adding a call to Synchronize directly after WhenAnyValue in my test and it did no difference. @kmgallahan The test works about 299 times of 300, in those cases you will get your print out. If you check the 1 time it doesn't work it is not printed. I think the issue is that WhenAnyValue internally gets the initial value first and starts to listen on changes after that, so any changes made in between are missed. It needs to be done the other way around to be thread safe, and if you want to avoid sending the initial value twice some check for that need to be added. If I run this modified test where the initial value from WhenAnyValue is skipped and the current value is emitted after the subscription to changes is set up, the test succeeds every time.
|
Beta Was this translation helpful? Give feedback.
-
According to the
So this code should never fail, but it does sometimes: var bag = new ConcurrentBag<int>();
var anyReactiveObject = new SomeReactiveObject();
var task = Task.Run(() => anyReactiveObject.Value = 2);
anyReactiveObject.WhenAnyValue(x => x.Value).Subscribe(x =>
{
if (x == 2) bag.Add(2);
});
await task;
Debug.Assert(!bag.IsEmpty); Either:
The bag should never be empty for the assertion, yet it sometimes is. There might be a race condition inside Where the sequence goes:
In this scenario a subscription to WhenAnyValue will only emit the initial unchanged value and not the second value. This might be resolved if property change notifications are setup before an I'm not entirely sure if this covers all race conditions in the logic, but it may help. |
Beta Was this translation helpful? Give feedback.
-
WhenAny is not "thread-safe", it has the same thread safety rules as any other C# object. You should not write to ViewModel properties on other threads, as they inherit the thread affinity of the Views they are bound to. If you wouldn't write from other threads to a View, you shouldn't do it to a ViewModel! |
Beta Was this translation helpful? Give feedback.
WhenAny is not "thread-safe", it has the same thread safety rules as any other C# object. You should not write to ViewModel properties on other threads, as they inherit the thread affinity of the Views they are bound to. If you wouldn't write from other threads to a View, you shouldn't do it to a ViewModel!