多线程系列篇章计划内容:
iOS多线程编程(一) 多线程基础
iOS多线程编程(二) Pthread
iOS多线程编程(三) NSThread
iOS多线程编程(四) GCD
iOS多线程编程(五) GCD的底层原理
iOS多线程编程(六) NSOperation
iOS多线程编程(七) 同步机制与锁
iOS多线程编程(八) RunLoop
NSThread 是苹果提供的一种面向对象的轻量级多线程解决方案,一个 NSThread 对象代表一个线程,使用比较简单,但是需要手动管理线程的生命周期、处理线程同步等问题。
1. 创建、启动NSThread线程
- 创建一个NSThread线程有类方法和实例方法。
类方法创建:
+ (void)detachNewThreadWithBlock:(void (^)(void))block ;
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
实例方法创建:
- (instancetype)initWithBlock:(void (^)(void))block;
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument;
使用实例方法创建线程返回线程对象,可以根据需要设置相应属性参数。
需要注意:block形式的创建方式 需在iOS10之后使用
- 创建完毕后不要忘记开启线程!
线程创建完毕后对应线程状态的新建态
,我们需要调用 start
方法启动线程(使用类方法创建的线程隐式的启动了线程),否则线程是不会执行的。
但是使用类方法创建或者使用实例方法创建并且调用start
方法之后,线程并不会立即执行,只是将线程加入可调度线程池,进入就绪状态
,具体何时执行需要等待CPU的调度。(关于线程状态可以参阅 多线程基础 中的线程生命周期)
2. NSThread线程属性
-
name
属性:设置线程的名字
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"线程:%@ start",[NSThread currentThread]);
}];
thread.name = @"测试线程";
[thread start];
打印结果如下:
线程:{number = 6, name = 测试线程} start
-
qualityOfService
属性:设置线程优先级
原本线程优先级threadPriority
属性,是一个double
类型,取值范围为0.0~1.0,值越大,优先级越高。不过,该属性已被qualityOfService
取代。qualityOfService
是一个枚举值。定义如下:
typedef NS_ENUM(NSInteger, NSQualityOfService) {
NSQualityOfServiceUserInteractive = 0x21,
NSQualityOfServiceUserInitiated = 0x19,
NSQualityOfServiceUtility = 0x11,
NSQualityOfServiceBackground = 0x09,
NSQualityOfServiceDefault = -1
} API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));
NSQualityOfServiceUserInteractive 优先级最高,从上到下依次降低,NSQualityOfServiceDefault 为默认优先级。
使用如下:
NSThread *thread1 = [[NSThread alloc] initWithBlock:^{
NSLog(@"\n 线程:%@ start",[NSThread currentThread]);
}];
thread1.name = @"测试线程 1 ";
[thread1 start];
NSThread *thread2 = [[NSThread alloc] initWithBlock:^{
NSLog(@"\n 线程:%@ start",[NSThread currentThread]);
}];
thread2.qualityOfService = NSQualityOfServiceUserInteractive;
thread2.name = @"测试线程 2 ";
[thread2 start];
虽然 thread1 先于 thread2 start
,但thread1优先级为默认,而thread2优先级为NSQualityOfServiceUserInteractive,在执行时,thread2 先于 thread1执行。
线程:{number = 7, name = 测试线程 2 } start
线程:{number = 6, name = 测试线程 1 } start
-
callStackReturnAddresses
和callStackSymbols
属性:
callStackReturnAddresses
属性定义如下:
@property (class, readonly, copy) NSArray *callStackReturnAddresses
线程的调用会有函数的调用,该属性返回的就是 该线程中函数调用的虚拟地址数组。
callStackSymbols
属性定义如下:
@property (class, readonly, copy) NSArray *callStackSymbols
该属性以符号的形式返回该线程调用函数。
callStackReturnAddress和callStackSymbols这两个函数可以同NSLog联合使用来跟踪线程的函数调用情况,是编程调试的重要手段。
-
threadDictionary
属性:
每个线程有自己的堆栈空间,线程内维护了一个键-值
的字典,它可以在线程里面的任何地方被访问。
你可以使用该字典来保存一些信息,这些信息在整个线程的执行过程中都保持不变。
比如,你可以使用它来存储在你的整个线程过程中 Run loop 里面多次迭代的状态信息。
- 其他属性
@property (class, readonly, strong) NSThread *mainThread; // 获取主线程
@property (class, readonly, strong) NSThread *currentThread;// 获取当前线程
@property NSUInteger stackSize; // 线程使用堆栈大小,默认512k
@property (readonly) BOOL isMainThread; // 是否是主线程
@property (class, readonly) BOOL isMainThread ; // reports whether current thread is main
@property (readonly, getter=isExecuting) BOOL executing ; // 线程是否正在执行
@property (readonly, getter=isFinished) BOOL finished ; // 线程是否执行完毕
@property (readonly, getter=isCancelled) BOOL cancelled; // 线程是否取消
3. NSThread线程的阻塞
NSThread提供了2个类方法,
+ (void)sleepUntilDate:(NSDate *)date; // 休眠到指定日期
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;// 休眠执行时常
对于上面设置线程优先级的示例代码,我们稍做些更改。
NSThread *thread1 = [[NSThread alloc] initWithBlock:^{
NSLog(@"\n 线程:%@ start",[NSThread currentThread]);
}];
thread1.name = @"测试线程 1 ";
[thread1 start];
// 加入休眠函数
[NSThread sleepForTimeInterval:1];
NSThread *thread2 = [[NSThread alloc] initWithBlock:^{
NSLog(@"\n 线程:%@ start",[NSThread currentThread]);
}];
thread2.qualityOfService = NSQualityOfServiceUserInteractive;
thread2.name = @"测试线程 2 ";
[thread2 start];
在 thread1 与 thread2 之间加入 [NSThread sleepForTimeInterval:1];
让主线程阻塞1秒,那么 thread1 将 先于 thread2 执行,即使thread2 的优先级是高于thread1。
这是因为,thread1先start
进入就绪状态
,此时,主线程休眠,在CPU时间到来之时,可调度线程池中只有thread1,thread1 被调度执行,此时主线程休眠时间结束,thread2 进入就绪态,并在下一次CPU时间时被调度执行。
4. NSThread的终止
- 取消线程
- (void)cancel ;
对于已被调度的线程是无法通过cancel取消的。
- 退出线程
+ (void)exit;
强制退出线程,使线程进入死亡态。
5. 线程的通信
在开发中,我们有时需要在子线程进行耗时操作,操作结束后切换到主线程进行刷新UI。这就涉及到线程间的通信,NSThread线程提供了对NSObject的拓展函数。
5.1 NSObject方式
// 在主线程上执行操作 wait表示是否阻塞该方法,等待主线程空闲再运行,modes表示运行模式kCFRunLoopCommonModes
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;
// equivalent to the first method with kCFRunLoopCommonModes
// 在指定线程上执行操作
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
// equivalent to the first method with kCFRunLoopCommonModes
// 隐式创建一个线程并执行
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
// NSObject函数: 在当前线程上执行操作,调用 NSObject 的 performSelector:相关方法
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
举个例子,我们来模拟子线程下载图片回到线程刷新 UI 的实现
// 开辟子线程模拟网络请求
- (void)downloadImage {
[NSThread detachNewThreadWithBlock:^{
// 1. 获取图片 imageUrl
NSURL *imageUrl = [NSURL URLWithString:@"https://xxxxx.jpg"];
// 2. 从 imageUrl 中读取数据(下载图片) -- 耗时操作
NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
// 通过二进制 data 创建 image
UIImage *image = [UIImage imageWithData:imageData];
// 主线程刷新UI
[self performSelectorOnMainThread:@selector(mainThreadRefreshUI) withObject:image waitUntilDone:YES];
}];
}
// 主线程刷新 UI 调用方法
- (void)mainThreadRefreshUI:(UIImage *)image {
self.imageView.image = image;
}
5.2 端口通信方式
端口通信需要使用 NSPort ,NSPort 是一个抽象类,具体使用的时候可以使用其子类NSMachPort。
通过下面方法传递将要在线程间通信的信息数据。
- (BOOL)sendBeforeDate:(NSDate *)limitDate components:(nullable NSMutableArray *)components from:(nullable NSPort *) receivePort reserved:(NSUInteger)headerSpaceReserved;
- (BOOL)sendBeforeDate:(NSDate *)limitDate msgid:(NSUInteger)msgID components:(nullable NSMutableArray *)components from:(nullable NSPort *)receivePort reserved:(NSUInteger)headerSpaceReserved;
实现NSPortDelegate 的方法,接受端口传递过来的数据。
- (void)handlePortMessage:(NSPortMessage *)message
注意:在使用端口的时候,需要注意将端口将入当前Runloop,否则消息无法传递
[[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];
6. NSThread通知
NSWillBecomeMultiThreadedNotification:由当前线程派生出第一个其他线程时发送,一般一个线程只发送一次
NSDidBecomeSingleThreadedNotification:这个通知目前没有实际意义,可以忽略
NSThreadWillExitNotification线程退出之前发送这个通知
7. NSThread 线程安全案例
只要涉及到多线程就有可能存在非线程安全的情况。根本原因就是多条线程同时操作一片临界区,导致临界区资源错乱。
我们来模拟多线程经典的售票案例:两个售票窗口同时售卖50张车票
- (void)initTicketStatusNotSave {
// 1. 设置剩余火车票为 50
self.ticketSurplusCount = 50;
// 2. 模拟窗口1售票的线程
self.ticketSaleWindow1 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
self.ticketSaleWindow1.name = @"售票窗口1";
// 3. 模拟窗口2售票的线程
self.ticketSaleWindow2 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
self.ticketSaleWindow2.name = @"售票窗口2";
// 4. 开始售卖火车票
[self.ticketSaleWindow1 start];
[self.ticketSaleWindow2 start];
}
/**
* 售卖火车票(非线程安全)
*/
- (void)saleTicketNotSafe {
while (1) {
//如果还有票,继续售卖
if (self.ticketSurplusCount > 0) {
self.ticketSurplusCount --;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread].name]);
[NSThread sleepForTimeInterval:0.2];
}
//如果已卖完,关闭售票窗口
else {
NSLog(@"所有火车票均已售完");
break;
}
}
}
截取部分结果如下:
2020-11-19 15:55:53.222575+0800 pthread[5018:211393] 剩余票数:49 窗口:售票窗口1
2020-11-19 15:55:53.222589+0800 pthread[5018:211394] 剩余票数:48 窗口:售票窗口2
2020-11-19 15:55:53.426619+0800 pthread[5018:211394] 剩余票数:46 窗口:售票窗口2
2020-11-19 15:55:53.426626+0800 pthread[5018:211393] 剩余票数:47 窗口:售票窗口1
2020-11-19 15:55:53.630102+0800 pthread[5018:211394] 剩余票数:45 窗口:售票窗口2
2020-11-19 15:55:53.630144+0800 pthread[5018:211393] 剩余票数:44 窗口:售票窗口1
2020-11-19 15:55:53.832564+0800 pthread[5018:211393] 剩余票数:43 窗口:售票窗口1
2020-11-19 15:55:53.832649+0800 pthread[5018:211394] 剩余票数:42 窗口:售票窗口2
2020-11-19 15:55:54.033279+0800 pthread[5018:211393] 剩余票数:41 窗口:售票窗口1
2020-11-19 15:55:54.033360+0800 pthread[5018:211394] 剩余票数:40 窗口:售票窗口2
2020-11-19 15:55:54.237370+0800 pthread[5018:211393] 剩余票数:39 窗口:售票窗口1
2020-11-19 15:55:54.237370+0800 pthread[5018:211394] 剩余票数:39 窗口:售票窗口2
2020-11-19 15:55:54.440124+0800 pthread[5018:211393] 剩余票数:38 窗口:售票窗口1
2020-11-19 15:55:54.440200+0800 pthread[5018:211394] 剩余票数:37 窗口:售票窗口2
2020-11-19 15:55:54.643881+0800 pthread[5018:211393] 剩余票数:35 窗口:售票窗口1
2020-11-19 15:55:54.643889+0800 pthread[5018:211394] 剩余票数:36 窗口:售票窗口2
2020-11-19 15:55:54.845543+0800 pthread[5018:211393] 剩余票数:33 窗口:售票窗口1
......
这就是多线程同时操作同一片临界区的结果,得到的票数是错乱的,这是不符合我们的预期的。
线程安全的解决方案,就是线程同步机制。比较常用的是使用【锁】。在一个线程占用临界区的时候,不允许其他线程进入。
iOS 实现线程加锁有很多种方式。@synchronized、 NSLock、NSRecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock、atomic等等。更详细的锁相关知识参见iOS多线程编程(七)-锁。这里我们使用@synchronized对此案例进行线程安全优化。
- (void)initTicketStatusNotSave {
// 1. 设置剩余火车票为 50
self.ticketSurplusCount = 50;
// 2. 模拟窗口1售票的线程
self.ticketSaleWindow1 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
self.ticketSaleWindow1.name = @"售票窗口1";
// 3. 模拟窗口2售票的线程
self.ticketSaleWindow2 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
self.ticketSaleWindow2.name = @"售票窗口2";
// 4. 开始售卖火车票
[self.ticketSaleWindow1 start];
[self.ticketSaleWindow2 start];
}
/**
* 售卖火车票(线程安全)
*/
- (void)saleTicketNotSafe {
while (1) {
// 互斥锁
@synchronized (self) {
//如果还有票,继续售卖
if (self.ticketSurplusCount > 0) {
self.ticketSurplusCount --;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread].name]);
[NSThread sleepForTimeInterval:0.2];
}
//如果已卖完,关闭售票窗口
else {
NSLog(@"所有火车票均已售完");
break;
}
}
}
}
运行后结果是正常的,这里就不贴了。