GCD pattern for shared resource access + UI update

2019-04-02 16:35发布

问题:

folks! I'm implementing a shared cache in my app. The idea is to get the cached data from the web in the background and then update the cache and the UI with the newly retrieved data. The trick is of course to ensure thread-safety, since the main thread will be continuously using the cache. I don't want to modify the cache in any fashion while someone else might be using it.

It's my understanding that using @synchronized to lock access to a shared resource is not the most elegant approach in ObjectiveC due to it trapping to the kernel and thus being rather sluggish. I keep reading that using GCD instead is a great alternative (let's ignore its cousin NSOperation for now), and I'd like to figure out what a good pattern for my situation would be. Here's some sample code:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// download the data in a background thread
dispatch_async(queue, ^{
    CacheData *data = [Downloader getLatestData];

    // use the downloaded data in the main thread
    dispatch_sync(dispatch_get_main_queue(), ^{
        [AppCache updateCache:data];
        [[NSNotificationCenter defaultCenter] postNotificationName:@"CacheUpdated" object:nil];
    });
});
  1. Would this actually do what I think it does, and if so, is this the cleanest approach as of today of handling this kind of situation? There's a blog post that's quite close to what I'm talking about, but I wanted to double-check with you as well.
  2. I'm thinking that as long as I only ever access shared the shared resource on the same thread/queue (main in my case) and only ever update UI on main, then I will effectively achieve thread-safety. Is that correct?

Thanks!

回答1:

Yes. Other considerations aside, instead of shunting read/write work onto the main thread consider using a private dispatch queue.

dispatch_queue_t readwritequeue;
readwritequeue = dispatch_queue_create("com.myApp.cacheAccessQueue", NULL);

Then update your AppCache class:

- (void)updateCache:(id)data {
 dispatch_sync(readwritequeue, ^{ ... code to set data ... });
}

- (id)fetchData:... {
 __block id data = nil;
 dispatch_sync(readwritequeue, ^{ data = ... code to fetch data ...});
 return data;
}

Then update your original code:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// download the data in a background thread
dispatch_async(queue, ^{
    CacheData *data = [Downloader getLatestData];
    **[AppCache updateCache:data];**

    // use the downloaded data in the main thread
    dispatch_async(dispatch_get_main_queue(), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:@"CacheUpdated" object:nil];
    });
});


回答2:

If you ask 100 developers here was is the most elegant way to do this, you will get at least 100 different answers (maybe more!)

What I do, and what is working well for me, is to have a singleton class doing my image management. I use Core Data, and save thumbnails directly in the store, but use the file system and a URL to it in Core Data for "large" files. Core Data is setup to use the new block based interface so it can do all its work on a private thread managed by itself.

Possible image URLS get registered with a tag on the main thread. Other classes can ask for the image for that tag. If the image is not there, nil is returned, but this class sets a fetchingFlag, uses a concurrent NSOperation coupled to a NSURLConnection to fetch the image, when it gets it it messages the singleton on its thread with the received image data, and the method getting that message uses '[moc performBlock:...]' (no wait) to process it.

When images are finally added to the repository, the moc dispatches a notification on the main queue with the received image tag. Classes that wanted the image can listen for this, and when they get it (on the main thread) they can then ask the moc for the image again, which is obviously there.