面试整理八(多线程)

你理解的多线程有什么?

多线程中涉及到同步、异步、串行、并发。

同步、异步主要影响是否能开启新的线程
同步:在当前线程中执行任务,不具备开启新的线程的能力
异步:在新的线程中执行任务,具备开启新的线程的能力

并发、串行主要影响任务的执行方式
并发:多个任务并发(同时)执行
串行:一个任务执行完毕后在执行下一个任务

各队列的执行效果.png

iOS的多线程方案有哪几种,你更倾向于哪个?

pthread

简介:一套通用的多线程API,适用于Linux/Unix/Windows等系统,跨平台/可移植,使用难度大。
使用的语言:C语言
线程生命周期:程序员管理
使用频率:几乎不用

NSThread

简介:使用更加面向对象,简单易用,可直接操作线程对象。
使用的语言:OC语言
线程生命周期:程序员管理
使用频率:偶尔使用

GCD

简介:旨在替代NSThread等线程技术,充分利用设备的多核。
使用的语言:C语言
线程生命周期:自动管理
使用频率:经常使用

NSOperation

简介:基于GCD(底层是GCD),比GCD多了一些简单实用的功能,使用更加面向对象。
使用的语言:OC语言
线程生命周期:自动管理
使用频率:经常使用

你项目中有用到过GCD吗?

GCD 的使用步骤:

1.创建一个任务队列(串行队列、并行队列)

//参数1代表队列的唯一标识符,可为空
//参数2用来设置为串行队列还是并发队列
//DISPATCH_QUEUE_CONCURRENT:并发队列
//DISPATCH_QUEUE_SERIAL:串行队列
dispatch_queue_t queue = dispatch_queue_create("queue",DISPATCH_QUEUE_CONCURRENT);

2.将任务添加到创建的等待队列并指定任务的执行类型(同步、异步)

    //异步
    dispatch_async(queue, ^{
        //执行的任务代码
    });
    //同步
    dispatch_sync(queue1, ^{
        //执行的任务代码
    });

GCD 队列组

dispatch_group 队列组
在多线程的操作中 有时候有多个任务异步去执行 执行完后想统一处理某些事情 就可以考虑用队列组。

//创建队列组
    dispatch_group_t group = dispatch_group_create();
    //创建并发队列
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    //添加异步任务
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 5; i ++) {
            NSLog(@"执行任务1=%@",[NSThread currentThread]);
        }
    });
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 5; i ++) {
            NSLog(@"执行任务2=%@",[NSThread currentThread]);
        }
    });
    
    //等前面的任务执行完毕后会自动执行这个任务
  dispatch_group_notify(group, queue, ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            for (int i = 0; i < 10; i ++) {
                NSLog(@"执行任务3=%@",[NSThread currentThread]);
            }
        });
    });

dispatch_group_notify:当任务管理组中的任务都已经执行完了会通知这个函数执行;
dispatch_group_enter:使任务管理组里面的任务数加1;
dispatch_group_leave:使任务管理组里面的任务数减1;
这三个方法必须在同一个任务队列中,dispatch_group_notify才会执行。
如果使用上面两个函数,那么只有在任务管理组中的dispatch_group_enter和dispatch_group_leave都平衡的情况下dispatch_group_notify才会执行。

NSOperation

NSOperation 是苹果公司对 GCD 的封装,完全面向对象,所以使用起来更好理解。
NSOperation 和 NSOperationQueue 分别对应 GCD 的 任务 和 队列 。

操作步骤:
1.将要执行的任务封装到一个 NSOperation 对象中。
2.将此任务添加到一个 NSOperationQueue 对象中。
3.然后系统就会自动在执行任务。

添加任务

NSOperation 只是一个抽象类,所以不能封装任务。
但他有两个子类用于封装任务:NSInvocationOperationNSBlockOperation
创建一个Operation后,需要调用start方法开启任务,默认在当前队列中同步执行,中途取消任务的话只需调用cancel 方法。

添加任务:

- (void)operationTest {
    NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
    [operation start];
//    [operation cancel];
    NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"--%@",[NSThread currentThread]);
    }];
    //给operation1添加多个Block,这样 Operation 中的任务 会并发执行,它会 在主线程和其它的多个线程 执行这些任务。
    for (int i = 0; i < 5; i ++) {
        [operation1 addExecutionBlock:^{
            NSLog(@"第%d次执行 %@",i,[NSThread currentThread]);
        }];
    }
    [operation1 start];
    //NOTE:addExecutionBlock 方法必须在 start() 方法之前执行,否则就会报错
}
- (void)run {
    NSLog(@"--%@",[NSThread currentThread]);
}

NSOperationQueue

调用一个 NSOperation 对象的 start() 方法来启动这个任务,但是这样默认是 同步执行 的。就算是 addExecutionBlock 方法,也会在 当前线程和其他线程 中执行,也就是说还是会占用当前线程。这是就要用到队列 NSOperationQueue 了。
按类型来说的话一共有两种类型:主队列、其他队列。
只要添加到队列,会自动调用任务的 start() 方法

主队列

这是一个特殊的线程,必须串行。所以添加到主队列的任务都会一个接一个地排着队在主线程处理。

NSOperationQueue *mineQueue = [NSOperationQueue mainQueue];

其他队列

因为主队列比较特殊,所以会单独有一个类方法来获得主队列。那么通过初始化产生的队列就是其他队列了。
注意:其他队列的任务会在其他线程并行执行。

创建队列:

 NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    //设置最大并发数,当设为1时,就是串行
    queue.maxConcurrentOperationCount = 1;
    
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"-%@",[NSThread currentThread]);
    }];
    
    for (int i = 0; i < 5; i ++) {
        [operation addExecutionBlock:^{
            NSLog(@"1--第%d次执行 %@",i,[NSThread currentThread]);
        }];
    }
    
    [queue addOperation:operation];
    
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 5; i ++) {
            NSLog(@"2--第%d次执行 %@",i,[NSThread currentThread]);
        }
    }];

NSOperation添加依赖

- (void)addDependency {
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任务1-%@",[NSThread currentThread]);
    }];
    NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任务2-%@",[NSThread currentThread]);
    }];
    for (int i = 0; i < 5; i ++) {
        [operation2 addExecutionBlock:^{
            NSLog(@"任务2--第%d次执行 %@",i,[NSThread currentThread]);
        }];
    }
    NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任务3-%@",[NSThread currentThread]);
    }];
    [operation2 addDependency:operation1]; //任务2依赖任务1
    [operation3 addDependency:operation2]; //任务3依赖任务2
//    //解除依赖
//    [operation3 removeDependency:operation2];
    [queue addOperations:@[operation1,operation2,operation3] waitUntilFinished:NO];
}

注意:不能添加相互依赖,会死锁。
可以在不同的队列之间依赖,依赖是添加到任务上的,和队列没关系.

线程安全的处理手段有哪些?

加锁

OC你了解的锁有哪些?

OSSpinLock

自旋锁,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源。
目前已经不再安全,可能会出现优先级反转问题:
如果等待锁的线程优先级较高,它会一直占着CPU资源,优先级低的线程就无法释放锁。
OSSpinLock 的使用:

//需要导入头文件
#import <libkern/OSAtomic.h>


@interface ViewController ()

@property (nonatomic, assign)OSSpinLock lock;

@end

//初始化锁
    _lock = OS_SPINLOCK_INIT;

//加锁
    OSSpinLockLock(&_lock);

//解锁
    OSSpinLockUnlock(&_lock);

//尝试加锁
    if (OSSpinLockTry(&_lock)) {
        //解锁
        OSSpinLockUnlock(&_lock);
    }

os_unfair_lock

用于取代不安全的OSSpinLock,从iOS10开始才支持
从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等。

使用:

//需要导入头文件
#import <os/lock.h>

//初始化
self.lock = OS_UNFAIR_LOCK_INIT;
//加锁
os_unfair_lock_lock(&_lock);
//解锁
os_unfair_lock_unlock(&_lock);

pthread_mutex

mutex 叫做互斥锁,等待锁的线程会处于休眠状态

使用:

//需要导入头文件
#import <pthread.h>

@property (nonatomic, assign)pthread_mutex_t mutex;

//初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    /*
     PTHREAD_MUTEX_NORMAL  //默认
     PTHREAD_MUTEX_ERRORCHECK //检错锁 如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
     PTHREAD_MUTEX_RECURSIVE //递归锁
     //递归锁:允许同一个线程对一把锁进行重复加锁,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。

     */
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
    //初始化锁
    pthread_mutex_init(&mutex, &attr);
    //销毁属性
    pthread_mutexattr_destroy(&attr);

//加锁
pthread_mutex_lock(&_mutex);
//解锁
pthread_mutex_unlock(&_mutex);

- (void)dealloc {
    //销毁锁
    pthread_mutex_destroy(&_ticketMutex);
    pthread_mutex_destroy(&_moneyMutex);
}

//初始化条件
    pthread_cond_t condition;
    pthread_cond_init(&condition, NULL);
    
    //等待条件(进入休眠,放开mutex锁,被唤醒后会再次对mutex加锁)
    pthread_cond_wait(&condition, &_moneyMutex);
    //激活一个等待该条件的线程
    pthread_cond_signal(&condition);
    //激活所有等待该条件的线程
    pthread_cond_broadcast(&condition);
    
    //销毁资源
    pthread_cond_destroy(&condition);

dispatch_semaphore

semaphore叫做信号量。
信号量的初始值,可以用来控制线程并发访问的最大数量
使用:

@property (nonatomic ,strong) dispatch_semaphore_t semaphore;

//设置最大并发数量
        self.semaphore = dispatch_semaphore_create(5);

//如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
    //如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);

//让信号量的值+1
    dispatch_semaphore_signal(self.semaphore);

dispatch_queue(DISPATCH_QUEUE_SERIAL)

直接使用GCD的串行队列,也可以实现线程同步的。

使用:

@property (nonatomic, strong) dispatch_queue_t ticketQueue;

//初始化串行队列
self.ticketQueue = dispatch_queue_create("ticketQueue", DISPATCH_QUEUE_SERIAL);

dispatch_sync(self.ticketQueue, ^{
       //执行操作
    });

NSLock

NSLock是对mutex普通锁的封装。
使用:

//初始化
NSLock *ticketLock = [[NSLock alloc] init];
//加锁
[self.ticketLock lock];
//解锁
[self.ticketLock unlock];

NSRecursiveLock

NSRecursiveLock是对mutex递归锁的封装。API跟NSLock基本上一致。

NSCondition

是对mutex和cond的封装。
使用:

@property (nonatomic, strong) NSCondition *condition;

//初始化
self.condition = [[NSCondition alloc] init];

//加锁
[self.condition lock];
//解锁
[self.condition unlock];
//信号
[self.condition signal];
//广播
[self.condition broadcast];


NSConditionLock

是对NSCondition的进一步封装,可以设置具体的条件值。
使用:

@interface NSConditionDemo ()

@property (nonatomic, strong) NSConditionLock *conditionLock;

@end

@implementation NSConditionDemo

- (instancetype)init {
    if (self = [super init]) {
        
        //初始化锁并添加条件值1
        self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
        
    }
    return self;
}

- (void)otherTest {
    [[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start];
    [[[NSThread alloc] initWithTarget:self selector:@selector(__two) object:nil] start];
    [[[NSThread alloc] initWithTarget:self selector:@selector(__three) object:nil] start];
}

- (void)__one {
//根据条件值加锁
    [self.conditionLock lockWhenCondition:1];
    NSLog(@"__one");
//根据条件值解锁
    [self.conditionLock unlockWithCondition:2];
}
- (void)__two {
    [self.conditionLock lockWhenCondition:2];
    NSLog(@"__two");
    [self.conditionLock unlockWithCondition:3];
}
- (void)__three {
    [self.conditionLock lockWhenCondition:3];
    NSLog(@"__three");
    //解锁
    [self.conditionLock unlock];
}

@end

@synchronized

@synchronized是对mutex递归锁的封装。

使用:

@synchronized (self) {
        //执行操作 
    }

自旋锁、互斥锁的比较

什么情况下使用自旋锁比较划算:

  • 预计线程等待锁的时间很短
  • 加锁的代码(临界区)经常被调用,但竞争情况很少发生
  • CPU资源不紧张
  • 多核处理器

什么情况用互斥锁比较划算:

  • 预计线程等待锁的时间较长
  • 单核处理器
  • 临界区有IO操作
  • 临界区代码复杂或者循环量大
  • 临界区竞争非常激烈

atomic和nonatomic

atomic:原子性

是默认的,给属性加上 atomic 修饰,可以保证属性的setter和getter都是原子性操作,相当于在getter和setter内部加了线程同步的锁,也就是保证setter和getter内部是线程同步的。
使用atomic并不能保证绝对的线程安全,对于要绝对保证线程安全的操作,还需要使用更高级的方式来处理,比如NSSpinLock、@syncronized等

nonatomic :非原子性

不是默认的,nonatomic修饰的属性,不做保持getter完整性保证,但在运行速度上要比atomic快。

iOS中的读写安全方案

思考如何实现以下场景:

  • 同一时间,只能有1个线程进行写的操作;
  • 同一时间,允许有多个线程进行读的操作;
  • 同一时间,不允许既有写的操作,又有读的操作。

上面的场景就是经典的“多读单写”,经常用于文件数据的读写操作,iOS中的实现方案有:

pthread_rwlock:读写锁
dispatxh_barrier_async:异步栅栏调用

pthread_rwlock

使用:

//导入头文件
#import <pthread/pthread.h>

@property (nonatomic ,assign) pthread_rwlock_t lock;

//初始化锁
    pthread_rwlock_init(&_lock,nil);

- (void)read {
    pthread_rwlock_rdlock(&_lock);
    sleep(1);
    NSLog(@"--%s--",__func__);
    pthread_rwlock_unlock(&_lock);
}
- (void)write {
    pthread_rwlock_wrlock(&_lock);
    sleep(1);
    NSLog(@"--%s--",__func__);
    pthread_rwlock_unlock(&_lock);
}

- (void)dealloc {
//销毁锁
    pthread_rwlock_destroy(&_lock);
}

dispatxh_barrier_async

这个函数传入的并发队列必须是自己通过dispatch_queue_create创建的
如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果

使用:

dispatch_queue_t queue = dispatch_queue_create("rw_queue",DISPATCH_QUEUE_CONCURRENT);
    //读
    dispatch_async(queue, ^{
        
    });
    //写
    dispatch_barrier_async(queue, ^{
        
    });
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容