4

I am developing an iPhone app which keeps some data. I am using archiving method from NSKeyedArchiver class to save the data to disk. I would like to periodically save the data to disk. The problem is, when the data grows bigger, it takes more time and it actually interrupts the user's current actions.

Hence, I want to use multithreading to solve this problem. From my understanding of multithreading, when I want to save the data to disk, I should create a new thread, run the saving task on the new thread, then terminate the thread. I should also make the thread so that it won't immediately terminate when the app terminates, to finish saving data. This way, the user can continue to interact with the interface.

That being said, I am not familiar with the actual code that does these work...what would the above look like in code?

Flying_Banana
  • 2,864
  • 2
  • 22
  • 38
  • 1
    C_X's answer is great for the writing in background (you may have to make a copy of your data first in case your user modifies the data while your background thread is writing it, bad). For the finishing writing when the user closes the app you are going to need to research this: http://stackoverflow.com/questions/13574974/continue-operation-when-app-did-enter-background-on-ios. – Putz1103 Jan 11 '14 at 16:00

3 Answers3

7

A couple of thoughts.

  1. You want to use a serial dispatch queue or operation queue.

    Note, we probably want it to write to persistent storage serially (if you're saving it to the same filename, for example), i.e. not permit another save to be initiated until the prior save is finished. I suspect that's exceedingly unlikely that your infrequent saves could ever trigger a save while the prior one is still in progress, but as a general principle you should not use concurrent queues unless you write code that supports concurrent operation (which we're not doing here). This means that you do not use the GCD global queues.

    For example, to create serial dispatch queue using Grand Central Dispatch (GCD) would be:

    @property (nonatomic, strong) dispatch_queue_t queue;
    

    Then instantiate this (e.g. in viewDidLoad):

    self.queue = dispatch_queue_create("com.domain.app.savequeue", 0);
    

    Then use this queue

    dispatch_async(self.queue, ^{
        // do your saving here
    });
    

    For a review of concurrency technologies, see the Concurrency Programming Guide. Both dispatch queues (GCD) and operation queues are solid choices.

  2. You might want to be careful about synchronization issues. What if your app proceeds to start changing the data while the save is in progress? There are a bunch of options here, but the easiest is to copy the data to some temporary object(s) in the main queue before you dispatch the save task to the background queue:

    // copy the model data to some temporary object(s)
    
    dispatch_async(self.queue, ^{
        // save the temporary object(s) here
    });
    
  3. Or, instead of creating a copy of the model, you can alternatively (and this is a little more complicated if you're not familiar with GCD) use a variation of the "reader-writer" pattern that Apple discusses in WWDC 2012 video Asynchronous Design Patterns with Blocks, GCD, and XPC. Bottom line, you can queue to not only perform asynchronous write to persistent storage, but also to synchronize your updates to your model using a "barrier" (see Using Barriers in the GCD reference):

    self.queue = dispatch_queue_create("com.domain.app.modelupdates", DISPATCH_QUEUE_CONCURRENT);
    

    Then, when you want to save to disk, you can do

    dispatch_async(self.queue, ^{
        // save model to persistent storage
    });
    

    But, whenever you want to update your model, you should use barrier so that the updating of the model will not happen concurrently with any read/save tasks:

    dispatch_barrier_async(self.queue, ^{
        // update model here
    });
    

    And, whenever you read from your model, you would:

    dispatch_sync(self.queue, ^{
        // read from model here
    });
    

    Theoretically, if you're worried about the possibility that you could conceivably do your save operations so frequently that one save could still be in progress when you initiate the next one, you might actually employ two queues, one serial queue for the saving operation (point 1, above), and the concurrent queue outlined here for the synchronization process.

  4. Finally, Putz1103 is correct, that if it's possible that the app can be terminated while a save is in progress, you might want to add the code to allow the write to persistent storage to complete:

    dispatch_async(self.queue, ^{
        UIBackgroundTaskIdentifier __block taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^(void) {
            // handle timeout gracefully if you can
    
            [[UIApplication sharedApplication] endBackgroundTask:taskId];
            taskId = UIBackgroundTaskInvalid;
        }];
    
        // save model to persistent storage
    
        // when done, indicate that the task has ended
    
        if (taskId != UIBackgroundTaskInvalid) {
            [[UIApplication sharedApplication] endBackgroundTask:taskId];
            taskId = UIBackgroundTaskInvalid;
        }
    });
    
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • An alternative solution to the "reading while writing" situation is to make sure that you use the same queue to read the data from. As it's a serial queue, the tasks will only happen in order. Serial queues are a handy way of solving such synchronisation problems. Use a data manager to abstract this all away. – Abizern Jan 11 '14 at 16:33
  • @Abizern I'm not so worried about "reading while writing" as I am "mutating while writing". You're right that there are a variety of other synchronization techniques, but you'd want to ensure you employ one that doesn't reintroduce the lack of responsiveness that OP was concerned about. If wanted queue to handle synchronization, I'd probably go for a variation on reader-writer pattern (concurrent GCD queue, `dispatch_barrier_async` when mutating the model, `dispatch_sync` reading from the model, `dispatch_async` for saving the model). I was afraid this was going to be too confusing for OP. – Rob Jan 11 '14 at 16:50
  • It's confusing for me as well :). I have to admit that I used Core Data or documents - which let me use async methods while hiding this all away. I've use serial queues to synchronise access to properties, though, which is a lot faster than writing to disk so responsiveness isn't an issue. – Abizern Jan 11 '14 at 17:17
  • I think I get the general idea of the things happening here, but I just need to go over them more carefully and implement them in the code. I just started reading documentations of multithreading and am not very familiar with the code yet. But I think this is quite an elaborated answer! – Flying_Banana Jan 12 '14 at 02:13
  • In any case, I should use the code in 4) to save data to persistent storage? I do not need to read data after app launches (the app keeps all the changes in the model in a variable so it can access that instead of the data in storage). So I'm done with 1) and 4)? – Flying_Banana Jan 12 '14 at 02:44
  • @LeJiacong Point #4 solves the "what if the app enters background in the middle of a save operation", so, yes, that's good practice. Point #1 achieves the asynchronous save problem, but is not technically thread safe (and if your model might mutate during a save operation, the save results could be in an inconsistent state). The most robust solution is combining point #3 and #4, but, I recognize that that might appear sufficiently complicated, so you might be able to get away with #1 and #4 (assuming you're confident that the model will not change while the asynchronous save is in progress). – Rob Jan 12 '14 at 16:52
  • @LeJiacong Having said all of this, I know you probably don't want to hear this, but the best solution is probably to incrementally save the data as the user goes along, in something like Core Data or other object persistence approach, that way you don't encumber the app with the slow "let me save everything in an archive" process, and largely eliminates this asynchronous save problem (and all of the synchronization problems that entails). – Rob Jan 12 '14 at 16:56
1

Adding multi-threading to an application where data is shared between multiple threads (in this case, the data being created by the user and the data you are saving) is a difficult task to manage.

Instead of creating a thread or trying to save off all the data at once, put data to be saved into an internal "this must be saved off" list, and then work it off one N elements at a time periodically in your main thread.

If you get to the point where the user is leaving the screen or the app, then save off all the work that is left in the queue to the database immediately.

You can create a simple timed event (a few times per second) to do the work, which is a very simple approach.

  • You can explicit control over how many items you save per update.
  • You should never have concurrency issues.
  • You should never have to worry about thread start/stop/termination issues or mutexes.

To create it:

-(void)viewDidAppear:(BOOL)animated
{
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(saveData) userInfo:nil repeats:YES];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(saveAllRemainingItems) name:@"APP EXITING KEY" object:nil]
}

Your update function:

-(void)saveData
{
    if([itemsToSave count] > 0)
    {
       ....save off N items, remove from the list
    }
}

-(void)saveAllRemainingItems
{
    while([itemsToSave count] > 0)
    {
        ...save first item.
       [itemsToSave removeObjectAtIndex:0];
    }
}

When you leave:

-(void)viewWillDisappear:(BOOL)animated
{
   [self.timer invalidate];
   [[NSNotificationCenter defaultCenter] removeObserver:self];
   [self saveAllRemainingData];
}

To make sure you handle the "app is closing" situation, in your app delegate:

- (void)applicationWillTerminate:(UIApplication *)application 
{
   [[NSNotificationCenter defaultCenter] postNotificationName:@"APP EXITING KEY" object:nil];

    ...OTHER CLEANUP ACTIVITIES
}
FuzzyBunnySlippers
  • 3,387
  • 2
  • 18
  • 28
  • The thing about this is that I only have one object to archive, a root archive object which takes up to a few seconds to finish saving. I understand if I have several files to save, your method would certainly be a workaround to multithreading! – Flying_Banana Jan 12 '14 at 02:12
  • 1
    @LeJiacong Bummer...too bad there isn't a way to partially archive it. It seems like that leaves with either writing a bunch of code to deal with multithread **or** blocking the user with a "please wait" screen at opportune time so you can safely save off the data without worrying about them changing something. – FuzzyBunnySlippers Jan 12 '14 at 13:03
0

You can achive mulithreading in IOS with different ways, like NSThread, Operation Queues and GCD. GCD is the best approch now a days, it uses block. You can execute a code in different thread like this. You can use this in any method.

void performArchiveData{

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
           //Now you are in different thread. You can add your code in here.

       });
}
Adnan Aftab
  • 14,377
  • 4
  • 45
  • 54