iOS多线程

GCD

  • 同步/异步 和 串行/ 并发
  • dispatch_barrier_async
  • dispatch_group

同步/异步 和 串行/ 并发

  • dispatch_sync(serial_queue, ^{//任务});
  • dispatch_async(serial_queue, ^{//任务});
  • dispatch_sync(concurrent_queue, ^{//任务});
  • dispatch_async(concurrent_queue, ^{//任务});

同步串行

//头条面试题
- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_sync(dispatch_get_main_queue(), ^{
        [self doSomething];
    });
}

上面代码的问题:
这段代码的逻辑会产生死锁,死锁的原因队列引起的循环等待.

iOS多线程_第1张图片
队列循环等待.png

viewDidLoad的执行过程中需要依赖于调用Block任务,而Block在队列中的排列(即栈的先进先出,因为Block任务在栈顶)导致他需要依赖viewDidLoad执行完毕,这种彼此依赖对方先完成,就导致了死锁发生。

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_sync(serialQueue, ^{
        [self doSomething];
    });
}

代码执行正常,没有问题:

iOS多线程_第2张图片
同步串行02.png
//美团
- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"1");
    dispatch_sync(global_queue, ^{
        NSLog(@"2");
        dispatch_sync(global_queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}//12345

只要同步去提交任务,无论队列是串行还是并发,最终都会在当前线程去执行

异步并发

//腾讯视频
- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_async(global_queue, ^{
        NSLog(@"1");
        [self performSelector:@selector(printLog) withObject:nil afterDelay:0];
        NSLog(@"3");
    });
  //输出结果13
}
- (void)printLog {
    NSLog(@"2");
}

performSelector:withObject:afterDelay:因为需要当前线程有runloop去执行timer事件,但GCD底层是不创建runloop的没有runloop,即使时间是0,方法也会失效。所以结果是13

dispatch_barrier_async()

iOS多线程_第3张图片
队列循环等待.png

怎样利用GCD实现多读单写?(滴滴,美团面试题)

iOS多线程_第4张图片
多读单写.png
#import "UserCenter.h"

@interface UserCenter()
{
    // 定义一个并发队列
    dispatch_queue_t concurrent_queue;
    
    // 用户数据中心, 可能多个线程需要数据访问
    NSMutableDictionary *userCenterDic;
}

@end

// 多读单写模型
@implementation UserCenter

- (id)init
{
    self = [super init];
    if (self) {
        // 通过宏定义 DISPATCH_QUEUE_CONCURRENT 创建一个并发队列
        concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
        // 创建数据容器
        userCenterDic = [NSMutableDictionary dictionary];
    }
    
    return self;
}

- (id)objectForKey:(NSString *)key
{
    __block id obj;
    // 同步读取指定数据
    dispatch_sync(concurrent_queue, ^{
        obj = [userCenterDic objectForKey:key];
    });
    
    return obj;
}

- (void)setObject:(id)obj forKey:(NSString *)key
{
    // 异步栅栏调用设置数据
    dispatch_barrier_async(concurrent_queue, ^{
        [userCenterDic setObject:obj forKey:key];
    });
}

@end

dispatch_group_async()

使用GCD实现三个需求:A、B、C三个任务并发,完成后执行任务D?(爱奇艺面试题)

#import "GroupObject.h"

@interface GroupObject()
{
    dispatch_queue_t concurrent_queue;
    NSMutableArray  *arrayURLs;
}

@end

@implementation GroupObject

- (id)init
{
    self = [super init];
    if (self) {
        // 创建并发队列
        concurrent_queue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
        arrayURLs = [NSMutableArray array];
    }

    return self;
}

- (void)handle
{
    // 创建一个group
    dispatch_group_t group = dispatch_group_create();
    
    // for循环遍历各个元素执行操作
    for (NSURL *url in arrayURLs) {
        
        // 异步组分派到并发队列当中
        dispatch_group_async(group, concurrent_queue, ^{
            
            //根据url去下载图片
            
            NSLog(@"url is %@", url);
        });
    }
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 当添加到组中的所有任务执行完成之后会调用该Block
        NSLog(@"所有图片已全部下载完成");
    });
}
@end

NSOperation

需要和NSOperationQueue配合使用来实现多线程方案

优势和特点:

  • 添加任务依赖
  • 任务执行状态控制
  • 最大并发量

任务执行状态控制

  • isReady - 当前任务是否就绪
  • isExecuting - 当前任务是否处于正在执行
  • isFinished - 当前任务是否完成
  • isCancelled - 当前任务是否取消

状态控制

  • 如果只重写main方法,底层控制变更任务执行完成状态,以及任务退出。
  • 如果只重写start方法,自行控制任务状态

系统是怎样移除一个isFinished=YES的NSOperation的?

系统是通过KVO监听实现的。

NSThread

启动流程

iOS多线程_第5张图片
NSThread.png

源码来自gnustep-base

- (void) start
{
  pthread_attr_t    attr;
  pthread_t     thr;

  if (_active == YES)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: @"[%@-$@] called on active thread",
        NSStringFromClass([self class]),
        NSStringFromSelector(_cmd)];
    }
  if (_cancelled == YES)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: @"[%@-$@] called on cancelled thread",
        NSStringFromClass([self class]),
        NSStringFromSelector(_cmd)];
    }
  if (_finished == YES)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: @"[%@-$@] called on finished thread",
        NSStringFromClass([self class]),
        NSStringFromSelector(_cmd)];
    }

  /* Make sure the notification is posted BEFORE the new thread starts.
   */
  gnustep_base_thread_callback();

  /* The thread must persist until it finishes executing.
   */
  [self retain];

  /* Mark the thread as active whiul it's running.
   */
  _active = YES;

  errno = 0;
  pthread_attr_init(&attr);
  /* Create this thread detached, because we never use the return state from
   * threads.
   */
  pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
  /* Set the stack size when the thread is created.  Unlike the old setrlimit
   * code, this actually works.
   */
  if (_stackSize > 0)
    {
      pthread_attr_setstacksize(&attr, _stackSize);
    }
    //指定线程的气筒函数为nsthreadLauncher
  if (pthread_create(&thr, &attr, nsthreadLauncher, self))
    {
      DESTROY(self);
      [NSException raise: NSInternalInconsistencyException
                  format: @"Unable to detach thread (last error %@)",
                  [NSError _last]];
    }
}
static void *nsthreadLauncher(void* thread)
{
    NSThread *t = (NSThread*)thread;//获取启动线程t
    setThreadForCurrentThread(t);
#if __OBJC_GC__
    objc_registerThreadWithCollector();
#endif
#if GS_WITH_GC && defined(HAVE_GC_REGISTER_MY_THREAD)
  {
    struct GC_stack_base    base;

    if (GC_get_stack_base(&base) == GC_SUCCESS)
      {
    int result;

    result = GC_register_my_thread(&base);
    if (result != GC_SUCCESS && result != GC_DUPLICATE)
      {
        fprintf(stderr, "Argh ... no thread support in garbage collection library\n");
      }
      }
    else
      {
    fprintf(stderr, "Unable to determine stack base to register new thread for garbage collection\n");
      }
  }
#endif

  /*
   * Let observers know a new thread is starting.
   */
  if (nc == nil)
    {
      nc = RETAIN([NSNotificationCenter defaultCenter]);
    }
  //发送通知
  [nc postNotificationName: NSThreadDidStartNotification
            object: t
          userInfo: nil];

  [t main];

  [NSThread exit];
  // Not reached
  return NULL;
}
- (void) main
{
  if (_active == NO)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: @"[%@-$@] called on inactive thread",
        NSStringFromClass([self class]),
        NSStringFromSelector(_cmd)];
    }

  [_target performSelector: _selector withObject: _arg];

}

多线程和锁

iOS当中都有哪些锁?

  • @synchronized
  • atomic
  • OSSpinLock
  • NSRecursiveLock
  • NSLock
  • dispatch_semaphore_t

@synchronized

  • 一般在创建单例对象的时候使用,保证多线程环境下创建对象是唯一的

@synchronized 的作用是创建一个互斥锁,保证此时没有其它线程对self对象进行修改。这个是objective-c的一个锁定令牌,防止self对象在同一时间内被其它线程访问,起到线程的保护作用。 一般在公用变量的时候使用,如单例模式或者操作类的static变量中使用。

指令@synchronized()需要一个参数。该参数可以使任何的Objective-C对象,包括self。这个对象就是互斥信号量。他能够让一个线程对一段代码进行保护,避免别的线程执行该段代码。

互斥锁使用格式

@synchronized(锁对象){ //需要锁定的代码 }

锁定一份代码只用1把锁,用多把锁是无效的。

互斥锁的优缺点

优点:能有效防止因多线程抢夺资源造成的数据安全问题

缺点:需要消耗大量的CPU资源

线程同步

多条线程在同一条线上执行(按顺序地执行任务)

互斥锁,就是使用了线程同步技术。

例如:一个电影院,有3个售票员。一场电影的总数量固定。3个售票员售票时,要判断是非还有余票。

#import "ViewController.h"

@interface ViewController ()
/** 售票员01 */
@property (nonatomic, strong) NSThread *thread01;
/** 售票员02 */
@property (nonatomic, strong) NSThread *thread02;
/** 售票员03 */
@property (nonatomic, strong) NSThread *thread03;

/** 票的总数 */
@property (nonatomic, assign) NSInteger ticketCount;

/** 锁对象 */
//@property (nonatomic, strong) NSObject *locker;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
//    self.locker = [[NSObject alloc] init];
    
    self.ticketCount = 100;
    
    self.thread01 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.thread01.name = @"售票员01";
    
    self.thread02 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.thread02.name = @"售票员02";
    
    self.thread03 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.thread03.name = @"售票员03";
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [self.thread01 start];
    [self.thread02 start];
    [self.thread03 start];
}

- (void)saleTicket
{
    while (1) {
        @synchronized(self) {
            // 先取出总数
            NSInteger count = self.ticketCount;
            if (count > 0) {
                self.ticketCount = count - 1;
                NSLog(@"%@卖了一张票,还剩下%zd张", [NSThread currentThread].name, self.ticketCount);
            } else {
                NSLog(@"票已经卖完了");
                break;
            }
        }
    }
}

@end
atomic
  • 修饰属性的关键字
  • 对被修饰对象进行原子操作(不负责使用)
@property(atomic) NSMutableArray * array;

self.array = [NSMutableArray array];//✅赋值是线程安全的

[self.array addObject:obj];//❎操作数组是不能保证线程安全的
atomic和nonatomic的对比

1、atomic和nonatomic用来决定编译器生成的getter和setter是否为原子操作。

2、atomic:系统生成的 getter/setter 会保证 get、set 操作的完整性,不受其他线程影响。getter 还是能得到一个完好无损的对象(可以保证数据的完整性),但这个对象在多线程的情况下是不能确定的,比如上面的例子。

也就是说:如果有多个线程同时调用setter的话,不会出现某一个线程执行完setter全部语句之前,另一个线程开始执行setter情况,相当于函数头尾加了锁一样,每次只能有一个线程调用对象的setter方法,所以可以保证数据的完整性。

atomic所说的线程安全只是保证了getter和setter存取方法的线程安全,并不能保证整个对象是线程安全的。

3、nonatomic:就没有这个保证了,nonatomic返回你的对象可能就不是完整的value。因此,在多线程的环境下原子操作是非常必要的,否则有可能会引起错误的结果。但仅仅使用atomic并不会使得对象线程安全,我们还要为对象线程添加lock来确保线程的安全。

4、nonatomic的速度要比atomic的快。

5、atomic与nonatomic的本质区别其实也就是在setter方法上的操作不同

atomic对象setter和getter方法的实现:

- (void)setCurrentImage:(UIImage *)currentImage
{
    @synchronized(self) {
        if (_currentImage != currentImage) {
            [_currentImage release];
            _currentImage = [currentImage retain];
        }
    }
}

- (UIImage *)currentImage
{
    @synchronized(self) {
        return _currentImage;
    }
}

OSSpinLock - 自旋锁

  • 循环等待询问,不释放当前资源
  • 用于轻量级数据访问。比如简单的int值+1/-1操作

runtime中有使用到,进行引用计数的+1/-1操作

NSLock

一般用于解决一些细粒度的线程同步问题,用来保证各个线程互斥来进入自己的临界区。

//蚂蚁金服
- (void)methodA {
  [nslock lock];
  [self methodB];
  [nslock unlock];
}
- (void)methodB {
  [nslock lock];
//操作逻辑
  [nslock unlock];
}

以上代码的问题:死锁

methodA中在某一线程的调用下,已经对线程加了lock,如果在methodB再次调用lock,由于锁已经被使用了且没有解锁,所以它需要等待锁被解除,这样就导致了死锁,线程被阻塞住了。

NSRecursiveLock - 递归锁

NSRecursiveLock实际上定义的是一个递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中。

递归锁会跟踪它被lock的次数。每次成功的lock都必须平衡调用unlock操作。只有所有达到这种平衡,锁最后才能被释放,以供其它线程使用。

上面的问题可以通过使用递归锁进行解决:

- (void)methodA {
  [recursiveLock lock];
  [self methodB];
  [recursiveLock unlock];
}
- (void)methodB {
  [recursiveLock lock];
//操作逻辑
  [recursiveLock unlock];
}

dispatch_semaphore_t

dispatch_semaphore_create(long value); // 创建信号量
dispatch_semaphore_signal(dispatch_semaphore_t semaphore); // 发送信号量
dispatch_semaphore_wait(dispatch_semaphore_t semaphore, dispatch_time_t timeout); // 等待信号量
  • dispatch_semaphore_create(long value);和GCD的group等用法一致,这个函数是创建一个dispatch_semaphore_类型的信号量,并且创建的时候需要指定信号量的大小。
  • dispatch_semaphore_signal(dispatch_semaphore_t semaphore);发送信号量。该函数会对信号量的值进行加1操作。
  • dispatch_semaphore_wait(dispatch_semaphore_t semaphore, dispatch_time_t timeout);等待信号量。如果信号量值为0,那么该函数就会一直等待,也就是不返回(相当于阻塞当前线程),直到该函数等待的信号量的值大于等于1,该函数会对信号量的值进行减1操作,然后返回。

dispatch_semaphore_create(long value);在底层会生成:

struct semaphore {
  int value; //信号量的值
  List ; //一个线程列表
}

dispatch_semaphore_wait(dispatch_semaphore_t semaphore, dispatch_time_t timeout)在底层的实现逻辑大致如下:

dispatch_semaphore_wait() 
{
  S.value -= 1;
  if S.value < 0 then Block(S.list);//阻塞-主动行为
}

dispatch_semaphore_signal()底层实现逻辑大致如下:

dispatch_semaphore_signal()
{
  S.value += 1;
  if S.value <= 0 then wakeup(S.list);//唤醒-被动行为
}

通常等待信号量和发送信号量的函数是成对出现的。并发执行任务时候,在当前任务执行之前,用dispatch_semaphore_wait函数进行等待(阻塞),直到上一个任务执行完毕后且通过dispatch_semaphore_signal函数发送信号量(使信号量的值加1),dispatch_semaphore_wait函数收到信号量之后判断信号量的值大于等于1,会再对信号量的值减1,然后当前任务可以执行,执行完毕当前任务后,再通过dispatch_semaphore_signal函数发送信号量(使信号量的值加1),通知执行下一个任务......如此一来,通过信号量,就达到了并发队列中的任务同步执行的要求。

总结

  • 怎样用GCD事项多读单写?

  • iOS系统为我们提供的几种多线程技术各自的特点是怎样的?

在iOS系统当中主要提供了三种多线程技术,分别为GCD, NSOperation, NSThread,一般使用GCD来解决一些简单的线程同步,包括一些子线程的分派,包括实现一些例如多读单写这种场景的问题的解决。对于NSOperation,比如AFNetworking,SDWebimageView,他们内部都会涉及到NSOperation,由于他的特点是可以方便我们对任务的状态进行控制,包括可以控制依赖的添加和移除依赖。对于NSThread往往我们用他来实现同一个常驻线程。

  • NSOperation对象在Finished之后是怎样从队列中移除掉的?

NSOperation对象在Finished之后,会在内部用KVO的形式通知NSOperationQueue达到对NSOperation的移除

你都用过哪些锁?结合实际谈谈你是怎样使用的?

你可能感兴趣的:(iOS多线程)