多線程的原理總結


 

1. 進程的概念

進程是指在系統中正在進行的一個應用程序;每個進程之間是獨立的,每個進程均運行在其專用且受保護的內存空間內。

比如,同時打開QQ和Xcode,系統就會分別啟動2個進程。

通過“活動監視器”可以查看Mac系統中所開的進程

 

2.  線程的概念

一個進程要想執行任務,必須得有線程(每一個進程至少要有一條線程)。線程是進程的基本執行單元,一個進程(程序)的所有任務都在線程中執行。

比如使用酷狗播放音樂、使用迅雷下載電影,都需要在線程中執行。

 

3. 線程的串行

1個線程中任務的執行是串行的,如果要在1個線程中執行多個任務,那么只能一個一個按照順序執行這些任務,即,在同一時間內,1個線程只能執行1個任務。

因此也可以認為 線程是進程中的一條執行任務。

 

4. 多線程

1個進程中可以開啟多條線程,每條線程可以並行(同時)執行不同的任務。多線程技術可以提高程序的執行效率。CPU只能處理1條線程,只有一條線程在工作。

 

5.多線程的原理

同一時間內,CPU只能處理1條線程,只有1條線程在工作(執行);多線程並發(同時)執行,其實是CPU快速地在多條線程之間調度(切換)。如果CPU調度線程的時間足夠快,就造成了多線程並發執行的假象。

思考:如果線程非常非常多,會發生什么情況?

CPU會在N多線程之間調度,CPU會類似,消耗大量的CPU資源;每條線程被調度執行的頻次會較低(線程的執行效率減低)。一般開到3-5條線程。

 

6. 多線程的優缺點

優點:能適當提高程序的執行效率;能適當提高資源利用率(CPU、內存利用率)。
缺點:開啟線程需要占用一定的內存空間(默認情況下,主線程占用1M,子線程占用512KB),如果開啟大量的線程,會占用大量的內存空間,降低程序的性能;線程越多,CPU在調度線程上的開銷就越大;程序設計更加復雜,例如線程之間的通信、多線程的數據共享。
創建線程大約需要90毫秒。

 

7. 多線程在iOS開發中的應用

7.1什么是主線程
1個iOS程序運行后,會默認開啟1條線程,稱為“主線程”或者“UI線程”。

7.2主線程的主要作用
顯示/刷新UI界面,處理UI時間(比如點擊時間、滾動時間、拖拽事件等)

7.3主線程的使用注意
別將比較消耗的操作放到主線程中;

耗時操作會卡住主線程,嚴重影響UI的流暢度,給用戶一種“卡”的壞體驗。例如,在一個界面上,點擊一個按鈕,需要耗時5秒;按下按鈕的后,立刻拖拽表格,那么,表格拖拽的反應會在5秒后才響應。

7.4 子線程
將耗時操作放在子線程中

 

 

8.NSThread的使用

  • 8.0 獲取線程(幾乎所有的人都在主線程中執行)
//獲取主線程
NSThread *mianThread = [NSTread mainThread];
//獲取當前線程
NSTread *currentTread = [NSTread currentThread];
//判斷是否是主線程
判斷number是否等於1; 調用方法 isMainThread
  • 8.1 創建和啟動線程

一個NSThread對象就代表一條線程。

創建和啟動線程的方法:

第一種方法:
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil]; [thread start]; //必須要調用這個方法,否者就不會啟動

第二種方法:開啟后台線程 隱式創建
[self performSelectorInBackground:@select(run) withObject:nil];

第三種方法:直接分離出一條子線程
[self detaNewThreaeSelector:@select(run) toTarget:self withObject:nil];


對比:
第一種:代碼量比較大,但是能拿到線程對象
第二種和第三種:無法拿到線程對象進行詳細設置

線程一啟動,就會告訴CPU准備就緒,可以隨時接受CPU調度,CPU調度當前線程后,就會在線程thread中執行self的run方法。

當線程完成任務后,就是被銷毀。

  • 8.2 主線程相關用法
+ (NSThread *)mainThread; // 獲得主線程
- (BOOL)isMainThread; // 是否為主線程
+ (BOOL)isMainThread; // 是否為主線程

 

線程的調度優先級:

+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;
- (double)threadPriority;
- (BOOL)setThreadPriority:(double)p;
調度優先級的取值范圍是0.0 ~ 1.0,默認0.5,值越大,優先級越高

 

線程的名字:

-   (void)setName:(NSString *)n;
-   (NSString *)name;

 

  • 8.3 線程的狀態
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];

[thread start];
  • 8.4       控制線程狀態
啟動線程:

- (void)start;

(進入就緒狀態 -> 運行狀態。當線程任務執行完畢,自動進入死亡狀態。)
 

阻塞(暫停)線程:

+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
 

強制停止線程:
+ (void)exit;
(進入死亡狀態)

 

  • 8.5 多線程的安全隱患
1)資源共享:
1塊資源可能會被多個線程共享,也就是多個線程可能會訪問同一塊資源。比如,多個線程訪問同一個對象、同一個變量和同一個文件
(2)當多個線程訪問同一塊資源時,很容易引發數據錯亂和數據安全問題。比如,下圖的存錢和取錢的問題以及買票和取票的問題。

 

  • 8.6 安全隱患解決-互斥鎖
互斥鎖使用格式:
@synchronized(鎖對象) { // 需要鎖定的代碼  }
注意:鎖定1份代碼只用1把鎖,用多把鎖是無效的。
   鎖對象,可以用self代替
   要注意加鎖的位置,在多個線程搶奪資源的時候才需要加鎖。
互斥鎖的優缺點: 優點:能有效防止因多線程搶奪資源造成的數據安全問題; 缺點:需要消耗大量的CPU資源。 互斥鎖的使用前提:多條線程搶奪同一塊資源。 相關專業術語:線程同步 線程同步的意思:多條線程在同一條線上執行(按順序地執行任務),互斥鎖,就是使用了線程同步技術

測試代碼如下:

#pragma mark - 賣票
- (void)addThread {
    self.count = 100;
    NSThread *threadA = [[NSThread alloc] initWithTarget:self selector:@selector(selectTick) object:nil];
    NSThread *threadB = [[NSThread alloc] initWithTarget:self selector:@selector(selectTick) object:nil];
    NSThread *threadC = [[NSThread alloc] initWithTarget:self selector:@selector(selectTick) object:nil];
    threadA.name = @"第一個";
    threadB.name = @"第二個";
    threadC.name = @"第三個";
    [threadA start];
    [threadB start];
    [threadC start];
}

- (void)selectTick {
    while (1) {
        if (self.count > 0) {
            self.count --;
            NSLog(@"%@還剩%ld張票",[NSThread currentThread].name, self.count);
        }else {
            NSLog(@"%@賣完了",[NSThread currentThread].name);
            break;
        }
    }
}
當我們調用上面那一段代碼的時候,是沒有出現賣票上票數不對的問題的;
因為代碼中並沒有做耗時的操作,每個方法調用一次就結束了,所以不會出現資源搶奪的情況;

如果在selctTick的方法中,添加一個耗時的操作,那么問題就會暴露,將selectTick的方法修改成如下:
- (void)selectTick {
    while (1) {
        if (self.count > 0) {
            self.count --;
            for (NSInteger i = 0; i < 100000; i++) { //這是一個耗時的操作 }
            NSLog(@"%@還剩%ld張票",[NSThread currentThread].name, self.count);
        }else {
            NSLog(@"%@賣完了",[NSThread currentThread].name);
            break;
        }
    }
}

查看打印的數據,就會發現有的票重復賣了。

解決方法:

加上一個同步鎖:
- (void)selectTick {
    
    while (1) {
        @synchronized (self) {
            if (self.count > 0) {
                self.count --;
                for (NSInteger i = 0; i < 1000; i++) {
                    
                }
                NSLog(@"%@還剩%ld張票",[NSThread currentThread].name, self.count);
            }else {
                NSLog(@"%@賣完了",[NSThread currentThread].name);
                break;
            }
        }
    }
}

注意加鎖的位置,如果鎖加在了while(1)之前,等於說,是一個人執行了賣票的流程,那么只有一個人把票全部賣完了,其他人才能賣票。

 

 

  • 8.7 原子與非原子屬性
1)OC在定義屬性時有nonatomic 和 atomic兩種選擇:

atomic:原子屬性,為setter方法加鎖(默認就是atomic);

nonatomic:非原子屬性,不會為setter方法加鎖。

(2)nonatomic和atomic對比:

atomic:線程安全,需要消耗大量的資源;

nonatomic:非線程安全,適合內存小的移動設備。

(3)ios開發建議:

所有屬性都聲明為nonatomic;

盡量避免多線程搶奪同一塊資源;

盡量將加鎖、資源搶奪的業務邏輯交給服務器端處理,減小移動客戶端的壓力。

 

  • 8.8 線程間通信
  1. 線程間通信:在1個進程種,線程往往不是獨立存在的,多個線程之間需要經常進行通信。
  2. 線程間通信的體現:
  3. 1個線程傳遞數據給另1個線程;
  4. 在1個線程中執行完特定任務后,轉到另一個線程繼續執行任務。

 

  • 線程間通信常用方法:
  1. - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
  2. - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;

 

9. GCD的使用

9.1 什么是GCD
全稱:Grand Central Dispatch, 可譯為“牛逼的中樞調度器”;純C語言,提供了非常多強大的函數。

9.2 GCD的優勢
 GCD是蘋果公司為多核的並行運算提出的解決方案
 GCD會自動利用更多的CPU內核(比如雙核、四核)
 GCD會自動管理線程的生命周期(創建線程、調度任務、銷毀線程)
程序員只需要告訴GCD想要執行什么任務,不需要編寫任何線程管理代碼
9.3 任務和隊列
(1)GCD中有2個核心概念:

任務:執行什么操作;

隊列:用來存放任務。

(2)GCD使用的2個步驟:

定制任務:確定想做的事情;

將任務添加到隊列中:GCD會自動將隊列中的任務取出,放到對應的線程中執行;任務的取出遵循隊列的FIFO原則:先進先出,后進后出。

 

  • 9.4 執行任務
GCD中有2個用來執行任務的函數:

(1)用同步的方式執行任務

     dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

     queue:隊列的意思;block:任務

(2)用異步的方式執行任務

     dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

(3)同步和異步的區別:

同步:只能在當前線程中執行任務,不具備開啟新線程的能力

異步:可以在新的線程中執行任務,具備開啟新線程的能力

 

  • 9.5 並發隊列

GCD默認已經提供了全局的並發隊列,供整個應用使用,不需要手動創建。

使用dispatch_get_global_queue函數獲得全局的並發隊列。

獲取一個全局並發隊列:

 dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

將任務添加到隊列中:

dispatch_async(queue, ^{

        //任務

        NSLog(@"renwu1%@",[NSThread currentThread]);

    });

 

  • 全局並發隊列的優先級:
#define DISPATCH_QUEUE_PRIORITY_HIGH  =2 //

#define DISPATCH_QUEUE_PRIORITY_DEFAULT = 0 // 默認(中)

#define DISPATCH_QUEUE_PRIORITY_LOW  =(-2) //

#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台

 

  • 9.6 串行隊列
 GCD中獲得串行有2中途徑:

(1)   使用dispatch_queue_create函數創建串行隊列;

// 創建一個串行隊列
    dispatch_queue_t queue = dispatch_queue_create(nil, DISPATCH_QUEUE_SERIAL);

// 將任務添加到隊列中

    dispatch_async(queue, ^{

        // 任務1

        NSLog(@"耗時任務1:%@",[NSThread currentThread]);

    });

(2)使用dispatch_get_main_queue()獲得主隊列:放在主隊列中的任務,都會放到主線程中執行;

        dispatch_queue_t queue = dispatch_get_main_queue();

 

 

  • 9.7 線程間通信示例

從子線程回到主線程:

dispatch_async(

dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    // 執行耗時的異步操作...

      dispatch_async(dispatch_get_main_queue(), ^{

        // 回到主線程,執行UI刷新操作

        });

});

 

  • 9.8 延時執行
iOS常見的延時執行有2種方法:

(1)   調用NSObject的方法:

[self performSelector:@selector(run) withObject:nil afterDelay:2.0];

(2)   使用GCD函數:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

    // 2秒后執行這里的代碼... 在哪個線程執行,跟隊列類型有關

});

(3)[NSTimer scheduledTimeWithTimeInterval:2.0 target:self seclector:@select(run) userInfo:nil repeat:No];

 

  • 9.9 一次性代碼(單利模式)

          單例模式的作用:可以保證在程序運行過程種,一個類只有一個實例,而且該實例易於供外界訪問;從而方便地控制了實例個數,並節約系統資源。

          單例模式的使用場合:在整個應用程序中,共享一份資源(這份資源只需要創建初始化1次)。  

          單例模式在ARC/MRC環境下的寫法有所不同,需要編寫2套不同的代碼。可以用宏定義判斷是否為ARC環境:

#if __has_feature(objc_arc)

     // ARC

#else

     // MRC

#endif

 使用dispatch_once函數能保證某段代碼在程序運行過程中只被執行1次

static dispatch_once_t onceToken;
//內部實現原理:判斷onceToken的值 == 0 來覺得是否執行block中的任務;當執行過后,onceToken的值為 -1
dispatch_once(&onceToken, ^{

    // 只執行1次的代碼(這里面默認是線程安全的)

});

 

alloc會調用allocWithZone(這個方法會分配存儲空間)

給某個類添加一個單利的方法:

// 提供一個全局的靜態變量(對外界隱藏)

MYCUserModelInfo *_userInfo;

 

@implementation MYCUserModelInfo

 // 從寫alloc方法保證永遠只分配一次存儲空間 alloc會調用allocWithZone(這個方法會分配存儲空間)

+ (instancetype)allocWithZone:(struct _NSZone *)zone {

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

        _userInfo = [super allocWithZone:zone];

    });

    return _userInfo;

}

// 提供類方法 

+ (instancetype)shareUserInfo {

    return [[self alloc] init];

}

//重寫 copy 需要繼承NSMutableCoping 

- (id)copyWithZone:(NSZone *)zone {

    return _userInfo;

}

 //需要繼承NSCoping 

- (id)mutableCopyWithZone:(NSZone *)zone {

    return _userInfo;

}

 

 

  • 9.10 隊列組

有這么1種需求:分別異步執行2個耗時的操作,等2異步操作都執行完畢后,再回到主線程執行操作。

如果想要快速高效地實現上述需求,可以考慮用隊列組:

dispatch_group_t group =  dispatch_group_create();

dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    // 執行1個耗時的異步操作

});

dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    // 執行1個耗時的異步操作

});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{

    // 等前面的異步操作都執行完畢后,回到主線程...

});

 

9.11 柵欄函數

dispath_barrier_asyn(queue,^
{
   NSlog(@"111111"); 
})

柵欄函數,之前的任務是並發執行,之后的任務也是並發執行;

 

 

10.   NSOperation的使用

基礎知識:

一般在開發中,直接使用GCD 開啟線程,做多線程的操作。如果,自己需要自定義框架/需要管理操作,這個時候,選擇NSOperation。

管理操作:取消操作/暫停/回復操作。

10.1 NSOperation的子類
NSOperation是個抽象類,並不具備封裝操作的能力,必須使用它的子類。

使用NSOperation子類的方式有3中:

(1)NSInvocationOperation

(2)NSBlockOperation

(3)自定義子類繼承NSOperation,實現內部相應的方法

10.2 NSInvocationOperation
使用方式:   
// 創建一個操作.-創建對象 NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(test) object:nil]; // 在當前線程執行. start 會在當前線程執行.直接啟動 [op start];
一旦執行操作,就會調用target的self方法。 注意:默認情況下,調用了start方法后並不會開一條新線程去執行操作,而是在當前線程同步執行操作。只有將NSOpeation放到一個NSOperationQueue中,才會異步執行操作。

 

  • 10.3 NSBlockOperation
1)創建NSBlockOperation對象:

+ (id)blockOperationWithBlock:(void (^)(void))block;

(2)通過addExecutionBlock:方法添加更多操作

- (void)addExecutionBlock:(void (^)(void))block;

         
   使用方法:

  // 創建一個操作.

    // 如果 NSBlockOperation 中封裝的操作數 > 1(追加操作了),這個時候,不能保證任務在哪條線程中執行.即使將操作添加到主隊列,依然會有任務在子線程執行.

    // 建議:如果使用 NSBlockOperation,為了確保任務在自己想要的線程執行,最好不要追加操作.

    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{

        //
        [self test];

    }];

    // 追加操作/任務

    [op addExecutionBlock:^{

        NSLog(@"--------------1 %@",[NSThread currentThread]);

    }];

 

  • 10.4 NSOperationQueue
NSOperationQueue 的作用:

(1)NSOperation可以調用start方法來執行任務,但默認是同步執行的;

(2)如果將NSOperation添加到NSOperationQueue(操作隊列中),系統會自動異步執行NSOperationQueue中的操作。

 
添加操作到NSOperationQueue中:

- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;

 

使用方法:

// 1.主隊列:主隊列中的操作,都要交給主線程執行.

    // 獲取主隊列
    NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];

    // 將操作添加到隊列中
    [mainQueue addOperation:op];

  
    // 2.非主隊列.添加到非主隊列中的操作,都交給子線程來執行.

   // 創建一個非主隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    // 將操作添加到非主隊列中
    [queue addOperation:op];  //內部會調用start方法,start方法會調用main方法

  
    // 這一句代碼,相當於之前的兩句.
    [mainQueue addOperationWithBlock:^{

        NSLog(@"直接往操作隊列中添加操作%@",[NSThread currentThread]);

    }];

 

  • 常規操作

 

10.5 最大並發數
並發數的概念:同時執行的任務數,比如同時開3個線程執行3個任務,並發數就是3.

最大並發數的相關方法:

- (NSInteger)maxConcurrentOperationCount;

- (void)setMaxConcurrentOperationCount:(NSInteger)cnt;

10.6 隊列的取消、暫停和恢復及監聽
(1)取消隊列的所有操作:- (void)cancelAllOperations;

(2)取消單個操作:- (void)cancel;(NSOperation的方法)   只能暫停后面的操作,不能暫停當前的任務

(3)暫停和恢復隊列:

- (void)setSuspended:(BOOL)b; // YES代表暫停隊列,NO代表恢復隊列

- (BOOL)isSuspended;

(4)操作的監聽:可以監聽一個操作的執行完畢;

- (void (^)(void))completionBlock;

- (void)setCompletionBlock:(void (^)(void))block;

  // 創建操作

    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{

        [self test1];

    }];

    // 當某個操作完成之后,就會回調這個方法.

    [op3 setCompletionBlock:^{

     // 對於Block,盡量用 set 方法,這樣就不用去猜測 Block 的類型了.

        NSLog(@"操作完成之后的回調");

    }];

  • 10.7 操作依賴
 
         

  NSOperation之間可以設置依賴來保證執行順序,比如一定要讓操作A執行完后,才能執行操作B,可以這么實現:

 
         

  [operationB addDependency:operationA]; // 操作B依賴於操作A

 
         

  但是要注意,不能相互依賴,即:A依賴B,而B依賴A;可以在不同queue的NSOperation之間創建依賴關系

 
        

 

  • 10.8 自定義NSOperation

  自定義NSOperation的步驟很簡單:第一個新建一個類,繼承NSOperation;第二步:重寫- (void)main 方法,在里面實現想執行的任務。

重寫- (void)mian方法的注意點:

自己創建自動釋放池(因為如果是異步操作,無法訪問主線程的自動釋放池);

經常通過- (BOOL)isCancelled方法檢驗操作是否被取消,對取消做出響應。蘋果官方建議:在自定義操作的時候,每執行完一個耗時操作就判斷一下當前是否取消,如果取消就返回。

// 當調用了 NSOperation 的 start 方法或者 將操作添加到操作隊列中之后,就會調用這個main 方法.(就會執行main 方法中的內容.)
//  @autoreleasepool {} 自動釋放池.一般情況下,操作在子線程執行,子線程的運行循環一般情況下不會開啟.子線程中的對象默認情況下不會訪問主線程中的自動釋放池,所以需要手動添加.

-(void)main
{
    @autoreleasepool {
        
        // 當程序運行完下面這句代碼之后,圖片就下載成功.
        UIImage *image = [self downloadWebImageWithUrlString:self.urlString];
     if(self.isCannel){
      return;
} // 顯示圖片.必須在主線程顯示. dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"顯示圖片%@",[NSThread currentThread]); self.imageView.image = image; }); } } // 下載網絡圖片的方法 - (UIImage *)downloadWebImageWithUrlString:(NSString *)urlString { NSLog(@"downloadWebImageWithUrlString:%@",[NSThread currentThread]); NSURL *url = [NSURL URLWithString:urlString]; NSData *data = [NSData dataWithContentsOfURL:url]; UIImage *image = [UIImage imageWithData:data]; return image; }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM