GCD 同步/异步, 串行/并发 , 几个面试题

首先,我们先来了解一下在 iOS 并发编程中非常重要的三个术语,这是我们理解 iOS 并发编程的基础:

  • 进程(process),指的是一个正在运行中的可执行文件。每一个进程都拥有独立的虚拟内存空间和系统资源,包括端口权限等,且至少包含一个主线程和任意数量的辅助线程。另外,当一个进程的主线程退出时,这个进程就结束了;

  • 线程(thread),指的是一个独立的代码执行路径,也就是说线程是代码执行路径的最小分支。在 iOS 中,线程的底层实现是基于 POSIX threads API 的,也就是我们常说的 pthreads ;
  • 任务(task),指的是我们需要执行的工作,是一个抽象的概念,用通俗的话说,就是一段代码。

串行 vs. 并发

从本质上来说,串行和并发的主要区别在于允许同时执行的任务数量。串行,指的是一次只能执行一个任务,必须等一个任务执行完成后才能执行下一个任务;并发,则指的是允许多个任务同时执行。

同步 vs. 异步

同样的,同步和异步操作的主要区别在于是否等待操作执行完成,亦即是否阻塞当前线程。同步操作会等待操作执行完成后再继续执行接下来的代码,而异步操作则恰好相反,它会在调用后立即返回,不会等待操作的执行结果。

队列 vs. 线程

有一些对 iOS 并发编程模型不太了解的同学可能会对队列和线程产生混淆,不清楚它们之间的区别与联系,因此,我觉得非常有必要在这里简单地介绍一下。在 iOS 中,有两种不同类型的队列,分别是串行队列和并发队列。正如我们上面所说的,串行队列一次只能执行一个任务,而并发队列则可以允许多个任务同时执行。iOS 系统就是使用这些队列来进行任务调度的,它会根据调度任务的需要和系统当前的负载情况动态地创建和销毁线程,而不需要我们手动地管理。

 

说结论 :

同步函数,使用dispatch_sync的不会开启线程 , 当前线程会等待block执行完成在接着向下执行

异步函数,使用dispatch_async的,会开启新的线程 , 当前线程不等待,直接向下执行

串行队列,任务都是一个接一个的

并发队列,任务是没有顺序的,依据线程调度执行

多说无益, 

 同步串行 :  1->2->死锁
 1.不会开启线程,在当前线程执行,
 2.任务一个接一个,
 3.会产生阻塞.


- (void)tt{
    NSLog(@"1");
    
    // DISPATCH_QUEUE_SERIAL 串行队列
    // DISPATCH_QUEUE_CONCURRENT 并行队列
    dispatch_queue_t serial = dispatch_queue_create("label", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(serial, ^{
        NSLog(@"2");
        dispatch_sync(serial, ^{
            NSLog(@"3---%@",[NSThread currentThread]);
        });
        NSLog(@"4---%@",[NSThread currentThread]);

    });
    NSLog(@"5");
    
}

同步并发 : 1->2->3->4->5
 1.不会开启线程,还是在当前线程
 2.虽然是并发队列,但是由于没有新线程,同步执行,会等待block执行完成在向下 , 任务还是一个接一个
 3.不会堵塞

- (void)tt1{
    NSLog(@"1");
    
    // DISPATCH_QUEUE_SERIAL 串行队列
    // DISPATCH_QUEUE_CONCURRENT 并行队列
    dispatch_queue_t serial = dispatch_queue_create("label", DISPATCH_QUEUE_CONCURRENT);
    dispatch_sync(serial, ^{
        NSLog(@"2");
        dispatch_sync(serial, ^{
            NSLog(@"3---%@",[NSThread currentThread]);
        });
        NSLog(@"4---%@",[NSThread currentThread]);

    });
    NSLog(@"5");
    
}

 异步串行 :  1->5->2->4->3 , 2,5其实是顺序不定的,一般而言是5在前 , 2,4肯定在3前
 1.会开启一个新线程
 2.在新线程中任务是一个接一个执行
 3.不会堵塞

- (void)tt2{
    NSLog(@"1");
    
    // DISPATCH_QUEUE_SERIAL 串行队列
    // DISPATCH_QUEUE_CONCURRENT 并行队列
    dispatch_queue_t serial = dispatch_queue_create("label", DISPATCH_QUEUE_SERIAL);
    dispatch_async(serial, ^{
        NSLog(@"2");
        dispatch_async(serial, ^{
            NSLog(@"3---%@",[NSThread currentThread]);

        });
        
        NSLog(@"4---%@",[NSThread currentThread]);

    });
    NSLog(@"5");
    
}

异步并发 : 1->5->2->(4/3) , 后2个顺序不定 , 一般是4在前,3在后 , 2和5也不太定,一般是5在前
 1.会开启多个新线程,根据任务数量,和cpu调度有关
 2.在新线程中任务是同时执行,没有先后顺序
 3.不会堵塞

- (void)tt3{
    NSLog(@"1");
    
    // DISPATCH_QUEUE_SERIAL 串行队列
    // DISPATCH_QUEUE_CONCURRENT 并行队列
    dispatch_queue_t serial = dispatch_queue_create("label", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(serial, ^{
        NSLog(@"2");
        dispatch_async(serial, ^{
            NSLog(@"3---%@",[NSThread currentThread]);
        });
        NSLog(@"4---%@",[NSThread currentThread]);

    });
    NSLog(@"5");
    
}

好了 , 总结一下 会产生死锁的条件, 一句话就是 使用sync函数往当前的串行队列中添加新任务,会产生死锁,其他情况都不会产生死锁.

这3个条件缺一不可:
1.使用同步函数 sync
2.使用串行队列 
3.向同一个串行队列中继续增加任务


下面代码会打印什么?

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_queue_t queue = dispatch_queue_create("aaa", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        NSLog(@"1");
        [self performSelector:@selector(test) withObject:nil afterDelay:0.0];
        NSLog(@"3");
    });

}

- (void)test {
    NSLog(@"2");
}

会打印1,3. 

        [self performSelector:@selector(test) withObject:nil afterDelay:0.0]; 可以等价于下面, 添加一个只执行一次的timer,

        [NSTimer scheduledTimerWithTimeInterval:0.0 target:self selector:@selector(test) userInfo:nil repeats:NO];

由于 [self performSelector:@selector(test) withObject:nil afterDelay:0.0]; 会往当前的Runloop中添加定时器,而调用是在子线程中, 子线程是没有开启RunLoop的, 所以这个timer无法执行, 所以test永远不会调用.


还是上面的代码, 把dispatch_async  换成了 dispatch_sync, 会打印什么

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_queue_t queue = dispatch_queue_create("aaa", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(queue, ^{
        NSLog(@"1");
        [self performSelector:@selector(test) withObject:nil afterDelay:0.0];
        NSLog(@"3");
    });
}

打印1, 3, 2

首先,判断会不会死锁, 根据上面的总结,

使用同步sync 满足,
使用串行队列 满足,  
但是不满足往同一个串行队列中添加任务, 当前是主队列, 往一个新的串行队列添加任务, 这个条件不满足,

所以 不会产生死锁.

能不能打印2?
能够打印, 因为虽然是往新的串行队列添加任务, 但是由于使用了sync, 还是在当前队列执行任务, 还是在主线程, 主线程的RunLoop是开启的, 所以2是能够打印的.

为什么2会在3的后面?
和timer的调用时机有关, timer的调用依赖于RunLoop, 需要等主线程把当前的任务执行完成后, 才会调到队列后面的任务.


子线程中添加runLoop run, 会打印什么

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_queue_t queue = dispatch_queue_create("aaa", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        NSLog(@"1");
        [self performSelector:@selector(test) withObject:nil afterDelay:0.0];
        NSLog(@"3");
        [[NSRunLoop currentRunLoop] run];
        NSLog(@"end");
    });
}

打印1, 3, 2

子线程中直接调用RunLoop run, 能不能run起来?
能, 因为- performSelector: withObject: afterDelay: 相当于添加了timer源到RunLoop中, 当前的RunLoop是有source的, 能run起来, 

子线程的RunLoop能保持run的状态, 那么加上去的source也能正常调用, 所以2 是可以正常打印的.

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值