Hello, World!

iOS并发编程基础

字数统计: 6.8k阅读时长: 28 min
2017/03/22 Share

并发 描述的概念是同时有多个任务在执行,这些任务在单核CPU上以分时的形式在运行(宏观上可以看成多个任务同时执行),而在多核CPU上才是真正的同时执行。在iOS开发中,Apple提供了四种**API(pthread,NSThread,GCD,NSOperation)**用于并发编程。本文以讲解四种API的使用方法为切入点,带你了解多线程的全貌。

在学习并发编程之前,首先我们要知道线程的概念,以及线程的一些相关基础知识,这是学习并发编程的基础。

什么是线程

线程(thread)是进程的基本单位,一个进程包含一个或多个线程(至少包含一个),操作系统的调度器可以直接调度线程。所有的并发编程API都是构建在线程上的。

进程和线程的区别:
1,进程是操作系统资源分配的最小单位。线程是CPU调度的最小单位。(进程有独立的资源和内存而线程没有)
2,进程有独立的地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护,这种操作非常昂贵。而线程是共享进程中的数据,使用相同的地址空间,因此CPU切换一个线程的花费远比进程小的多,同时创建一个线程的开销也比进程要小的多。
3,线程间通信更方便,同一进程下的线程共享全局变量静态变量的数据。
注意:1,iOS不能主动通过代码创建进程fork()。2,iOS一个程序就是一个进程,进程间不会相互影响,一个线程挂掉会导致整个进程挂掉。

线程的状态

Screen%20Shot%202017-02-22%20at%209.04.58%20PM

  • 新建:线程对象在内存中被创建出来。

  • 就绪:除了CPU以外的所有资源都已分配好了,等待CPU调度。(已经加入可调度线程池中)

  • 运行:CPU正在运行该线程。(当CPU切换执行其它线程的时候,当前线程退回到就绪状态)

  • 阻塞:除了CPU以外,还有其它资源没有分配,(阻塞状态的线程,被移出了可调度线程池)

  • 死亡:线程被销毁,占用的内存空间被释放。

并发编程的四种API介绍

pthread

pthread(posix线程) 是一套纯C的API,平时几乎不会使用,如果没有兴趣可以不用了解,当然如果想多掌握一些的话,可以继续往下阅读。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
如何使用:
1,导入头文件 #import <pthread.h>
2,调用函数
int pthread_create(pthread_t * __restrict,pthread_attr_t * __restrict,void * (*) (void *),void * __restrict);

/*参数解释
返回值是0 表示创建线程成功 非0表示创建线程失败

1,pthread_t * 线程的标示
2,pthread_attr_t 线程的属性
3,void* (*) (void *)
返回值 函数名 参数 函数签名 void * 约等于OC中的id任意类型
4,void * 给函数的参数
*/

NSThread

NSThread是pthread面向对象的封装。在面向对象的编程环境中,使用也更加习惯方便。一个NSThread实例对应一个线程。

一 如何使用:

【类方法/自动开启】
优点:使用简单,线程创建好之后处于就绪(runnable)状态,CPU可直接调度。
缺点:无法拿到线程对象本身做更详细的设置。

1
2
+ (void)detachNewThreadWithBlock:(void (^)(void))block;
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;

【实例方法/手动开启】
优点:可以获取到线程对象,做更详细的设置。(设置线程优先级、线程名称等)
缺点:线程创建好以后处于新建(new)状态,需要手动调用start/main方法进入就绪(runnable)状态,没有类方法使用简单。

1
2
3
4
5
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument;
- (instancetype)initWithBlock:(void (^)(void))block;

- (void)start; // start方法在子线程执行,不能重复开启(不能调用两遍以上start方法)
- (void)main; // main方法在主线程执行,可以重复调用

注意:- (instancetype)init;方法得到的是主线程。

【隐式创建】
优缺点和类方法相同

1
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;

注意:隐式创建方法不属于NSThread,而是属于NSObject的NSThreadPerformAdditions分类。

【自定义】

1
2
3
第一步:自定义一个线程类。例如XXThread,继承自NSThread类。
第二步:在XXThread类中,重写main方法,把需要在后台子线程中执行的代码放进去。
第三步:通过alloc+init方法创建XXThread对象,然后调用start方法即可。

二 常见属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 线程优先级,double类型,取值范围是:0.0~1.0,默认是0.5,1.0表示优先级最高,优先级越高,表示被CPU调度的概率越高。逐渐被废弃,用qualityOfService替代
thread.threadPriority

// 线程服务质量约等于线程优先级,枚举值,默认是NSQualityOfServiceDefault=-1,优先级最低。iOS8之后比threadPriority好用
thread.qualityOfService;

// 线程的名称,主线程默认名称是main,子线程默认名称为null,
thread.name;

// 判断当前线程是否是主线程,只读属性
mainThread

// 判断当前线程是否正在执行任务,只读属性
executing

// 判断当前线程是否执行结束,只读属性
finished

// 判断当前线程是否被取消,只读属性
cancelled

// 线程在stack栈上占用的存储空间,通常为512kb,线程释放后,空间收回。
thread.stackSize;

三 常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 休眠到指定时间点
+ (void)sleepUntilDate:(NSDate *)date
// 休眠一段时间
+ (void)sleepForTimeInterval:(NSTimeInterval)time

// 获取当前线程对象
[NSThread currentThread];
// 获取主线程对象
[NSThread mainThread];

// 退出线程
[NSThread exit];

// 如果自定义线程类,把要执行的任务,放在重写的main方法中。
- (void)main // thread body method

直接操作线程对象可能引发一个问题,如果你的代码和项目中使用到的框架代码都创建了自己的线程时,那么活动的线程数量有可能以指数级增长。这在大型项目中是一个常见问题。例如:在8核CPU中,你创建了8个线程来发挥CPU性能。然而在这些线程中调用到的框架代码也做了同样的事情(因为框架不知道你也创建了线程),这样就会很快产生成百上千的线程,代码每个部分自身都没有问题,但是最终还是产生了问题。每个线程都会消耗一些内存和内核资源。
接下来,我将介绍并发编程API:GCD,它们是通过管理一个可以协同使用的线程池,来解决上面的问题的。

GCD

GCD(Grand Central Dispatch)是多核优化的解决方案,是一套纯C的API,核心思想是在系统底层维护了一个线程池,自动管理线程对象,开发者不用直接和线程打交道,只需要关注队列和任务即可。

在学习GCD编程之前,首先我们要理解队列和任务的概念,这是基础,也是GCD的核心概念。

队列

用来存放任务的,先进先出的(FIFO,First in first out)容器就是队列。

1
2
// 队列
dispatch_queue_t

队列分为两类:串型队列(Serial Queue)和并发队列(Concurrent Queue)。

【串行队列】
内部的任务一个接着一个执行,等待上一个任务执行完毕,才会接着执行下一个任务。

1
2
3
4
5
6
7
8
// 获取串行队列的方式  1.系统提供  2.手动创建
// 1.获取系统提供的串行队列
dispatch_queue_t queue = dispatch_get_main_queue();

// 2.创建一个串行队列
// 队列的名称com.app.serial
// 队列的类型,是枚举值。DISPATCH_QUEUE_SERIAL表示串行
dispatch_queue_t queue = dispatch_queue_create("com.app.serial", DISPATCH_QUEUE_SERIAL);

【并发队列】
队列内部的多个任务同一时刻执行。

1
2
3
4
5
6
7
8
9
10
// 获取并发队列的方式  1.系统提供  2.手动创建
// 1.获取系统提供的并发队列
// 队列优先级 DISPATCH_QUEUE_PRIORITY_DEFAULT
// 第二个参数flags作为保留字段备用,一般都直接填0
dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// 2.创建一个并发队列
// 队列的名称。 作用:可以协助开发调试和崩溃报告分析
// 队列的类型,是枚举值。DISPATCH_QUEUE_CONCURRENT表示并发。
dispatch_queue_t queue = dispatch_queue_create("com.app.concurrent", DISPATCH_QUEUE_CONCURRENT);

【主队列】也叫【全局串行队列】
主队列是特殊的串行队列,由系统提供,使用的时候只要去获取即可,无需开发者手动创建,特殊点在于主队列中的任务只能由主线程去执行。

特点:
1,主线程正在执行代码暂时不调度任务,等主线程执行结束后再调度任务。这是主队列中同步执行导致死锁的原因。
2,UI更新的操作都要放在主线程中处理。

1
2
// 获取系统提供的主队列
dispatch_queue_t queue = dispatch_get_main_queue();

【全局并发队列】
全局并发队列,使用的时候去获取即可,无需开发者手动创建。

1
2
3
4
5
6
7
8
9
// 获取系统提供的全局并发队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
// 参数解释
1,指定队列的优先级。
DISPATCH_QUEUE_PRIORITY_HIGH (2)
DISPATCH_QUEUE_PRIORITY_DEFAULT (0)// 自定义队列的优先级都是默认优先级
DISPATCH_QUEUE_PRIORITY_LOW (-2)
DISPATCH_QUEUE_PRIORITY_BACKGROUND (INT16_MIN)
2,作为保留字段备用(一般为0)

任务

添加到队列中的可执行代码块就是任务。

1
2
// 类型
dispatch_block_t

注意:dispatch_block_t的定义是typedef void (^dispatch_block_t)(void),
由定义可以看出它本质就是一个无参数无返回值的block。

任务添加到队列的方式

任务添加到队列的方式有两个:同步执行(dispatch_sync)和异步执行(dispatch_async),两者区别在于是否开启新线程,同步执行不开启新线程,任务在当前线程执行,异步开启新线程,任务在新线程执行。

【同步执行】

1
2
3
4
5
6
7
8
// 队列
dispatch_queue_t queue = dispatch_queue_create("com.app.concurrent", DISPATCH_QUEUE_CONCURRENT);
// 任务
dispatch_block_t block = ^ {
[NSThread sleepForTimeInterval:2.f];
NSLog(@"%@", [NSThread currentThread]);
};
dispatch_sync(queue, block);

【异步执行】

1
2
3
4
5
6
7
8
9
// 队列
dispatch_queue_t queue = dispatch_queue_create("com.app.concurrent", DISPATCH_QUEUE_CONCURRENT);
// 任务
dispatch_block_t block = ^ {
[NSThread sleepForTimeInterval:2.f];
NSLog(@"%@", [NSThread currentThread]);
};

dispatch_async(queue, block);

上面这么写的目的,是帮助理解,在开发中代码要简写。

1
2
3
4
5
6
7
8
// 1,创建串行队列
dispatch_queue_t queue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
//
// 2,异步执行
dispatch_async(queue, ^{
NSLog(@"%@",[NSThread currentThread]);
});

在了解完GCD的核心基本概念之后,我们学习GCD的使用方法

串型队列的执行

【串行队列➕同步执行】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- (void)syncAndSerial {
NSLog(@"__BEGIN:%@__", [NSThread currentThread]);

dispatch_queue_t queue = dispatch_queue_create("com.app.serial", DISPATCH_QUEUE_SERIAL);

// 第一个任务
dispatch_sync(queue, ^{

// 这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];

NSLog(@"----执行第一个任务---当前线程%@", [NSThread currentThread]);
});

// 第二个任务
dispatch_sync(queue, ^{

// 这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];

NSLog(@"----执行第二个任务---当前线程%@", [NSThread currentThread]);
});

// 第三个任务
dispatch_sync(queue, ^{

// 这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];

NSLog(@"----执行第三个任务---当前线程%@", [NSThread currentThread]);
});

NSLog(@"__END:%@__", [NSThread currentThread]);
}

log信息显示如下:

1
2
3
4
5
6
__BEGIN:<NSThread: 0x6000032ec0c0>{number = 1, name = main}__
----执行第一个任务---当前线程<NSThread: 0x6000032ec0c0>{number = 1, name = main}
----执行第二个任务---当前线程<NSThread: 0x6000032ec0c0>{number = 1, name = main}
----执行第三个任务---当前线程<NSThread: 0x6000032ec0c0>{number = 1, name = main}
__END:<NSThread: 0x6000032ec0c0>{number = 1, name = main}__

执行过程:当前线程执行到第一个任务时,会把任务添加到串行队列中,由于是同步的,必须再把任务执行完成,才可以继续向下执行第二个任务。
结论:同步执行 ➕ 串行队列,不开启新线程,在当前线程中依次执行。

【串行队列➕异步执行】

开启一个子线程,任务在子线程执行,为什么只开启一个,因为是串行队列,只有一个线程就可以按顺序执行队列中的所有任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- (void)asyncAndSerial {
NSLog(@"__BEGIN:%@__", [NSThread currentThread]);

dispatch_queue_t queue = dispatch_queue_create("com.app.serial", DISPATCH_QUEUE_SERIAL);

// 第一个任务
dispatch_async(queue, ^{

//这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];

NSLog(@"----执行第一个任务---当前线程%@",[NSThread currentThread]);
});

// 第二个任务
dispatch_async(queue, ^{

//这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];

NSLog(@"----执行第二个任务---当前线程%@",[NSThread currentThread]);
});

// 第三个任务
dispatch_async(queue, ^{

//这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];

NSLog(@"----执行第三个任务---当前线程%@",[NSThread currentThread]);
});

NSLog(@"__END:%@__", [NSThread currentThread]);
}

log信息显示如下:

1
2
3
4
5
__BEGIN:<NSThread: 0x6000032ec0c0>{number = 1, name = main}__
__END:<NSThread: 0x6000032ec0c0>{number = 1, name = main}__
----执行第一个任务---当前线程<NSThread: 0x6000032a2300>{number = 3, name = (null)}
----执行第二个任务---当前线程<NSThread: 0x6000032a2300>{number = 3, name = (null)}
----执行第三个任务---当前线程<NSThread: 0x6000032a2300>{number = 3, name = (null)}

结论:异步执行 ➕ 串行队列,开启一个新线程,任务依次执行。

并发队列的执行

【并发队列➕同步执行】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (void)syncAndConcurrent {
NSLog(@"__BEGIN:%@__", [NSThread currentThread]);

dispatch_queue_t queue = dispatch_queue_create("com.app.concurrent", DISPATCH_QUEUE_CONCURRENT);

dispatch_sync(queue, ^{

//这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];

NSLog(@"----执行第一个任务---当前线程%@",[NSThread currentThread]);
});

dispatch_sync(queue, ^{

//这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];

NSLog(@"----执行第二个任务---当前线程%@",[NSThread currentThread]);
});


dispatch_sync(queue, ^{

//这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];

NSLog(@"----执行第三个任务---当前线程%@",[NSThread currentThread]);
});

NSLog(@"__END:%@__", [NSThread currentThread]);
}

log信息显示如下:

1
2
3
4
5
6
__BEGIN:<NSThread: 0x6000032ec0c0>{number = 1, name = main}__
----执行第一个任务---当前线程<NSThread: 0x6000032ec0c0>{number = 1, name = main}
----执行第二个任务---当前线程<NSThread: 0x6000032ec0c0>{number = 1, name = main}
----执行第三个任务---当前线程<NSThread: 0x6000032ec0c0>{number = 1, name = main}
__END:<NSThread: 0x6000032ec0c0>{number = 1, name = main}__

结论:执行情况与同步执行 ➕ 串行队列一模一样,不开启新线程,在当前线程中依次执行。

【并发队列➕异步执行】

开启多个子线程,任务在子线程执行,具体开启多少个线程是不固定的(线程数,不由我们控制是由gcd来决定的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- (void)asyncAndConcurrent {
NSLog(@"__BEGIN:%@__", [NSThread currentThread]);

dispatch_queue_t queue = dispatch_queue_create("com.app.concurrent", DISPATCH_QUEUE_CONCURRENT);

// 第一个任务
dispatch_async(queue, ^{

//这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];

NSLog(@"----执行第一个任务---当前线程%@",[NSThread currentThread]);
});

// 第二个任务
dispatch_async(queue, ^{

//这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];

NSLog(@"----执行第二个任务---当前线程%@",[NSThread currentThread]);
});

// 第三个任务
dispatch_async(queue, ^{

//这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];

NSLog(@"----执行第三个任务---当前线程%@",[NSThread currentThread]);
});

NSLog(@"__END:%@__", [NSThread currentThread]);
}

log信息显示如下:

1
2
3
4
5
__BEGIN:<NSThread: 0x6000032ec0c0>{number = 1, name = main}__
__END:<NSThread: 0x6000032ec0c0>{number = 1, name = main}__
----执行第二个任务---当前线程<NSThread: 0x60000328cb40>{number = 11, name = (null)}
----执行第三个任务---当前线程<NSThread: 0x6000032bb2c0>{number = 12, name = (null)}
----执行第一个任务---当前线程<NSThread: 0x6000032b2380>{number = 8, name = (null)}

结论:开启若干新线程,任务并发执行。

主队列的执行

系统已经为我们提供了2个常用队列,主队列(全局串行队列)和全局并发队列,开发中很多时候都是使用系统提供的队列,不用自己手动创建

【主队列➕同步执行】

  • 前提:当前在主线程。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    - (void)syncAndMain {
    NSLog(@"__BEGIN:%@__", [NSThread currentThread]);

    dispatch_queue_t queue = dispatch_get_main_queue();

    // 第一个任务
    dispatch_sync(queue, ^{

    //这里线程暂停2秒,模拟一般的任务的耗时操作
    [NSThread sleepForTimeInterval:2];

    NSLog(@"----执行第一个任务---当前线程%@",[NSThread currentThread]);
    });

    // 第二个任务
    dispatch_sync(queue, ^{

    //这里线程暂停2秒,模拟一般的任务的耗时操作
    [NSThread sleepForTimeInterval:2];

    NSLog(@"----执行第二个任务---当前线程%@",[NSThread currentThread]);
    });

    // 第三个任务
    dispatch_sync(queue, ^{

    //这里线程暂停2秒,模拟一般的任务的耗时操作
    [NSThread sleepForTimeInterval:2];

    NSLog(@"----执行第三个任务---当前线程%@",[NSThread currentThread]);
    });

    NSLog(@"__END:%@__", [NSThread currentThread]);
    }

    结论:在主线程会死锁导致应用crash。
    注意:当程序执行到这段代码的时候,向主队列中添加一个任务,此时如果主线程要继续往下执行的话,必须要执行刚添加到主队列中的任务,只有把这个任务执行完毕之后,才可以继续,所以现在要等待调度该任务。而主队列有个特点就是只有主线程不忙的时候,才会去调度主线程执行任务,现在的情况是主线程很忙,所以不会去调度任务,一个等待调度任务执行,一个不让去调度任务,最终导致了死锁。

  • 前提:当前在子线程。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    - (void)othersyncAndMain {
    dispatch_queue_t concurrentQueue = dispatch_get_global_queue(0, 0);
    dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_async(concurrentQueue, ^{
    NSLog(@"__BEGIN:%@__", [NSThread currentThread]);

    // 第一个任务
    dispatch_sync(queue, ^{

    //这里线程暂停2秒,模拟一般的任务的耗时操作
    [NSThread sleepForTimeInterval:2];

    NSLog(@"----执行第一个任务---当前线程%@",[NSThread currentThread]);
    });

    // 第二个任务
    dispatch_sync(queue, ^{

    //这里线程暂停2秒,模拟一般的任务的耗时操作
    [NSThread sleepForTimeInterval:2];

    NSLog(@"----执行第二个任务---当前线程%@",[NSThread currentThread]);
    });

    // 第三个任务
    dispatch_sync(queue, ^{

    //这里线程暂停2秒,模拟一般的任务的耗时操作
    [NSThread sleepForTimeInterval:2];

    NSLog(@"----执行第三个任务---当前线程%@",[NSThread currentThread]);
    });

    NSLog(@"__END:%@__", [NSThread currentThread]);
    });
    }

    log信息显示如下:

    1
    2
    3
    4
    5
    __BEGIN:<NSThread: 0x600002bcd1c0>{number = 7, name = (null)}__
    ----执行第一个任务---当前线程<NSThread: 0x600002ba4900>{number = 1, name = main}
    ----执行第二个任务---当前线程<NSThread: 0x600002ba4900>{number = 1, name = main}
    ----执行第三个任务---当前线程<NSThread: 0x600002ba4900>{number = 1, name = main}
    __END:<NSThread: 0x600002bcd1c0>{number = 7, name = (null)}__

    结论:当前在子线程中可以正常执行。

【主队列➕异步执行】

不开线程,因为主队列的任务只有主线程可以执行。同步执行任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- (void)asyncAndMain {
NSLog(@"__BEGIN:%@__", [NSThread currentThread]);

dispatch_queue_t queue = dispatch_get_main_queue();

// 第一个任务
dispatch_async(queue, ^{

//这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];

NSLog(@"----执行第一个任务---当前线程%@",[NSThread currentThread]);
});

// 第二个任务
dispatch_async(queue, ^{

//这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];

NSLog(@"----执行第二个任务---当前线程%@",[NSThread currentThread]);
});

// 第三个任务
dispatch_async(queue, ^{

//这里线程暂停2秒,模拟一般的任务的耗时操作
[NSThread sleepForTimeInterval:2];

NSLog(@"----执行第三个任务---当前线程%@",[NSThread currentThread]);
});

NSLog(@"__END:%@__", [NSThread currentThread]);
}

log信息显示如下:

1
2
3
4
5
__BEGIN:<NSThread: 0x600002ba4900>{number = 1, name = main}__
__END:<NSThread: 0x600002ba4900>{number = 1, name = main}__
----执行第一个任务---当前线程<NSThread: 0x600002ba4900>{number = 1, name = main}
----执行第二个任务---当前线程<NSThread: 0x600002ba4900>{number = 1, name = main}
----执行第三个任务---当前线程<NSThread: 0x600002ba4900>{number = 1, name = main}

队列的区别比较表格

并发队列 串行队列 主队列
同步(sync) 在当前线程,依次执行任务 在当前线程,依次执行任务 死锁
异步(async) 开启新线程,并行执行任务 开启新线程,依次执行任务 在当前线程,依次执行任务

NSOperation

NSOperation是GCD面向对象的封装,在面向对象的编程环境中使用更加习惯方便,是苹果官方推荐的安全可靠的并发编程方式。

首先介绍两个核心类NSOperationQueue和NSOperation。

NSOperationQueue

  • 通过NSOperationQueue可以获取到主队列,也可以创建新队列。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (void)queueKinds {

// 获取主队列
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];

[mainQueue addOperationWithBlock:^{
NSLog(@"mainQueue1::%@", [NSThread currentThread]);
}];

[mainQueue addOperationWithBlock:^{
NSLog(@"mainQueue2::%@", [NSThread currentThread]);
}];


// 创建新队列
NSOperationQueue *customQueue = [[NSOperationQueue alloc] init];

[customQueue addOperationWithBlock:^{
NSLog(@"customQueue::%@", [NSThread currentThread]);
}];

[customQueue addOperationWithBlock:^{
NSLog(@"customQueue1::%@", [NSThread currentThread]);
}];

[customQueue addOperationWithBlock:^{
NSLog(@"customQueue2::%@", [NSThread currentThread]);
}];
}

log信息显示如下:

1
2
3
4
5
customQueue2::<NSThread: 0x600003df4000>{number = 5, name = (null)}
customQueue1::<NSThread: 0x600003df4080>{number = 4, name = (null)}
customQueue::<NSThread: 0x600003df9b40>{number = 7, name = (null)}
mainQueue1::<NSThread: 0x600003db00c0>{number = 1, name = main}
mainQueue2::<NSThread: 0x600003db00c0>{number = 1, name = main}

结论:通过alloc+init创建的队列是并发队列。

  • 通过控制最大并发数属性maxConcurrentOperationCount,可以实现串行和并发的切换。默认情况下为-1,表示不进行限制,队列是并发队列。为1时,队列为串行队列。大于1时,队列为并发队列。当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为min{自己设定的值,系统设定的默认最大值}。

NSOperation

NSOperation是个抽象类,不能够直接使用,系统提供了两个子类NSInvocationOperation和NSBlockOperation供使用,也可以自定义子类。

【NSInvocationOperation】

  • 操作直接执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)operation {
// 1,创建 NSInvocationOperation 对象
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(taskMethod:) object:@2];
// 2,不开新线程,在当前线程开始执行操作
[op1 start];
// [op1 main]; main方法和start作用相同
}

- (void)taskMethod:(NSNumber *)count {
for (int i = 0; i < count.intValue; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"---%@", [NSThread currentThread]); // 打印当前线程
}
}
  • 操作添加到队列
1
2
3
4
5
6
7
8
9
10
11
- (void)demo {
// 默认最大并发数是-1,队列属于并发队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
for (int i = 0; i < 6; ++i) {
// 创建操作
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:
@selector(taskMethod:) object:@1];
// 操作添加到队列
[queue addOperation: operation];
}
}

log信息显示如下:

1
2
3
4
5
6
---<NSThread: 0x6000030a0500>{number = 5, name = (null)}
---<NSThread: 0x6000030a18c0>{number = 4, name = (null)}
---<NSThread: 0x6000030dbd80>{number = 3, name = (null)}
---<NSThread: 0x600003082200>{number = 6, name = (null)}
---<NSThread: 0x6000030a9640>{number = 8, name = (null)}
---<NSThread: 0x6000030a9f40>{number = 7, name = (null)}

【NSBlockOperation】

相比NSInvocationOperation使用起来更加简便,所以开发中经常使用。

  • 操作直接执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- (void)demo {

// 1.创建 NSBlockOperation 对象
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];

// 2.添加额外的操作
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
}
}];

// 3.调用 start 方法开始执行操作
[op start];

}
1
2
3
4
5
6
7
8
9
4---<NSThread: 0x6000037aacc0>{number = 7, name = (null)}
1---<NSThread: 0x6000037f0840>{number = 1, name = main}
3---<NSThread: 0x6000037b8700>{number = 5, name = (null)}
2---<NSThread: 0x6000037bc000>{number = 6, name = (null)}
2---<NSThread: 0x6000037bc000>{number = 6, name = (null)}
4---<NSThread: 0x6000037aacc0>{number = 7, name = (null)}
3---<NSThread: 0x6000037b8700>{number = 5, name = (null)}
1---<NSThread: 0x6000037f0840>{number = 1, name = main}

  • 操作添加到队列
1
2
3
4
5
6
7
// 默认最大并发数是-1,队列属于并发队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 1.创建 NSBlockOperation 对象
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
}];
[queue addOperation:op];

简便写法

1
2
3
4
5
6
// 队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 添加操作
[queue addOperationWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
}];

在NSOperation单独使用情况下,调用start/main方法,是否开启新线程,取决于操作的个数。如果操作的个数是一个的话,任务在当前线程执行。如果操作个数大于等于两个,就会自动开启新线程。

【NSInvocationOperation与NSBlockOperation异同】
共同点: NSInvocationOperation类只能封装一个操作,所以当调用start方法时,任务在当前线程执行。NSBlockOperation是否开启新线程,取决于操作的个数。如果添加的操作的个数是一个的话,调用start方法时,任务在当前线程执行。如果添加的操作个数大于等于两个,就会自动开启新线程。NSCustomOperation调用start方法时,任务在当前线程执行。

区别: NSBlockOperation可以添加多个操作。而NSInvocationOperation和NSCustomOperation只能添加一个操作。

接下来再介绍NSOperationQueue的一些其它的特性和用法。

【最大并发数】

队列通过控制最大并发数属性,可以实现串行与并发的切换功能。
maxConcurrentOperationCount 默认情况下为-1,表示不进行限制,是并发队列。为1时,队列为串行队列。大于1时,队列为并发队列。当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为min{自己设定的值,系统设定的默认最大值}。

【优先级】

队列的优先级
iOS8.0之后队列优先级使用qualityOfService属性

1
2
3
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 优先级高代表被执行的概率高,而不是一定会先执行。
queue.qualityOfService = NSQualityOfServiceUserInteractive;

任务的优先级
iOS8.0之后任务优先级使用qualityOfService属性替代queuePriority属性

1
2
3
4
5
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
}];
//优先级高代表被执行的概率高,而不是一定会先执行。
//qualityOfService的值是枚举类型,可到文档中具体查看,这里不一一介绍了。
operation.qualityOfService = NSOperationQueuePriorityNormal;

【队列挂起和任务取消】

挂起:暂停队列中还没有被调度的任务,正在调度的任务,不能被暂停
取消队列中的所有操作,同样不会影响到正在执行中的操作

1
2
3
4
5
6
7
8
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
// 挂起 暂停队列中还没有被调度的任务,正在调度的任务,不能被暂停
[operationQueue setSuspended:YES];
// 取消挂起 线程继续调度任务执行
[operationQueue setSuspended:NO];

// 取消队列中的所有操作,同样不会影响到正在执行中的操作
[operationQueue cancelAllOperations];

【队列的操作数】

队列的操作数属性可以获取队列中操作的数量

1
2
3
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
// operationCount属性可以获取当前队列中的任务数量
NSLog(@"%@",operationQueue.operationCount);
CATALOG
  1. 1. 什么是线程
  2. 2. 线程的状态
  3. 3. 并发编程的四种API介绍
    1. 3.1. pthread
    2. 3.2. NSThread
    3. 3.3. GCD
      1. 3.3.1. 队列
      2. 3.3.2. 任务
      3. 3.3.3. 任务添加到队列的方式
      4. 3.3.4. 串型队列的执行
      5. 3.3.5. 并发队列的执行
      6. 3.3.6. 主队列的执行
      7. 3.3.7. 队列的区别比较表格
    4. 3.4. NSOperation
      1. 3.4.1. NSOperationQueue
      2. 3.4.2. NSOperation