-
Notifications
You must be signed in to change notification settings - Fork 767
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce FiringMode.Serial #527
Comments
I didn't think stateless supported being used in a multithreaded way. Having multiple threads attempting state changes seems extremely problematic. One thread could be checking a guard whilst another thread changes the state immediately. The first thread then approves the state change and moves to the target state which may not be a permittable transition. I wouldn't be encouraging users to interact with a state machine in a multithreaded fashion. If I were you, I would be looking to queue the transitions and have them processed sequentially. |
You are right, stateless is not thread-safe, and it's documented as such. But the fact the
As a workaround, I'm currently using the |
There is a caveat with my proposal: if the entry action of one state |
I would always wrap anything in a multithreaded environment to de-parallelise as soon as possible (wherever that makes sense) at a boundary so that everything within can be safely single threaded. |
Right, the workaround exists. The problem with any workaround is that it must be remembered every time a change is introduced in the code. A new developer who will maintain the code after me, or even me, must remember to wrap the call with a semaphore. The question is if the built-in support is legitimate. I think it is, but with a different implementation to avoid deadlocks. |
I think there is some misconception about the way in which the firing modes are designed to work, as well as their intended use-cases. Perhaps this needs to be cleaned up in the documentation.
Stateless is not designed to handle any concurrency. I'd like to avoid using the term "mulithreaded", because it is confusing when discussing Stateless' async methods. A state machine object is fine to be called from many different threads, so long as the methods are not called concurrently. For example the execution flow might bounce between many different threadpool threads when calling Stateless' async methods, but they're still only being run in a serialized fashion, one after the other. Back to Consider the following triggers:
With With It sounds like what you want with This is similar to the "do activities" issue here: #77 See my other comment here for an example implementation of a work queue which can serialize the state machine fire events: #77 (comment) Considering that this usecase keeps coming up again and again, it's worth considering how we can build this directly into Stateless itself, I'm just not sure how the |
@crozone, thanks for the explanation of the Currently, Continuing the idea of the worker task and priorities when using the new mode (you can call it I think this way, thread safety can be built-in into the library while providing additional functionality. Note that |
@mclift Do you think we should start a proper design discussion for a I think the work items would be:
|
I've been looking into this and considering the use of Since we're simply serializing the firing of triggers, we can get away with as something as simple as a private object currentTaskLock = new object();
private Task currentTask = Task.CompletedTask;
private Task EnqueueTask(Func<Task> action)
{
lock (currentTaskLock)
{
currentTask = currentTask.IsCompleted
? action()
: currentTask.ContinueWith((t) => action(),
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default).Unwrap();
return currentTask;
}
} The basic idea is that any currently executing Fire() task is stored as the Synchronous Fire() calls can be handled by wrapping them in Tasks and then waiting. |
I'm not an expert on |
Hey Guys,
Making anything thread-safe is no trivial task. Plus it has performance
implications for all single-threaded usages plus increased maintenance
complexity going forward - as developers need to ensure they do not
introduce a new possible vector for corruption in a
multi-threaded environment.
I've recently spent some effort to ensure the synchronization context is
not lost so that the state machine can be used within Microsoft Orleans.
Any introduction of continuations etc needs to ensure they do not undo this.
Please may I ask what the use cases are that we are trying to solve? In my
experience, synchronising threads should be done well before it would reach
a component such as our state machine.
Lee.
…On Thu, 13 Jul 2023 at 09:57, Artur Gordashnikov ***@***.***> wrote:
I'm not an expert on ContinueWith methods, so that I may be wrong. But
according to the docs, the TaskContinuationOptions.ExecuteSynchronously
will run the continuation state change on the same thread as the previous
state change. It means the previous state change caller will be
unnecessarily blocked for a prolonged time. I think you should use
TaskContinuationOptions.RunContinuationsAsynchronously here.
—
Reply to this email directly, view it on GitHub
<#527 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AARWVQ2OO6GMPXYQ6YA4NK3XP6L35ANCNFSM6AAAAAAXQIQLIQ>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
@leeoades I appreciate your concern, stateless has very large consumers that we don't want to subtly break with a change like this. The use-case for Since the state machine is not threadsafe, the trigger needs to be first put into some sort of work queue (like a The state machine already contains an internal queuing mechanism for the case when triggers are fired from within a state transition, which is enabled when the firing mode is set to This would be implemented as an additional firing mode, The default firing mode, In any case we are definitely not going to rush into implementing this without plenty of prototyping. It may turn out to not be a good idea. Ryan. |
Thanks for taking the time to respond, @crozone If this were an event-driven scenario - where the events come in, adjust the state and cause change events - then I think queuing the state changes would work. This is essentially a read model. Funnily enough, I built an RX State Machine a looong time ago. In this case, RX itself was responsible for synchronising the calls, and there was no ability to read the state machine. The events coming in only led to events going out. Any interrogation of the current state by the consumer is unreliable and could lead to unintended behaviour since of course the current state could be behind with queued triggers yet to be processed. The consumer might raise a trigger thinking they were affecting the state in one way but instead they are queuing the trigger and they act against a potentially different state once the trigger is processed. If the entire interaction with the state machine was atomic - it could interrogate the current state when it was that consumer's turn and triggers fired would occur immediately. Queuing these interactions at this higher level is safer and more intuitive. Synchronise all traffic through not only the state machine but through the wider context. Why not fork the repo and build your multi-threaded version? I'll be happy to throw some multithreaded unit tests at it. |
This is true also today. If the SM is not encapsulated and its state is exposed to the external caller, he can do the same before queuing the trigger and will suffer from the same problems. But doesn't it the whole point of the SM that you don't write code like this?
And instead, rely on SM rules to decide what to do with the trigger in the current state? So, this example is just a wrong usage of the SM rather than a problem with SM implementation. |
Currently, stateless supports two
FiringMode
's:Immediate
andQueued
. Both of them have their use cases, but unfortunately, neither of them matches my needs.Immediate
will change the state even if another state change is in progress by another thread hence can be used only in a single-threaded environment.Queued
will enqueue the state change and return to the caller immediately, while the enqueued job will be executed by the thread which executes the current state change. Both immediate return and forwarding the work to another thread are unacceptable in my scenario. In addition, it has its own multi-threaded usage problems.I suggest introducing a new
FiringMode.Serial
. In this mode, the call will block if there is an active state change and will fire only after it is finished. I'm just starting to learn the source code of the library, but it looks like this can be achieved very easily by introducing aSemaphoreSlim
based lock in theInternalFire(Async)
methods.Also, since the lock can be held for a long time, it's good to introduce
Fire(Async)
overloads that acceptCancellationToken
.The text was updated successfully, but these errors were encountered: