This applies to C (and probably to any other similar non-object oriented language). If I have a central data store and potentially concurrent access there are two ways I can see of protecting it.

Let's say I have a data store with a few data elements...

struct MyStore
{
    int data1, data2, data3, ...., dataN;
} store[M];

In this example the data types are the same but let's imagine this applies to something a bit more varied with different types etc... this is just to keep the question simple.

To grant other users access to the data within I could do a few things.

  1. Could provide critical region functions and rely on the caller to get the protection correct.
  2. I could provide a setter and getter function for each data item type in the store and manage the critical regions within the module and protect the user from having to deal with any logic.

My trouble with the first method is that the burden is placed on the caller.

My trouble with the second, at least in C, is that firstly I end up writing a whole load of boiler plate accessors and secondly more complex stuff like test-and-set or needing to hold a lock on the data whilst doing several operations becomes messy. E.g. for method 2 I need

int GetData1(unsigned int index) { 
    int data; 

    ENTER_CR(); 
    data = store[index].data1; 
    LEAVE_CR(); 

    return data;
}

...
...

int GetDataN(unsigned int index) { 
    int data; 

    ENTER_CR(); 
    data = store[index].dataN; 
    LEAVE_CR(); 

    return data;
}

And the same for all the setters. And then what if I want to set multiple items atomically? Gets hard!

How to get some of the benefits of modularity and encapsulation but still have a flexible interface?

up vote 3 down vote accepted

When providing a thread-safe interface (with or with OOP) you need to make sure that your operations are at the level you want to be atomic. If setting or getting a single field is the level of atomic operation you want to support, then option 1 works. (On the other hand, getting and setting int's is atomic anyways)

The reality probably is that a simple data object isn't the correct place in a design to provide thread safety, because as you noted you really do not know what the "real" operations the data is being used for are. You can still encapsulate thread safety, but it needs to be where you are doing something, not just storing something.

  • And where in the question is anything about thread safety? – Euphoric Dec 2 '13 at 16:31
  • 2
    @Euphoric The entire question is about "potentially concurrent access" and the maintenance of critical regions. – Chris Pitman Dec 2 '13 at 16:33
  • 1
    Yeah, now I noticed. My bad. – Euphoric Dec 2 '13 at 16:34
  • Thanks for the answer. I think you're right. I went back to have a look at where I was actually doing something with the data and it seems CRs can be moved to better locations leaving the data stores as is. – Jimbo Dec 2 '13 at 16:56

You can do in C everything you'd do in another language to make a clean interface - you just have to do it with less language support.

  1. if you want to avoid boilerplate accessors, store all your simple types in a union: then you have a single access by enumerated field ID (ie, an index into an array of unions), and the caller can worry about what type each field is

    struct MyStore;
    union Atom {
        int i;
        double d;
        /* any other types */
    };
    
    /* access any type by index */
    union Atom *field(struct MyStore *, int id);
    
    • you obviously lose type-safety guarantees that other languages might provide, and client errors here can be nasty, especially if one of the values in your union is a pointer
    • you can make this a discriminated union if you want more safety, and don't mind checking the type tag by hand
  2. if you want to bundle multiple operations into an atomic event, you can do that:

    struct MyStore;
    struct BatchOperation;
    
    struct BatchOperation *startBatch(struct MyStore*);
    void endBatch(struct BatchOperation*);
    
    /* force client to call startBatch before accessing */
    union Atom *field(struct BatchOperation *, int id);
    

    here, starting the batch operation would lock your store, and ending it would release the lock.

    • again, you don't get any automatic management of the BatchOperation lifetime, so the client code must end the batch or cause a deadlock

PS. Note the opaque types used above: only the union and the public functions are fully visible to client code. This really is encapsulation, with no scare quotes required.

  • This post reminds me of a similar option: provide a wrapper function that takes a function pointer that operates on the data store. The wrapper function can then handle the resource management and then execute the passed function pointer. – Chris Pitman Dec 2 '13 at 16:35
  • Thanks for the answer. Unfortunately I can't collapse the data types into a union. Your point 2 was my point 1... I don't think I worded that very well. Good answer tho so +1.. – Jimbo Dec 2 '13 at 16:54

Your Answer

 

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.