-
Notifications
You must be signed in to change notification settings - Fork 3.2k
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
Add a guard to prevent interleaving of async calls #3291
Comments
The EF6 implementation of guards around async calls appears to focus on wrapping top level API calls in semaphore checks via memory barrier (using Interlocked exchanges to increment and decrement a EF6 updates the semaphore (and throws if already set) on the following operations:
There is also a method on the This throwing but not mutating method is called in the following locations:
Note: The |
Based on discussions with the team so far, this enhancement has the following requirements / considerations:
We can get the same effect as the EF6 There are fewer entry points to the current EF7 implementation
Notes on Query:
The previous implementation relied on placing guards around the entry points to the API. While effective, this does not give any deep / common coverage. This kind of coverage could be added in the consolidated
|
Could I have you take a look at my description of the considerations for query and relational commands for this enhancement to see if there are any important factors that I have overlooked? |
I may have missed some entry points in the |
@mikary Personally I think this should probably be scoped only to entry points that should be awaited. Also, when you say, "It has been suggested that such a guard could be implemented without using memory barriers, to avoid a performance impact if we are willing to accept a lower level of reliability for concurrency detection" how is this going to work without the potential for throwing when the code is correct? What is to stop the second thread getting a stale value if no memory barriers are used? |
@ajcvickers As long as the calling code is correct, there shouldn't be a second thread running at the same time. It shouldn't be any different from a standard sync or async code path that increments and decrements a counter. |
@mikary Thread 1 runs, increments counter, decrements counter. Thread 2 runs looks at counter but doesn't see the decremented value because no memory barrier; throws. Code is correct. Throws anyway. |
Unless I am missing something, I agree with @ajcvickers is right that doing this in unsafe way could lead to false positives, and obviously we don't want that. I am interested in hearing more details about this:
Initially I was of the same opinion but now I am finding it harder to defend it. I get that a guard at the database access level would not catch any concurrent access of the sync entry points that don't access the database it should catch most other misuses. Is it that the cost on the happy path would be too high? |
@ajcvickers I don't see how that is a correct use case, we're talking about a counter that is scoped to a single DbContext instance. If more than one thread is operating on the context the code is not correct. |
@mikary If more than one thread is operating concurrently on the context the code is not correct. One thread making a call and that call ending, followed by another thread making a call on the same instance is valid. @divega It's a balance between the value of the check and the overhead (in terms of pure perf, possible contention, and implementation/maintenance cost). Async invites, even encourages, people to do multi-threading without even thinking about it. Catching this has high value. Also, async by its nature in our code means an I/O operation, and async is slow, so the perf/contention factor is less likely to be a problem. On the other extreme, to @mikary's question, If we think that the value in trying to catch general multi-threaded use is high enough, then we should try to test what the overhead is of the check for very fast, non I/O operations. But this investigation is also a cost we have to pay. I don't think it is worth it. |
@divega If the concern @ajcvickers raised about reference counting and threading is an issue for this case, we have other areas in the code (i.e. |
@ajcvickers Wouldn't there need to be some memory barrier between the calls on different threads in that case anyway? Otherwise how are they synchronized not to overlap? |
After further discussion, we are looking at putting the guards around the main entry points to the API (SaveChanges, ADO commands like ExecuteSqlCommand, and Query). These guards should be in Core if possible, but deep enough in the stack to cover as many code paths as we can. SaveChanges
Recommendation:
ExecuteSqlCommand
Query
Potential locations (compilation):
Recommendation:
Potential locations (execution and enumeration):
Recommendation:
|
Additional thoughts on Query execution and enumeration
|
@mikary As a starting point, put it anywhere we do diagnostics exception interception: This gets you all queries and SaveChanges. |
It seems to me we shouldn't need to put a guard around query parameterization and compilation because chances are we can detect misuse later during query execution. But perhaps I am missing something? |
Singleton queries get executed immediately. |
You mean that is what I was missing? I see this as another query execution path that we need to put guards around (i.e. it is not only about results enumeration) but still not a reason to put a guard around query compilation and parameterization. |
Yes, and those paths are in QueryCompiler. |
I just wanted to make clear that putting a guard around query compilation and parameterization is not a goal. |
We did this in EF6 to prevent multiple threads from using the same context concurrently, and to guide users to the right async patterns.
See #3240.
The text was updated successfully, but these errors were encountered: