Question

I've read a fair amount on thread-safety, and have been using GCD to keep the math-heavy code off the main thread for a while now (I learned about it before NSOperation, and it seems to still be the easier option). However, I wonder if I could improve part of my code that currently uses a lock.

I have an Objective-C++ class that is a wrapper for a c++ vector. (Reasons: primitive floats are added constantly without knowing a limit beforehand, the container must be contiguous, and the reason for using a vector vs NSMutableData is "just cause" it's what I settled on, and NSMutableData will still suffer from the same "expired" pointer when it goes to resize itself).

The class has instance methods to add data points that are processed and added to the vector (vector.push_back). After new data is added I need to analyze it (by a different object). That processing happens on a background thread, and it uses a pointer directly to the vector. Currently the wrapper has a getter method that will first lock the instance (it suspends a local serial queue for the writes) and then return the pointer. For those that don't know, this is done because when the vector runs out of space push_back causes the vector to move in memory to make room for the new entries - invalidating the pointer that was passed. Upon completion, the math-heavy code will call unlock on the wrapper, and the wrapper will resume the queued writes finish.

I don't see a way to pass the pointer along -for an unknown length of time- without using some type of lock or making a local copy -which would be prohibitively expensive.

Basically: Is there a better way to pass a primitive pointer to a vector (or NSMutableData, for those that are getting hung up by a vector), that while the pointer is being used, any additions to the vector are queued and then when the consumer of the pointer is done, automatically "unlock" the vector and process the write queue

Current Implementation

Classes:

  • DataArray: a wrapper for a C++ vector
  • DataProcessor: Takes the most raw data and cleans it up before sending it to the 'DataArray'
  • DataAnalyzer: Takes the 'DataArray' pointer and does analysis on array
  • Worker: owns and initializes all 3, it also coordinates the actions (it does other stuff as well that is beyond the scope here). it is also a delegate to the processor and analyzer

What happens:

  1. Worker is listening for new data from another class that handles external devices
  2. When it receives a NSNotification with the data packet, it passes that onto DataProcessor by -(void)checkNewData:(NSArray*)data
  3. DataProcessor, working in a background thread cleans up the data (and keeps partial data) and then tells DataArray to -(void)addRawData:(float)data (shown below)
  4. DataArray then stores that data
  5. When DataProcessor is done with the current chunk it tells Worker
  6. When Worker is notified processing is done it tells DataAnalyzer to get started on the new data by -(void)analyzeAvailableData
  7. DataAnalyzer does some prep work, including asking DataArray for the pointer by - (float*)dataPointer (shown below)
  8. DataAnalyzer does a dispatch_async to a global thread and starts the heavy-lifting. It needs access to the dataPointer the entire time.
  9. When done, it does a dispatch_async to the main thread to tell DataArray to unlock the array.
  10. DataArray can is accessed by other objects for read only purposes as well, but those other reads super quick.

Code snips from DataArray

-(void)addRawData:(float)data {
    //quick sanity check
    dispatch_async(addDataQueue, ^{
        rawVector.push_back(data);
    });
}

- (float*)dataPointer {
    [self lock];
    return &rawVector[0];
}

- (void)lock {
    if (!locked) {
        locked = YES;
        dispatch_suspend(addDataQueue);
    }
}

- (void)unlock {
    if (locked) {
        dispatch_resume(addDataQueue);
        locked = NO;
    }
}

Code snip from DataAnalyzer

-(void)analyzeAvailableData {
    //do some prep work

    const float *rawArray = [self.dataArray dataPointer];
    dispatch_async(global_queue, ^{
        //lots of analysis

        //done
        dispatch_async(main_queue, ^{
            //tell `Worker` analysis is done

            [self.dataArray unlock];
        };
    };
}
Was it helpful?

Solution

If you have a shared resource (your vector) which will be concurrently accessed through reads and writes from different tasks, you may associated a dedicated dispatch queue with this resource where these tasks will exclusively run.

That is, every access to this resource (read or write) will be executed on that dispatch queue exclusively. Let's name this queue "sync_queue".

This "sync_queue" may be a serial queue or a concurrent queue.

If it's a serial queue, it should be immediately obvious that all accesses are thread-safe.

If it's a concurrent queue, you can allow read accesses to happen simultaneously, that is you simply call dispatch_async(sync_queue, block):

dispatch_async(sync_queue, ^{
    if (_shared_value == 0) {
        dispatch_async(otherQueue, block);
    }
});

If that read access "moves" the value to a call-site executing on a different execution context, you should use the synchronous version:

__block int x;
dispatch_sync(sync_queue, ^{
    x = _shared_value;
});
return x;

Any write access requires exclusive access to the resource. Having a concurrent queue, you accomplish this through using a barrier:

dispatch_barrier_async(sync_queue, ^{
    _shared_value = 0;
    dispatch_async(mainQueue, ^{
        NSLog(@"value %d", _shared_value);
    });
});

OTHER TIPS

It really depends what you're doing, most of the time I drop back to the main queue (or a specifically designated queue) using dispatch_async() or dispatch_sync().

Async is obviously better, if you can do it.

It's going to depend on your specific use case but there are times when dispatch_async/dispatch_sync is multiple orders of magnitude faster than creating a lock.

The entire point of grand central dispatch (and NSOperationQueue) is to take away many of the bottlenecks found in traditional threaded programming, including locks.

Regarding your comment about NSOperation being harder to use... that's true, I don't use it very often either. But it does have useful features, for example if you need to be able to terminate a task half way through execution or before it's even started executing, NSOperation is the way to go.

There is a simple way to get what you need even without locking. The idea is that you have either shared, immutable data or you exclusive, mutable data. The reason why you don't need a lock for shared, immutable data is that it is simply read-only, so no race conditions during writing can occur.

All you need to do is to switch between both depending on what you currently need:

  • When you are adding samples to your storage, you need exclusive access to the data. If you already have a "working copy" of the data, you can just extend it as you need. If you only have a reference to the shared data, you create a working copy which you then keep for later exclusive access.
  • When you want to evaluate your samples, you need read-only access to the shared data. If you already have a shared copy, you just use that. If you only have an exclusive-access working copy, you convert that to a shared one.

Both of these operations are performed on demand. Assuming C++, you could use std::shared_ptr<vector const> for the shared, immutable data and std::unique_ptr<vector> for the exclusive-access, mutable data. For the older C++ standard those would be boost::shared_ptr<..> and std::auto_ptr<..> instead. Note the use of const in the shared version and that you can convert from the exclusive to the shared one easily, but the inverse is not possible, in order to get a mutable from an immutable vector, you have to copy.

Note that I'm assuming that copying the sample data is not possible and doesn't explode the complexity of your algorithm. If that doesn't work, your approach with the scrap space that is used while the background operations are in progress is probably the best way to go. You can automate a few things using a dedicated structure that works similar to a smart pointer though.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top