iOS開發-iOS多線程開發中踩過的坑-GCD的特性-NSOperation線程依賴-iOS多線程踩坑小結

本期內容:

  • iOS開發中從其他線程回到主線程的方法
  • dispatch_group_create 組的概念
  • dispatch_sync同步調度主線程會死鎖的原因
  • 項目中什麼時候選擇GCD什麼時候選擇NSOperation
  • NSOperation 線程依賴的簡單例子
  • GCD的計時器和延時執行
  • 簡單說說線程死鎖和線程安全⭐️

iOS開發中從其他線程回到主線程的方法

在開發中我們經常使用簡單的多線程,用來讓數據和視圖更好分配來提高項目的性能。一般在調用其他線程和自己創建的線程完成數據的加載以及處理之後,我們需要回到主線程來更新我們的UI視圖,該怎麼做呢?其實很簡單,以下三種方式都可以回到主線程來執行任務:

- (void)backToMainThread{
    // 方法1,可以直接使用NSThread
    [self performSelectorOnMainThread:@selector(dosomething:) withObject:nil waitUntilDone:NO];
    [self performSelectorOnMainThread:@selector(dosomething:) withObject:nil waitUntilDone:NO modes:nil];
    
    // 方法2,在使用GCD的時候,可以直接在線程中使用
    dispatch_async(dispatch_get_main_queue(), ^{
        // 使用異步線程獲取主線程,如果在同步線程獲取主線程,會阻塞線程
        // 關於爲什麼會阻塞線程,下面會仔細講解
        [self dosomething:nil];
    });
    
    // 方法3,使用NSOperationQueue
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        // NSOperationQueue有類方法,直接獲取就可以使用了
        [self dosomething:nil];
    }];
}

- (void)dosomething:(id)sender{
    NSLog(@"dosomething");
}

dispatch_group_create 組的概念

dispatch_group_create 有一個組的概念,可以把相關的任務歸併到一個組內來執行
dispatch_group_async把異步任務提交到指定任務組和指定下拿出隊列執行
參數有:
dispatch_group_enter
dispatch_group_leave
dispatch_group_wait
前兩個要成對使用,wait會阻塞線程,等enter和leave執行完纔會結束阻塞
dispatch_group_notify 待任務組執行完畢時調用,不會阻塞當前線程

因爲下劃線在MACDOWN語言中是斜體所以使用代碼塊來表述,以下來看看Group的使用:

- (void)downLoadImage{
    // 創建全局隊列
    dispatch_queue_t imageDownLoadQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 把全局隊列添加到異步線程裏面
    dispatch_async(imageDownLoadQueue, ^{
        // 使用分組執行下載人物
        // 創建一個組
        dispatch_group_t imageGroup =dispatch_group_create();
        
        // 添加接收圖片的數據對象
        //__block UIImage *image1 = nil;
        //__block UIImage *image2 = nil;
        //__block UIImage *image3 = nil;
        //__block UIImage *image4 = nil;
        
        // 在組裏面創建異步任務
        dispatch_group_async(imageGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            // image1 = [self loadImage:imgUrl1];
        });
        dispatch_group_async(imageGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            // image2 = [self loadImage:imgUrl2];
        });
        dispatch_group_async(imageGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            // image3 = [self loadImage:imgUrl3];
        });
        dispatch_group_async(imageGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            // image4 = [self loadImage:imgUrl4];
        });
        
        // 等到所有任務都完成了,notify纔會執行組的最後任務,在這裏使用主線程刷新我們的UI
        dispatch_group_notify(imageGroup, dispatch_get_main_queue(), ^{
            //self.imageview1.image = image1;
            //self.imageView2.image = image2;
            //self.imageview3.image = image3;
            //self.imageView4.image = image4;
        });
        
    });
}

dispatch_sync同步調度主線程會死鎖的原因

先看看以下代碼:

- (void)GCDTest{
	// 以下代碼沒有輸出,還會報錯
	dispatch_queue_t mainThreadQueue = dispatch_get_main_queue();
	dispatch_sync(mainThreadQueue, ^{
        NSLog(@"看不到我");
    });
    
}

sync 用於將一個任務提交到我們的隊列中同步調度執行,完成一個任務後纔會返回。主線程隊列mainThreadQueue是一個串行隊列,在App啓動的時候就有很多任務在執行,但是因爲sync的特性是同步執行,所以在執行sync同步調度的時候,這些任務就會互相等待,就會造成阻塞。

上面的代碼有點少,按照引述,我們的執行的過程好比以下代碼:

- (void)GCDTest{
	dispatch_queue_t myQueue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
	dispatch_sync(myQueue, ^{
    	// 任務1
    	NSLog(@"1");
	});
	dispatch_sync(myQueue, ^{
    	// 任務2
    	NSLog(@"2");
	});
	dispatch_sync(myQueue, ^{
    	// 任務3
    	NSLog(@"3");
	});
	
	// 最後才輪到我們的主線程隊列,輸出:1,2,3,愛就像藍天白雲晴空萬里,突然就暴風雨(崩潰)
	dispatch_sync(myQueue, ^{
    	dispatch_queue_t mainThreadQueue = dispatch_get_main_queue();
    	dispatch_sync(mainThreadQueue, ^{
        	NSLog(@"看不到我");
    	});
	});
	
	// 把上面這個代碼替換爲下面的代碼,輸出:1,2,3,看到我了
	
	// 因爲同步調度是串行的,按照順序執行,所以我們在同步調度的時候使用主線程也是要按規矩排隊
	dispatch_sync(myQueue, ^{
    	dispatch_queue_t mainThreadQueue = dispatch_get_main_queue();
    	dispatch_sync(mainThreadQueue, ^{
        	NSLog(@"看到我了");
    	});
	});
}

sync同步調度主線程崩潰的坑估計90%的iOS開發者都踩過,哈哈哈😝也是老生常談的了


項目中什麼時候選擇GCD什麼時候選擇NSOperation

1.GCD內部實現,GCD是基於OSX內核實現的,最大的優點就是簡單、易用,根據官方的說法就是更加安全高效。不用做繁雜的多線程操作,可以在一定量的節省代碼量。【可用複雜項目的模塊或者簡單項目的】

2.NSOperationQueue是基於GCD的OC版本封裝,劇本面向對象的特性(複用、封裝),NSOperationQueue可以很方便地調整執行順序、設置最大併發數量。 NSOperationQueue可以在輕鬆在Operation間設置依賴關係,而GCD需要寫很多的代碼才能實現,NSOperationQueue支持KVO,可以監測operation是否正在執行。【可用於複雜項目】


NSOperation 線程依賴的簡單例子

先來簡單說一下NSOperation,是執行操作的意思,是指在線程中執行的代碼塊。在NSOperation中,官方推薦我們使用NSOperation子類 NSInvocationOperation、NSBlockOperation,或者自定義子類來封裝操作。NSOperationQueue指操作隊列,用來存放操作的隊列。NSOperationQueue對於添加到隊列中的操作,先進入準備就緒的狀態,然後進入就緒狀態的操作的開始執行順序。這裏要說一點,就緒狀態的操作,添加過依賴的就按照依賴的關係執行,再繼續操作隊列的後續執行。

NSOperation、NSOperationQueue創建步驟:

- (void)createNSOperation{
    // 1.創建隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    // 2.創建操作
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        // 操作中使用你的代碼
    }];

    // 3.添加操作
    [queue addOperation:operation];
}

NSOperation 線程依賴:

- (void)addDependency {
    // 1.創建隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    // 2.創建操作
    NSBlockOperation *opertaion1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"1");
    }];
    NSBlockOperation *opertaion2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"2");
    }];
    NSBlockOperation *opertaion3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"3");
    }];

    // 3.添加依賴
    // opertaion3 依賴於 opertaion1和opertaion2
    [opertaion3 addDependency:opertaion1]; 
    [opertaion3 addDependency:opertaion2]; 
	
    // 4.添加操作到隊列中
    [queue addOperation: opertaion1];
    [queue addOperation: opertaion2];
    [queue addOperation: opertaion3];
    
    // 輸出爲 1,2,3因爲依賴關係需要等依賴的操作執行完纔會繼續,所以,1,2執行完纔會執行3,這就是NSOperation的依賴
}

GCD的計時器和延時執行

使用GCD的source特性來做一個計時器,之前在做馬甲包的時候簡單的使用了這一特性來做短信獲取的倒計時,代碼如下:

- (void)dealTimeBtnAciton{
    // 1.設定倒計時時間
    __block int timeout = 60; 
    // 2.獲取全局隊列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 3.創建一個定時器,並將定時器交給全局隊列執行,不會造成主線程阻塞
    dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,queue);
    // 4.設定每秒執行(1.0爲參數)
    dispatch_source_set_timer(_timer,dispatch_walltime(NULL, 0),1.0*NSEC_PER_SEC, 0); 
    // 5.設置定時器的觸發事件
    dispatch_source_set_event_handler(_timer, ^{
        if(timeout <= 0){
            // 操作:倒計時結束,關閉
            dispatch_source_cancel(_timer);
            // 操作:刷新UI
            dispatch_async(dispatch_get_main_queue(), ^{
                // 設置界面的顯示 根據自己需求設置
                
                return;
            });
        }else{
        	  // 如果倒計時沒結束,繼續輪詢以下方法:
            int seconds = timeout % 60;
            // 每秒刷新UI,更新秒數
            dispatch_async(dispatch_get_main_queue(), ^{
                // 設置界面的顯示 根據自己需求設置
                if (seconds != 0) {
                		// 如果秒數不爲0需要做的操作    
                }
            });
            // 更新倒計時的時間
            timeout--;
        }
    });
    // 啓用定時器
    dispatch_resume(_timer);
    
}

延時執行,使用GCD的after特性,有時候在複雜UI賦值的時候用上,或者在dismiss一些提示頁面的時候很受用,代碼如下:

- (void)setDelayTime{
    // 1.設定延時時間
    double delayInSeconds = 2.0;
    // 2.創建延時時間
    dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
    dispatch_after(delayTime, dispatch_get_main_queue(), ^(void){
        NSLog(@"延遲了兩秒執行");
    });
}

簡單說說線程死鎖和線程安全⭐️

線程死鎖

線程中每一個資源每次只能被一個進程使用,一個線程因請求資源而阻塞時,對已獲得的資源保持不放,但是線程已獲得的資源,在末使用完之前,不能釋放,若干進程之間都在互相循環等待資源這時候就造成了死鎖。

通俗易懂的解釋:在一條河上有一座橋,橋面較窄,只能容納一輛汽車通過,無法讓兩輛汽車並行。如果有兩輛汽車A和B分別由橋的兩端駛上該橋,則對於A車來說,它走過橋面左面的一段路(即佔有了橋的一部分資源),要想過橋還須等待B車讓出所在的橋面,此時A車不能前進;對於B車來說,要想過橋還須等待A車讓出的橋面,此時B車也不能前進。兩邊的車都不倒車,結果造成互相等待對方讓出橋面,但是誰也不讓路,就會無休止地等下去。這種現象就是死鎖。

線程安全

線程安全,爲了避免線程死鎖做的操作。常用的方式就是對資源的獲取操作加鎖,以便保證資源被唯一訪問。常見的鎖:NSLock、@synchronized、NSConditionLock條件鎖、NSRecursiveLock遞歸鎖。

鎖是最常用的同步工具。一段代碼段在同一個時間只能允許被一個線程訪問,比如一個線程A進入加鎖代碼之後由於已經加鎖,另一個線程B就無法訪問,只有等待前一個線程A執行完加鎖代碼後解鎖,B線程才能訪問加鎖代碼。使用線程鎖就是做了一個簡單的線程安全處理,防止多個線程去搶同一個資源,避免死鎖或是阻塞。

這時候會有人說,原子性(atomic)可以自動加鎖保證線程安全。來引申一下原子性:

原子性:指的是編譯器會在property上自動添加原子鎖,非原子性nonatomic,不考慮多線程情況時使用,提高效率。atomic本質上就是給get/set方法加鎖,即原子鎖,以避免線程A還沒執行完setter,線程B又開始執行的,導致讀取數據錯誤的問題。

atomic一定是線程安全的麼??肯定不是啊!

首先atomic的釋義是原子性,並不是線程安全。原子性這個概念表示一個操作序列就像一個操作一樣不被打斷,而不像一個操作序列一樣中間容許被打斷。所以nonatomic一定是線程不安全的,但是atomic卻不一定是線程安全的。假設線程A執行在對某屬性get之前線程B release了該屬性,會導致程序崩潰。

atomic的作用只是給getter和setter加了個鎖,atomic只能保證代碼進入getter或者setter函數內部時是安全的,一旦出了getter和setter,多線程安全只能靠自己保障了。所以atomic屬性和使用property的多線程安全並沒什麼直接的聯繫。另外,atomic由於加鎖也會帶來一些性能損耗,所以我們在編寫iOS代碼的時候,一般聲明property爲nonatomic,在需要做多線程安全的場景,自己去額外加鎖做同步。

所以,我們如何做到多線程安全?沒有絕對的安全,其實就是,在寫代碼的時候,能保證代碼串行的執行,代碼執行到一半的時候,不會有另一個線程介入。這就是我們所追求的線程安全。

所有筆記都出自日常踩坑小記,會持續更新

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章