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

1

There are 1 answers

0
Zim-Zam O'Pootertoot On

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 singleton global_timestamp and a local_timestamp. When you update an object you first check to see if global_timestamp == long.MaxValue: if true, then return a destructively updated object; if global_timestamp != long.MaxValue and global_timestamp == local_timestamp, then return a destructively updated object. However if global_timestamp != long.MaxValue and global_timestamp != local_timestamp then return an updated copy of the object and set local_timestamp = global_timestamp. When you perform a sync, use an Interlocked update to set global_timestamp = DateTime.UtcNow.ToBinary, and when the sync is complete set global_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.

// APIObject provided to you
public class APIObject {
  private string foo;
  public void setFoo(string _foo) {
    this.foo = _foo;
  }
}

// Global Timestamp, readonly version for wrappers and readwrite version for sync
public class GlobalTimestamp {
  protected long timestamp = long.MaxValue;

  public long getTimestamp() {
    return timestamp;
  }
}

public class GlobalTimestampRW extends GlobalTimestamp {
  public void startSync(long _timestamp) {
    long value = System.Threading.Interlocked.CompareExchange(ref timestamp, _timestamp, long.MaxValue);
    if(value != long.MaxValue) throw exception; // somebody else called this method already
  }

  public void endSync(long _timestamp) {
    long value = System.Threading.Interlocked.CompareExchange(ref timestamp, long.MaxValue, _timestamp);
    if(value != _timestamp) throw exception; // somebody else called this method already
  }
}

// Wrapper
public class APIWrapper {
  private APIObject apiObject;
  private GlobalTimestamp globalTimestamp;
  private long localTimestamp = long.MinValue;

  public APIObject setFoo(string _foo) {
    long tempGlobalTimestamp = globalTimestamp.getTimestamp();
    if(tempGlobalTimestamp == long.MaxValue || tempGlobalTimestamp == localTimestamp) {
      apiObject.setFoo(_foo);
      return apiObject;
    } else {
      apiObject = apiObject.copy();
      apiObject.setFoo(_foo);
      localTimestamp = tempGlobalTimestamp;
      return apiObject;
    }
  }
}

GlobalTimestampRW globalTimestamp;
new Task(() =>
{
    while(true)
    {
      long timestamp = DateTime.UtcNow.ToBinary();
      globalTimestamp.startSync(timestamp);
      API.CriticalOperationStart(); // Between start and end no API Object may be used
      API.CriticalOperationEnd();
      globalTimestamp.endSync(timestamp);
    }
}, TaskCreationOptions.LongRunning).Start();