2015年7月6日 星期一

iOS的多線程設計,NSThread與NSRupLoop的應用

最近工作需要多線程技術,我也趁機研究了網路上許多關於iOS的相關教學,我把學到的概念和查到的資源放在這和大家分享。
  1. Grand Central Dispatch In-Depth,介紹GCD和thread的基本概念,這篇文章出自於Raywenderlich,除了GCD API的使用以外也介紹了何謂thread,對我這個半路出家的開發者來說很有幫助。
  2. How To Use NSOperations and NSOperationQueues,這篇文章同樣出自Raywenderlich,這篇文章帶著讀者一步步寫出客製化main函式。
  3. Run Loops in Thread Programming Guide of Apple,這篇是Apple官方的教學文件,裡面講到如何讓thread接收外來的資訊進行相應的動作。
這篇文章主要在談NSThread,如上面所列除了NSThread以外,Apple提供了其他的API,操作起來比NSThread簡單,但是我遇到的問題是:

  • 我希望可以控制Thread在使用者做某些動作的當下馬上開始或結束。
因此,我不能使用GCD(無法在任務執行到一半時結束動作)和NSOperation(無法保證動作會馬上開始執行,因為iOS會視系統狀況調整目前的concurrent thread數量,在一般情況下是比較安全的選擇)我的做法直接開啟NSThread,同時我會接收系統memory warning的訊息,當系統提示記憶體不足,我會在GUI提示使用者,並且關閉最近開啟的NSThread。



我把所學的概念寫成Sample Code(也有Swift版本),接下來,我將會講解Sample Code的內容,還有Swift和Objc-C兩種語言在一些小地方的差異。

首先,進入App之後會看到以下的畫面,有兩個Label分別是$counter1, $counter2,按下Thread1之後$counter1就會開始出現遞增的數字,按下Thread2之後$counter2也會出現遞增的數字。這兩個Label遞增的動作分別是在兩個Thread中完成,等等會有進一步的說明。



按下中間的Thread_Safety會出現下面的Log,我會用兩個Thread對同一個Array塞資料,一個Thread在Array尾端加入10,另外一個在Array的開端加入0。(Obj-C版本的sample code,只有在Thread1與Thread2都開啟的狀態下按下Thread_Safety才能正常動作)






接下來我將會用說明在sample code中,我做了哪些事情:
  • 建立一個NSThread與如何結束一個NSThread
  • 讓一個NSThread持續等待外界呼叫在其內部執行方法(Obj-C only)
  • Thread Safety,如何處理兩個Thread爭搶資源的方法


建立一個NSThread

請打開ViewController.m或是ViewController.swift檔,viewDidLoad的方法內我初始化了兩個NSThread。
- (void)viewDidLoad {
    [super viewDidLoad];
    /*
     Setup _thread1 and _thread2 with their enter method
    */
    _thread1 = [[NSThread alloc]initWithTarget:self selector:@selector(thread1Routine) object:nil];
    _thread2 = [[NSThread alloc]initWithTarget:self selector:@selector(thread2Routine) object:nil];
    
    _arr = [NSMutableArray new];
}
Objective C
override func viewDidLoad() {
        super.viewDidLoad()
        self.thread1 = NSThread(target: self, selector: "thread1Routine", object: nil);
        self.thread2 = NSThread(target: self, selector: "thread2Routine", object: nil);
       
    }
Swift

上面兩段程式碼分別是Objective-C與Swift的sample code的片段,我在viewDidLoad方法裡面初始化兩個NSThread,並且指定好target和thread進入點所要執行的方法與參數。在這邊target的就是告訴系統,等等在thread中如果出現"self",這個"self"是誰。

接著要是定義thread進入點所要執行的方法,這個概念有點類似C的程式進入點,這個thread在創造之後會執行這個方法,這個進入點方法內所呼叫到的方法會在這個thread執行,以下就用thread1的進入點方法為例。

-(void)thread1Routine{
    //Create a autoReleasePool for _thread1
    @autoreleasepool {
        //Hold the pointer of current thread, which means _thread1
        NSThread * curThread = [NSThread currentThread];
        //Hold the pointer of current runloop
        NSRunLoop * curRunloop = [NSRunLoop currentRunLoop];
        
        int counter = 0;
        //Run the loop while current thread is not cancelled
        while (curThread.isCancelled == NO) {
            counter += 1;
            
            //Back to main thread to update GUI
            dispatch_async(dispatch_get_main_queue(), ^{
                _counter1Lb.text = [NSString stringWithFormat:@"%d",(counter * 2) - 1];
            });
            
            //Force current thread to sleep 0.5 sec
            usleep(500000);
            
            //Wait input source
            if ([curRunloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) {
                
            }
        }
        [NSThread exit];
    }
}

Objective C
func thread1Routine(){
        autoreleasepool{
            let curThread = NSThread.currentThread()
            let curRunLoop = NSRunLoop.currentRunLoop()
            var counter = 0
            
            while curThread.cancelled == false {
                counter += 1
                dispatch_async(dispatch_get_main_queue(), { () -> Void in
                    
                    self.counter1Lb.text = "\(counter * 2)"
                });
                
                usleep(500000);
               
            }
            
            NSThread.exit()
        }
    }

Swift

由於我現在自行創造了自己的thread,所以要幫它做個autoreleasepool(請參考這裡),這樣幫助thread裡面的記憶體更有效率回收。

我利用NSThread的類別方法currentThread來抓住currentThread的指標,在這個例子裡我抓住的就是thread1。

利用NSRunLoop的類別方法,我抓住了這個thread中的runloop指標。

接著我讓thread跑一個while迴圈,在thread結束之前,它會改變counter1Lb的內容,然後休眠0.5秒。(注意!所有UI的更新都要切回main thread,否則你會無法預期UI什麼時候會被更新!)

當while迴圈結束時,結束這個thread。


NSThread等待外界Input

也許你已經察覺到Obj-C的程式碼和Swift的程式碼有些不同,Obj-C的程式碼中有這麼一段:

//Wait input source
            if ([curRunloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) {
                
            }
這段程式之所以被放在while迴圈中是因為我希望在runloop在這個迴圈中等待外界的input,當thread收到外界的input時runMode:beforeDate:將會回傳YES,此時runloop會run一次然後回到這一行並且block住直到我剛剛給的時間點或是它再次接收到外界的input,但我給的時間點是極遙遠的未來,所以基本上它會等到外界再次input。
(未完待續...)

沒有留言:

張貼留言