iOS多線程實現(xiàn)方案之 -- NSThread

書接上回, 上次談到iOS 多線程知識點總結(jié)之: 進程和線程, 接著就是 多線程實現(xiàn)方案里面的 NSThread 了.

NSThread 多線程創(chuàng)建方法

方法一: alloc init, 需要手動啟動線程

    // 1. 創(chuàng)建線程
  NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test:) object:nil];
 
  // 2. 啟動線程
  [thread start];
  • 通過 NSThread 調(diào)用的方法是必須只傳遞一個參數(shù), 而且不一定要有返回值,在文檔中是這樣解釋的
selector 
The selector for the message to send to target. This selector must take only one argument and must not have a return value.

調(diào)用方法實現(xiàn):

- (void)test:(NSString *)string {
  NSLog(@"test - %@ - %@", [NSThread currentThread], string);
}

通過打印結(jié)果知此時已經(jīng)創(chuàng)建了一個子線程(number = 2)

test - <NSThread: 0x7fc56070cbe0>{number = 2, name = (null)} - (null)

方法二: 分離子線程, 會自動啟動線程

[NSThread detachNewThreadSelector:@selector(test:) toTarget:self withObject:@"分離子線程"];

打印結(jié)果:

test - <NSThread: 0x7ff482c12b30>{number = 2, name = (null)} - 分離子線程

方法三: 開啟一條后臺線程, 也會自動啟動線程

[self performSelectorInBackground:@selector(test:) withObject:@"后臺線程"];

打印結(jié)果:

test - <NSThread: 0x7f983960fc50>{number = 2, name = (null)} - 后臺線程

三種方法對比

方法一

  • 優(yōu)點: 可以拿到線程對象, 并設(shè)置相關(guān)屬性
  • 缺點: 代碼量相對多一點, 需要手動啟動線程

方法二和方法三

  • 優(yōu)點: 創(chuàng)建線程簡單快捷
  • 缺點: 無法拿到線程對象, 無法設(shè)置相關(guān)屬性

NSThread 常用屬性設(shè)置

NSThread 里有很多的方法和屬性, 常用的有下圖中的兩個:



當(dāng)通過NSThread創(chuàng)建了不止一條線程的時候,就能用到這些了.

name (線程名字)

例如我們創(chuàng)建三條子線程,并設(shè)置子線程的name 屬性

 // 創(chuàng)建線程A
  NSThread *threadA = [[NSThread alloc] initWithTarget:self selector:@selector(test:) object:@"子線程"];
  threadA.name = @"子線程A";
  [threadA start];
 
  // 創(chuàng)建線程B
  NSThread *threadB = [[NSThread alloc] initWithTarget:self selector:@selector(test:) object:@"子線程"];
  threadB.name = @"子線程B";
  [threadB start];
 
  // 創(chuàng)建線程C
  NSThread *threadC = [[NSThread alloc] initWithTarget:self selector:@selector(test:) object:@"子線程"];
  threadC.name = @"子線程C";
  [threadC start];

這樣在想知道是哪條線程的時候,只需要打印鮮明名字就可以了[NSThread currentThread].name,方便查看, 打印結(jié)果如下:

2016-07-27 14:51:20.520 多線程[75816:852836] test - 子線程A
2016-07-27 14:51:20.520 多線程[75816:852837] test - 子線程B
2016-07-27 14:51:20.520 多線程[75816:852838] test - 子線程C

threadPriority(線程優(yōu)先級)

threadPriority 的取值范圍是 0.0 -- 1.0, 默認(rèn)是0.5. 數(shù)值越大, 優(yōu)先級越高 ,通過代碼來演示下
這里給三個子線程設(shè)置了不同的優(yōu)先級, 線程A < 線程C < 線程B

 threadA.threadPriority = 0.1;
  threadB.threadPriority = 1.0;
  threadC.threadPriority = 0.5;

讓三個線程都執(zhí)行100次, 打印一下各個線程的運行次數(shù)和線程名字:

for (int i = 0; i < 100; ++i) {
    NSLog(@"%d - %@", i + 1, [NSThread currentThread].name);
  }

執(zhí)行結(jié)果如下:


同一時間, 三個線程的執(zhí)行次數(shù)有很大差別,這是因為 線程B 的優(yōu)先級最大,被執(zhí)行的概率也最大, 執(zhí)行次數(shù)自然也最多, 線程A 的優(yōu)先級最小, 被執(zhí)行的概率最小, 執(zhí)行的次數(shù)自然也最小.

NSThread 線程的生命周期

  • 只有當(dāng)需要執(zhí)行的任務(wù)全部執(zhí)行完畢之后才會被釋放掉.

這個證明起來也很簡單, 自定義一個 Thread 類繼承字 NSThread , 里面重寫一下 dealloc 方法, 打印一下方法名即可. 用自定義 Thread 創(chuàng)建一個線程, 會發(fā)現(xiàn)任務(wù)指向完畢之后, dealloc 方法被調(diào)用.

線程的狀態(tài)

做了一張圖


控制線程狀態(tài)

  • 啟動線程
- (void)start;

線程進入就緒狀態(tài), 當(dāng)線程執(zhí)行完畢,進入死亡狀態(tài)

  • 阻塞(暫停)線程
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

線程進入阻塞狀態(tài)
代碼演示:

  // 阻塞線程
  //[NSThread sleepForTimeInterval:3.0];
 
  [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:3.0]];

上面兩種方法的執(zhí)行效果是相同的, 開始和結(jié)束的之間線程阻塞或者說休眠了3秒

2016-07-27 17:10:51.223 控制線程狀態(tài)[84187:952380] test - <NSThread: 0x7fae6249f250>{number = 2, name = (null)}
2016-07-27 17:10:54.231 控制線程狀態(tài)[84187:952380] ----end----
  • 強制停止線程
+ (void)exit;

線程進入死亡狀態(tài)
代碼演示:
讓任務(wù)執(zhí)行100次, 看下效果

- (void)test {
 
  for (int i = 0; i < 100; ++i) {
    NSLog(@"%d - %@", i, [NSThread currentThread]);
   
  }
 
  NSLog(@"----end----");

}

執(zhí)行完畢之后, 自動結(jié)束


讓任務(wù)在執(zhí)行過程中強制停止

- (void)test {
 
  for (int i = 0; i < 100; ++i) {
    NSLog(@"%d - %@", i, [NSThread currentThread]);
   
    if (i == 10) {
      [NSThread exit];
    }
  }
}

當(dāng)達到停止條件時, 線程就強制退出了


線程一旦進入到死亡狀態(tài), 線程也就停止了, 就不能再次啟動任務(wù).

線程安全

多線程的安全隱患

  • 資源共享
  1. 一塊資源可能會被多個線程共享, 也就是多個線程可能會訪問同一塊資源
  2. 比如多個線程訪問同一個對象, 同一個變量, 同一個文件
  • 當(dāng)多個線程訪問同一塊資源時, 很容易引發(fā)數(shù)據(jù)錯亂和數(shù)據(jù)安全問題

買火車票的例子
舉這個例子, 是為了模仿我們實際 iOS 開發(fā)中可能會用到多線程下載網(wǎng)絡(luò)數(shù)據(jù)的情況, 因為數(shù)據(jù)量可能會很大, 看是否會出現(xiàn)問題.

  // 火車票總數(shù)
  self.ticketCount = 100;
 
  // 三個售票員
  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];

買票方法:

- (void)saleTicket {
 
 
  while (1) {
    NSInteger count = self.ticketCount;
    if (count > 0) {
     
      for (int i = 0; i < 1000000; ++i) {
        // 只是耗時間, 沒有其他用
      }
     
      self.ticketCount = count - 1;
      NSLog(@"%@賣出一張票,還剩- %zd", [NSThread currentThread].name, self.ticketCount);
    } else {
      NSLog(@"票賣完了");
      break;
    }
   
  }
}

因為簡單的買票操作執(zhí)行非匙Γ快,無法看出效果,就在其中加了一段耗費時間的代碼,這時候看到的結(jié)果是這樣的


顯然, 是有問題的, 多次出現(xiàn)賣出同一張票的情況. 也就是造成了數(shù)據(jù)混亂. 那該怎么解決呢? 這個時候就要用到 -- 互斥鎖.

- (void)saleTicket {
  while (1) {
    // 加 互斥鎖, 全局唯一, self, 代表鎖對象
    @synchronized(self) {
      NSInteger count = self.ticketCount;
      if (count > 0) {
       
        for (int i = 0; i < 1000000; ++i) {
          // 只是耗時間, 沒有其他用
        }
       
        self.ticketCount = count - 1;
        NSLog(@"%@賣出一張票,還剩- %zd", [NSThread currentThread].name, self.ticketCount);
      } else {
        NSLog(@"票賣完了");
        break;
      }
    }
  }
}

這樣就可以了,運行看結(jié)果:


不會出現(xiàn)數(shù)據(jù)混亂的情況了, 也達到了三個線程賣票的功能.

加鎖的注意點

  1. 必須是全局唯一的.
  2. 加鎖的位置
  3. 加鎖的前提條件(多條線程搶奪同一塊資源)
    加鎖的優(yōu)點
  • 能有效的防止因為多線程搶奪資源造成的數(shù)據(jù)安全問題

加鎖的缺點

  • 會耗費一些額外的 CPU 資源
  • 造成線程同步(多條線程在同一條線上執(zhí)行,而且是按順序的執(zhí)行)

原子和非原子 屬性

OC 在定義屬性時有 nonatomic atomic

  • atomic: 原子性, 為 setter 方法加鎖(默認(rèn)是 atomic)
  • nonatomic: 非原子性, 不會為 setter 方法加鎖

nonatomic 和**atomic **對比

  • atomic: 線程安全, 需要消耗大量的資源
  • nonatomic: 非線程安全, 適合內(nèi)存小的移動設(shè)備

iOS 開發(fā)建議

  • 所有屬性都聲明為 nonatomic
  • 盡量避免多線程搶奪同一塊資源
  • 盡量將加鎖, 資源搶奪的業(yè)務(wù)邏輯交給服務(wù)器端處理, 減小移動端的壓力.

線程間通信

什么叫線程間通信
在一個進程中, 線程往往不是孤立存在的, 多個線程之間需要經(jīng)常的進行通信

線程間通信的體現(xiàn)

  • 一個線程傳遞數(shù)據(jù)給另一個線程
  • 在一個線程中執(zhí)行完畢特定任務(wù)后, 轉(zhuǎn)到另一個線程繼續(xù)執(zhí)行任務(wù)

線程鍵通信常用的方法

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;

例如, 給一個在 view 上的 UIImageView 添加網(wǎng)絡(luò)圖片的操作

一般情況下,我們是直接給 imageView 設(shè)置圖片

  // 網(wǎng)絡(luò)圖片 URL
  NSURL *url = [NSURL URLWithString:@"http://pic1.win4000.com/wallpaper/2/4fcec0bf0fb7f.jpg"];
 
  // 根據(jù) URL 下載圖片到本地, 保存為二進制文件
  NSData *data = [NSData dataWithContentsOfURL:url];
 
  // 轉(zhuǎn)換圖片格式
  UIImage *image = [UIImage imageWithData:data];
 
  // 設(shè)置圖片
  self.imageView.image = image;

但是如果圖片比較大, 下載所需要的事件比較長, 這個時候就會造成主線程的阻塞, 影響用戶體驗.我們可以開啟一個子線程去加載圖片, 下載完畢之后再回到主線程顯示圖片, 這個就是線程之間的通信.

[NSThread detachNewThreadSelector:@selector(download) toTarget:self withObject:nil];

下載方法的實現(xiàn):

- (void)download {
 
  // 網(wǎng)絡(luò)圖片 URL
  NSURL *url = [NSURL URLWithString:@"http://pic1.win4000.com/wallpaper/2/4fcec0bf0fb7f.jpg"];
 
  // 根據(jù) URL 下載圖片到本地, 保存為二進制文件
  NSData *data = [NSData dataWithContentsOfURL:url];
 
  // 轉(zhuǎn)換圖片格式
  UIImage *image = [UIImage imageWithData:data];
 
  // 查看當(dāng)前線程
  NSLog(@"download - %@", [NSThread currentThread]);
 
  // 在子線程下載后要回到主線程設(shè)置 UI
  /*
  第一個參數(shù): 回到主線程之后要調(diào)用哪個方法
  第二個參數(shù): 調(diào)用方法要傳遞的參數(shù)
  第三個參數(shù): 是否需要等待該方法執(zhí)行完畢再往下執(zhí)行
  */
  [self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];
}

設(shè)置并顯示圖片方法實現(xiàn):

- (void)showImage:(UIImage *)image {
  // 設(shè)置圖片
  self.imageView.image = image;
  // 查看當(dāng)前線程
  NSLog(@"showImage - %@", [NSThread currentThread]);
}

控制臺打印結(jié)果是:



可以看到,下載圖片是在子線程, 設(shè)置圖片是回到了主線程操作的
關(guān)于回到主線程設(shè)置圖片, 除了上面提到的方法,還是使用

[self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];

這個也是需要調(diào)用 showImage 方法,效果一樣.

也可以直接使用self.imageView調(diào)用performSelectorOnMainThread: withObject: waitUntilDone:方法, 這樣不需要再去生命一個showImage方法, 就可以回到主線程設(shè)置圖片.

[self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];

也能達到我們想要的效果.
這也是線程間通信最常用的情景.

  • 關(guān)于 NSThread 多線程的總結(jié)就到這里, 下篇將對 GCD 進行總結(jié)學(xué)習(xí)

相關(guān)文章:
iOS 多線程知識點總結(jié)之 -- 進程和線程
iOS多線程實現(xiàn)方案之--GCD

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市剧蹂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌酬滤,老刑警劉巖隧膏,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異誉己,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門宿饱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來体捏,“玉大人腐泻,你說我怎么就攤上這事纵搁。” “怎么了愧驱?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵慰技,是天一觀的道長。 經(jīng)常有香客問我组砚,道長吻商,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任糟红,我火速辦了婚禮艾帐,結(jié)果婚禮上乌叶,老公的妹妹穿的比我還像新娘。我一直安慰自己柒爸,他們只是感情好准浴,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著捎稚,像睡著了一般乐横。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上今野,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天晰奖,我揣著相機與錄音,去河邊找鬼腥泥。 笑死,一個胖子當(dāng)著我的面吹牛啃匿,可吹牛的內(nèi)容都是我干的蛔外。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼溯乒,長吁一口氣:“原來是場噩夢啊……” “哼夹厌!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起裆悄,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤矛纹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后光稼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體或南,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年艾君,在試婚紗的時候發(fā)現(xiàn)自己被綠了采够。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡冰垄,死狀恐怖蹬癌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情虹茶,我是刑警寧澤逝薪,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站蝴罪,受9級特大地震影響董济,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜洲炊,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一感局、第九天 我趴在偏房一處隱蔽的房頂上張望尼啡。 院中可真熱鬧,春花似錦询微、人聲如沸崖瞭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽书聚。三九已至,卻和暖如春藻雌,著一層夾襖步出監(jiān)牢的瞬間雌续,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工胯杭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留驯杜,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓做个,卻偏偏與公主長得像鸽心,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子居暖,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 一: 多線程的基本概念1.同步與異步的概念1.1 同步 必須等待當(dāng)前語句執(zhí)行完畢顽频,才可以執(zhí)行下一個語句。1.2...
    程序_猿閱讀 3,352評論 1 16
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,089評論 1 32
  • 在2013年的時候每個月都會寫總結(jié)太闺,2014年沒有這么做糯景,主要是因為和工作相關(guān)的內(nèi)容不能公開,而其他方面又沒有量化...
    清風(fēng)捷影閱讀 227評論 0 1
  • 2017微信公開課省骂,由于朋友提前已經(jīng)購票沒有時間蟀淮,臨時由我代為參加,因此钞澳,昨天很偶然的機會灭贷,在現(xiàn)場有幸聆聽了張小龍...
    樂趣先生閱讀 297評論 0 0
  • 其實泰迪熊對約翰來說是童年,泰迪和約翰所做的很多事情略贮,打雷時約翰和泰迪一起唱歌甚疟,這是他們從小的習(xí)慣。泰迪從...
    會讀書的貓cat閱讀 861評論 0 0