1、进程与线程

进程:可以理解成一个运行中的应用程序,是系统进行资源分配和调度的基本单位,是操作系统结构的基础,主要管理资源
线程:是进程的基本执行单元,一个进程对应多个线程

2、GCD

Grand Central Dispatch是异步执行任务的技术
在GCD之前,Cocoa框架提供处理异步执行任务的API

performSelectorInBackground: withObject: //后台处理任务 performSelectorOnMainThread:withObject: waitUntilDone: //回到主线程处理任务

(1)Dispatch Queue

开发者要做的只是定义想执行的任务并追加到适当的Dispatch Queue中。
Dispatch Queue是执行处理的等待队列,按照FIFO的执行处理。
两种不同的执行队列:

Dispatch Queue种类 说明
Serial Dispatch Queue 等待现在执行中处理结束 (串行队列)
Concurrent Dispatch Queue 不等待现在执行中处理结束(并行队列)

(2)dispatch_queue_create

dispatch_queue_create函数用于生成Dispatch Queue

  1. dispatch_queue_t queue = dispatch_queue_create("com.name", DISPATCH_QUEUE_SERIAL);

两个参数:

  • 第一个参数指定Queue的名称。推荐使用逆序全程域名
  • 第二个参数指定Queue的类型,NULL或DISPATCH_QUEUE_SERIAL都是表示Serial Dispatch Queue,指定为DISPATCH_QUEUE_CONCURRENT表示Concurrent Dispatch Queue

dispatch_release函数释放创建的队列,对应的也就有dispatch_retain函数来持有队列。

(3) Main Dispatch Queue / Global Dispatch Queue

Main Dispatch Queue 是Serial Dispatch Queue,因为主线程只有一个。添加到Main Dispatch Queue的处理在主线程的Runloop中执行。
Global Dispatch Queue是Concurrent Dispatch Queue。Global Dispatch Queue有4个执行优先级,分别是高优先级,默认优先级,低优先级,后台优先级。但是通过XNU内核用于Global Dispatch Queue的线程并不能保证实时性,因此执行的优先级知识大致的判断

名称 种类 说明
Main Dispatch Queue Serial Dispatch Queue 主线程执行
Global Dispatch Queue(High Priority) Concurrent Dispatch Queue 高优先级
Global Dispatch Queue(Default Priority) Concurrent Dispatch Queue 默认
Global Dispatch Queue(Low Priority) Concurrent Dispatch Queue 低优先级
Global Dispatch Queue(Background Priority) Concurrent Dispatch Queue 后台

获取全局队列,第一个参数是优先级,第二个参数作为保留字段,一般为0

// 获取全局队列
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
// 获取主队列
dispatch_get_main_queue()
参数类型 说明
DISPATCH_QUEUE_PRIORITY_HIGH 高优先级
DISPATCH_QUEUE_PRIORITY_DEFAULT 默认
DISPATCH_QUEUE_PRIORITY_LOW 低优先级
DISPATCH_QUEUE_PRIORITY_BACKGROUND 后台执行

注意⚠️:

Main Dispatch Queue和Global Dispatch Queue中执行dispatch_release函数和dispatch_retain函数不会引起任何变化

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       //全局队列
        NSLog(@"currentThread---%@",[NSThread currentThread]);
        dispatch_async(dispatch_get_main_queue(), ^{
            // 主队列
            NSLog(@"currentThread---%@",[NSThread currentThread]);
        });
    });

(4)dispatch_set_target_queue

dispatch_set_target_queue函数主要作用是变更Dispatch Queue的执行优先级。

dispatch_queue_t serialQueue = dispatch_queue_create("com.baidu.xxx", NULL);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);    
// 第一个参数是要变更执行优先级的Queue
// 第二个参数是指定执行优先级的目标
dispatch_set_target_queue(serialQueue, globalQueue);

第一个参数不能是系统指定的Main Dispatch Queue和Global Dispatch Queue,因为不确定最终发生了什么。
如果在多个Serial Dispatch Queue中使用dispatch_set_target_queue函数指定目标为某一个Serial Dispatch Queue,那么原先本应并行执行的多个Serial Dispatch Queue,在目标Serial Dispatch Queue上只能执行一个处理。所以dispatch_set_target_queue函数可以防止多个并行执行的Serial Dispatch Queue按照目标一次执行。

(5)dispatch_after

dispatch_after并不是指定时间后执行处理,而是在指定时间追加处理到队列中。

// 延迟3秒添加到队列中
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue() , ^{
    NSLog(@"3s after");
});

上述代码,是将block处理在3秒后添加到主队列中,因为主队列是在Runloop中执行的,所以每隔1/60执行一次runloop,所以Block最快在3秒后执行,最慢在3+1/60秒之后执行
dispatch_after函数参数说明:

  • 第一个参数是指定时间。这个值可以由dispatch_time和dispatch_walltime函数生成。
  • 第二个参数是需要追加到的队列
  • 第三个参数block是需要执行的处理

dispatch_time函数通常用于计算相对时间。
dispatch_walltime函数用于计算绝对时间,根据系统时钟创建绝对时间。

// 第一个参数是一个时间结构体,如果传入NULL,方法会默认使用当前时间
// 第二个参数是单位是纳秒
dispatch_time_t wtime = dispatch_walltime(NULL, 1000 * 1000 * 1000)

(6)Dispatch Group

Dispatch Group可以监视加入的所有执行队列结束。一旦检测到所有任务执行结束,就可将结束的任务追加到Dispatch Queue中。

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_group_async(group, queue, ^{
    NSLog(@"执行任务A");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"执行任务B");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"执行任务C");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"执行任务D");
});
dispatch_group_notify(group, queue, ^{
    NSLog(@"最后执行");
});

// 第一个参数是group
// 第二个参数是等待时间(超时),DISPATCH_TIME_FOREVER代表永不超时
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

NSLog(@"group 执行结束");

最后执行结果:

2021-02-23 16:57:19.582588+0800 GCD演示[11718:280292] 执行任务B 2021-02-23 16:57:19.582620+0800 GCD演示[11718:280297] 执行任务C 2021-02-23 16:57:19.582617+0800 GCD演示[11718:280290] 执行任务D 2021-02-23 16:57:19.582584+0800 GCD演示[11718:280291] 执行任务A 2021-02-23 16:57:19.582772+0800 GCD演示[11718:280297] 最后执行 2021-02-23 16:57:19.582781+0800 GCD演示[11718:280220] group 执行结束

dispatch_group_async函数与dispatch_async函数相同。
dispatch_group_notify函数会在group中所有执行队列完成之后追加到最后队列中。不管该函数指定的Dispatch Queue是否是哪个,只要属于Dispatch Group中的所有队列都执行结束在追加该函数的Block到队列时。
dispatch_group_wait函数等待全部处理执行结束。如果返回值不为0,则在指定时间之后,还存在队列正在执行,如果返回值为0,则代表执行队列已经全部执行结束。

(7)dispatch_barrier_async

使用dispatch_barrier_async同新创建的Concurrent Dispatch Queue一起使用,可以实现高效率的数据库访问和文件访问。
dispatch_barrier_async函数能避免数据竞争问题


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

    dispatch_async(queue, ^{
        NSLog(@"执行任务A");
    });
    dispatch_async(queue, ^{
        NSLog(@"执行任务B");
    });

    // 该函数会在任务A和任务B执行完成之后再执行
    // 并且该函数执行完成之后才会执行后面的任务
    dispatch_barrier_async(queue, ^{
        NSLog(@"barrier执行任务");
    });

    dispatch_async(queue, ^{
        NSLog(@"执行任务C");
    });
    dispatch_async(queue, ^{
        NSLog(@"执行任务D");
    });
    dispatch_async(queue, ^{
        NSLog(@"执行任务E");
    });

输出结果:

2021-02-23 17:22:31.147158+0800 GCD演示[12304:298523] 执行任务A 2021-02-23 17:22:31.147169+0800 GCD演示[12304:298521] 执行任务B 2021-02-23 17:22:31.147309+0800 GCD演示[12304:298521] barrier执行任务 2021-02-23 17:22:31.147420+0800 GCD演示[12304:298521] 执行任务C 2021-02-23 17:22:31.147423+0800 GCD演示[12304:298523] 执行任务D 2021-02-23 17:22:31.147434+0800 GCD演示[12304:298529] 执行任务E

(8)dispatch_sync

dispatch_sync同步函数。
dispatch_sync使用主队列会造成死锁问题。同样的,与串行队列一起使用也会造成死锁问题。
下面几种死锁的情况:

// 情况一
dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"AAAA");
});
// 情况二
dispatch_async(dispatch_get_main_queue(), ^{
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"BBB");
    });
});
// 情况三
dispatch_queue_t queue = dispatch_queue_create("com.xxx", NULL);
dispatch_async(queue, ^{
    dispatch_sync(queue, ^{
        NSLog(@"BBB");
    });
});

(9)dispatch_apply

dispatch_apply函数是dispatch_sync函数和Dispatch Group的关联API。该函数按指定的次数将指定的执行任务追加到Dispatch Queue队列中,并等待全部处理执行结束。

dispatch_apply(10, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^(size_t index){
    NSLog(@"%d",index);
});

dispatch_apply参数说明:

  • 参数一:执行重复次数
  • 参数二:追加任务的执行队列
  • 参数三:执行任务处理的Block

(10)dispatch_suspend / dispatch_resume

dispatch_suspend函数:挂起指定的队列。
dispatch_resume函数:恢复挂起的指定队列。
这两个函数对已经执行的队列没有任何的影响。挂起尚未执行的处理将停止,在恢复之后才可以继续执行。

(11)Dispatch Semephore

Dispatch Semephore是GCD中的信号量。是持有计数的信号,Semephore使用计数来实现该功能,计数为0时等待,计数为1或大于1时,减去1而不等待。
核心方法:

  • dispatch_semaphore_create(long value): 创建一个信号量
  • dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout):接收一个信号和等待时间,若信号的信号量为0,则会阻塞当前线程,直到信号量大于0或者超过设定时间值;若信号量大于0,则会使信号量减1并返回,程序继续住下执行。
  • dispatch_semaphore_signal(dispatch_semaphore_t dsema): 使信号量加1并返回

线程同步

利用信号量的特性,先创建信号量为0 在需要保持同步的地方,使用dispatch_semaphore_wait等待线程完成 在子线程完成之后调用dispatch_semaphore_signal去触发等待往下执行

示例代码

// 保持线程同步
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); // 关键,设置信号量为0
// 该函数等待semaphore的计数大于或等于1
__block int a = 10;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    a = 20;
    // 触发信号+1
    dispatch_semaphore_signal(semaphore);
});
long result = dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (result == 0) {
    // 成功等待返回

    NSLog(@"%zd",a);

} else {
    // 超过设定时长
}

打印结果是 20,所以到result之后,需要等着子线程执行完成之后才往下走。

线程加锁

  • 创建信号量为1的信号
  • 在需要加锁的地方之前调用dispatch_semaphore_wait
  • 在需要解锁的地方使用dispatch_semaphore_signal

示例代码:

 // 线程加锁
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
__block int a = 100;
for (int i = 0; i < 100; i ++) {
    // for循环用于开辟多个线程
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        if (a > 0) {
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // 相当于加锁
            a -= 1;
            NSLog(@"%zd",a);
            dispatch_semaphore_signal(semaphore); //相当于解锁
        }
    });
}

(12)dispatch_once

dispatch_once函数保证在应用程序执行中只执行一次指定处理。
一般常用于单利模式

+ (instancetype)shared {
    static NSObject *instance;
    static dispatch_once_t once;
    dispatch_once(&once, ^{
        instance = [[NSObject alloc] init];
    });
    return instance;
}

(13)Dispatch I/O

dispatch_io_create 函数生成Dispatch I/O,并指定发生error时用来执行处理的block,以及执行该block的Dispatch Queue。
dispatch_io_set_low_water 函数设置一次读取的大小
dispatch_io_read 函数使用Global Dispatch Queue 开始并发读取。每当各个分割的文件块读取结束时,将含有文件块数据的 Dispatch Data(这里指pipedata) 传递给 “ dispatch_io_read 函数指定的读取结束时回调用的block ”,这个block拿到每一块读取好的 Dispatch Data(这里指pipe data),然后进行合并处理。
如果想提高文件读取速度,可以尝试使用 Dispatch I/O.
以下源码来源:https://opensource.apple.com/source/Libc/Libc-763.11/gen/asl.c


static int
_asl_auxiliary(aslmsg msg, const char *title, const char *uti, const char *url, int *out_fd)
{
    asl_msg_t *merged_msg;
    asl_msg_aux_t aux;
    asl_msg_aux_0_t aux0;
    fileport_t fileport;
    kern_return_t kstatus;
    uint32_t outlen, newurllen, len, where;
    int status, fd, fdpair[2];
    caddr_t out, newurl;
    dispatch_queue_t pipe_q;
    dispatch_io_t pipe_channel;
    dispatch_semaphore_t sem;
    /* ..... 此处省略若干代码.....*/

    // 创建串行队列
    pipe_q = dispatch_queue_create("PipeQ", NULL);
    // 创建 Dispatch I/O
    pipe_channel = dispatch_io_create(DISPATCH_IO_STREAM, fd, pipe_q, ^(int err){
        close(fd);
    });

    *out_fd = fdpair[1];

    // 该函数设定一次读取的大小(分割大小)
    dispatch_io_set_low_water(pipe_channel, SIZE_MAX);
    //
    dispatch_io_read(pipe_channel, 0, SIZE_MAX, pipe_q, ^(bool done, dispatch_data_t pipedata, int err){
        if (err == 0) // err等于0 说明读取无误
        {
            // 读取完“单个文件块”的大小
            size_t len = dispatch_data_get_size(pipedata);
            if (len > 0)
            {
                // 定义一个字节数组bytes
                const char *bytes = NULL;
                char *encoded;

                dispatch_data_t md = dispatch_data_create_map(pipedata, (const void **)&bytes, &len);
                encoded = asl_core_encode_buffer(bytes, len);
                asl_set((aslmsg)merged_msg, ASL_KEY_AUX_DATA, encoded);
                free(encoded);
                _asl_send_message(NULL, merged_msg, -1, NULL);
                asl_msg_release(merged_msg);
                dispatch_release(md);
            }
        }

        if (done)
        {
            dispatch_semaphore_signal(sem);
            dispatch_release(pipe_channel);
            dispatch_release(pipe_q);
        }
    });
}

(14)Dispatch Source

dispatch source是基础数据类型,协调特定底层系统事件的处理
Dispatch source替代了异步回调函数,来处理系统相关的事件。
当你配置一个dispatch source时,你指定要监测的事件、dispatch queue、以及处理事件的代码(block或函数)。
当事件发生时,dispatch source会提交你的block或函数到指定的queue去执行和手工提交到queue的任务不同,dispatch source为应用提供连续的事件源。除非你显式地取消,dispatch source会一直保留与dispatch queue的关联。只要相应的事件发生,就会提交关联的代码到dispatch queue去执行。为了防止事件积压到dispatch queue,dispatch source实现了事件合并机制。 如果新事件在上一个事件处理器出列并执行之前到达,dispatch source会将新旧事件的数据合并。 根据事件类型的不同,合并操作可能会替换旧事件,或者更新旧事件的信息。
Dispatch是BSD系内核惯有功能kqueue的包装。kqueue是在XNU内核中发生各种事件时,在应用程序编程方执行处理的技术。其CPU负荷非常小,尽量不占用资源。kqueue可以说是应用程序处理XUN内核中发生的各种事件的方法中最优秀的一种。
Dispatch Source的种类

名称 内容
DISPATCH_SOURCE_TYPE_DATA_ADD 数据增加
DISPATCH_SOURCE_TYPE_DATA_OR 获取的内容进行OR运算
DISPATCH_SOURCE_TYPE_DATA_REPLACE 获取的内容替换
DISPATCH_SOURCE_TYPE_MACH_SEND MACH端口发送
DISPATCH_SOURCE_TYPE_MACH_RECV MACH端口接受
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE 内存压力
DISPATCH_SOURCE_TYPE_PROC 检测到与进程相关的事件
DISPATCH_SOURCE_TYPE_READ 可读取文件映像
DISPATCH_SOURCE_TYPE_SIGNAL 接受信号
DISPATCH_SOURCE_TYPE_TIMER 定时器
DISPATCH_SOURCE_TYPE_VNODE 文件系统有变更
DISPATCH_SOURCE_TYPE_WRITE 可写入文件映像
DISPATCH_MACH_SEND_DEAD

自定义定时器

__block int a = 0;
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
// 设定 10秒间隔,允许1秒延迟
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC, 1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
    // 定时任务
    NSLog(@"event");
    a = a + 1;
    if (a > 10) { //满足某一条件之后,取消定时器

        // 取消Dispatch Source
        dispatch_source_cancel(timer);
    }
});
// 指定取消定时器的任务处理
dispatch_source_set_cancel_handler(timer, ^{
    NSLog(@"cancel timer");
});

// 启动定时器
dispatch_resume(timer);

3、NSOperation

NSOperation、NSOperationQueue 是苹果提供给我们的一套多线程解决方案。
NSOperation、NSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。NSOperation 需要配合 NSOperationQueue 来实现多线程。
NSOperation是一个抽象类,使用前需要子类化(NSInvocationOperation,NSBlockOperation)。NSOperation本身是线程安全的,当我们在子类重写或自定义方法时同样需要保证线程安全。
NSOperation、NSOperationQueue的优势:

  • 可添加完成的代码块,在操作完成后执行。(setCompletionBlock)
  • 添加操作间的依赖关系,方便控制执行顺序。(addDependency)
  • 设定操作执行的优先级。(setQueuePriority)
  • 可以很方便的取消一个操作执行。(- (void)cancel;)
  • 使用KVO观察操作的状态更改。 (isExecuteing,isFinished,isCancelled)

3.1、单独使用NSOperation

(1)使用NSInvocationOperation子类

代码示例:

- (void)testOpreation {
    // 使用子类NSInvocationOperation
    NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(doWork) object:nil];
    // 开启操作
    [operation start];
}
- (void)doWork {
    NSLog(@"dowork---%@", [NSThread currentThread]);
}

打印结果:

dowork—-{number = 1, name = main}

从打印结果中可以看出:在没有使用 NSOperationQueue、在主线程中单独使用使用子类 NSInvocationOperation 执行一个操作的情况下,操作是在当前线程执行的,并没有开启新线程。

(2)使用NSBlockOperation子类
示例代码:

 // 使用子类NSBlockOperation
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"block---%@", [NSThread currentThread]);
}];
// 开启执行操作
[blockOperation start];
addExecutionBlock

打印输出:

block—-{number = 1, name = main}

从结果看出和NSInvocationOperation一样的效果。如果在其他线程中开启,则在当前线程中。
但NSBlockOpreation提供了额外的方法addExecutionBlock来添加更多的操作。

// 使用子类NSBlockOperation
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"block---%@", [NSThread currentThread]);
}];

//添加额外操作
[blockOperation addExecutionBlock:^{
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"block---%@", [NSThread currentThread]);
}];
//添加额外操作
[blockOperation addExecutionBlock:^{
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"block---%@", [NSThread currentThread]);
}];

// 开启执行操作
[blockOperation start];

打印结果:

block—-{number = 1, name = main} block—-{number = 5, name = (null)} block—-{number = 6, name = (null)}

使用子类 NSBlockOperation,并调用方法 addExecutionBlock: 的情况下,blockOperationWithBlock:方法中的操作 和 addExecutionBlock: 中的操作是在不同的线程中并发执行的。而且,这次执行结果中 blockOperationWithBlock:方法中的操作也不是在当前线程(主线程)中执行的。从而印证了blockOperationWithBlock: 中的操作也可能会在其他线程(非当前线程)中执行。

综上:一般情况下,如果一个 NSBlockOperation 对象封装了多个操作。NSBlockOperation 是否开启新线程,取决于操作的个数。如果添加的操作的个数多,就会自动开启新线程。当然开启的线程数是由系统来决定的。

(3)使用自定义NSOperation子类
自定义继承自 NSOperation 的子类,可以通过重写 main 或者 start 方法 来定义自己的 NSOperation 对象。
示例代码:

@interface SonOperation : NSOperation

@end

@implementation SonOperation

- (void)main {
    // 重写main方法

    if (!self.isCancelled) {   
        [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
        NSLog(@"SonOperation---%@", [NSThread currentThread]);
    }
}

@end

打印结果:

SonOperation—-{number = 1, name = main}

从结果可以看出:在没有使用 NSOperationQueue、在主线程单独使用自定义继承自 NSOperation 的子类的情况下,是在主线程执行操作,并没有开启新线程。

3.2、使用NSOperationQueue

NSOperationQueue包含主队列和自定义队列。
主队列:(添加到主队列的都在主线程中执行,addExecutionBlock添加的操作可能在其他线程执行)

 NSOperationQueue *queue = [NSOperationQueue mainQueue];

自定义队列:

 // 自定义操作队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 控制最大并发数
// queue.maxConcurrentOperationCount = 1; // 串型队列
queue.maxConcurrentOperationCount = 2; // 并发队列

最大并发数:maxConcurrentOperationCount (这里控制的不是并发线程的数量,而是一个队列中同时能并发执行的最大操作数。而且一个操作也并非只能在一个线程中运行)

maxConcurrentOperationCount = 1 则是串型队列 maxConcurrentOperationCount > 1 时则是并发队列,操作并发最大不应该超过系统限制,超过最终也会取系统最大值。 maxConcurrentOperationCount 默认情况下是-1,允许并发控制


设置操作依赖:**
通过操作依赖,我们可以很方便的控制操作之间的执行先后顺序。
NSOperation 提供了3个接口供我们管理和查看依赖。

    • (void)addDependency:(NSOperation *)op; //添加操作依赖,使当前操作依赖于op操作的完成
    • (void)removeDependency:(NSOperation *)op; // 移除操作依赖,取消当前操作对op操作的依赖
  1. @property (readonly, copy) NSArray *dependencies; // 在当前操作开始执行之前完成的所有操作对象数组。
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 创建操作A
NSBlockOperation *bOpA = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"NSBlockOperation A");
}];
NSBlockOperation *bOpB = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"NSBlockOperation B");
}];
NSBlockOperation *bOpC = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"NSBlockOperation C");
}];
//设置依赖,先执行C再执行A
[bOpA addDependency:bOpC];

//将操作添加到队列
[queue addOperation:bOpA];
[queue addOperation:bOpB];
[queue addOperation:bOpC];

运行日志:

NSBlockOperation B NSBlockOperation C NSBlockOperation A

可以看出操作A永远在操作C之后执行。
注意⚠️:

设置依赖需要在添加队列之前完成

NSOperation优先级
提供了queuePriority优先级属性,以下是优先级的取值。

typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};
@property NSOperationQueuePriority queuePriority;

默认情况下,所有操作的默认优先级都是NSOperationQueuePriorityNormal 。我们可以通过setQueuePriority:方法来改变当前操作在同一队列中的执行优先级。
操作设置优先级执行顺序:

  • 当一个操作的所有依赖都已经完成时,操作对象通常会进入准备就绪状态,等待执行。
  • queuePriority 属性决定了进入准备就绪状态下的操作之间的开始执行顺序。并且,优先级不能取代依赖关系。
  • 如果一个队列中既包含高优先级操作,又包含低优先级操作,并且两个操作都已经准备就绪,那么队列先执行高优先级操作。
  • 如果一个队列中既包含了准备就绪状态的操作,又包含了未准备就绪的操作,未准备就绪的操作优先级比准备就绪的操作优先级高。那么,虽然准备就绪的操作优先级低,也会优先执行。
  • 优先级不能取代依赖关系。如果要控制操作间的启动顺序,则必须使用依赖关系。

线程间的通信

NSBlockOperation *bOpA = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"NSBlockOperation A");

    // 线程间的通信
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        // 回到主线程
    }];
}];

4、NSThread

NSThread 是苹果官方提供的,使用起来比 pthread 更加面向对象,简单易用,可以直接操作线程对象。不过也需要需要程序员自己管理线程的生命周期(主要是创建),我们在开发的过程中偶尔使用 NSThread。比如我们会经常调用[NSThread currentThread]来显示当前的进程信息。
使用NSThread的代码示例:

// NSThread
NSThread *thread = [[NSThread alloc] initWithBlock:^{

    NSLog(@"%@",[NSThread currentThread]);
}];
// 启动线程
[thread start];

//创建线程后自动启动线程
[NSThread detachNewThreadWithBlock:^{
    NSLog(@"%@",[NSThread currentThread]);
}];

// 隐式创建并启动线程
[self performSelectorInBackground:@selector(run) withObject:nil];

NSThread类的属性和方法

@interface NSThread : NSObject  {
@private
    id _private;
    uint8_t _bytes[44];
}
//获取当前属性
@property (class, readonly, strong) NSThread *currentThread;
// 快速创建新线程的类方法(block和SEL两种方式),并直接启动
+ (void)detachNewThreadWithBlock:(void (^)(void))block;
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;

//是否是多线程
+ (BOOL)isMultiThreaded;

//保存线程的信息
@property (readonly, retain) NSMutableDictionary *threadDictionary;

// 阻塞线程的方法,在指定日期或者时间内阻塞
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

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

// 线程优先级
+ (double)threadPriority;
// 设置线程的优先级
+ (BOOL)setThreadPriority:(double)p;

@property double threadPriority ; // To be deprecated; use qualityOfService below

@property NSQualityOfService qualityOfService; // read-only after the thread is started

@property (class, readonly, copy) NSArray<NSNumber *> *callStackReturnAddresses;
@property (class, readonly, copy) NSArray<NSString *> *callStackSymbols;

// 线程名称
@property (nullable, copy) NSString *name;

//栈大小
@property NSUInteger stackSize;

// 是否是主线程
@property (readonly) BOOL isMainThread;
@property (class, readonly) BOOL isMainThread; // reports whether current thread is main
// 获取主线程
@property (class, readonly, strong) NSThread *mainThread;

// 线程初始化
- (instancetype)init ;
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument;
- (instancetype)initWithBlock:(void (^)(void))block ;

@property (readonly, getter=isExecuting) BOOL executing ;
@property (readonly, getter=isFinished) BOOL finished ;
@property (readonly, getter=isCancelled) BOOL cancelled ;

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

- (void)main;    // thread body method

@end

5、pthread

pthread 是一套通用的多线程的 API,可以在Unix/Linux/Windows 等系统跨平台使用,使用 C 语言编写,需要程序员自己管理线程的生命周期,使用难度较大,我们在 iOS 开发中几乎不使用 pthread。

// C语言函数
void *run(void *param) {
    NSLog(@"%@",[NSThread currentThread]);
    return NULL;
}

// 定义pthread_t变量
pthread_t pthread;
// 开启线程
pthread_create(&pthread, NULL,run,NULL);
// 设置子线程的状态为detach
pthread_detach(pthread);

pthread_create的参数说明:

  • 第一个参数是指向线程变量的指针
  • 第二个参数是线程属性,可赋值为NULL
  • 第三个参数是指向函数的指针
  • 第四个参数是运行函数的参数


pthread相关API**

  • pthread_create() 创建一个线程
  • pthread_exit() 终止当前线程
  • pthread_cancel() 中断另外一个线程的运行
  • pthread_join() 阻塞当前的线程,直到另外一个线程运行结束
  • pthread_attr_init() 初始化线程的属性
  • pthread_attr_setdetachstate() 设置脱离状态的属性(决定这个线程在终止时是否可以被结合)
  • pthread_attr_getdetachstate() 获取脱离状态的属性
  • pthread_attr_destroy() 删除线程的属性
  • pthread_kill() 向线程发送一个信号

6、线程安全问题

线程安全是指变量或方法( 这些变量或方法是多线程共享的) 可以在多线程的环境下被安全有效的访问。
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

  • 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;
  • 若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。

线程同步即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多,临界区对象就是其中一种。
可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。

典型列子:售票系统

场景:总的有100张火车票,分别在售票窗口A和售票窗口B进行同事售票,一直到火车票卖完为止。

转化为代码

// 非线程安全的情况
{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    // 窗口A
    NSBlockOperation *opA = [NSBlockOperation blockOperationWithBlock:^{
        [self sellTickets];
    }];
    // 窗口B
    NSBlockOperation *opB = [NSBlockOperation blockOperationWithBlock:^{
        [self sellTickets];
    }];
    // 加入队列
    [queue addOperation:opA];
    [queue addOperation:opB];
}

- (void)sellTickets {
    while (self.tickets > 0) {
        self.tickets -= 1;
        NSLog(@"还剩%zdp票,窗口%@",self.tickets,[NSThread currentThread]);
    }
}

打印结果中出现异常条目:
image.png
出现两次都是A,B线程进行减1操作之后,还是97的情况,这里就出现了线程安全问题

使用NSlock加锁处理,保证线程安全。以下是关键代码:

@property(nonatomic, strong) NSLock *lock;

{
    //初始化锁
    self.lock = [[NSLock alloc] init];   
}

- (void)sellTickets {
    while (1) {
        [self.lock lock]; //加锁操作
        if (self.tickets > 0) {
            self.tickets -= 1;
            NSLog(@"还剩%zdp票,窗口%@",self.tickets,[NSThread currentThread]);
        } else {
            NSLog(@"票已经卖完了");
            break;
        }
        [self.lock unlock]; //解锁
    }
}

7、解决线程安全的几种方式

(1) @synchronized

@synchronized指令使用对象作为该锁的唯一标识。只有标识相同时,才满足互斥条件。
@synchronized 指令实现锁的优点就是我们不需要在代码中显式的创建锁对象,便可以实现锁的机制,但作为一种预防措施,@synchronized 块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。

@synchronized (object) {
    //TODO 需要加锁的内容
}

(2) OSSpinLock 自旋锁

OSSpinLock 自旋锁,性能最高的锁。原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。
自旋锁存在的问题
当多个线程有优先级的时候,那么自旋锁就会出现问题。如果一个优先级低的线程先去访问某个数据,此时使用自旋锁进行了加锁,然后一个优先级高的线程又去访问这个数据,那么优先级高的线程会一直占着CPU资源,优先级低的线程就无法释放锁。由于自旋锁本身存在的问题,所以苹果已经废弃了OSSpinLock。
苹果在iOS10之后使用os_unfair_lock代替了OSSpinLock。
os_unfair_lock就是让线程睡眠,所以它避免了自旋锁导致的优先级反转问题

os_unfair_lock_t *lock;
os_unfair_lock_lock(&lock);
//TODO 加锁代码片段
os_unfair_lock_unlock(&lock);

(3) NSConditionLock 条件锁

@interface NSConditionLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name;

@end

(4) NSRecursiveLock 递归锁

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

- (void)test8 {

//    self.lock = [[NSLock alloc] init]; //使用互斥锁就会造成死锁
    self.lock = [[NSRecursiveLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self revFunc:5];
    });
}
- (void)revFunc:(int)value {
    if (value > 0) {
        // 递归调用
        [self.lock lock];
        NSLog(@"value = %d", value);
        sleep(1); //休眠1s
        [self revFunc:value - 1];
        [self.lock unlock];
    }
}

从示例中看出如果使用NSLock,则会造成死锁的情况,原因是在递归调用的过程中,第二次去调用的时候,由于锁已经使用了,并且没有解锁。他需要等待锁被解除,所以就进入死锁的状态,线程阻塞。
将锁修改为NSRecursiveLock之后,则线程正常运行,这是因为NSRecursiveLock允许同一个线程多次加锁而不会造成死锁
NSRecursiveLock类相关方法和属性

@interface NSRecursiveLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name ;

@end

(5) dispatch_semaphore GCD信号量

详细的解释看上面[GCD->Dispatch Semephore]
**

(6) pthread_mutex

pthread_mutex表示互斥锁,和信号量的实现原理类似,也是阻塞线程并进入睡眠,需要进行上下文切换。

pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);

pthread_mutex_t lock;
pthread_mutex_init(&lock, &attr);    //设置属性

pthread_mutex_lock(&lock);    //上锁
//需要执行的代码
pthread_mutex_unlock(&lock);    //解锁

(7) pthread_mutex(recursive)

pthread_mutex锁的一种,属于递归锁。一般一个线程只能申请一把锁,但是,如果是递归锁,则可以申请很多把锁,只要上锁和解锁的操作数量就不会报错。

(8) NSLock

NSLock是Cocoa提供给我们最基本的锁对象,他属于互斥锁。
在使用的时候需要使用 lock方法和unlock方法一起使用

NSLock *lk = [[NSLock alloc] init];
//加锁部分
[lk lock];
// TODO 需要加锁的内容
[lk unlock];

NSLock相关方法

@interface NSLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name ;

@end

(9) NSCondition

一种最基本的条件锁。手动控制线程wait和signal。
[condition lock];一般用于多线程同时访问、修改同一个数据源,保证在同一时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到unlock ,才可访问。
[condition unlock];与lock 同时使用。
[condition wait];让当前线程处于等待状态。
[condition signal];CPU发信号告诉线程不用在等待,可以继续执行。

NSCondition方法属性

@interface NSCondition : NSObject <NSLocking> {
@private
    void *_priv;
}

- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;

@property (nullable, copy) NSString *name;

@end