1. 程式人生 > >多執行緒之NSThread的簡單使用

多執行緒之NSThread的簡單使用

  NSThread是基於Objective-C的,相比pthread而言,它使用起來更簡單和方便。下面我們就新建一個工程,來看一下NSThread的簡單使用。

一、NSThread的基本使用

  
  NSThread有三種開啟子執行緒的方法,分別是- initWithTarget: selector: object:、- detachNewThreadSelector: toTarget: withObject:和- performSelectorInBackground: withObject:。下面就簡單的演示一下這三種方法如何使用。

  1、使用- initWithTarget: selector: object:建立子執行緒

- (void)viewDidLoad {
    [super viewDidLoad];

    // 建立子執行緒
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test:) object:@"我是引數"];

    // 給子執行緒命名
    thread.name = @"我是子執行緒";

    // 設定子執行緒的優先順序
    thread.threadPriority = 0.5;  // 取值範圍是(0.0 ~ 1.0), 預設為0.5

    // 啟動執行緒
    [thread start];
}

- (void)test:(id)argument {

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

  通過- initWithTarget: selector: object:方法來建立的子執行緒,它預設是處於暫停狀態的,必須拿到執行緒物件呼叫- start方法來開啟。另外,selector:後面傳入的方法是可以帶引數的,而這個引數就是通過object:後面的引數進行傳遞。

  再補充一點,關於執行緒的優先順序,它的取值範圍是從0.0 ~ 1.0,子執行緒建立完成以後,系統預設它的優先順序是0.5。在同時建立有多個子執行緒的場合中,優先順序高的子執行緒被系統執行的概率要高於優先順序低的子執行緒。

  2、使用- detachNewThreadSelector: toTarget: withObject:建立子執行緒

- (void)viewDidLoad {
    [super viewDidLoad];

    // 建立子執行緒
    [NSThread detachNewThreadSelector:@selector(test:) toTarget:self withObject:@"我是引數"];
}

- (void)test:(id)argument {

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

  - detachNewThreadSelector: toTarget: withObject:的使用方法跟- initWithTarget: selector: object:方法差不多,只不過,通過這種方式創建出來的子執行緒,你無法拿到它,這樣一來,你就沒辦法給它設定優先順序了。

  3、使用- performSelectorInBackground: withObject:建立子執行緒

- (void)viewDidLoad {
    [super viewDidLoad];

    // 建立子執行緒
    [self performSelectorInBackground:@selector(test:) withObject:@"我是引數"];  // 開啟一條後臺執行緒
}

- (void)test:(id)argument {

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

  通過- performSelectorInBackground: withObject:建立的是一條隱式執行緒。它的使用方法跟- detachNewThreadSelector: toTarget: withObject:差不多,通過這種方式創建出來的子執行緒,你同樣拿不到,你無法對執行緒進行更詳細的設定。

二、使用NSThread建立的執行緒的生命週期

  
  我們來看一下NSThread的生命週期。新建一個繼承自NSThread的ESThread,然後在這個類中重寫- dealloc方法,最後用我們自定義的這個ESThread來替換NSThread:

- (void)viewDidLoad {
    [super viewDidLoad];

    // 建立子執行緒
    ESThread *thread = [[ESThread alloc] initWithTarget:self selector:@selector(test:) object:nil];

    // 給子執行緒命名
    thread.name = @"我是子執行緒";

    // 設定子執行緒的優先順序
    thread.threadPriority = 0.5;  // 取值範圍是(0.0 ~ 1.0), 預設為0.5

    // 啟動執行緒
    [thread start];
}

- (void)test:(id)argument {

    for (int i; i < 1000; i++) {

        NSLog(@"%d, %@", i, [NSThread currentThread]);
    }
}

  執行程式,看一下控制檯列印資訊:

1240
NSThread的生命週期.png

  從控制檯打印出來的訊息看,NSThread是在子執行緒中的程式碼執行完畢以後才會被銷燬的。

三、執行緒的狀態

  
  執行緒是分狀態的,這個在作業系統的基礎知識中有學過。通常情況下,執行緒的執行分為三種狀態:執行(執行)狀態、就緒狀態和阻塞狀態。下面我們就分別來演示一下這三種狀態。

  要想設定NSThread子執行緒的狀態,首先就必須要拿到它。從上面的講解可知,只有通過- initWithTarget: selector: object:方法建立的子執行緒才能拿得到:

- (void)viewDidLoad {
    [super viewDidLoad];

    // 建立子執行緒
    ESThread *thread = [[ESThread alloc] initWithTarget:self selector:@selector(test:) object:nil];

    // 給子執行緒命名
    thread.name = @"我是子執行緒";

    // 設定子執行緒的優先順序
    thread.threadPriority = 0.5;  // 取值範圍是(0.0 ~ 1.0), 預設為0.5

    // 啟動執行緒(此時,執行緒處於就緒狀態或者執行狀態)
    [thread start];
}

// MARK:- 阻塞執行緒
- (void)test:(id)argument {

    for (int i; i < 10; i++) {

        NSLog(@"%d, %@", i, [NSThread currentThread]);
    }

    // 阻塞執行緒
    [NSThread sleepForTimeInterval:3.0];  // 讓執行緒進入阻塞狀態以後,它是不能執行任何操作的

    NSLog(@"%s, Line = %d", __FUNCTION__, __LINE__);

    // 阻塞執行緒
    [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:3.0]];
}

  建立完NSThread子執行緒物件以後,呼叫- start方法時,子執行緒就處於就緒或者執行狀態。在子執行緒執行程式碼的過程中,通過呼叫- sleepForTimeInterval:方法或者- sleepUntilDate:方法就可以讓它處於阻塞狀態。

  如果要想讓子執行緒在執行程式碼的中途退出,只需要呼叫- exit方法:

- (void)viewDidLoad {
    [super viewDidLoad];

    // 建立子執行緒
    ESThread *thread = [[ESThread alloc] initWithTarget:self selector:@selector(test) object:nil];

    // 給子執行緒命名
    thread.name = @"我是子執行緒";

    // 設定子執行緒的優先順序
    thread.threadPriority = 0.5;  // 取值範圍是(0.0 ~ 1.0), 預設為0.5

    // 啟動執行緒(此時,執行緒處於就緒狀態或者執行狀態)
    [thread start];
}

// MARK:- 退出執行緒
- (void)test {

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

        NSLog(@"%d", i);

        if (i == 50) {

            // 退出執行緒
            [NSThread exit];  // 當i == 50 時,退出當前執行緒,有點類似於for迴圈裡面的break,不過含義還是不一樣的
        }
    }
}

四、執行緒安全

  
  在iOS開發中,我們可以建立多個子執行緒,並且每個子執行緒都可以訪問同一塊資源,這樣一來,就很容易引發資料錯亂,甚至是資料安全問題:

1240
多個子執行緒在同時訪問同一塊資源時可能存在的安全問題.png

  如上圖所示,Thread A和Thread B都可以訪問Integer,並且在訪問過程中,它們都對Integer進行了加1操作,最終的結果應該是19。但是,可能在Thread A訪問Integer並且將最終的資料18重新寫入之前,恰好Thread B又對Integer進行了訪問,它獲得的資料還是原來的17(而不是Thread A重寫後的18),此時Thread B對Integer進行加1操作以後就會得到錯誤的資料18。為了避免這種錯誤的發生,我們需要引入互斥鎖技術:

1240
利用互斥鎖來解決安全隱患問題.png

  互斥鎖技術可以保證Thread A對Integer獨佔,也就是在Thread A對Integer訪問結束之前,其它子執行緒不可以訪問Integer。當Thread A將新的資料18重新寫入Integer以後,它會對Integer進行解鎖,此時,其它子執行緒才可以訪問Integer。同樣,在Thread B對Integer進行訪問期間,其它子執行緒也不可以訪問Integer,除非Thread B對Integer的訪問結束。下面,我們就通過程式碼來演示一下,如何在程式碼中使用互斥鎖。

  在日常生活中,我們都有過買票的經歷。我們都知道,同一趟列車,它的售票視窗可能有多個,但是每趟列車的總票數都是固定的。這個就好比多個子執行緒共享同一塊資源一樣。下面,我們就通過程式碼來模擬一下這種場景。首先,在類擴充套件中宣告3個NSThread型別的屬性,用於表示不同的售票員,宣告一個NSInteger型別的屬性,用於記錄列車中的總票數:

@interface ViewController ()

/** 售票員A */
@property (strong, nonatomic) NSThread *threadA;

/** 售票員B */
@property (strong, nonatomic) NSThread *threadB;

/** 售票員C */
@property (strong, nonatomic) NSThread *threadC;

/** 總票數 */
@property (assign, nonatomic) NSInteger totalTickets;

@end

  在- viewDidLoad方法中建立3個NSThread物件,用來表示3個不同的售票員。為了方便區別和描述,我們可以通過NSThread的name屬性來給這3個售票員命名。另外,為了方便在票賣完以後順利退出,我們在模擬售票的方法中使用了while迴圈:

- (void)viewDidLoad {
    [super viewDidLoad];

    // 建立子執行緒
    self.threadA = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.threadB = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.threadC = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];

    // 設定子執行緒名稱
    self.threadA.name = @"售票員A";
    self.threadB.name = @"售票員B";
    self.threadC.name = @"售票員C";

    // 啟動執行緒(此時,執行緒處於就緒狀態或者執行狀態)
    [self.threadA start];
    [self.threadB start];
    [self.threadC start];

    // 總票數
    self.totalTickets = 100;
}

// MARK:- 模擬售票
- (void)saleTicket {

    while (1) {

        NSInteger tickets = self.totalTickets;

        if (tickets > 0) {

            self.totalTickets = tickets - 1;

            NSLog(@"%@賣出了一張票,還剩%zd張票。", [NSThread currentThread].name, self.totalTickets);

        } else {

            NSLog(@"賣完了,沒有餘票了。");

            break;  // 退出迴圈
        }
    }
}

  執行程式,看一下控制檯打印出來的訊息:

1240
在沒有加互斥鎖的情況下,同一張票被賣了3次.png

  在上面的程式碼中,我們沒有對子執行緒加互斥鎖。也就是說,子執行緒threadA、threadB和threadC可以根據CPU的排程情況來自由訪問總票數。從控制檯的列印結果來看,我們可以很清楚的發現兩個問題:1、餘票資訊比較混亂(第一次顯示餘票還有97張、第二次顯示餘票還有98張……這個顯然不符合邏輯);2、這個問題也是最嚴重的,第85張車票分別被三個不同的售票員賣出了3次,在現實生活中,這種情況是會打架的!

  上面的結果很好的說明了,在多執行緒程式設計環境中,如果不對子執行緒加互斥鎖,是很容易造成資料錯亂,甚至是嚴重影響資料安全的。下面,我們就來給個子執行緒加一個互斥鎖。加互斥鎖的方式非常簡單,只需要將子執行緒執行任務的程式碼寫在關鍵字@synchronized(鎖物件) { // 需要鎖定的程式碼 }裡面就可以了:

// MARK:- 模擬售票
- (void)saleTicket {

    while (1) {

        // 加互斥鎖(互斥鎖必須是全域性唯一)
        @synchronized (self) {

            NSInteger tickets = self.totalTickets;

            if (tickets > 0) {

                self.totalTickets = tickets - 1;

                NSLog(@"%@賣出了一張票,還剩%zd張票。", [NSThread currentThread].name, self.totalTickets);

            } else {

                NSLog(@"賣完了,沒有餘票了。");

                break;  // 退出迴圈
            }
        }
    }
}

  在家互斥鎖的時候一定要注意:1、互斥鎖必須加在合適的位置,要保證每個子執行緒都能順利執行相應的打碼;2、所物件必須具有全域性唯一性,通常情況下,我們用self來做所物件。執行程式,看一下控制檯列印的情況:

1240
加了互斥鎖以後的售票情況.png

  從執行的結果來看,在加了互斥鎖以後,資料錯亂的問題得到了很好的控制,並且一票多賣的情況也得到了有效控制。下面,我們來簡單總結一下加互斥鎖的注意點:

1、加鎖的位置是有講究的,必須保證每一個子執行緒都能有效執行相應的程式碼;
2、加鎖的前提是,程式碼中存在多執行緒共享同一塊資源;
3、加鎖會耗費額外的CPU效能,不能隨便亂加;
5、鎖定一份程式碼只需一把鎖就夠了(多加是無用功);
4、沒加鎖時,執行緒預設是非同步併發的執行任務。加鎖以後,執行緒是同步執行,即按順序執行任務。

  加鎖的好處是,可以防止多個執行緒之間搶奪資源,從而導致資料錯亂和不安全,缺點是需要消耗大量的CPU資源。所以,是否需要加鎖要視具體情況而定。

五、atomic還是nonatomic

  
  在之前的開發過程中,我們在宣告一個屬性時,經常會碰到要求寫nonatomic的情況。那麼,為什麼要這樣寫呢?nonatomic和atomic是相對的,其中,atomic表示原子屬性,它會給setter方法加鎖,如果不寫的話,預設就是atomic;而nonatomic是非原子屬性,即不會為setter方法加鎖。

  那麼,什麼時候用atomic,什麼時候用nonatomic呢?atomic可以保證執行緒安全,但是需要消耗大量的資源;nonatomic表示非執行緒安全,適合記憶體小的移動裝置。在iOS開發過程中,為了避免多執行緒搶奪同一塊資源,建議將所有的屬性都宣告為nonatomic,將加鎖、資源搶奪的業務邏輯交給伺服器端處理,以減小移動客戶端的壓力。

  以上就是NSThread的簡單使用,後續會繼續整理多執行緒的相關知識。詳細程式碼參見NSThreadExercise