iOS多线程之三:NSThread,NSOperation,GCD超详细总结

我正在参与掘金创作者训练营第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 是操作任务。

具体步骤如下:

  1. 创建操作:先将需要执行的操作封装到一个 NSOperation 对象中。
  2. 创建队列:创建 NSOperationQueue 对象。
  3. 将操作加入到队列中:将 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]);
        }];
    }
}
复制代码

看下打印结果:

20220217162503.jpg

可以看到是执行顺序是无序。也记住一点就是最大并发数不是开启了多少条线程,开启线程数量是由系统决定的,不需要我们来管理。

操作依赖和优先级

任务之间可以相互依赖,没有依赖关系,那就看谁的优先级高,就先执行谁。

- (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后面。

我们看下打印结果:

20220217163941.jpg

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]);
}
复制代码

20220217173341.jpg

可以看出来,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]);

}
复制代码

既然是异步栅栏函数,那也就是说需要等前面的队列任务执行完,再执行自己的,然后再执行后面的。我们看下结果:

20220217174635.jpg

看结果,的确是没有阻塞主线程,而且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"]);
        });
    }
 }
复制代码

我们看下打印结果:

20220217221446.jpg

读和写的输出并没有问题,这个就相当于起到读写锁的效果,除了这个还可以用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]);
}
复制代码

我们看下输出结果:

20220217232354.jpg

很明显可以看出来,并没有阻塞当前线程,而且是等前面2个任务执行完毕后,再执行dispatch_group_notify的任务。

很多人会想,我 dispatch_group_async 里面要是用的是第三方网络框架调取异步网络请求,异步网络请求是在其框架的并发队列中,这时候数据还没请求返回,dispatch_group_async就走完了,这时候就执行了dispatch_group_notify,达不到想要的效果。

那我们难道用信号量去控制吗,既然要用信号量的话,我们干嘛还要用dispatch_group_t去做呢,这里我们直接用 dispatch_group_enterdispatch_group_leave 就好了。

dispatch_group_enterdispatch_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]);
}
复制代码

我们先看下打印结果:

20220218104845.jpg

看到效果了吗,dispatch_group_enterdispatch_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]);

    });

}
复制代码

我们先看下打印结果:

20220218113644.jpg

的确是实现了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_waitdispatch_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 等线程技术,可灵活操作线程和队列,附有其它强大功能,自动管理生命周期。

参考资料

猜你喜欢

转载自juejin.im/post/7065961731461382151