我正在参与掘金创作者训练营第4期,点击了解活动详情,一起学习吧!
前言
最近在参加掘金创作者训练营,听了1节课,收益良多,其中印象比较深刻的一句话是10篇水文不如1篇高质量文章
。豁然大悟,往自己前面写的一些文章,的确是有点水。有时候担心写的过长,会看的累,有时候是自己没有那么多时间,就分开成几部分写。
现在想想,我自己写文章究竟想干啥,是为了加薪吗,是为了让大家点赞有自豪感吗,好像是有点。总的来说无非就是想把自己学习了东西进行好好总结,让自己成长,当然过程中能帮到别人更好,总而言之不要违背初心就好。
NSThread
NSThread
是一个对pthread
对象化的封装,是苹果官方提供面向对象操作线程的技术,简单易用,可以直接操作线程对象,不过需要我们自己管理线程的生命周期。
NSThread线程创建
我们直接通过initWithBlock
进行初始化。其中[thread start]
就是启动线程。
- (void)nsthreadDemo
{
NSThread *thread = [[NSThread alloc] initWithBlock:^{
// 打印当前线程
NSLog(@"%@",[NSThread currentThread]);
}];
[thread start];
}
复制代码
实际上除了这种,我们还有其它初始化方法。
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument;
复制代码
还有类方法也可以进行线程创建,并且不需要start
。
+ (void)detachNewThreadWithBlock:(void (^)(void))block;
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
复制代码
线程睡眠
sleepUntilDate
方法是指睡眠当某个date
。
+ (void)sleepUntilDate:(NSDate *)date;
复制代码
sleepForTimeInterval
是指让线程睡眠几秒。
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
复制代码
线程控制
线程启动,我们用start
方法。
- (void)start;
复制代码
线程取消,我们用cancel
方法。
- (void)cancel;
复制代码
获取当前线程,我们用currentThread
方法。
[NSThread currentThread]
复制代码
主线程执行
我们可以直接用NSObject的方法。用performSelectorOnMainThread
回到主线程。
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
复制代码
子线程执行,回到主线程刷新UI
这里我们用个例子来运用NSThread
。
- (void)nsthreadDemo
{
NSThread *thread = [[NSThread alloc] initWithBlock:^{
// 打印当前线程
NSLog(@"%@",[NSThread currentThread]);
//让当前线程睡2秒
[NSThread sleepForTimeInterval:2.0];
// 回到主线程
[self performSelectorOnMainThread:@selector(updateUI) withObject:nil waitUntilDone:NO];
}];
[thread start];
}
- (void)updateUI
{
NSLog(@"%@",[NSThread currentThread]);
self.view.backgroundColor = [UIColor systemRedColor];
}
复制代码
NSThread
平常还是很少用这个来操作线程的,那我们就看下面的。
NSOperation
NSOperation
是基于 GCD
更高一层的封装,使用更加面向对象。比 GCD
多了一些更简单的功能。自动管理生命周期,这个我喜欢。那下面我们就一起来看看吧。
NSOperation和NSOperationQueue使用
在 GCD 中,我们创建一个队列,然后把任务添加到 Block 上。然后由队列进行调度任务。
既然是基于 GCD ,那 NSOperation 大体上逻辑也会和 GCD 一样。
其中 NSOperationQueue
是操作队列,NSOperation
是操作任务。
具体步骤如下:
- 创建操作:先将需要执行的操作封装到一个 NSOperation 对象中。
- 创建队列:创建 NSOperationQueue 对象。
- 将操作加入到队列中:将 NSOperation 对象添加到 NSOperationQueue 对象中。
- (void)operationdemo
{
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//创建操作任务
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
}];
//队列添加操作任务
[queue addOperation:operation];
}
复制代码
队列控制
cancelAllOperations
:取消当前队列的所有操作任务。
- (void)cancelAllOperations;
复制代码
suspended
:队列挂起。
@property (getter=isSuspended) BOOL suspended;
复制代码
mainQueue
:主队列。
[NSOperationQueue mainQueue]
复制代码
currentQueue
:当前队列。
[NSOperationQueue currentQueue]
复制代码
waitUntilAllOperationsAreFinished
:会等当前队列执行完所有任务,才会继续走,会阻塞当前线程。
- (void)waitUntilAllOperationsAreFinished;
复制代码
NSOperationQueue控制并发数
这里我们就直接用addOperationWithBlock
来实现队列添加操作任务。
我们设置最大并发为1,当前队列就是串行队列了。
- (void)operationdemo
{
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//创建串行队列
queue.maxConcurrentOperationCount = 1;
for (int i = 0; i < 5; i++) {
//队列添加操作任务
[queue addOperationWithBlock:^{
NSLog(@"%d: %@",i,[NSThread currentThread]);
}];
}
}
复制代码
打印结果:
2022-02-17 16:17:38.379723+0800 多线程[80881:1209674] 0: <NSThread: 0x600003108380>{number = 6, name = (null)}
2022-02-17 16:17:38.380011+0800 多线程[80881:1209674] 1: <NSThread: 0x600003108380>{number = 6, name = (null)}
2022-02-17 16:17:38.380219+0800 多线程[80881:1209674] 2: <NSThread: 0x600003108380>{number = 6, name = (null)}
2022-02-17 16:17:38.380460+0800 多线程[80881:1209672] 3: <NSThread: 0x6000031023c0>{number = 3, name = (null)}
2022-02-17 16:17:38.380838+0800 多线程[80881:1209672] 4: <NSThread: 0x6000031023c0>{number = 3, name = (null)}
复制代码
结果分析:打印结果的确是按顺序执行,也就是说操作任务是1个1个执行的。
我们设置最大并发数大于1,当前队列就是并发队列了。
- (void)operationdemo
{
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//创建并发队列
queue.maxConcurrentOperationCount = 2;
for (int i = 0; i < 5; i++) {
//队列添加操作任务
[queue addOperationWithBlock:^{
NSLog(@"%d: %@",i,[NSThread currentThread]);
}];
}
}
复制代码
看下打印结果:
可以看到是执行顺序是无序。也记住一点就是最大并发数不是开启了多少条线程,开启线程数量是由系统决定的,不需要我们来管理。
操作依赖和优先级
任务之间可以相互依赖,没有依赖关系,那就看谁的优先级高,就先执行谁。
- (void)operationdemo1
{
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//创建并发队列
queue.maxConcurrentOperationCount = 2;
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%d: %@",1,[NSThread currentThread]);
}];
//设置普通优先级
[operation1 setQueuePriority:NSOperationQueuePriorityNormal];
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%d: %@",2,[NSThread currentThread]);
}];
//2依赖1
[operation2 addDependency:operation1];
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%d: %@",3,[NSThread currentThread]);
}];
//设置高的优先级
[operation3 setQueuePriority:NSOperationQueuePriorityHigh];
NSBlockOperation *operation4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%d: %@",4,[NSThread currentThread]);
}];
//4依赖3
[operation4 addDependency:operation3];
// 队列添加操作
[queue addOperation:operation1];
[queue addOperation:operation2];
[queue addOperation:operation3];
[queue addOperation:operation4];
}
复制代码
我们先分析一波:创建了1个队列,最大并发数是2,操作任务2依赖操作任务1,操作任务4依赖操作任务3。所以执行后,2是排在1后面,4是排在3后面。1的优先级是普通,3的优先级是高。所以3会先执行,1会在3后面。
我们看下打印结果:
3 -> 1 -> 4 -> 2 ,是不是和我们分析的一样样啊。
子线程执行,回到主线程刷新UI
最后,我们也来下子线程执行任务,然后回到主线程执行代码。
- (void)operationdemo2
{
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 队列添加任务
[queue addOperationWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
sleep(2);
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
}];
}];
}
复制代码
看起来也还OK吧,那下面我们再看下GCD吧。
GCD
GCD 是也是自动管理生命周期的,也是我们日常处理线程用的最多的一个方式。 GCD 的核心就是队列+执行方式,首先创建一个队列,然后向队列中追加任务,系统会根据任务的类型执行任务。
在前面写的一篇 iOS多线程之一:进程,线程,队列的关系 ,就已经写了关于同步串行,同步并发,异步串行,异步并发了。所以这章节,我就写下GCD其它相关的内容。
子线程执行,回到主线程刷新UI
看下面的代码,感觉和 NSOperation 的代码是差不多的。这个也是我们常用的方法。
- (void)gcddemo1
{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(2);
NSLog(@"%@",[NSThread currentThread]);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"%@",[NSThread currentThread]);
});
});
}
复制代码
这里用的是异步并发执行任务,用的是全局队列,改成dispatch_queue_create("jj.com", DISPATCH_QUEUE_CONCURRENT);
并发队列也是可以的。
dispatch_once
我们可以用dispatch_once
来创建1个单例。
@implementation GCDDemo
+ (instancetype)shareInstance {
static GCDDemo *demo;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
demo = [GCDDemo new];
});
return demo;
}
@end
复制代码
dispatch_once
下的代码,在整个程序运行过程中只执行一次。这个在多线程的时候也可以保证线程安全的。
dispatch_after
我们可以使用dispatch_after
来执行延迟方法。
- (void)gcddemo2 {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"2:%@",[NSThread currentThread]);
});
NSLog(@"1:%@",[NSThread currentThread]);
}
复制代码
可以看出来,dispatch_after
是不会阻塞线程的,并且延时后,会回到主线程执行任务。
dispatch_barrier_async
dispatch_barrier_async
是一个栅栏函数,这里我们讲解的是异步栅栏,所以并不会阻塞当前线程。那它有什么用呢?
我们看下这个例子:
- (void)dispatch_barrier_async_request
{
dispatch_queue_t queue = dispatch_queue_create("jj.com", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"2:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"3:%@", [NSThread currentThread]);
});
dispatch_barrier_async(queue, ^{
NSLog(@"4:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"5:%@", [NSThread currentThread]);
});
NSLog(@"1:%@", [NSThread currentThread]);
}
复制代码
既然是异步栅栏函数,那也就是说需要等前面的队列任务执行完,再执行自己的,然后再执行后面的。我们看下结果:
看结果,的确是没有阻塞主线程,而且dispatch_barrier_async
的确起到栅栏效果。
除了这个用法,我们还在多读单写
方面起到重要作用。我们看下代码
// 定义一个并发队列
@property (nonatomic, strong) dispatch_queue_t concurrent_queue;
// 多个线程需要数据访问
@property (nonatomic, strong) NSMutableDictionary *dataCenterDic;
复制代码
先定义一个并发队列和一个字典,字典是可能多个线程需要数据访问。
- (instancetype)init{
self = [super init];
if (self){
// 创建一个并发队列:
self.concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
// 创建数据字典:
self.dataCenterDic = [NSMutableDictionary dictionary];
}
return self;
}
复制代码
先初始化,我们直接添加一个读和写方法。
#pragma mark - 读数据
- (id)jj_objectForKey:(NSString *)key{
__block id obj;
// 同步读取指定数据:
dispatch_sync(self.concurrent_queue, ^{
obj = [self.dataCenterDic objectForKey:key];
});
return obj;
}
#pragma mark - 写数据
- (void)jj_setObject:(id)obj forKey:(NSString *)key{
// 异步栅栏调用设置数据
dispatch_barrier_async(self.concurrent_queue, ^{
NSLog(@"写--%@",obj);
[self.dataCenterDic setObject:obj forKey:key];
});
}
复制代码
我们外部开始调用写读和写方法。
- (void)readWriteLock {
dispatch_queue_t queue = dispatch_queue_create("jj.com", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 5; i++) {
dispatch_async(queue, ^{
[self.rwLock jj_setObject:[NSString stringWithFormat:@"jj---%d",i] forKey:@"key"];
});
}
for (int i = 0; i < 5; i++) {
dispatch_async(queue, ^{
NSLog(@"读1--%@",[self.rwLock jj_objectForKey:@"key"]);
});
}
}
复制代码
我们看下打印结果:
读和写的输出并没有问题,这个就相当于起到读写锁的效果,除了这个还可以用pthread_rwlock_rdlock
,这个我们下一篇会详细说明。
dispatch_group
一般来说,队列组适用于在请求几个异步任务,然后等任务执行完后,再到dispatch_group_notify
执行所在的任务。
- (void)dispatch_group_request
{
// 创建1个队列组
dispatch_group_t group = dispatch_group_create();
// 创建1个并发队列
dispatch_queue_t queue = dispatch_queue_create("jj.com", DISPATCH_QUEUE_CONCURRENT);
// 异步添加1个并发队列
dispatch_group_async(group, queue, ^{
// 延迟模拟
sleep(1);
NSLog(@"1--%@",[NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
// 延迟模拟
sleep(1);
NSLog(@"2--%@",[NSThread currentThread]);
});
// 上面的任务执行完后会来到这里
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"3--%@",[NSThread currentThread]);
});
NSLog(@"0--%@",[NSThread currentThread]);
}
复制代码
我们看下输出结果:
很明显可以看出来,并没有阻塞当前线程,而且是等前面2个任务执行完毕后,再执行dispatch_group_notify
的任务。
很多人会想,我 dispatch_group_async
里面要是用的是第三方网络框架调取异步网络请求,异步网络请求是在其框架的并发队列中,这时候数据还没请求返回,dispatch_group_async
就走完了,这时候就执行了dispatch_group_notify
,达不到想要的效果。
那我们难道用信号量去控制吗,既然要用信号量的话,我们干嘛还要用dispatch_group_t去做呢,这里我们直接用 dispatch_group_enter
和 dispatch_group_leave
就好了。
dispatch_group_enter
、 dispatch_group_leave
是等同于dispatch_group_async
这个效果的,但用法就是这么奇妙。
- (void)dispatch_group_request1
{
// 创建1个队列组
dispatch_group_t group = dispatch_group_create();
// 创建1个并发队列
dispatch_queue_t queue = dispatch_queue_create("jj.com", DISPATCH_QUEUE_CONCURRENT);
// 进组
dispatch_group_enter(group);
//异步网络请求
dispatch_async(queue, ^{
//模拟网络请求
sleep(1);
NSLog(@"1--%@",[NSThread currentThread]);
//出组
dispatch_group_leave(group);
});
// 进组
dispatch_group_enter(group);
//异步网络请求
dispatch_async(queue, ^{
//模拟网络请求
sleep(1);
NSLog(@"2--%@",[NSThread currentThread]);
//出组
dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"3--%@",[NSThread currentThread]);
});
NSLog(@"0--%@",[NSThread currentThread]);
}
复制代码
我们先看下打印结果:
看到效果了吗,dispatch_group_enter
和 dispatch_group_leave
必须是成对出现的,一旦有进组了,dispatch_group_notify
就不会调用,直到dispatch_group_leave
调用后,才会调取。和信号量其实是差不多的效果的。
dispatch_semaphore
dispatch_semaphore
用来初始化信号量,通过信号量的值可以控制线程哪个执行,哪个需要等待。并且设置GCD的最大并发数。值为1的时候,还能达到同步锁的效果,这个下一篇文章会详细说明。
-
dispatch_semaphore_create
:创建一个Semaphore
并初始化信号的总量。 -
dispatch_semaphore_signal
:发送一个信号,让信号总量加 1。 -
dispatch_semaphore_wait
:如果信号量大于0,则正常执行,而且信号量会减 1 ;如果信号量为 0 ,则会一直等待,等接收到通知信号量大于 0 后才可以正常执行。如果等待的时候会起到阻塞当前线程的效果。
我们看个例子,如何实现2个任务执行完后,才执行第三个任务。
- (void)dispatch_semaphore_t_request
{
//创建1个信号量,且信号量的值为0
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
// 并发队列
dispatch_queue_t queue = dispatch_queue_create("jj.com", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
sleep(1);
NSLog(@"1--%@",[NSThread currentThread]);
//信号值+1
dispatch_semaphore_signal(sema);
});
dispatch_async(queue, ^{
sleep(1);
NSLog(@"2--%@",[NSThread currentThread]);
//信号值+1
dispatch_semaphore_signal(sema);
});
dispatch_async(dispatch_get_main_queue(), ^{
// 等待2个
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
NSLog(@"0--%@",[NSThread currentThread]);
});
}
复制代码
我们先看下打印结果:
的确是实现了2个任务执行完,才走后面的任务。
我们简单的分析一下:3个都是异步函数调用,所以3个都是并发进行的,任务1进去后,睡眠1s,任务2进去后,也睡眠1s,任务0进去后,按理其它2个都睡眠,它就直接走dispatch_semaphore_wait
了。
这时候,我们创建的信号量的值为0,只能等待了。 等任务2睡醒了,执行了dispatch_semaphore_signal
,信号值为1了,所以第一个dispatch_semaphore_wait
就可以走了,且信号量的值减1。这时候又遇到第二个dispatch_semaphore_wait
,也只能等。直到任务1的执行dispatch_semaphore_signal
后,信号量加1了,可以继续执行任务0。
我们不管任务1和任务2谁先执行完,我们最后的任务都需要等待他们执行完才可以执行,因为dispatch_semaphore_wait
和dispatch_semaphore_signal
是一一对应的。
Dispatch_source
一般我们用Dispatch_source
,用的做多就是计时器了。dispatch_source_t
的定时器不受RunLoop
影响,而且dispatch_source_t
是系统级别的源事件,精度很高,系统自动触发。
- (void)dispatch_source_request
{
if (_timer) {
//计时器在运行,不动
return;
}
// 创建定时器
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
// 设置定时器时间
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
// 设置事件触发的回调
dispatch_source_set_event_handler(_timer, ^{
if (self.timeout <= 0) {
dispatch_source_cancel(self.timer);
self.timer = nil;
}else {
dispatch_async(dispatch_get_main_queue(), ^{
self.timeout--;
NSLog(@"计时--%ld", self.timeout);
});
}
});
// 开始执行
dispatch_resume(_timer);
// 暂停
// dispatch_suspend(timer);
}
复制代码
总结
NSThread
:使用更加面向对象,简单易用,可直接操作线程对象,需要手动管理生命周期。NSOperation
:基于 GCD 封装,使用更加面向对象,可操作依赖关系,优先级,以及最大并发数,自动管理生命周期。GCD
:旨在替换 NSTread 等线程技术,可灵活操作线程和队列,附有其它强大功能,自动管理生命周期。
参考资料