多線程學習筆記-01

  • 多線程概念
  • NSThread:基本使用 現成狀態 資源搶奪 互斥鎖&原子屬性 線程間通訊
  • GCD:隊列&任務 GCD常用代碼&隊列的選擇 其他功能(延時,一次性執行,分組)
  • NSOperation:簡單使用 其他功能(最大併發數,隊列的暫停&繼續,任務的依賴關係) 網絡圖像加載實現(無沙盒緩存,沙盒緩存,SDWebImage實現原理)

多線程基礎

什麼是進程?

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

什麼是線程

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

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

線程的串行

1個線程中任務的執行是串行(順序執行)的
如果要在1個線程中執行多個任務,那麼只能一個一個地按順序執行這些任務
也就是說,在同一時間內,1個線程只能執行一個任務

比如在1個線程中下載3個文件(分別是文件A,文件B,文件C)

這裏寫圖片描述

因此,也可以認爲線程是進程中的1條執行路徑

多線程

1.什麼是多線程?
1個進程中可以開啓多條線程,每條線程可以併發(同時)執行不同的任務
進程 -> 車間,線程 -> 車間工人
多線程技術可以提高程序的執行效率

比如同時開啓3條線程分別下載3個文件(分別是文件A,文件B,文件C)

這裏寫圖片描述

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

這裏寫圖片描述

思考:如果線程非常非常多,會發生什麼情況?
CPU會在N多線程之間調度,CPU會累死,消耗大量的CPU資源
每條線程被調度執行的頻次會降低(線程的執行效率降低)

多線程的優缺點

1.多線程的優點
能適當提高程序的執行效率
能適當提高資源的利用率(CPU,內存利用率)

2.多線程的缺點
開啓線程需要佔用一定的內存空間(默認情況下,主線程佔用1M,子線程佔用512K),如果開啓大量的線程,會佔用大量的內存空間,降低程序的性能
線程越多,CPU在調度線程上的開銷就越大
程序設計更加複雜:比如線程之間的通信、多線程的數據共享

多線程在iOS開發中的應用

1.什麼是主線程?

一個iOS程序運行後,默認會開啓1條線程,成爲“主線程”或“UI線程”

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

3.主線程的使用注意
別將比較耗時的操作放到主線程中
耗時操作會卡住主線程,嚴重影響UI的流暢度,給用戶一種“卡”的壞體驗

如果將耗時操作放在主線程

這裏寫圖片描述

4.耗時操作的執行

如果將耗時操作放在子線程(後臺線程,非主線程)

這裏寫圖片描述

iOS中多線程的實現方案

這裏寫圖片描述

NSThread的創建

三種創建線程方式

- (void)test1
{
    // 實例化一個線程對象
    NSThread * thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil];

    //讓線程開始工作,啓動線程,在新開的線程中執行run方法
    [thread start];
}
- (void)test2
{
    NSLog(@"%@",[NSThread currentThread]);
    [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"hello"];
    NSLog(@"test1 ---- %@",[NSThread currentThread]);
}
- (void)test3
{
    // "隱式"創建線程方法
    [self performSelectorInBackground:@selector(run:) withObject:@"hello"];
}

線程的屬性

name:可以給NSThread的對象設置name屬性,這個屬性在當程序有bug打印log時,幫主我們識別是哪個線程中出了問題
threadPriority:優先級,介於0.0~1.0之間,默認值0.5,優先級高的會優先執行。但是在測試時哪個線程優先體現得不是很明顯,多次測試可以觀察到。

#pragma mark - 線程的屬性
- (void)test4
{
    NSThread * threadA = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"hello"];
    // 給thread設置name屬性,可以在程序出bug時打印的log裏面看出是哪個線程的問題。
    threadA.name = @"thread A";
    //線程優先級
    // 是一個浮點數,0.0~1.0.默認值0.5
    // 開發的時候,一般不去修改優先級的值。
    // 優先級,必須調用很多次的時候,才能體現出來
    threadA.threadPriority = 0.1;
    //開始工作
    [threadA start];
    NSThread * threadB = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"hello"];
    threadB.name = @"thread B";
    //線程優先級
    // 是一個浮點數,0.0~1.0.默認值0.5
    threadB.threadPriority = 1.0;
    //開始工作
    [threadB start];
}

線程的狀態

  • 新建一個線程,被放進一個可調度線程池中(這個線程池中有新建的線程以及其他線程)
  • start之後進入就緒狀態(Runnable),隨時可被CPU調度
  • CPU調度之後進入運行(Running)狀態,如果CPU調度其他線程,則新建線程進入就緒狀態
  • 如果線程在運行中,被調用了sleep方法或者等待同步鎖,則進入阻塞(blocked)狀態,阻塞狀態下線程被從可調度線程池中移除,CPU不會調用可調度線程池之外的線程。sleep到時或者得到同步鎖,線程再次進入就緒狀態,即進入可調度線程池
  • 線程任務執行完畢或者異常、強制退出時,線程死亡(dead),就從內存中被銷燬

這裏寫圖片描述

控制線程狀態

1.啓動線程

- (void)start;

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

2.阻塞(暫停)線程

- (void)sleepUntilData:(NSDate*)date;
- (void)sleepForTimeInterval:(NSTimeInteval)ti;

//進入阻塞狀態
3.強制停止線程

+(void)exit;

//進入死亡狀態

注意:一旦線程停止(死亡)了,就不能再次開啓任務

示例代碼:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self test];
}
- (void)test
{
    //1.新建一個線程
    NSThread * thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];

    //2.放進一個可調度線程池,等待被調度。就緒狀態
    [thread start];

}

- (void)run
{
    NSLog(@"%s",__func__);
    //剛進來就睡會
  //  [NSThread sleepForTimeInterval:2.0];
    // 睡到指定的時間點
   // [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:5.0]];
    for(int i = 0;i < 20; i++){
        //滿足某一個條件以後,阻塞線程的執行.也就是讓線程休息一會
        if(i == 10){
            [NSThread sleepForTimeInterval:3.0];
        }
        //一旦達到某一個條件,就強制終止線程的執行
        if(i == 15){
            //一旦強制終止,就再也不能重新啓動了
            //一旦強制終止,後面的代碼都不會執行了。
            [NSThread exit];
        }
        NSLog(@"%@ ----- %d",[NSThread currentThread],i);
    }
    NSLog(@"線程結束"); // 線程退出之後這句代碼就執行不到了
}

多線程的安全隱患

1.資源共享
1塊資源可能會被多個線程共享,也就是多個線程可能會訪問同一塊資源
比如多個線程訪問同一個對象、同一個變量、同一個文件
當多個線程訪問同一塊資源時,很容易引發數據錯亂和數據安全問題
安全隱患示例-01

這裏寫圖片描述

安全隱患示例02-賣票

這裏寫圖片描述

示例代碼

#import "ViewController.h"

@interface ViewController ()
/**
 總票數
 */
@property(nonatomic, assign)int tickets;
@end

@implementation ViewController

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    //剩餘20張票
    self.tickets = 20;
    //主線程工作
  //  [self saleTickets];
    //增加子線程,賣票
    NSThread * threadA = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
    threadA.name = @"售票員A";
    [threadA start];
    NSThread * threadB = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
    threadB.name = @"售票員B";
    [threadB start];
}

#pragma mark - 賣票
/**
 1.開發比較複雜的多線程程序時,可以先再主線程把功能實現
 2.實現功能以後,可以把耗時的功能再放到子線程。
 3.再增加一個線程,建議開發的時候,線程一個一個增加
 */
- (void)saleTickets
{
    while (YES) {
        //模擬一下延時,賣一張休息1秒
       // [NSThread sleepForTimeInterval:1.0];
        //1.判斷是否有餘票
        if(self.tickets > 0){
            //2.如果有票,就賣一張
            self.tickets--;
            NSLog(@"剩餘的票數 --- %d ---- %@",self.tickets,[NSThread currentThread]);
        }else{
            //3.如果沒有就返回
            NSLog(@"沒票了");
            break;
        }
    }
}
@end

運行結果
這裏寫圖片描述

安全隱患分析

這裏寫圖片描述

安全隱患解決 - 互斥鎖

這裏寫圖片描述
示例代碼:

#import "ViewController.h"

@interface ViewController ()
/**
 總票數
 */
@property(nonatomic, assign)int tickets;
@end

@implementation ViewController

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{

    //剩餘20張票
    self.tickets = 20;

    //主線程工作
  //  [self saleTickets];

    //增加子線程,賣票
    NSThread * threadA = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
    threadA.name = @"售票員A";
    [threadA start];

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

}

#pragma mark - 賣票
/**
 1.開發比較複雜的多線程程序時,可以先再主線程把功能實現
 2.實現功能以後,可以把耗時的功能再放到子線程。
 3.再增加一個線程,建議開發的時候,線程一個一個增加
 */

/**
 加鎖,互斥鎖
 加鎖,鎖定的代碼儘量的少
 加鎖範圍內的代碼,同一時間只允許一個線程執行
 互斥鎖的參數:任何繼承自NSObject *對象
 要保證這個鎖,所有的線程都能訪問到,並且所有線程訪問的是同一個鎖對象
 */
- (void)saleTickets
{
    while (YES) {
        //模擬一下延時,賣一張休息1秒
       // [NSThread sleepForTimeInterval:1.0];
      //  NSObject * obj = [[NSObject alloc] init];
        //爲什麼沒有提示?因爲蘋果不推薦是用加鎖。因爲加鎖,性能太差。
        @synchronized(self){ //開發的時候,一般就是用self就ok
            //1.判斷是否有餘票
            if(self.tickets > 0){
                //2.如果有票,就賣一張
                self.tickets--;
                NSLog(@"剩餘的票數 --- %d ---- %@",self.tickets,[NSThread currentThread]);
            }else{
                //3.如果沒有就返回
                NSLog(@"沒票了");
                break;
            }
        }
    }
}
@end

1.互斥鎖是用格式
@synchronized(鎖對象){//需要鎖定的代碼}
注意:鎖定一份代碼只用1把鎖,用多把鎖是無效的
2.互斥鎖的優缺點
優點:能有效防止因多線程搶奪資源造成的數據安全問題
缺點:需要消耗大量的CPU資源

3.互斥鎖的是用前提:多條線程搶奪同一資源
4.相關專業術語:線程同步
線程同步的意思是:多條線程按順序執行任務
互斥鎖,就是使用了線程同步技術

原子屬性&線程安全

@property (atomic,strong)NSObject * obj;

nonatomic:非原子屬性
atomic:原子屬性 – 默認屬性
原子屬性就是針對多線程設計的。原子屬性實現單(線程)寫多(線程)讀
因爲寫的安全級別要求更高。讀的要求低一些,可以多讀幾次來保證數據的正確性

atomic情況下,只要重寫了set方法,getter也得重寫

- (void)setObj:(NSObject *)obj
{
    // 原子屬性內部使用的自旋鎖
    // 自璇鎖和互斥鎖
    // 共同點:都可以鎖定一段代碼。同一時間,只有線程能夠執行這段鎖定的代碼
    // 區別:互斥鎖,在鎖定的時候,其他線程會睡眠,等待條件滿足,再喚醒
    // 自旋鎖,在鎖定的時候,其他的線程會做死循環,一直到等待條件滿足,一旦條件滿足,立馬去執行,少了一個喚醒過程
    //互斥鎖,
    @synchronized(self){ //模擬鎖。真實情況下,使用的不是互斥鎖
        _obj = obj;
    }
}

如果同時重寫了setter和一個getter方法,”_成員變量”就不會提供,可以使用@synthesize合成指令,告訴編譯器屬性的成員變量的名稱

@synthesize obj = _obj;
- (NSObject *)obj
{
    return _obj;
}

線程安全的概念
就是在多個線程同時執行的時候,能夠保證資源信息的準確性

‘UI線程’ – 主線程
** UIKit 中絕大部分的類,都不是“線程安全”的
怎麼解決這個線程不安全的問題?

蘋果約定,所有程序的更新UI都在主線程進行,也就不會出現多個線程同時改變一個資源

在主線程更新UI,有什麼好處
1.只在主線程更新UI,就不會出現多個線程同時改變 同一個UI控件
2.主線程的優先級最好。也就意味着UI的更新優先級高。會讓用戶感覺很流暢

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