Pages

dispatch_once() in extream depth

        In this post I will be discussing all about dispatch_once(), why to use it?, when to use it?, what exactly it does? and what is happening under the hood?
        dispatch_once() behaves as it is specified by it's name, whatever it does it is once and only once. And that's what makes it very interesting API of all in GCD. It's syntactical form in  implemented below, it takes two argument, first is flag that takes care of the once thing and second is the task that you want to perform only once.

         static var flag: dispatch_once_t = 0
         dispatch_once(&static.flag) {
                    //perform some task here.
         } 

        It is a very best and suitable API for lazily initializing shared state of any type kind of dictionary, array or anything. Also it is much faster API than locks which do same thing.
        Now a million dollar thing this API is kind of a swordsman without it's sword in Single-threaded environment and can be replace by simple if-else block. We use it and Apple documentation recommends it to use in multi-threaded environment because of it's ability of being Thread-Safe, means if multiple threads are executing same shared code then it will perform a kind of lock on block/task thread is executing and won't leave until thread unlock it, meanwhile all other threads have to wail for that lock to relinquished.
 
 Below is Single-threaded implementation:

func justOnce(flag: dispatch_once_t, block: dispatch_block_t) { 
               if(!flag) {
                         //execute block...
                         block()
                         flag = 1;
               }
}

        In the above simple implementation flag is 0 by default and after first-time execution flag is set to 1 so that subsequent call to this function will not execute this block again. This is what we exactly desired for out block to be executed at most once except of being thread-safe. So far I was talking about being thread-safe but doing so sometimes cause lack of performance as well.

In dispatch_once() implementation there are some points to note about it timing:
1.) Whenever we call it, block  will execute according to the flag of dispatch_once().
2.) All other callers have to wait till this block finishes it execution.
3.) Loop will repeat simultaneously for all callers without waiting after blocks finishes execution.

        First and third task are very much Important if performance is concern because first task is used to require lock in case of dispatch_once() and importance of Third task is due to the fact that if call to this block is from several hundreds of thread comes at same time. And updated value will reflects to other callers.

A.) Locks:
        There are different ways to achieve this one-time-execution mechanism i.e. by using locks, spin locks, memory barriers etc. Below is simple example of how do we achieve this with simple Locks to access shared data. In this example I used low-level C-API for threading(p_thread).

func justOnceLock(flag: dispatch_once_t, block: dispatch_block_t) {
        static lock: p_thread = PTHREAD_MUTEX_INITIALIZER 

        pthread_mutex_lock(&lock)
               if(!flag) {
                         //execute block...
                         block()
                         flag = 1;
               }
        pthread_mutex_unlock(&lock)
}

        In above implementation pthread_mutex_lock() function will block all simultaneous call to flag and once done  pthread_mutex_unlock() will unlock this lock so that other thread will access the shared information.
 
        Same can be achieved using NSLock from Cocoa Touch. Example is below but performs the same task and is slow as above implementation with p_thread.

func justOnceLock(flag: dispatch_once_t, block: dispatch_block_t) {
        static lock = NSLock()

        lock.lock()
               if(!flag) {
                         //execute block...
                         block()
                         flag = 1;
               }
        lock.unlock()
}

B.) Spin Locks: By using Spin Locks we repeat the same above implementation but it will be way faster than Normal locks because normal locks talks with OS to make all waiting threads sleeps if any lock is acquired  by any thread and when lock is relinquished again normal locks talks to OS to wake up seeping threads. This talking/coordinating with CPU takes time to complete a task and Spin Locks minimized this time by executing itself when lock is acquired by any thread to see if lock has been relinquished by thread or not. I will be using OSSpinLock function which is defined in  library "#include <libkern/OSAtomic.h>" 

func justOnceSpinLock(flag: dispatch_once_t, block: dispatch_block_t) {
        static lock: OSSpinLock = OS_SPINLOCK_INIT

        OSSpinLockLock(&lock)
               if(!flag) {
                         //execute block...
                         block()
                         flag = 1;
               }
        OSSpinLockUnlock(&lock)
}

        I believe this will do the trick of understanding how dispatch_once() works and answers all the question that I stated on top of this tuto.

No comments:

Post a Comment