多线程编程基础

iOS 多线程

进程与线程

进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础,是正在运行的程序的实例(An instance of a computer program that is being executed)。

进程的概念主要有两点:

  • 进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(Text Region)、数据区域(Data Region)和堆栈(Stack Region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区存储着活动过程调用的指令和本地变量。
  • 进程是一个执行中的程序。

线程

线程是程序执行流的最小单元。一个标准的线程由线程ID、当前指令指针、寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分配的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可以与同属一个进程的其它线程共享进程所拥有的全部资源。

进程与线程的关系与区别

进程是资源分配的基本单位。所有与该进程有关的资源,都被记录在进程控制块PCB中,以表示该进程拥有这些资源或正在使用它们。另外,进程也是抢占处理机的基本单位,它拥有一个完整的虚拟地址空间。当进程发生调度时,不同的进程拥有不同的虚拟地址空间,而同一进程内的不同线程共享同一地址空间。

与进程相对应,线程与资源分配无关,它属于某一个进程,并与进程内的其它线程一起共享进程的资源。线程只由相关堆栈、寄存器和线程控制表TCB组成。寄存器可被用来存储线程内的局部变量,但不能存储其它线程的相关变量。

通常在一个进程中可以包含多个线程,它们可以利用进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更叫高效的提高系统内多个程序之间并发执行的程度。

线程和进程区别:

  • 地址空间和其它资源:进程之间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
  • 通信:进程间通信主要方式有管道、系统IPC(消息队列、信号、共享存储)、套接字(Socket)。而线程间可以直接读写进程数据段来进行通信(需要进程同步和互斥手段的辅助,以保证数据的异质性)
  • 调度和切换:线程上下文切换比进程快的多。
  • 在多线程OS中,进程不是一个可执行的实体

线程安全

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其它线程不能进行访问直到该线程读取完,其它线程才可以使用。

iOS中的多线程

目前在iOS中有四种多线程解决方案:

  • Pthreads
  • NSThread
  • GCD
  • NSOperation & NSOperationQueue

Pthreads

POSIX线程,简称Pthreads,是线程的POSIX标准。该标准定义了创建和操作线程的一整套API。在类Unix操作系统中,都使用Pthreads作为操作系统的线程。

NSThread

NSThread是经常Apple封装的完全面向对象的。你可以直观方便的操控线程对象,但是生命周期需要手动管理。

GCD

GCD是Apple为多核的并行运算提出的的解决方案,能够自动合理地利用更多的CPU内核,并自动管理线程的声明周期。GCD使用C进行编写,并使用了Block。

####任务

即代码所要完成的操作。

任务的执行方式有两种:同步执行和异步执行。

同步执行操作,它会阻塞当前线程并等待任务执行完毕,然后当前线程才会继续往下运行。

异步执行操作,则不会阻塞当前线程,当前线程会直接往下执行。

队列

用于存放要执行的任务。

串行队列中的任务,GCD会遵循FIFO原则来执行,串行队列的同步执行任务,会在当前线程一个一个执行,而异步执行任务,则会在它线程中一个一个执行。

并行队列中的任务执行顺序则要复杂一点,任务会根据同步或异步有不同的执行方式。并行队列中的同步执行任务会在当前线程中一个一个执行,而异步执行则会开很多线程一起执行。

如何创建队列?
  • 主队列:特殊的串行队列,主要用于刷新UI,任何需要刷新UI的工作都必须在主队列中执行。

dispatch_queue_t mainQueue = ispatch_get_main_queue();

  • 自己创建的队列:

    创建串行队列

dispatch_queue_t serialQueue = dispatch_queue_create("CustomSerialQueue",DISPATCH_QUEUE_SERIAL);

​ 创建并行队列

dispatch_queue_t concurrentQueue = dispatch_queue_create("CustomConcurrentQueue",DISPATCH_QUEUE_CONCURRENT);

  • 全局队列:这是一个并行队列,并行任务一般都加入到这个队列。

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)

如何创建任务?

  • 创建同步任务

    dispatch_sync(,^{// execute code));

  • 创建异步任务

    dispatch_async(,^{// execute code});

队列组

队列组可以将很多队列添加到一个组里,当组中的所有任务都执行完了,队列组将会通知给用户。

dispatch_group_enterdispatch_group_leave 分别表示一个任务追加到队列组和一个任务执行完毕离开了队列组。

只有当group中未执行完毕的任务数量为0时,才会使 dispatch_group_wait 解除阻塞,以及执行追加到 dispatch_group_notify 的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIOITY_DEFAULT,0);
dispatch_group_async(group, queue, ^{
// execute code
});
// 向group中添加在主队列中执行的任务
dispatch_group_async(group, dispatch_get_main_queue(),^{
// execute code
});
// 向group中追加任务
dispatch_group_enter(group);
dispatch_async(queue,^{
// execute code
dispatch_group_leave(group);
});

// 直到前面加入到group中的所有任务都执行完毕后,才会执行
dispatch_group_notify(group,dispatch_get_main_queue(),^{
// execute code
});
OR
// 直到前面加入到group中的所有任务都执行完成后,才会继续往下执行
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

GCD的信号量机制

并发队列可以分配多个线程,同时处理不同的任务,虽然提升了效率,但是多线程的并发是通过时间片轮转的方法实现的,线程的创建、销毁、上下文切换等会消耗资源。适当的并发可以提高效率,但是无节制的并发,则会抢占CPU资源,造成性能下降。此外,提交给并发队列的任务中,有些任务内部会有全局的锁,会导致线程休眠、阻塞,一旦这类任务过多,并发队列还需要创建新的线程来执行其它任务,会造成线程数量的增加。

因此控制并发队列中的线程数量就成了不能忽视的问题。

GCD并发线程数量控制

GCD中的信号量(dispatch_semaphore)是一个整形值,有初始计数值,可以接收通知信号和等待信号。当信号量收到通知信号时,计数+1;当信号量收到等待信号时,计数-1。如果信号量为0,线程会被阻塞,直到信号量大于0,才会继续执行。

使用信号量机制可以实现线程的同步,也可以控制最大并发数。

使用GCD信号量机制实现并发线程数量控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dispatch_queue_t	workConcurrentQueue	=	dispatch_queue_create(@"WORK_CONCURRENT_QUEUE",DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t workSerialQueue = dispatch_queue_create(@"WORK_SERIAL_QUEUE",DISPATCH_QUEUE_SERIAL);
int maxConcurrent = 10;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(maxConcurrent);
for (NSInteger i = 0; i < 10; i++) {
dispatch_async(workSerialQueue, ^{
// 使信号量-1,当信号量为0时就一直等待,即阻塞所在线程
// 这里使信号量 maxConcurrent-1,表示最大并发数量已被占用一个位置
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(workConcurrentQueue, ^{
// 发送一个信号,让信号量+1
// 这里使信号量 maxConcurrent+1,表示任务被执行,释放了最大并发数量中的一个位置
dispatch_semaphore_signal(semaphore);
});
});
}
使用GCD信号量机制实现线程同步

有时候我们会遇到需要异步执行一些耗时任务,并在这些任务完成后进行一些额外的操作,相当于将异步执行任务转化为同步执行任务。

1
2
3
4
5
6
7
8
9
10
11
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

dispatch_async(queue, ^{
// 执行耗时任务
···
// 任务完成后使信号量+1,被阻塞的线程继续执行
dispatch_semaphore_signal(semaphore);
});
// 如果信号量为0,则会阻塞当前线程,直到信号量
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
使用GCD信号量机制实现线程安全

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其它变量的值也和预期的是一样的,就是线程安全的。

1
2
3
4
5
6
7
8
9
dispatch_semaphore_t semaphoreLock	=	dispatch_semaphore_create(1);
// 相当于加锁
dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);
{
// 需要保证安全的执行代码
}
// 相当于解锁
dispatch_semaphore_singal(semaphoreLock);

GCD的一些使用场景

使用GCD实现延迟执行
1
2
3
4
5
dispatch_queue_t queue	=	dispatch_ger_gloabl_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
double delay = 3;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW), (int64_t)(delay*NSEC_PER_SEC)), queue, ^{
// execute code
});
使用GCD实现单例模式
1
2
3
4
5
6
7
8
static id _instance;
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// initial code
});
retur _instance;
}
任务同步

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

dispatch_barrier_async 函数会等待前边追加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中。然后在 dispatch_barrier_async 函数追加的任务执行完毕之后,异步队列才恢复为一般动作,接着追加任务到该异步队列并开始执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dispatch_queue_t queue = dispatch_queue_create("com.codelei.queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{
// Task 1
});
dispatch_async(queue, ^{
// Task 2
});

dispatch_barrier_async(queue, ^{
// 追加的任务 Task 3
});

dispatch_async(queue, ^{
// 追加的任务 Task 4
});

dispatch_async(queue, ^{
// 追加的任务 Task 5
});

直到 Task 1 和 Task 2 执行完成后,才会执行使用 dispatch_barrier_asynce 追加的任务 Task 3,然后在 Task 3 执行完成后,并行队列会正常执行。

NSOperation & NSOperationQueue

NSOperation是Apple对GCD的封装,完全面向对象。NSOperation和NSOperationQueue分别对应GCD的任务和队列。

添加任务

NSOperation只是一个抽象类,并不能直接封装任务。它有两个子类NSInvocationOperation和NSBlockOperation用来完成封装任务的操作。创建一个Operation后,需要调用start方法来启动任务,它默认在当前队列同步执行。如果需要在执行途中取消执行一个任务,调用cancel方法即可。

  • NSInvocationOperation

    1
    2
    NSInvocationOperation *invocationOperation = [[NSInvocation alloc] initWithTarget:self selector:@selector(executeMethod)];
    invocationOperation start];
  • NSBlockOperation

    1
    2
    3
    4
    5
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    // execute code
    }];
    [blockOperation start];

    上面提到,NSInvocationOperation和NSBlockOperation创建的任务默认在当前线程执行,但是NSBlockOperation可以通过addExecutionBlock:方法向Operation中添加多个可执行的Block。这样的Operation中的任务会并发执行,它会在主线程和其它多个线程执行这些任务。

    1
    2
    3
    4
    5
    6
    7
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    // execute code
    }];
    [blockOpertation addExecutionBlock:^{
    // execute code
    }];
    [blockOperation start];

    NOTE:addExecutionBlock:方法必须在start方法之前执行,否则会报错。

  • 自定义的Operation

    自定义Operation类需要继承Operation类,并实现其main()方法,因为在调用start()方法的时候,内部会调用main()方法完成相关逻辑。

创建队列

通过调用一个NSOperation类的start()方法来启动的任务,默认在当前线程同步执行。如果要避免占用当前线程,就需要使用到队列NSOperationQueue。只要将Operation添加到队列,就会自动调用任务的start()方法。

  • 主队列

    添加到主队列中的任务时串行执行的。

    1
    NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
  • 其它队列

    其它队列中的任务会在其它线程并行执行。

    1
    2
    3
    4
    5
    NSOperationQueue *queue = [NSOperatin alloc] init];
    NSBlockOperatin *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    // execute code
    }]
    [queue addOperation:blockOperation];

    如果需要任务在队列中串行执行,可以通过设置NSOperationQueue的maxConcurrentOperationCount来实现。

    1
    2
    NSOperationQueue *queue = [NSOperatin alloc] init];
    [queue setMaxConcurrentOperationCount:1];

    你还可以通过addOperationWithBlock:方法来向队列中添加新任务。

    1
    2
    3
    4
    NSOperationQueue *queue = [NSOperatin alloc] init];
    [queue addOperationWithBlock:^{
    // cxecute code
    }]

添加任务依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
NSBlockOperation	*blockOperationFirst  = [NSBlockOperation blockOperationWithBlock:^{
// execute code
}];
NSBlockOperation *blockOperationSecond = [NSBlockOperation blockOperationWithBlock:^{
// execute code
}];
NSBlockOperation *blockOperationThird = [NSBlockOperation blockOperationWithBlock:^{
// execute code
}];
[blockOperationSecond addDependency:blockOperationFirst];
[blockOperationThird addDependency:blockOperationSecond];
NSOperationQueue *queue = [NSOperatin alloc] init];
[queue addOperations:@[blockOperationFirst,blockOperationSecond,blockOperationThird]];

NOTE:

  • 添加相互依赖会造成死锁。
  • 使用removeDependency方法来移除依赖关系

其它方法

  • NSOperation类的一些其它方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 判断任务是否正在执行
    BOOL exccuting;
    // 判断任务是否完成
    BOOL finished;
    // 设置任务完成后的后续操作
    void (^completionBlock) (void);
    // 取消任务
    - (void)cancle;
    // 阻塞当前线程直到此任务执行完毕
    - (void)waitUntilFinished;
  • NSOperationQueue

    1
    2
    3
    4
    5
    6
    7
    8
    // 获取队列的任务数量
    NSUInteger operationCount;
    // 取消队列中的所有任务
    - (void)cancelAllOperations;
    // 阻塞当前线程直到此队列中的所有任务执行完毕
    - (void)waitUntilAllOperationsAreFinished;
    // 暂停或继续队列
    BOOL suspended;
作者

Y2hlbmdsZWk=

发布于

2015-11-01

更新于

2021-09-01

许可协议