1

I have an item repository which is used from both view-model (main thread) and a background service (background thread). Both can query the item repository and update items.

class ItemRepository
{
   Item GetItem(int id);

   void UpdateItem(Item item);
}

public class Item
{
    ItemState State { get; set; }
}

// Component A (view-model)
// Runs on main thread     
void ExecuteUpdateItemCommand()
{
   var item = _itemRepo.GetItem(1);
   if(item.State == ItemState.First)
   {
       itemState.State = ItemState.Second;
      _itemRepo.UpdateItem(item); // Ideally this should be on a separate thread but for now lets' assume it's on main thread
   }
}

// Component B (background service - receives notifications from backedn) 
// Runs on a background thread
void OnNotificationReceived()
{
   var item = _itemRepo.GetItem(1);
   if(item.State == ItemState.First)
   {
       item.State = GetNextState(); // some busines logic specific to Component B
       _itemRepo.UpdateItem(item);
    }
}

The issue I have is with implementing synchronization of item query and update across the two threads.

Without synchronization, the state of item can become corrupt, because after the if statement returns on one thread, the other thread might be updating the item state to another value, which could break the business logic of the item state.

A simple solution is to just have the Component A and Component B lock on same shared object. From a design perspective, how to share this object across components (each with different purpose) ?

Another solution I see is to have a service which does both the business logic of component A and component B just for the sake of synchronizing the calls, but that's not a good solution in my opinion.

I am looking for better implementations than these.

My feeling is a good solution might be one which allows me to run things on a context. Something like

   ItemSyncContext.Run(ExecuteUpdateItemCommand);

and

   ItemSyncContext.Run(OnNotificationReceived);      
Don Box
  • 3,166
  • 3
  • 26
  • 55
  • I see two votes to close the question. Why? You should at least bother to place a comment before voting to close the question. – Don Box May 15 '18 at 15:13
  • Do the "`DoSomething`" and "`SomeOperation`" methods involve any database access? This question is vague and too general to answer definitively; some more details would help - like a complete example showing the most pertinent classes involved, including a specific race condition. – samus May 15 '18 at 16:23
  • @sαmosΛris The issue is with keeping item data consistency. If all checks and operations logic were done inside the item service, then the synchronization is easy, I could just have a `lock(_dataLock)` wrapped around every logic. One solution would be for the item service to expose a synchronization object which other can use, but I am not sure about that... – Don Box May 15 '18 at 16:40
  • @sαmosΛris does it make more sense now? – Don Box May 15 '18 at 16:40
  • https://blogs.msdn.microsoft.com/ericlippert/2009/03/06/locks-and-exceptions-do-not-mix/ – samus May 15 '18 at 17:15
  • https://learn.microsoft.com/en-us/dotnet/standard/threading/overview-of-synchronization-primitives – samus May 15 '18 at 17:18
  • Your background service is running as a thread within the same process as the item service (and app), not within it's own, correct? – samus May 15 '18 at 17:22
  • @sαmosΛris Umm, thanks but how do those two links help? one is talking about exceptions inside lock and the other one lists synchronization primitives... – Don Box May 15 '18 at 17:22
  • @sαmosΛris yes, everything is inside same process... but there's separate threads, main thread and background service thread. The item service does not have its own thread. – Don Box May 15 '18 at 17:22
  • One link gives you your available options, and Eric Lipperts article brings up a exceedingly important and fundamental point about proper lock usage with exception handling. Here are some more details https://stackoverflow.com/questions/3735293/what-is-the-difference-between-lock-and-mutex – samus May 15 '18 at 17:28
  • @sαmosΛris Thanks. But my problem is with interleaving logic which needs to be synchronized. What I mean is if you look to my code, in background service there's both `DoSomething() \\background service specific logic` and `itemService.SomeItemUpdateOperation()` The issue is these two calls are from different components, but they have to work as jus one block of work, so to speak – Don Box May 15 '18 at 17:30
  • *"If item service throws an event and the view-model is listening to it, if the view-model enters the same lock on handling the event, it will be a deadlock."* **Event Wait Handles** sound like they may help with this. – samus May 15 '18 at 17:40
  • Whatever locking mechanism you decide to use, be sure to test it thoroughly, and set break points at all the critical steps along all of the possible code paths, and try to traverse them in a logical order, starting with the simplest case first. There could be many ordering combinations between the two threads, try to break them all. – samus May 15 '18 at 17:45
  • 1
    @sαmosΛris I edited the question completely, it should be much more clear now. – Don Box May 15 '18 at 17:51
  • https://stackoverflow.com/questions/12804879/is-it-ok-to-use-a-string-as-a-lock-object – samus May 15 '18 at 18:01
  • https://stackoverflow.com/questions/5053172/why-does-the-lock-object-have-to-be-static – samus May 15 '18 at 18:04

2 Answers2

0

Ideally you will only have one call to the service that does both the updates. If that is not an option, you need to answer the question "when does the update happen?" in the service. if the call is blocking, so the updated is committed before the call returns, you can safely keep your code as is. Otherwise, you need to look into some mechanism (i.e. locking) to ensure that the calls happen in the right order.

Z .
  • 12,657
  • 1
  • 31
  • 56
  • The problem I am having is not with finding the synchronization primitive to use but with the interleaving logic which does not belong to one component. – Don Box May 15 '18 at 17:26
  • What I mean is if you look to my code, in background service there's both `background service specific logic` and `itemService.SomeItemUpdateOperation` – Don Box May 15 '18 at 17:27
  • I edited the question completely, it should be much more clear now – Don Box May 15 '18 at 17:51
0

If your repository is over a database (i.e. SQL), then serializing access through a transaction, with the appropriate isolation level, may provide an alternative mean to your synchronization end:

// Component A (view-model)
// Runs on main thread     
void ExecuteUpdateItemCommand()
{
    try
    {           
       _itemRepo.BeginTransaction();

       var item = _itemRepo.GetItem(1);
       if(item.State == ItemState.First)
       {
           itemState.State = ItemState.Second;
          _itemRepo.UpdateItem(item); // Ideally this should be on a separate thread but for now lets' assume it's on main thread
       }
    }
    catch
    {
        if (_itemRepo.InTransaction)
        {
            _itemRepo.RollbackTransaction();
        }

        throw;
    }
    finally
    {
        if (_itemRepo.InTransaction)
        {
            _itemRepo.CommitTransaction();
        }
    }
}

// Component B (background service - receives notifications from backedn) 
// Runs on a background thread
void OnNotificationReceived()
{
    try
    {
        _itemRepo.BeginTransaction();

        var item = _itemRepo.GetItem(1);
        if(item.State == ItemState.First)
        {
           item.State = GetNextState(); // some busines logic specific to Component B
           _itemRepo.UpdateItem(item);
        }
    }
    catch
    {
        if (_itemRepo.InTransaction)
        {
            _itemRepo.RollbackTransaction();
        }

        throw;
    }
    finally
    {
        if (_itemRepo.InTransaction)
        {
            _itemRepo.CommitTransaction();
        }
    }
}
samus
  • 6,102
  • 6
  • 31
  • 69
  • Thanks but I don't have the db transaction support. – Don Box May 15 '18 at 18:27
  • What underlies your repository, a web service? – samus May 15 '18 at 18:35
  • it's a database but it doesn't have transaction support – Don Box May 15 '18 at 18:45
  • So you have to manually rollback (does each repo method (update/insert) clean up after itself during an exception, or must this be tracked up in the service layer)? – samus May 15 '18 at 18:55
  • I'm no authority on the subject, but personally wouldn't attempt to emulate the integrity and consistency that DB transactions provide with "code-level" locks, unless maybe I was compensated accordingly. – samus May 15 '18 at 19:01
  • I agree :) Transaction is not a solution for me unfortunately. – Don Box May 15 '18 at 19:04
  • Depending on the scale of your problem you may be fine, but for a complex piece of software with a sizable code base I would map such an implementation out in documents to assess the feasibility of such an undertaking. Marching head on class by class only to find a show stopper half way through could be potentially disastrous. – samus May 15 '18 at 19:09
  • Not sure what you mean by "you may be fine". I may be fine with what? I still don't have a good solution – Don Box May 15 '18 at 19:27
  • Just slap some locks around your "critical sections", set some break points, and test for some race conditions already! If the `lock` primitive/keyword (which is actually implemented as a Monitor and wrapped in a try/catch/finally), then try the other locking options that are covered in the msdn link I gave you. There's nothing to it but to do it :) – samus May 15 '18 at 19:29
  • You are aware of the "watch" window, correct (CTRL+ALT+W+1/2/3/4), or CTRL+ALT+Q, or SHIFT+F9)? – samus May 15 '18 at 19:33
  • 1
    I've also found that overthinking a grand scheme before doing any actual coding and testing, even to a quick prototype that implements the most fundamental aspects, is invaluable in pointing out unforeseen flaws. The early their found the better b/c there is nothing worse than scrapping a whole bunch of code, which gets tallied onto the time wasted in brainstorming over it. – samus May 15 '18 at 19:42
  • I agree with overthinking part. The thing is right now the only solution I found unfortunately is to dispatch on main thread just so things stay all on main thread – Don Box May 15 '18 at 20:26
  • What do you mean *"dispatch on the main thread just so things stay all on the main thread"*? – samus May 15 '18 at 20:39
  • [**`SynchronizationContext`**](https://msdn.microsoft.com/magazine/gg598924.aspx) sounds to be the most interesting option, but you stated that you *think* **lock** will cause deadlock. Until you can actually prove this condition exists, I would try to make the simpler option work first. – samus May 15 '18 at 20:53