iOS多线程之GCD

多线程

在iOS开发中为提高程序的运行效率会将比较耗时的操作放在子线程中执行,iOS系统进程默认启动一个主线程,用来响应用户的手势操作以及UI刷新,因此主线程又叫做UI线程。

Grand Central Dispatch (GCD) 是Apple开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并行任务。在Mac OS X 10.6雪豹中首次推出,也可在IOS 4及以上版本使用。

一、 线程与进程进程与线程

  1. 进程概念:
  • 进程是程序在计算机上的一次执行活动,打开一个app,就开启了一个进程,可包含多个线程。
  • 线程概念: 独立执行的代码段,一个线程同时间只能执行一个任务,反之多线程并发就可以在同一时间执行多个任务。
  • iOS程序中,主线程(又叫作UI线程)主要任务是处理UI事件,显示和刷新UI,(只有主线程有直接修改UI的能力)耗时的操作放在子线程(又叫作后台线程、异步线程)。在iOS中开子线程去处理耗时的操作,可以有效提高程序的执行效率,提高资源利用率。但是开启线程会占用一定的内存,(主线程的堆栈大小是1M,第二个线程开始都是512KB,并且该值不能通过编译器开关或线程API函数来更改)降低程序的性能。所以一般不要同时开很多线程。
  1. 线程相关
  • 同步线程:同步线程会阻塞当前线程去执行线程内的任务,执行完之后才会反回当前线程。
  • 异步线程:异步线程不会阻塞当前线程,会开启其他线程去执行线程内的任务。
  • 串行队列:线程任务按先后顺序逐个执行(需要等待队列里面前面的任务执行完之后再执行新的任务)。
  • 并发队列:多个任务按添加顺序一起开始执行(不用等待前面的任务执行完再执行新的任务),但是添加间隔往往忽略不计,所以看着像是一起执行的。
    并发VS并行:并行是基于多核设备的,并行一定是并发,并发不一定是并行。
  1. 多线程中会出现的问题
  • Critical Section(临界代码段)
    指的是不能同时被两个线程访问的代码段,比如一个变量,被并发进程访问后可能会改变变量值,造成数据污染(数据共享问题)。
  • Race Condition (竞态条件)
    当多个线程同时访问共享的数据时,会发生争用情形,第一个线程读取改变了一个变量的值,第二个线程也读取改变了这个变量的值,两个线程同时操作了该变量,此时他们会发生竞争来看哪个线程会最后写入这个变量,最后被写入的值将会被保留下来。
  • Deadlock (死锁)
    两个(多个)线程都要等待对方完成某个操作才能进行下一步,这时就会发生死锁。
  • Thread Safe(线程安全)
    一段线程安全的代码(对象),可以同时被多个线程或并发的任务调度,不会产生问题,非线程安全的只能按次序被访问。
    所有Mutable对象都是非线程安全的,所有Immutable对象都是线程安全的,使用Mutable对象,一定要用同步锁来同步访问(@synchronized)。
  1. 互斥锁:能够防止多线程抢夺造成的数据安全问题,但是需要消耗大量的资源

  2. 原子属性(atomic)加锁
    atomic :原子属性,为setter方法加锁,将属性以atomic的形式来声明,该属性变量就能支持互斥锁了。
    nonatomic: 非原子属性,不会为setter方法加锁,声明为该属性的变量,客户端应尽量避免多线程争夺同一资源。

  3. Context Switch (上下文切换)
    当一个进程中有多个线程来回切换时,context switch用来记录执行状态,这样的进程和一般的多线程进程没有太大差别,但会产生一些额外的开销。

程序:由源代码生成的可执行的应用
进程:一个正在运行的程序可以看做一个进程(正在运行的qq就是一个进程)进程拥有独⽴立运⾏行所需的全部资源
线程:程序中独立运行的代码段
一个进程是由一个或多个线程组成,进程值负责资源的调度和分配,线程才是程序真正的执行单位,负责代码的执行
进程是分配资源的最小单位
线程是程序运行的最小单位
一个进程里至少有一个线程,就是主线程,称作单线程
主线程负责执行程序的所有代码(UI展现以及刷新,网络请求本地存储等等),这些代码只能顺序执行,无法并发执行
在iOS开发过程中,关于UI的添加和刷新必须在主线程中执行,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。

多线程编程技术的优缺点比较

  1. NSObject
    自带了一个performSelectorInBackground:withObject:(最简单最轻量级别的,是隐身的创建线程)
    [Object performSelectorInBackground:@selector(doSomething:) withObject:nil];
  2. NSThread (抽象层次:低)
    优点:轻量级,简单易用,可以直接操作线程对
    缺点: 需要自己管理线程的生命周期,线程同步。线程同步对数据的加锁会有一定的系统开销。
  3. Cocoa NSOperation(抽象层次:中)
    优点:不需要关心线程管理,数据同步的事情,可以把精力放在学要执行的操作 上。基于GCD,是对GCD 的封装,比GCD更加面向对象
    缺点: NSOperation是个抽象类,使用它必须使用它的子类,可以实现它或者使用它定义好的两个子类NSInvocationOperation、NSBlockOperation.
  4. GCD 全称Grand Center Dispatch (抽象层次:高)
    优点:是 Apple 开发的一个多核编程的解决方法,简单易用,效率高,速度快,基于C语言,更底层更高效,并且不是Cocoa框架的一部分,自动管理线程生命周期(创建线程、调度任务、销毁线程)。
    缺点:使用GCD的场景如果很复杂,就有非常大的可能遇到死锁问题。

GCD抽象层次最高,使用也简单,因此,苹果也推荐使用GCD

二、 NSThread

1. 先创建线程,再启动线程

// 第一种创建方式 (这种方式必须手动开启)       
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(jisuanNumber) object:nil];
   [thread start];   
//  第二种创建方式
    [NSThread detachNewThreadSelector:@selector(jisuanNumber) toTarget:self withObject:nil];
    
-(void)jisuanNumber{
    @autoreleasepool {
        NSLog(@"%@, %d", [NSThread currentThread], [NSThread isMainThread]);
        NSInteger sum = 0;
        for (NSInteger i = 0; i < 1000000000 + 1; i++) {
            sum += i;
        }
        NSLog(@"%ld", sum);
        // 从子线程回到主线程
        [self performSelectorOnMainThread:@selector(result:) withObject:@(sum) waitUntilDone:YES];
    }
}
-(void)result:(NSNumber *)sum{
    NSLog(@"%ld" , sum.integerValue);
}

在多线程的执行方法doSomething中需要自行管理内存的释放,否则可能会警告提示:

XXXXX nsthread autoreleased with no pool in place – just leaking
或者创建一个自动释放池进行内存管理@autoreleasepool

2. 创建线程后自动启动线程

[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];

3. 隐式创建并启动线程

[self performSelectorInBackground:@selector(run) withObject:nil];

多线程中常用的方法

  • 从子线程回到主线程
  • 获取当前线程
  • 获得主线程
  • 判断一个线程是否是主线程
  • 线程名字的setting getting方法
// 从子线程回到主线程的两个方法
   1. [self performSelectorOnMainThread:@select withObject:nil waitUntilDone:YES]; 
   2. dispatch_async(dispatch_get_main_queue(), ^{ });
    
// 获取当前线程
   NSThread *current = [NSThread currentThread];
   
// 判断一个线程是否是主线程 (BOOL)
   [NSThread isMainThread];
   
// 获得主线程
  NSThread * mainthread = [NSThread mainThread];    

// 线程的名字——setter方法
- (void)setName:(NSString *)n;    

// 线程的名字——getter方法
- (NSString *)name;

4. 线程状态控制方法

  1. 启动线程方法

     -(void)start;  
     // 线程进入就绪状 态 -> 运行状态。当线程任务执行完毕,自动进入死亡状态
    

2. 阻塞(暂停)线程方法

 ```
 +(void)sleepUntilDate:(NSDate *)date;
     +(void)sleepForTimeInterval:(NSTimeInterval)ti;
  1. 线程进入阻塞状态,强制停止线程

    +(void)exit;
    // 线程进入死亡状态
    

三、 NSOperation

NSOperation是苹果提供给我们的一套多线程解决方案。实际上NSOperation是基于GCD更高一层的封装,但是比GCD更简单易用、代码可读性也更高。

NSOperation需要配合NSOperationQueue来实现多线程。因为默认情况下,NSOperation单独使用时系统同步执行操作,并没有开辟新线程的能力,只有配合NSOperationQueue才能实现异步执行。

注意事项:

  1. NSOperation和NSOperationQueue本身并不是多线程
  2. NSOperation : 任务快 NSOperationQueue:任务队列
  3. 实现多线程的方式是把若干个任务快添加到任务队列,这个时候操作系统会自己创建合适数量的Tread(线程),来完成队列里的任务块
  4. 队列里的任务块会按照队列里的顺序一一开始,但是每个任务块结束的时间并不一定,可能最后开始的任务最先完成
  5. NSOperation是一个抽象类 一般使用他的子类 iOS里自带了两个他的子类,一种是target思想的子类,另外一种是block思想的子类
  6. 操作系统创建线程的数量取决于:
* 队列里面任务的数量; 
* CPU的使用情况; 
* 内存的使用情况 
* 其他的资源

1. NSOperation和NSOperationQueue的基本使用

创建任务

NSOperation是个抽象类,并不能封装任务。我们只有使用它的两个子类来封装任务

  1. 使用子类NSInvocationOperation
  2. 使用子类NSBlockOperation
NSOperation 第一个子类 NSInvocationOperation

在没有使用NSOperationQueue、单独使用NSInvocationOperation的情况下,NSInvocationOperation在主线程执行操作,并没有开启新线程。

// 1.创建NSInvocationOperation对象
    NSInvocationOperation *ope1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(ope1Action) object:nil];
    NSInvocationOperation *ope2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(ope2Action) object:nil];
    NSInvocationOperation *ope3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(ope3Action) object:nil];

// 2.调用start方法开始执行操作
    [ope1 start];
    [ope2 start];
    [ope3 start];
NSOperation第二个子类NSBlockOperation

在没有使用NSOperationQueue、单独使用NSBlockOperation的情况下,NSBlockOperation也是在主线程执行操作,并没有开启新线程。

    NSBlockOperation *blockOp1 = [NSBlockOperation blockOperationWithBlock:^{
       // block 体里面写具体完成的任务
        NSLog(@"任务一 %@ %d", [NSThread currentThread], [NSThread isMainThread]);
    }];
    NSBlockOperation *blockOp2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任务二 %@ %d", [NSThread currentThread], [NSThread isMainThread]);
    }];
    NSBlockOperation *blockOp3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任务三 %@ %d", [NSThread currentThread], [NSThread isMainThread]);
    }];
    
    [blockOp1 start];
    [blockOp2 start];
    [blockOp3 start];

但是,NSBlockOperation还提供了一个方法addExecutionBlock:,通过addExecutionBlock:就可以为NSBlockOperation添加额外的操作,这些额外的操作就会在其他线程并发执行。

    NSBlockOperation * blockOp = [NSBlockOperation blockOperationWithBlock:^{
        // 在主线程
        NSLog(@"1------%@", [NSThread currentThread]);
    }];    

    // 添加额外的任务(在子线程执行)
    [blockOp addExecutionBlock:^{
        NSLog(@"2------%@", [NSThread currentThread]);
    }];
    
    [blockOp start];
创建队列

和GCD中的并发队列、串行队列略有不同的是:NSOperationQueue一共有两种队列:主队列、其他队列。其中其他队列同时包含了串行、并发功能。下边是主队列、其他队列的基本创建方法和特点。

  1. 主队列
    凡是添加到主队列中的任务(NSOperation),都会放到主线程中串行执行
    NSOperationQueue *queue = [NSOperationQueue mainQueue];

  2. 其他队列(非主队列)
    添加到这种队列中的任务(NSOperation),就会自动放到子线程中执行
    同时包含了:串行、并发功能
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

将任务加入到队列中
  1. 第一种 -(void)addOperation:(NSOperation *)ope;

需要先创建任务,再将创建好的任务加入到创建好的队列中去

   NSInvocationOperation *ope = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(ope1Action) object:nil];
   NSOperationQueue *queue = [NSOperationQueue mainQueue];
   [queue addOperation:ope];
  1. 第二种 -(void)addOperationWithBlock:(void (^)(void))block;

无需先创建任务,在block中添加任务,直接将任务block加入到队列中。

```

// 1. 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

// 2. 添加操作到队列中:addOperationWithBlock:
[queue addOperationWithBlock:^{
    for (int i = 0; i < 2; ++i) {
        NSLog(@"-----%@", [NSThread currentThread]);
    }
}];
```

2. 最大并发数

NSOperationQueue创建的其他队列同时具有串行、并发功能,之前演示的为并发功能,串行功能的实现就需要这个关键参数来实现maxConcurrentOperationCount——最大并发数。

  • maxConcurrentOperationCount默认情况下为-1,表示不进行限制,默认为并发执行。
  • 当maxConcurrentOperationCount为1时,进行串行执行。
  • 当maxConcurrentOperationCount大于1时,进行并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整。

3. 一些其他方法

  1. -(void)cancel;

NSOperation提供的方法,可取消单个操作

  1. -(void)cancelAllOperations;

    NSOperationQueue提供的方法,可以取消队列的所有操作

  2. -(void)setSuspended:(BOOL)b;

可设置任务的暂停和恢复,YES代表暂停队列,NO代表恢复队列

  1. -(BOOL)isSuspended;

判断暂停状态

注意: 这里的暂停和取消并不代表可以将当前的操作立即取消,而是当当前的操作执行完毕之后不再执行新的操作。
暂停和取消的区别就在于:暂停操作之后还可以恢复操作,继续向下执行;而取消操作之后,所有的操作就清空了,无法再接着执行剩下的操作。

四、 GCD 多线程

GCD是苹果最推荐的多线程技术,GCD的核心是往dispatch queue里添加要执行的任务,由queue管理任务的执行。

dispatch queue有两种:

  • serial queue(串行) 依次执行任务
  • concurrent queue(并行) 同时开始执行任务,但不一定同时结束

1. GCD创建步骤

队列的创建方式:

// 串行队列的创建方法
dispatch_queue_t queue= dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue= dispatch_queue_create("test.queue", DISPATCH_QUEUE_CONCURRENT);

**执行方式的创建: **

// 同步执行任务创建方法
dispatch_sync(queue, ^{
    NSLog(@"%@",[NSThread currentThread]);    // 这里放任务代码
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
    NSLog(@"%@",[NSThread currentThread]);    // 这里放任务代码
});

2. GCD共有三种队列,两种执行方式,就有6种组合方式,分别如下

  1. 并发队列 + 同步执行
  2. 并发队列 + 异步执行
  3. 串行队列 + 同步执行
  4. 串行队列 + 异步执行
  5. 主队列 + 同步执行
  6. 主队列 + 异步执行

下表为6种组合的区别:

并发队列 串行队列 主队列
同步(sync) 同步(sync) 没有开启新线程,串行执行任务 没有开启新线程,串行执行任务 没有开启新线程,串行执行任务
异步(async) 有开启新线程,并发执行任务 有开启新线程(1条),串行执行任务 没有开启新线程,串行执行任务

3. 串行队列的创建方式

第一种: 主队列

特点:这些任务都是在主线程一次完成; 任务块全部采用block的形式来封装,任务添加到队列和任务块的书写合在了一起

    dispatch_queue_t queue = dispatch_get_main_queue();// 获取主队列函
    dispatch_async(queue, ^{
        NSLog(@"任务一:%@, %d", [NSThread currentThread], [NSThread isMainThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任务二:%@, %d", [NSThread currentThread], [NSThread isMainThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任务三:%@, %d", [NSThread currentThread], [NSThread isMainThread]);
    });
第二种: 用户队列

特点是:队列里面的任务会依次在子线程里按顺序完成
参数设置:

第一个参数:队列的名字,字符串类型,苹果推荐使用反向域名的形式com.baidu.www

第二个参数:决定了这个队列到底是串行还是并行

 dispatch_queue_t queue = dispatch_queue_create("com.guohh.www", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        NSLog(@"任务一:%@, %d", [NSThread currentThread], [NSThread isMainThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任务二:%@, %d", [NSThread currentThread], [NSThread isMainThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任务三:%@, %d", [NSThread currentThread], [NSThread isMainThread]);
    });

4. 并发队列的创建方式

第一种: 全局队列

参数设置:
第一个参数:队列的优先级,在这里有4个优先级

  • DISPATCH_QUEUE_PRIORITY_HIGH
  • DISPATCH_QUEUE_PRIORITY_DEFAULT
  • DISPATCH_QUEUE_PRIORITY_LOW
  • DISPATCH_QUEUE_PRIORITY_BACKGROUND

第二个参数:苹果的保留参数,目前没有用到

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        NSLog(@"任务一:%@, %d", [NSThread currentThread], [NSThread isMainThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任务二:%@, %d", [NSThread currentThread], [NSThread isMainThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任务三:%@, %d", [NSThread currentThread], [NSThread isMainThread]);
    });
第二种
dispatch_queue_t queue = dispatch_queue_create("com.guohh.lxj", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"任务一:%@, %d", [NSThread currentThread], [NSThread isMainThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任务二:%@, %d", [NSThread currentThread], [NSThread isMainThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任务三:%@, %d", [NSThread currentThread], [NSThread isMainThread]);
    });

5. GCD的其他方法

1. GCD的栅栏方法 dispatch_barrier_async

我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于栅栏一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。这就需要用到dispatch_barrier_async方法在两个操作组间形成栅栏。

-(void)barrier
{
    dispatch_queue_t queue = dispatch_queue_create("12312312", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"----1-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----2-----%@", [NSThread currentThread]);
    });

    dispatch_barrier_async(queue, ^{
        NSLog(@"----barrier-----%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"----3-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----4-----%@", [NSThread currentThread]);
    });
}

也就是说,先执行栅栏之前的任务,执行完之后才执行栅栏之后的任务。

2. GCD的延时执行方法 dispatch_after

当我们需要延迟执行一段代码时,就需要用到GCD的dispatch_after方法。

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    // 2秒后异步执行这里的代码...
   NSLog(@"run-----");
});

3. GCD的一次性代码(只执行一次) dispatch_once

我们在创建单例、或者有整个程序运行过程中只执行一次的代码时,我们就用到了GCD的dispatch_once方法。使用dispatch_once函数能保证某段代码在程序运行过程中只被执行1次。

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    // 只执行1次的代码(这里面默认是线程安全的)
});

<font style="color:green">单例的创建方法</font>

// 第一种方式
static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if (_dataManager == nil) {
            _dataManager = [[DataManager alloc] init];
        };
    });
    return _dataManager;
        
 // 第二种方式
 @synchronized(_dataManager) {
        if (_dataManager == nil) {
            _dataManager = [[DataManager alloc] init];
        }
    }
    return _dataManager;

4. 队列组 dispatch_group

dispatch_queue中所有的任务执行完成后在做某种操作,通常情况下还需要使用dispatch_group_notify 用来检测分组里面的任务。在串行队列中,可以把该操作放到最后一个任务执行完成后继续;在并行队列中,可以把该操作放在除了第一个的任意位置(此时还检测不到组里面有其他的任务)。
使用场景 :分别异步执行2个耗时操作,然后当2个耗时操作都执行完毕后再回到主线程执行操作。这时候我们可以用到GCD的队列组。

  • 我们可以先把任务放到队列中,然后将队列放入队列组中。
  • 调用队列组的dispatch_group_notify回到主线程执行操作。
dispatch_group_t group =  dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
   // 执行1个耗时的异步操作
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
   // 执行1个耗时的异步操作
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
   // 等前面的异步操作都执行完毕后,回到主线程...
});

通过都多线程对数据库进行操作是需要注意:可以同时又多个任务对同一块数据进行读取,但是多个任务同时往一个地方写入数据的时候会出现问题。

5. GCD的快速迭代方法 dispatch_apply

通常我们会用for循环遍历,但是GCD给我们提供了快速迭代的方法dispatch_apply,使我们可以同时遍历。比如说遍历0~5这6个数字,for循环的做法是每次取出一个元素,逐个遍历。dispatch_apply可以同时遍历多个数字。但得到的结果不是按顺序排列的,而是随机产生的。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {
    NSLog(@"%zd------%@",index, [NSThread currentThread]);
});

6. async和sync的区别

两个错误说法 :

  • 错误说法一:sync 是同步 async是异步
  • 错误说法二:sync 是主线程 async是子线程

区别在于:sync会等到线程任务处理完成之后再去执行那个block外面的任务;对于async来说不会等线程block里面的任务执行完毕就会去block外面去执行任务

五、 线程死锁的两种场景

1. 同步线程中使用 dispatch_get_main_queue (回到主线程)

场景:

NSLog(@"1"); // 任务1
dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"2"); // 任务2
});
NSLog(@"3"); // 任务3

分析: 首先执行任务1,程序遇到了同步线程,那么它会进入等待,等待任务2执行完,然后执行任务3。但这是队列,有任务来,当然会将任务加到队尾,然后遵循FIFO原则执行任务,那么,现在任务2就会被加到最后,任务3排在了任务2前面, 这样就出现问题了:任务3要等任务2执行完才能执行,任务2由排在任务3后面,意味着任务2要在任务3执行完才能执行,所以他们进入了互相等待的局面。造成了死锁问题。

2. 异步线程中使用同步线程不当

场景:

dispatch_queue_t queue = dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"1"); // 任务1
dispatch_async(queue, ^{
   NSLog(@"2"); // 任务2
   dispatch_sync(queue, ^{  
       NSLog(@"3"); // 任务3
   });
   NSLog(@"4"); // 任务4
});
NSLog(@"5"); // 任务5

分析:
这个案例没有使用系统提供的串行或并行队列,而是自己通过dispatch_queue_create函数创建了一个DISPATCH_QUEUE_SERIAL的串行队列。

  1. 执行任务1;
  2. 遇到异步线程,将【任务2、同步线程、任务4】加入串行队列中。因为是异步线程,所以在主线程中的任务5不必等待异步线程中的所有任务完成;
  3. 因为任务5不必等待,所以2和5的输出顺序不能确定;
  4. 任务2执行完以后,遇到同步线程,这时,将任务3加入串行队列;
  5. 又因为任务4比任务3早加入串行队列,所以,任务3要等待任务4完成以后,才能执行。但是任务3所在的同步线程会阻塞,所以任务4必须等任务3执行完以后再执行。这就又陷入了无限的等待中,造成死锁。

3.其他锁🔐

在iOS开发中,除了同步锁有时候还会用到一些其他锁类型,在此简单介绍一下:

  1. NSRecursiveLock :递归锁,有时候“加锁代码”中存在递归调用,递归开始前加锁,递归调用开始后会重复执行此方法以至于反复执行加锁代码最终造成死锁,这个时候可以使用递归锁来解决。使用递归锁可以在一个线程中反复获取锁而不造成死锁,这个过程中会记录获取锁和释放锁的次数,只有最后两者平衡锁才被最终释放。
  2. NSDistributedLock:分布锁,它本身是一个互斥锁,基于文件方式实现锁机制,可以跨进程访问。
  3. pthread_mutex_t:同步锁,基于C语言的同步锁机制,使用方法与其他同步锁机制类似。

提示:在开发过程中除非必须用锁,否则应该尽可能不使用锁,因为多线程开发本身就是为了提高程序执行顺序,而同步锁本身就只能一个进程执行,这样不免降低执行效率。

总结:造成线程死锁的主要原因是同步线程(sync)的使用不当造成的。串行与并行针对的是队列,而同步与异步,针对的则是线程。最大的区别在于,同步线程要阻塞当前线程,必须要等待同步线程中的任务执行完,返回以后,才能继续执行下一任务;而异步线程则是不用等待。

六、多线程的使用

在iOS开发中为提高程序的运行效率会将比较耗时的操作放在子线程中执行,iOS系统进程默认启动一个主线程,用来响应用户的手势操作以及UI刷新,因此主线程又叫做UI线程。

  1. 在实际项目开发中并不是线程越多越好,如果开了大量的线程,会消耗大量的CPU资源,CPU会
    被累死,所以一般手机只开1~3个线程为宜,不超过5个。
  2. 多线程的优缺点:

优点:
* 能适当提高程序的执行效率

  • 能适当提高资源的利用率,这个利用率表现在(CPU,内存的利用率)

缺点:

 * 开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,

子线程占用512KB,如果开启大量的线程,会占用大量的内存空间,降低程序
的性能)
* 线程越多,CPU在调度线程上的开销就越大
* 程序设计就越复杂:比如线程之间的通信,多线程的数据共享,这些
都需要程序的处理,增加了程序的复杂度。

  1. 作用:
    • 显示\刷新UI界面;
    • 处理UI事件(例点击事件、滚动事件、拖拽事件等);
  2. 在iOS开发中使用线程的注意事项:
    • 别将比较耗时的操作放在主线程中
    • 耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的坏体验

七、总结

  1. 无论使用哪种方法进行多线程开发,每个线程启动后并不一定立即执行相应的操作,具体什么时候由系统调度(CPU空闲时就会执行)。

  2. 更新UI应该在主线程(UI线程)中进行,并且推荐使用同步调用,常用的方法如下:

    • -(void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait (或者-(void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL) wait;方法传递主线程[NSThread mainThread])
    • [NSOperationQueue mainQueue] addOperationWithBlock:
    • dispatch_sync(dispatch_get_main_queue(), ^{})
  3. NSThread适合轻量级多线程开发,控制线程顺序比较难,同时线程总数无法控制(每次创建并不能重用之前的线程,只能创建一个新的线程)。

  4. 对于简单的多线程开发建议使用NSObject的扩展方法完成,而不必使用NSThread。

  5. 可以使用NSThread的currentThread方法取得当前线程,使用 sleepForTimeInterval:方法让当前线程休眠。

  6. NSOperation进行多线程开发可以控制线程总数及线程依赖关系。

  7. 创建一个NSOperation不应该直接调用start方法(如果直接start则会在主线程中调用)而是应该放到NSOperationQueue中启动。

  8. 相比NSInvocationOperation推荐使用NSBlockOperation,代码简单,同时由于闭包性使它没有传参问题。

  9. NSOperation是对GCD面向对象的ObjC封装,但是相比GCD基于C语言开发,效率却更高,建议如果任务之间有依赖关系或者想要监听任务完成状态的情况下优先选择NSOperation否则使用GCD。

  10. 在GCD中串行队列中的任务被安排到一个单一线程执行(不是主线程),可以方便地控制执行顺序;并发队列在多个线程中执行(前提是使用异步方法),顺序控制相对复杂,但是更高效。

  11. 在GDC中一个操作是多线程执行还是单线程执行取决于当前队列类型和执行方法,只有队列类型为并行队列并且使用异步方法执行时才能在多个线程中执行(如果是并行队列使用同步方法调用则会在主线程中执行)。

  12. 相比使用NSLock,@synchronized更加简单,推荐使用后者。

  13. nonatomic和atomic对比,atomic线程安全,需要消耗大量的资源,nonatomic非线程安全,适合内存小的移动设备;iOS开发建议,所有属性声明为nonatomic,尽量避免多线程请多同一块资源,尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力;

参考:
iOS多线程——伯恩的遗产的博客
GCD死锁的五个案例

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,757评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,478评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,540评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,593评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,903评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,329评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,659评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,383评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,055评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,337评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,864评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,227评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,820评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,999评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,750评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,365评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,260评论 2 258

推荐阅读更多精彩内容