多線程初識(shí)

線程概述

有些程序是一條直線,起點(diǎn)到終點(diǎn);有些程序是一個(gè)圓,不斷循環(huán),直到將它切斷
一個(gè)運(yùn)行著的程序就是一個(gè)進(jìn)程或者叫做一個(gè)任務(wù),一個(gè)進(jìn)程至少包含一個(gè)線程,線程就是程序的執(zhí)行流。Mac和iOS中的程序啟動(dòng),創(chuàng)建好一個(gè)進(jìn)程的同時(shí), 一個(gè)線程便開始運(yùn)行,這個(gè)線程叫主線程。主線程在程序中的地位和其他線程不同,它是其他線程最終的父線程,且所有界面的顯示操作即AppKit或 UIKit的操作必須在主線程進(jìn)行。
系統(tǒng)中的每一個(gè)進(jìn)程都有自己獨(dú)立的虛擬內(nèi)存空間,而同一個(gè)進(jìn)程中的多個(gè)線程則共用進(jìn)程的內(nèi)存空間。每創(chuàng)建一個(gè)新的線程,都需要一些內(nèi)存(如每個(gè)線程有自己的Stack空間)和消耗一定的CPU時(shí)間。另外當(dāng)多個(gè)線程對(duì)同一個(gè)資源出現(xiàn)爭(zhēng)奪的時(shí)候需要注意線程安全問題
多線程的實(shí)現(xiàn)原理:雖然在同一時(shí)刻,CPU只能處理1條線程,但是CPU可以快速地在多條線程之間調(diào)度(切換),造成了多線程并發(fā)執(zhí)行的假象。

多線程的優(yōu)點(diǎn)

能適當(dāng)提高程序的執(zhí)行效率。
能適當(dāng)提高資源利用率(CPU、內(nèi)存利用率)。

多線程的缺點(diǎn)

創(chuàng)建線程是需要成本的:iOS下主要成本包括:在??臻g的子線程512KB、主線程1MB,創(chuàng)建線程大約需要90毫秒的創(chuàng)建時(shí)間。
線程越多,CPU在調(diào)度線程上的開銷就越大。
線程越多,程序設(shè)計(jì)就越復(fù)雜:因?yàn)橐紤]到線程之間的通信,多線程的數(shù)據(jù)共享。

下面開始擼代碼:
------------------------------------------------------------------華麗的分割線

1.耗時(shí)操作的問題演示

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self longOperation];
}
- (void)longOperation
{
    NSLog(@"start");

    NSTimeInterval start = CACurrentMediaTime();

    for (int i = 0; i < 10000000; i++) {

        // 存儲(chǔ)在棧區(qū)
        //  int num = 10;

        // 存儲(chǔ)在常量區(qū)
        //  NSString *str1 = @"hello";

        // 存儲(chǔ)在堆區(qū)
        //  NSString *str2 = [NSString stringWithFormat:@"hello_%d",i];

        // I/O操作 : 把數(shù)據(jù)從內(nèi)存輸出到外接設(shè)備,或者由外接設(shè)備輸入到內(nèi)存;
        NSLog(@"%d",i);
    }

    NSLog(@"over %f", CACurrentMediaTime() - start);
}

結(jié)論

  1. 空的for循環(huán)不耗時(shí)
  2. 操作內(nèi)存的棧區(qū)速度很快;棧區(qū)存儲(chǔ)空間地址是連續(xù)的;
  3. 操作內(nèi)存的常量區(qū)速度很快;內(nèi)存空間只開辟一次;
  4. 操作內(nèi)存的堆區(qū)速度相對(duì)棧區(qū)和常量區(qū)要慢些;堆區(qū)內(nèi)存空間不連續(xù),需要尋址;
  5. I/O操作是很耗時(shí)的; (把數(shù)據(jù)從內(nèi)存輸出到外接設(shè)備,或者由外接設(shè)備輸入到內(nèi)存)
  6. 耗時(shí)操作對(duì)UI交互的影響 : 卡死了主屏幕,直到耗時(shí)操作執(zhí)行完,屏幕的交互才能正常進(jìn)行;
  7. 解決耗時(shí)操作卡頓UI的辦法 : 多線程技術(shù);
  8. 學(xué)習(xí)多線程的目的 : 把耗時(shí)操作放在后臺(tái)執(zhí)行,不讓耗時(shí)操作卡頓UI;

2.解決耗時(shí)操作卡頓UI的辦法

使用多線程技術(shù) : 解決屏幕卡死的問題

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//    [self longOperation];

    // 使用多線程技術(shù)
    [self performSelectorInBackground:@selector(longOperation) withObject:nil];
}

3.多線程基本概念

同步 & 異步

  • 同步異步任務(wù)執(zhí)行的兩種方式

  • 同步

    • 我們之前寫程序的時(shí)候代碼都是從上往下,順序執(zhí)行的,就叫做同步執(zhí)行.
    • 1個(gè)人執(zhí)行多個(gè)任務(wù),是要依次執(zhí)行的.因?yàn)?個(gè)人同一時(shí)間只能執(zhí)行1個(gè)任務(wù).
    • 多個(gè)任務(wù)按序依次執(zhí)行,就是同步執(zhí)行.
  • 異步

    • 多個(gè)任務(wù)同時(shí)執(zhí)行,就是異步執(zhí)行.
    • 異步是多線程的代名詞.
    • 我們學(xué)習(xí)多線程就是為了實(shí)現(xiàn)如何讓任務(wù)異步執(zhí)行.

進(jìn)程 & 線程

  • 進(jìn)程

    • 在系統(tǒng)中正在運(yùn)行的一個(gè)應(yīng)用程序叫進(jìn)程.
    • 通過活動(dòng)監(jiān)視器可以查看MAC系統(tǒng)中正在運(yùn)行的所有應(yīng)用程序.
    • 每個(gè)進(jìn)程之間都是獨(dú)立的,均運(yùn)行在其專用受保護(hù)的內(nèi)存空間內(nèi).
    • 兩個(gè)進(jìn)程之間是無法通信的,迅雷無法幫助酷我下載正在播放的音樂.
    • 進(jìn)程可以類比成正在正常運(yùn)營公司.
  • 線程

    • 線程可以類比成公司中的員工.
    • 進(jìn)程要想執(zhí)行任務(wù),必須要有線程,且每個(gè)進(jìn)程至少有一條線程.
    • 線程是進(jìn)程的基本執(zhí)行單元,進(jìn)程中的所有任務(wù)都在線程中執(zhí)行.
    • 程序啟動(dòng)(進(jìn)程開啟)會(huì)默認(rèn)開啟一條線程.
    • 1個(gè)進(jìn)程中可以有多個(gè)線程.

多線程

  • 多線程 : 一個(gè)進(jìn)程中可以開啟多條線程,多條線程可以**同時(shí)**執(zhí)行不同的任務(wù).
  • 進(jìn)程-公司,線程-員工,老板是什么?
  • 多線程可以解決程序阻塞的問題
  • 多線程可以提高程序的執(zhí)行效率,給用戶良好的使用體驗(yàn).
  • 比如,酷我音樂的邊下載邊聽歌,迅雷的邊下載邊播放.

4.多線程執(zhí)行原理

  • 單核CPU同一時(shí)間,CPU只能處理1個(gè)線程,只有1個(gè)線程在執(zhí)行任務(wù).
  • 多線程的同時(shí)執(zhí)行 : 其實(shí)是CPU在多條線程之間快速切換(調(diào)度任務(wù)).
  • 如果CPU調(diào)度線程的速度足夠快,就造成了多線程**同時(shí)**執(zhí)行的**假象**
  • 如果線程非常多,CPU會(huì)在多條線程之間不斷的調(diào)度任務(wù),結(jié)果就是消耗了大量的CPU資源,CPU會(huì)累趴下.
    • 每個(gè)線程調(diào)度的頻率會(huì)降低
    • 線程的執(zhí)行效率會(huì)下降

5.多線程優(yōu)缺點(diǎn)

優(yōu)點(diǎn)

  • 能"適當(dāng)"提高程序的執(zhí)行效率.
  • 能"適當(dāng)"提高CPU和內(nèi)存的利用率.
  • 線程上的任務(wù)執(zhí)行完成后,線程會(huì)自動(dòng)銷毀,節(jié)省內(nèi)存.

缺點(diǎn)

  • 開啟線程需要占用一定的內(nèi)存空間,如果開啟的線程過多,會(huì)占用大量的CPU資源,降低程序的性能
  • 占用內(nèi)存空間:默認(rèn)情況下,子線程512KB,主線程1M.PS:iOS8中,主線程512KB.
  • 線程越多,CPU調(diào)度線程的開銷就越大.
    • 時(shí)間開銷
    • 空間開銷
  • 程序設(shè)計(jì)更加復(fù)雜:比如線程之間的通信,多線程的數(shù)據(jù)共享

6.主線程

  • 一個(gè)程序運(yùn)行后,默認(rèn)會(huì)開啟1個(gè)線程,稱為主線程UI線程.

    • 關(guān)注main函數(shù)的執(zhí)行
  • 主線程一般用來刷新UI界面,處理UI事件.

  • 處理UI事件 : 點(diǎn)擊、滾動(dòng)、拖拽等事件

  • 主線程使用注意

    • 別將耗時(shí)的操作放到主線程中
    • 耗時(shí)操作會(huì)卡住主線程,嚴(yán)重影響UI的流暢度,給用戶一種卡的壞體驗(yàn),影響UI交互質(zhì)量.

7.多線程的實(shí)現(xiàn)方案

(二)創(chuàng)建線程三種方式

1.準(zhǔn)備新線程執(zhí)行的方法

- (void)demo:(id)obj
{
    NSLog(@"傳入?yún)?shù) => %@",obj);
    NSLog(@"hello %@",[NSThread currentThread]);
}

2.對(duì)象方法創(chuàng)建

  • 實(shí)例化線程對(duì)象的同時(shí)指定線程執(zhí)行的方法@selector(demo:).
  • 需要手動(dòng)開啟線程.
- (void)threadDemo1
{
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(demo:) object:@"alloc"];
    // 手動(dòng)啟動(dòng)線程
    [thread start];
}

3.類方法創(chuàng)建

  • 分離出一個(gè)線程,并且自動(dòng)開啟線程執(zhí)行@selector(demo:).
  • 無法獲取到線程對(duì)象
- (void)threadDemo2
{
    [NSThread detachNewThreadSelector:@selector(demo:) toTarget:self withObject:@"detach"];
}

4.NSObject(NSThreadPerformAdditions) 的分類創(chuàng)建

  • 方便任何繼承自NSObject的對(duì)象,都可以很容易的調(diào)用線程方法
  • 無法獲取到線程對(duì)象
  • 自動(dòng)開啟線程執(zhí)行@selector(demo:).
- (void)threadDemo3
{
    [self performSelectorInBackground:@selector(demo:) withObject:@"perform"];
}

5.總結(jié)

  • 以上三種創(chuàng)建線程的方式,各有不同.隨意選擇.
  • 使用哪種方式需要根據(jù)具體的需求而定.比如 : 如果需要線程對(duì)象,就使用對(duì)象方法創(chuàng)建.

(三)target和selector的關(guān)系

1.target和selector的關(guān)系分析

  • target : 指方法從屬于的對(duì)象.
    • 比如 : 本對(duì)象--self;其他對(duì)象--self.person.
  • @selector : 指對(duì)象里面的方法.
    • 比如 : 要執(zhí)行的是self中或者self.person中的哪個(gè)方法.
  • 提示 : 不要看見 target 就寫 self.
  • target@selector的關(guān)系 : 執(zhí)行哪個(gè)對(duì)象上的哪個(gè)方法.

2.代碼演練

準(zhǔn)備Person對(duì)象

@interface Person : NSObject

/// 人名
@property (nonatomic,copy) NSString *name;
/// 創(chuàng)建人的構(gòu)造方法
+ (instancetype)personWithDict:(NSDictionary *)dict;
/// 人有個(gè)方法
- (void)personDemo:(id)obj;

@end

@implementation Person

+ (instancetype)personWithName:(NSString *)name
{
    Person *person = [[Person alloc] init];
    person.name = name;
    return person;
}

- (void)personDemo:(id)obj
{
    NSLog(@"創(chuàng)建的人名 => %@",self.name);
    NSLog(@"hello %@",[NSThread currentThread]);
}

@end

控制器中的使用

定義屬性

@interface ViewController ()
@property (nonatomic,strong) Person *person;
@end

懶加載Person

@implementation ViewController

- (Person *)person
{
    if (_person==nil) {
        _person = [Person personWithName:@"zhangjie"];
    }
    return _person;
}

新的實(shí)例化方法

  • 使用self調(diào)用@selector(personDemo:)就會(huì)崩潰.因?yàn)?code>self中沒有@selector(personDemo:).

  • 分類方法

// 崩潰
[self performSelectorInBackground:@selector(personDemo:) withObject:@"perform"];
// 正確的調(diào)用方式
[self.person performSelectorInBackground:@selector(personDemo:) withObject:@"perform"];
  • 類方法
// 崩潰
[NSThread detachNewThreadSelector:@selector(personDemo:) toTarget:self withObject:@"detach"];

// 正確的調(diào)用方式
[NSThread detachNewThreadSelector:@selector(personDemo:) toTarget:self.person withObject:@"detach"];
  • 對(duì)象方法
// 崩潰
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(personDemo:) object:@"alloc"];
// 手動(dòng)開啟線程
[thread start];
// 正確的調(diào)用方式
NSThread *thread = [[NSThread alloc] initWithTarget:self.person selector:@selector(personDemo:) object:@"alloc"];
// 手動(dòng)開啟線程
[thread start];

(四)線程狀態(tài)-生命周期

線程生命周期的控制

  • 新建
    • 內(nèi)存中創(chuàng)建了一個(gè)線程對(duì)象
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadDemo) object:nil];
  • 就緒
    • 將線程放進(jìn)可調(diào)度線程池,等待被CPU調(diào)度
[thread start];
  • 運(yùn)行

    • CPU負(fù)責(zé)調(diào)度 可調(diào)度線程池 中的處于 就緒狀態(tài) 的線程
    • 線程執(zhí)行結(jié)束之前,狀態(tài)可能會(huì)在 就緒狀態(tài)運(yùn)行狀態(tài) 之間來回的切換
    • 就緒狀態(tài)運(yùn)行狀態(tài) 之間的狀態(tài)切換由CPU來完成,程序員無法干涉
  • 阻塞

    • 正在運(yùn)行的線程,當(dāng)滿足某個(gè)條件時(shí),可以用休眠或者來阻塞線程的執(zhí)行

      • sleepForTimeInterval:休眠指定時(shí)長
      [NSThread sleepForTimeInterval:1.0];
      
      • sleepUntilDate:休眠到指定日期
      [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
      
      • 互斥鎖
      @synchronized(self)
      
  • 死亡

    • 正常死亡:線程執(zhí)行結(jié)束
    • 非正常死亡
      • 程序突然崩潰
      • 當(dāng)滿足某個(gè)條件后,在線程內(nèi)部強(qiáng)制線程退出,調(diào)用exit方法

代碼演練

創(chuàng)建線程對(duì)象和就緒狀態(tài)

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 新建狀態(tài)
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadDemo) object:nil];
    // 就緒狀態(tài) : 將線程放進(jìn)"可調(diào)度線程池",等待被CPU調(diào)度.
    [thread start];
}

新線程執(zhí)行的方法

- (void)threadDemo
{
    // 提示 : 能執(zhí)行到這里說明線程是運(yùn)行狀態(tài)
    NSLog(@"%@",[NSThread currentThread]);

    // 使當(dāng)前線程休眠2秒鐘 : 休眠指定時(shí)長
    [NSThread sleepForTimeInterval:2.0];

    NSLog(@"第一次睡醒");

    // 使當(dāng)前線程休眠到指定日期
    [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]];

    NSLog(@"第二次睡醒");

    // 使當(dāng)前線程退出 : 當(dāng)前線程一旦退出,后續(xù)的所有代碼都不會(huì)執(zhí)行
    // 注意 : 該方法不能在主線程使用,會(huì)使主線程退出
    [NSThread exit];

    NSLog(@"沒戲了?");
}

關(guān)于exit的結(jié)論

  • 使當(dāng)前線程退出.
  • 不能在主線程中調(diào)用該方法.會(huì)使主線程退出.
  • 當(dāng)前線程死亡之后,這個(gè)線程中的剩下的所有代碼都不會(huì)被執(zhí)行.
  • 在調(diào)用此方法之前一定要注意釋放之前由C語言框架創(chuàng)建的對(duì)象.
  • 調(diào)用exit方法屬于在線程內(nèi)部取消線程,有時(shí)候需要在線程外部,當(dāng)某一條件滿足時(shí)就取消線程

線程的取消 (在線程執(zhí)行的方法的外部取消)

  • 取消線程的方法
- (void)cancel NS_AVAILABLE(10_5, 2_0);
  • 使用
[thread cancel]
  • 這個(gè)方法只是修改了線程的狀態(tài)而已,并沒有真正的取消線程
  • 如果想真正的取消線程需要在線程執(zhí)行的過程中判斷線程的狀態(tài)是否是已取消
  • 如果該線程已經(jīng)被取消,就直接返回,不再執(zhí)行后面的代碼
if ([NSThread currentThread].isCancelled) {
    NSLog(@"該線程已經(jīng)被取消");
    return;
}

(五)線程屬性

1.常用屬性

  • name - 線程名稱

    • 設(shè)置線程名稱可以當(dāng)線程執(zhí)行的方法內(nèi)部出現(xiàn)異常時(shí),記錄異常和當(dāng)前線程
  • stackSize - 棧區(qū)大小

    • 默認(rèn)情況下,無論是主線程還是子線程,棧區(qū)大小都是 512K
    • 棧區(qū)大小可以設(shè)置 [NSThread currentThread].stackSize = 1024 * 1024;
    • 必須是 4KB 的倍數(shù)
  • isMainThread - 是否主線程

  • threadPriority - 線程優(yōu)先級(jí)

    • 優(yōu)先級(jí),是一個(gè)浮點(diǎn)數(shù),取值范圍從 0~1.0
    • 1.0 表示優(yōu)先級(jí)最高
    • 0.0 表示優(yōu)先級(jí)最低
    • 默認(rèn)優(yōu)先級(jí)是 0.5
    • 優(yōu)先級(jí)高只是保證 CPU 調(diào)度的可能性會(huì)高
  • qualityOfService - 服務(wù)質(zhì)量(iOS 8.0 推出)

    • NSQualityOfServiceUserInteractive - 用戶交互,例如繪圖或者處理用戶事件
    • NSQualityOfServiceUserInitiated - 用戶需要
    • NSQualityOfServiceUtility - 實(shí)用工具,用戶不需要立即得到結(jié)果
    • NSQualityOfServiceBackground - 后臺(tái)
    • NSQualityOfServiceDefault - 默認(rèn),介于用戶需要和實(shí)用工具之間

關(guān)于優(yōu)先級(jí)和服務(wù)質(zhì)量

  • 多線程的目的:是將耗時(shí)的操作放在后臺(tái),不阻塞主線程和用戶的交互!
  • 多線程開發(fā)的原則:簡(jiǎn)單
  • 在開發(fā)時(shí),最好不要修改優(yōu)先級(jí),不要相信 用戶交互 服務(wù)質(zhì)量
  • 內(nèi)核調(diào)度算法在決定該運(yùn)行哪個(gè)線程時(shí),會(huì)把線程的優(yōu)先級(jí)作為考量因素
* 較高優(yōu)先級(jí)的線程會(huì)比較低優(yōu)先級(jí)的線程具有更多的運(yùn)行機(jī)會(huì)
* 較高優(yōu)先級(jí)不保證你的線程具體執(zhí)行的時(shí)間,只是相比較低優(yōu)先級(jí)的線程,更有可能被調(diào)度器選擇執(zhí)行而已

2.代碼演示

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"主線程棧區(qū)空間大小 == %tu KB 是否是主線程 %zd",[NSThread currentThread].stackSize / 1024,[NSThread currentThread].isMainThread);

    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];

    // 給線程起名字
    thread1.name = @"download A";

    // 設(shè)置線程優(yōu)先級(jí)
    thread1.threadPriority = 1.0;

    // 線程就緒
    [thread1 start];

    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
    thread2.name = @"download B";
    thread2.threadPriority = 0;
    [thread2 start];
}

- (void)demo
{
    NSLog(@"子線程棧區(qū)空間大小 == %tu KB 是否是主線程 %zd",[NSThread currentThread].stackSize / 1024,[NSThread currentThread].isMainThread);

    for (int i = 0; i < 10; i++) {
        NSLog(@"%@",[NSThread currentThread]);
    }
}

3.補(bǔ)充

  • NSInteger 有符號(hào)整數(shù)(有正負(fù)數(shù))用 %zd
  • NSUInteger 無符號(hào)整數(shù)(沒有負(fù)數(shù))用 %tu
  • 是為了自適應(yīng)32位和64位CPU的架構(gòu).

(六)線程安全-資源共享

1.多線程操作共享資源的問題

  • 共享資源

    • 資源 : 一個(gè)全局的對(duì)象、一個(gè)全局的變量、一個(gè)文件.
    • 共享 : 可以被多個(gè)對(duì)象訪問.
    • 共享資源 :可以被多個(gè)對(duì)象訪問的資源.比如全局的對(duì)象,變量,文件.
  • 多線程的環(huán)境下,共享的資源可能會(huì)被多個(gè)線程共享,也就是多個(gè)線程可能會(huì)操作同一塊資源.

  • 當(dāng)多個(gè)線程操作同一塊資源時(shí),很容易引發(fā)數(shù)據(jù)錯(cuò)亂和數(shù)據(jù)安全問題,數(shù)據(jù)有可能丟失,有可能增加,有可能錯(cuò)亂.

  • 經(jīng)典案例 : 賣票.

  • 線程安全

    • 同一塊資源,被多個(gè)線程同時(shí)讀寫操作時(shí),任然能夠得到正確的結(jié)果,稱之為線程是安全的.

開發(fā)提示

  • 實(shí)際開發(fā)中確定開發(fā)思路邏輯比及時(shí)的寫代碼更重要.
  • 多線程開發(fā)的復(fù)雜度相對(duì)較高,在開發(fā)時(shí)可以按照以下套路編寫代碼
    • 首先確保單個(gè)線程執(zhí)行正確
    • 然后再添加線程

代碼實(shí)現(xiàn)賣票邏輯

  • 先定義共享資源
@interface ViewController ()

/// 總票數(shù)(共享的資源)
@property (nonatomic,assign) int tickets;

@end
  • 初始化余票數(shù)共享資源
- (void)viewDidLoad {
    [super viewDidLoad];

    // 設(shè)置余票數(shù)
    self.tickets = 20;
}
  • 賣票邏輯實(shí)現(xiàn)
-  (void)saleTickets
{
    // while 循環(huán)保證每個(gè)窗口都可以單獨(dú)把所有的票賣完
    while (YES) {

        // 判斷是否有票
        if (self.tickets>0) {

            // 模擬網(wǎng)絡(luò)延遲 : 放大出錯(cuò)時(shí)的效果,沒有實(shí)際意義
            [NSThread sleepForTimeInterval:1.0];

            // 有票就賣一張
            self.tickets--;
            // 賣完一張票就提示用戶余票數(shù)
            NSLog(@"剩余票數(shù) => %zd %@",self.tickets,[NSThread currentThread]);
        } else {
            // 沒有就提示用戶
            NSLog(@"沒票了");
            // 此處要結(jié)束循環(huán),不然會(huì)死循環(huán)
            break;
        }
    }
}

單線程

  • 先確保單線程中運(yùn)行正常
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 在主線程中賣票
    [self saleTickets];
}

多線程

  • 如果單線程運(yùn)行正常,就修改代碼,實(shí)現(xiàn)多線程環(huán)境
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 在主線程中賣票
    // [self saleTickets];

    // 售票口 A
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
    thread1.name = @"售票口 A";
    [thread1 start];

    // 售票口 B
    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
    thread2.name = @"售票口 B";
    [thread2 start];
}

資源搶奪結(jié)果

  • 數(shù)據(jù)錯(cuò)亂,數(shù)據(jù)增加.


出錯(cuò)原因分析

2.解決多線程操作共享資源的問題

  • 解決辦法 : 使用互斥鎖/同步鎖.

添加互斥鎖

- (void)saleTickets
{
    // while 循環(huán)保證每個(gè)窗口都可以單獨(dú)把所有的票賣完
    while (YES) {

        // 添加互斥鎖
        @synchronized(self) {
            // 判斷是否有票
            if (self.tickets>0) {

                // 模擬網(wǎng)絡(luò)延遲 : 放大出錯(cuò)時(shí)的效果,沒有實(shí)際意義
                [NSThread sleepForTimeInterval:1.0];

                // 有票就賣一張
                self.tickets--;
                // 賣完一張票就提示用戶余票數(shù)
                NSLog(@"剩余票數(shù) => %zd",self.tickets);
            } else {
                // 沒有就提示用戶
                NSLog(@"沒票了");
                // 此處要結(jié)束循環(huán),不然會(huì)死循環(huán)
                break;
            }
        }
    }
}

互斥鎖小結(jié)

  • 互斥鎖,就是使用了線程同步技術(shù).
  • 同步鎖/互斥鎖:可以保證被鎖定的代碼,同一時(shí)間,只能有一個(gè)線程可以操作.
  • self :鎖對(duì)象,任何繼承自NSObject的對(duì)像都可以是鎖對(duì)象,因?yàn)閮?nèi)部都有一把鎖,而且默認(rèn)是開著的.
  • 鎖對(duì)象 : 一定要是全局的鎖對(duì)象,要保證所有的線程都能夠訪問,self是最方便使用的鎖對(duì)象.
  • 互斥鎖鎖定的范圍應(yīng)該盡量小,但是一定要鎖住資源的讀寫部分.
  • 加鎖后程序執(zhí)行的效率比不加鎖的時(shí)候要低.因?yàn)榫€程要等待解鎖.
  • 犧牲了性能保證了安全性.

(七)原子屬性

1.原子屬性相關(guān)概念

  • nonatomic : 非原子屬性

  • atomic : 原子屬性

    • 線程安全的,針對(duì)多線程設(shè)計(jì)的屬性修飾符,是默認(rèn)值.
    • 特點(diǎn) : 單寫多讀
    • **單寫多讀 : **保證同一時(shí)間,只有一個(gè)線程能夠執(zhí)行setter方法,但是可以有多個(gè)線程執(zhí)行getter方法.
    • atomic 屬性的setter里面里面有一把鎖,叫做自旋鎖.
    • 原子屬性的setter方法是線程安全的;但是,getter方法不是線程安全的.
  • nonatomicatomic對(duì)比

    • nonatomic : 非線程安全,適合內(nèi)存小的移動(dòng)設(shè)備.
    • atomic : 線程安全,需要消耗大量的資源.性能比非原子屬性要差一點(diǎn)兒點(diǎn)兒.

2.模擬原子屬性

  • 模擬原子屬性的核心思想 : 在屬性的setter方法里面加鎖.但是getter方法里面不加鎖;

  • 定義屬性

/// 非原子屬性
@property (nonatomic,strong) NSObject *obj1;
/// 原子屬性:內(nèi)部有"自旋鎖"
@property (atomic,strong) NSObject *obj2;
/// 用于模擬原子屬性
@property (atomic,strong) NSObject *obj3;
  • 重寫非原子屬性的settergetter方法
    • 重寫了原子屬性的setter方法之后,會(huì)覆蓋原子屬性內(nèi)部的自旋鎖,使其失效.然后我們加入互斥鎖,來模擬單寫多讀.
    • 重寫了屬性的settergetter方法之后,系統(tǒng)就不會(huì)再幫我們生成待下劃線的成員變量.使用合成指令@synthesize,就可以手動(dòng)的生成帶下劃線的成員變量.

3.模擬原子屬性

// 合成指令
@synthesize obj3 = _obj3;

/// obj3的setter方法
- (void)setObj3:(NSObject *)obj3
{
    // 使用互斥鎖替代看不見的自旋鎖
    @synchronized(self) {
        _obj3 = obj3;
    }
}

/// obj3的getter方法
- (NSObject *)obj3
{
    return _obj3;
}

4.性能測(cè)試

/// 測(cè)試"非原子屬性","互斥鎖","自旋鎖"的性能
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSInteger largeNum = 1000*1000;

    NSLog(@"非原子屬性");
    CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
    for (int i = 0; i < largeNum; i++) {
        self.obj1 = [[NSObject alloc] init];
    }
    NSLog(@"非原子屬性 => %f",CFAbsoluteTimeGetCurrent()-start);

    NSLog(@"原子屬性");
    start = CFAbsoluteTimeGetCurrent();
    for (int i = 0; i < largeNum; i++) {
        self.obj2 = [[NSObject alloc] init];
    }
    NSLog(@"原子屬性 => %f",CFAbsoluteTimeGetCurrent()-start);

    NSLog(@"模擬原子屬性");
    start = CFAbsoluteTimeGetCurrent();
    for (int i = 0; i < largeNum; i++) {
        self.obj3 = [[NSObject alloc] init];
    }
    NSLog(@"模擬原子屬性 => %f",CFAbsoluteTimeGetCurrent()-start);
}

測(cè)試結(jié)果

5.互斥鎖和自旋鎖對(duì)比

共同點(diǎn)

  • 都能夠保證同一時(shí)間,只有一條線程執(zhí)行鎖定范圍的代碼

不同點(diǎn)

  • 互斥鎖:如果發(fā)現(xiàn)有其他線程正在執(zhí)行鎖定的代碼,線程會(huì)進(jìn)入休眠狀態(tài),等待其他線程執(zhí)行完畢,打開鎖之后,線程會(huì)重新進(jìn)入就緒狀態(tài).等待被CPU重新調(diào)度.
  • 自旋鎖:如果發(fā)現(xiàn)有其他線程正在執(zhí)行鎖定的代碼,線程會(huì)以死循環(huán)的方式,一直等待鎖定代碼執(zhí)行完成.

6.開發(fā)建議

  • 所有屬性都聲明為nonatomic,原子屬性和非原子屬性的性能幾乎一樣.
  • 盡量避免多線程搶奪同一塊資源.
  • 要實(shí)現(xiàn)線程安全,必須要用到.無論什么鎖,都是有性能消耗的.
  • 自旋鎖更適合執(zhí)行非常短的代碼.死循環(huán)內(nèi)部不適合寫復(fù)雜的代碼.
  • 盡量將加鎖,資源搶奪的業(yè)務(wù)邏輯交給服務(wù)器端處理,減小移動(dòng)客戶端的壓力.
  • 為了流暢的用戶體驗(yàn),UIKit類庫的線程都是不安全的,所以我們需要在主線程(UI線程)上更新UI.
  • 所有包含NSMutable的類都是線程不安全的.在做多線程開發(fā)的時(shí)候,需要注意多線程同時(shí)操作可變對(duì)象的線程安全問題.

(八)NSThread線程間通信

1.ATS

使用http地址時(shí)Xcode會(huì)認(rèn)為不夠安全從而保存,為解決此問題需要在info文件的Xml文件內(nèi)添加下列代碼
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>

2.代碼實(shí)現(xiàn)

定義屬性

@interface ViewController ()

/// 滾動(dòng)視圖
@property (nonatomic,strong) UIScrollView *scrollView;
/// 圖片視圖
@property (nonatomic,weak) UIImageView *imageView;

@end

loadView 方法復(fù)習(xí)

  • 當(dāng) self.view == nil 時(shí),會(huì)調(diào)用;
  • 先于 viewDidLoad 調(diào)用;
  • 一旦重寫了這個(gè)方法,storyboard里面就不會(huì)去加載根視圖了;

加載視圖層次

- (void)loadView
{
    // 創(chuàng)建滾動(dòng)視圖
    self.scrollView = [[UIScrollView alloc] initWithFrame:[UIScreen mainScreen].bounds];
    // 將滾動(dòng)視圖設(shè)置成根視圖
    self.view = self.scrollView;
    self.scrollView.backgroundColor = [UIColor redColor];

    // 創(chuàng)建圖片視圖
    UIImageView *imageView = [[UIImageView alloc] init];
    [self.view addSubview:imageView];
    self.imageView = imageView;
}

異步下載圖片

- (void)viewDidLoad {
    [super viewDidLoad];

    // 主線程中下載圖片
    // [self downloadImageData];

    // 開啟新線程異步下載圖片
    [self performSelectorInBackground:@selector(downloadImageData) withObject:nil];
}

下載圖片主方法

- (void)downloadImageData
{
    // 圖片資源地址
    NSURL *url = [NSURL URLWithString:@"http://h.hiphotos.baidu.com/image/pic/item/c995d143ad4bd1130c0ee8e55eafa40f4afb0521.jpg"];
    // 所有的網(wǎng)絡(luò)數(shù)據(jù)都是以二進(jìn)制的形式傳輸?shù)?所以用NSData來接受
    NSData *data = [NSData dataWithContentsOfURL:url];
    UIImage *image = [UIImage imageWithData:data];

    // 回到主線程更新UI
    // waitUntilDone:是否等待主線程中的`updateUIwWithImage`方法執(zhí)行結(jié)束再執(zhí)行"下一行代碼",一般設(shè)置成NO,不用等待
    [self performSelectorOnMainThread:@selector(updateUIwWithImage:) withObject:image waitUntilDone:NO];

    // 測(cè)試 waitUntilDone:
    NSLog(@"下一行代碼");
}

刷新UI

- (void)updateUIwWithImage:(UIImage *)imgae
{
    NSLog(@"updateUIwWithImage");

    // 設(shè)置圖片視圖
    self.imageView.image = image;
    // 設(shè)置圖片視圖的大小跟圖片一般大
    [self.imageView sizeToFit];

    // 設(shè)置滾動(dòng)視圖的滾動(dòng):滾動(dòng)范圍跟圖片一樣大
    [self.scrollView setContentSize:image.size];
}

線程間通信

  • 因?yàn)槎嗑€程共享地址空間和數(shù)據(jù)空間

所以一個(gè)線程的數(shù)據(jù)可以直接提供給其他線程使用,叫做線程間通信;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容