We are using a proprietary API that requires synchronization of data at some point. I've thought about some ways of ensuring data consistency but am eager to get more input on better solutions.
Here is a long running Task
outlining the API syncing
new Task(() =>
{
while(true)
{
// Here other threads can access any API object (that's fine)
API.CriticalOperationStart(); // Between start and end no API Object may be used
API.CriticalOperationEnd();
// Here other threads can access any API object (that's fine too)
}
}, TaskCreationOptions.LongRunning).Start();
This is a separate task that actually does some data syncing.
The area between Start and End is critical. No other API call may be done while the API is in this critical step.
Here are some non guarded Threads using distinct API Objects:
// multiple calls to different API objects should not be exclusive
OtherThread1
APIObject1.SetData(42);
OtherThread2
APIObject2.SetData(43);
Constraints:
No APIObject
Method is allowed to be called during the API is in the critical section.
Both SetData
calls are allowed to be done simultaneously. They do not interfere with each other, only with the critical section.
Generally speaking accessing one APIObject
from multiple threads is not thread-safe but accessing multiple APIObjects
does not interfere with the API except during critical section.
The critical section must never be executed while any APIObject
Method is used.
Guarding access to one APIObject
from multiple threads is not required.
The trivial approach
Use a lock Object and lock
the critical section and every call to API Objects.
This would effectively work but creates many unnecessary locks because of the fact that then also only one APIObject
at a time could be accessed too.
Concurrent container of Actions
Use a single instance of a concurrent container where each modification of an APIObject
is placed into a thread safe container and is executed in the task above explicitly by traversing the container outside the critical section and calling all actions. (Not a Consumer pattern, as waiting for new entries of the container must not block the task since the critical section must be executed periodically)
This imposes some drawbacks. Closure issues when capturing contexts could be one. Another would be reading from an APIObject
returns old data as long as the actions in the container are not executed. Even worse if the creation of an APIObject
is put in the container and subsequent code assumes it has already be created.
Make something up with Wait Handles and atomic increments
Every APIObject
access could be guarded with a ManualResetEvent
. The critical section would wait for the signal to be set by the APIObjects
, the signal would only be set when all calls to APIObjects
have finished (some sort of atomic increments/decrement around accessing APIObjects
).
Sounds like a great way for deadlocks. May lock out the critical section for long periods of time when continuous APIObject
calls prevent the signal from being ever set.
Does not solve the problem that APIObjects
may not be accessed during critical section since this construct only guards in the other direction.
Requires additional locking (e.g Monitor.IsEntered
on the critical section to not lock out simultaneous calls to distinct APIObjects
).
=> Awful way, making a complex situation even more complex
If copying an
APIObject
is relatively inexpensive (or if it's moderately expensive and you don't sync very often) then you can put the objects in a wrapper that contains a singletonglobal_timestamp
and alocal_timestamp
. When you update an object you first check to see ifglobal_timestamp == long.MaxValue
: if true, then return a destructively updated object; ifglobal_timestamp != long.MaxValue
andglobal_timestamp == local_timestamp
, then return a destructively updated object. However ifglobal_timestamp != long.MaxValue
andglobal_timestamp != local_timestamp
then return an updated copy of the object and setlocal_timestamp = global_timestamp
. When you perform a sync, use anInterlocked
update to setglobal_timestamp = DateTime.UtcNow.ToBinary
, and when the sync is complete setglobal_timestamp = long.MaxValue
. This way the rest of the program doesn't have to pause while a sync is performed, and the sync should have consistent data.