上一節中,我們詳細的學習了和多線程有關的概念,像進程、線程、多線程、CPU內核、并發、并行、串行、隊列、同步、異步等概念。這一節中,我們將用代碼來實現多線程。
如果對多線程概念不太清楚的,可以參考上一節內容,鏈接如下:
詳解多線程(概念篇——進程、線程以及多線程原理)
說明:源碼親測,拒絕搬磚,源碼可下載。
源碼地址:https://github.com/weiman152/Multithreading.git
在iOS中,多線程的實現方法有多種,有OC的也有C語言的,有常用的,也有不常用的。本節中,我們就先探究NSThread這個OC的類對于實現多線程是如何進行的。
多線程的實現方法
- NSThread(OC)
- GCD(C語言)
- NSOperation(OC)
- C語言的pthread(C語言)
- 其他實現多線程方法
1.NSThread(OC)
NSThread是蘋果提供的面向對象的操作線程的方法。簡單方便,可以直接操作線程對象。
我們查看一下NSThread的API,發現內容并不多,屬性和方法不是特別多,我們一個個來看看(根據字面意思理解的)。
注:不想看的可以跳過喲,直接到下面看代碼。
先看看類的聲明:
NSThread繼承自NSObject。
-
currentThread
聲明的第一個屬性,currentThread,當前上下文所在的線程。這也是我們非常常用的一個屬性。
image.png -
類方法創建線程
image.png -
isMultiThreaded 判斷是否有多個線程
image.png threadDictionary 線程字典
每個線程都維護了一個鍵-值的字典,它可以在線程里面的任何地方被訪問。你可以使用該字典來保存一些信息,這些信息在整個線程的執行過程中都保持不變。
-
讓當前線程阻塞一段時間
image.png -
退出線程
image.png -
線程的優先級
image.png -
這幾個字面上看不出來干嘛的
image.png -
線程的名字
image.png -
棧的大小
image.png -
是否是主線程和獲取主線程
image.png -
初始化線程
image.png -
線程狀態(正在執行、結束、被取消)
image.png -
線程主函數 在線程中執行的函數 都要在-main函數中調用,自定義線程中重寫-main方法
image.png -
線程有關的通知
image.png
上面的API都是我根據字面意思理解的,不一定正確,下面我們就用代碼來試驗一下NSThread實現多線程的過程吧。
1》類方法創建子線程,并在子線程中執行想要的操作
//類方法創建線程
- (IBAction)createThreadC:(id)sender {
NSLog(@"------------detachNewThreadWithBlock-------");
//block創建,并在子線程進行想要的操作
[NSThread detachNewThreadWithBlock:^{
NSLog(@"--block--%@",[NSThread currentThread]);
}];
NSLog(@"------------detachNewThreadSelector-------");
//在子線程中執行某方法
[NSThread detachNewThreadSelector:@selector(printHi) toTarget:self withObject:nil];
}
-(void)printHi {
NSLog(@"---printHi---");
NSLog(@"Hi, 我要在子線程中執行");
NSLog(@"--Sel--%@",[NSThread currentThread]);
}
打印結果:
分析:
createThreadC在主線程中,因為開辟子線程需要耗費時間,所以會先打印主線程的:
------------detachNewThreadWithBlock-------
------------detachNewThreadSelector-------
然后在打印子線程的內容。因為子線程是并發的,誰先執行完并不確定,所以先打印哪個子線程的內容也是不確定的。
注意:如果主線程和子線程都有一個for循環,循環很多次,那么主線程和子線程中的for循環打印很可能是交叉進行的。
我們再次運行,看看結果是否與上次一樣呢。
與上次結果不太一樣喲,與我們上面的分析是一致的。
2》判斷當前是否開啟了多個線程 isMultiThreaded
我們分別在子線程和主線程中使用isMultiThreaded,看看結果:
子線程中:
打印結果:
結果是 YES,就是開啟了多線程。我們把開啟的子線程注釋掉再看看。
看看打印結果:
結果也是1,也是YES,這是為什么呢?
多方搜索,也沒有找到答案。
我想,因為我是在一個應用程序中,應用程序默認開啟主線程,是不是應用程序默認還開啟了別的線程?我們看一下系統的CPU占用情況:
上圖是程序程序剛啟動的時候CPU的使用情況,我們并沒有開啟線程,但是系統卻開啟了5個線程,并且線程2是有使用的,所以我們打印是否開啟了多線程的時候,會是YES。
我們靜置了一會兒,再看看系統的線程情況:
現在就剩下線程1和線程8了。
我們自己開啟了線程之后,看看CPU中線程開啟情況:
在圖中我們找到了我們自己創建的線程一,編號為12 。
現在,我們明白了,為什么在應用程序中打印 [NSThread isMultiThreaded]結果為什么一直是YES了。
那么,我們新建一個控制臺項目,打印看看:
果然,打印是0,也就是NO,認為沒有多個線程。
3》是否是主線程,打印主線程
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"000 %d", [NSThread isMultiThreaded]);
NSLog(@"isMainThread: %d", [NSThread isMainThread]);
NSLog(@"currentThread: %@", [NSThread currentThread]);
}
4》對象方法創建子線程
對象方法初始化子線程,我們可以得到一個子線程對象,然后使用這個子線程對象。如果我們要開啟子線程,一定要調用start方法,不然線程是不會開啟的。
- (IBAction)createThreadO:(id)sender {
NSLog(@"新建多線程");
//對象方法創建多線程 一
self.thread1 = [[NSThread alloc] initWithBlock:^{
NSLog(@"thread1: %@",[NSThread currentThread]);
for (int i=0; i<100; i++) {
NSLog(@"i= %d", i);
[NSThread sleepForTimeInterval:1];
}
}];
self.thread1.name = @"線程一";
//對象方法創建多線程 二
NSThread * thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(hello:) object:@"小明"];
thread2.name = @"線程二";
[thread2 start];
}
-(void)hello:(NSString *)name {
NSLog(@"你好!%@",name);
NSLog(@"當前線程是: %@",[NSThread currentThread]);
}
看看打印結果:
因為線程一沒有開啟,只是初始化了,所以不會執行線程一的內容。
使用對象方法創建子線程,要想讓線程執行,必須調用start方法開啟子線程。
5》取消線程——cancel,并不能取消一個子線程
我們在NSThread中找到一個方法叫做cancel,看起來像是可以取消一個線程,我們來試一試。
- (IBAction)createThreadO:(id)sender {
NSLog(@"新建多線程");
//對象方法創建多線程 一
self.thread1 = [[NSThread alloc] initWithBlock:^{
NSLog(@"thread1: %@",[NSThread currentThread]);
for (int i=0; i<10000; i++) {
NSLog(@"i= %d", i);
}
}];
self.thread1.name = @"線程一";
//對象方法創建多線程 二
NSThread * thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(hello:) object:@"小明"];
thread2.name = @"線程二";
[thread2 start];
}
- (IBAction)threadStart:(id)sender {
NSLog(@"thread1開始");
[self.thread1 start];
}
- (IBAction)threadCancel:(id)sender {
NSLog(@"thread1 取消");
[self printState:self.thread1];
[self.thread1 cancel];
NSLog(@"cancel 后:");
[self printState:self.thread1];
if([self.thread1 isCancelled]==YES){
NSLog(@"thread1 被取消了,開始銷毀它");
[NSThread exit];
self.thread1 = nil;
}
}
執行后發現,根本不能取消,線程還是在執行完循環之后才停止的。我們看看該方法的官方文檔:
Instance Method
cancel
Changes the cancelled state of the receiver to indicate that it should exit.
意思是說,這個方法只是把cancelled的屬性置為YES,并不能真正的取消當前線程。
看看打印結果:
我們要想取消一個子線程,只是使用cancel是做不到的,cancel只是把屬性isCancelled設置為YES,并不能真正的取消一個子線程。我們可以配合isCancelled屬性,使用類方法exit,取消一個子線程。
注意:上面我們的案例中,由于使用的是按鈕取消,按鈕方法是在主線程中進行的,在主線程中執行exit是不會有效果的。所以,在這種狀態下,我們的線程一是不能被取消的。要想取消線程一,我們需要在子線程內部進行。
例如:
//再次測試取消線程
- (IBAction)cancelThreadAgain:(id)sender {
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
}
- (void)run {
NSLog(@"當前線程%@", [NSThread currentThread]);
for (int i = 0 ; i < 100; i++) {
NSLog(@"i = %d", i);
if (i == 20) {
//取消線程
[[NSThread currentThread] cancel];
NSLog(@"取消線程%@", [NSThread currentThread]);
}
if ([[NSThread currentThread] isCancelled]) {
NSLog(@"結束線程%@", [NSThread currentThread]);
//結束線程
[NSThread exit];
NSLog(@"這行代碼不會打印的");
}
}
}
看看結果:
只打印了前20個數字,說明線程取消了。
網上有人說,如果在線程中使用了sleep方法,就不能取消線程了,我們試一試:
看看結果:
跟之前一樣,還是可以取消的。說明sleep是不會影響線程的取消退出操作的。
6》線程狀態
使用NSThread創建的子線程,我們可以得到線程的三個狀態:是否結束、是否取消、是否正在執行
-(void)printState:(NSThread *)thread{
NSLog(@"狀態,isCancelled: %d",[thread isCancelled]);
NSLog(@"狀態,isFinished: %d",[thread isFinished]);
NSLog(@"狀態,isExecuting: %d",[thread isExecuting]);
}
7》讓線程阻塞一段時間
有的時候,我們希望線程等待一會兒再執行,這個時候,我們可以使用
+(void)sleepUntilDate:(NSDate *)date;
+(void)sleepForTimeInterval:(NSTimeInterval)ti;
這兩個方法,讓線程阻塞一會兒在執行。觀察后發現,這兩個方法也是類方法,那么我們調用的時候,會阻塞當前線程,還是把所有線程都阻塞呢?我們試一試吧。
- (IBAction)sleepAction:(id)sender {
NSThread * threadA = [[NSThread alloc] initWithBlock:^{
//threadA 阻塞2秒后執行
[NSThread sleepForTimeInterval:2.0];
for (int i=0; i<10; i++) {
NSLog(@"%@, i = %d", [NSThread currentThread].name, i);
}
NSLog(@"threadA 結束了");
}];
threadA.name = @"線程A";
[threadA start];
NSThread * threadB = [[NSThread alloc] initWithBlock:^{
for (int i=0; i<10; i++) {
NSLog(@"%@, i = %d", [NSThread currentThread].name, i);
}
NSLog(@"threadB 結束了");
}];
threadB.name = @"線程B";
[threadB start];
}
打印結果:
先打印了線程B的內容,說明sleep方法并不會阻塞所有的線程,只會阻塞當前的線程。
另一個方法傳入一個日期類型,也就是等到某一個特殊日期的時候才會執行。
//讓這個線程等到某個日期的時候在執行,這里給的是當前時間的2秒后執行,只是為了測試。
[NSThread detachNewThreadWithBlock:^{
NSDate * date = [NSDate dateWithTimeIntervalSinceNow:2];
[NSThread sleepUntilDate:date];
NSLog(@"終于等到這一天啦!我執行啦!");
}];
結果:
8》案例:售票問題
描述:
假如我們有三個售票員ABC同時都在售票,每售出一張票,就從庫存中減去一張,直到所有的票售完。
我們用代碼去模擬這個過程。
分析一下:三個售票員我們用三個線程模擬,設置總票數為100,每個線程都執行一個總票數減1的操作,直到總票數為0 。
實現代碼如下:
//售票
- (IBAction)sellTickets:(id)sender {
self.totalTickets = 100;
NSThread * t1 = [[NSThread alloc] initWithTarget:self selector:@selector(sell) object:nil];
t1.name = @"售票員:王美美";
[t1 start];
NSThread * t2 = [[NSThread alloc] initWithTarget:self selector:@selector(sell) object:nil];
t2.name = @"售票員:李帥帥";
[t2 start];
NSThread * t3 = [[NSThread alloc] initWithTarget:self selector:@selector(sell) object:nil];
t3.name = @"售票員:張靚靚";
[t3 start];
}
- (void)sell{
NSLog(@"開始售票,當前余票:%d", self.totalTickets);
while (self.totalTickets > 0) {
[NSThread sleepForTimeInterval:1.0];
self.totalTickets--;
NSLog(@"%@ 賣出一張,余票:%d", [NSThread currentThread].name, self.totalTickets);
}
}
看看打印結果:
我們發現,結果并不像我們預期的那樣啊,輸出有點錯亂,而且居然出現了-1,這實在是不能容忍的。
為什么會出現這樣的問題呢?
因為三個線程同時訪問我們的公共資源self.totalTickets,當線程一訪問了,還沒有減1的時候,線程二或者線程三也進來訪問了,這個時候,線程二或者線程三讀取的還是之前的self.totalTickets,所以就會出現打印兩次甚至三次相同余票的情況。
為了解決這個問題,我們在線程訪問公共資源的時候加個鎖,也就是說,當線程一準備訪問公共資源的時候,我們就把公共資源鎖住,不讓其他線程進來。當線程一訪問完了,再進行解鎖,其他線程繼續訪問。
代碼如下:
- (void)sell{
NSLog(@"開始售票,當前余票:%d", self.totalTickets);
while (self.totalTickets > 0) {
[NSThread sleepForTimeInterval:1.0];
//互斥鎖--鎖內的代碼在同一時間只有一個線程在執行
@synchronized (self) {
if(self.totalTickets > 0){
self.totalTickets--;
NSLog(@"%@ 賣出一張,余票:%d", [NSThread currentThread].name, self.totalTickets);
}else{
NSLog(@"余票不足,出票失??!");
}
}
}
}
為了盡快打印,所以把總票數改成10張。
看看打印結果:
解決了問題。
NSThread小結:
NSThread是官方提供的,面向對象的創建多線程的方法。
- NSThread可以使用類方法快速創建子線程,但是得不到子線程對象,線程自動開啟。
- NSThread可以使用對象方法創建子線程,能夠得到子線程對象,但是要手動開啟子線程。
- NSThread可以取消子線程、可以隨時查看線程的狀態(正在執行、被取消、結束)。
- NSThread可以隨時查看當前代碼所在的線程。
關于NSThread就先到這里吧,有任何問題請留言,謝謝!
祝大家生活愉快!