iOS线程知识整理

EleanorDana 发布于1年前

什么是线程

1、线程的定义、状态、属性

进程

进程:(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;

在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位)

线程

线程:有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。

一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。

另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。

一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。

就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;
运行状态是指线程占有处理机正在运行;
阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。

每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。

多线程:线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。


同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)

多进程是指操作系统能同时运行多个任务(程序)。

多线程是指在同一程序中有多个顺序流在执行。

线程与进程的共同点和区别

共同点:线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。

区别:

线程和进程的区别在于,子进程和父进程有不同的代码和数据空间,而多个线程则共享数据空间,每个线程有自己的执行堆栈和程序计数器为其执行上下文。多线程主要是为了节约CPU时间,发挥利用,根据具体情况而定。线程的运行中需要使用计算机的内存资源和CPU。

线程与进程的区别可以归纳为以下几点:

1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。

2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。

3)调度和切换:线程上下文切换比进程上下文切换要快得多。

4)在多线程OS中,进程不是一个可执行的实体。
   

线程的状态

就绪:线程分配了CPU以外的全部资源,等待获得CPU调度

执行:线程获得CPU,正在执行

阻塞:线程由于发生I/O或者其他的操作导致无法继续执行,就放弃处理机,转入线程就绪

线程的特性

线程在多线程OS中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性。

①轻型实体

线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源,比如,在每个线程中都应具有一个用于控制线程运行的线程控制块TCB,用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。

②独立调度和分派的基本单位。

在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小。

③可并发执行。

在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行。

④共享进程资源。

在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。

2、线程之间的通信

什么是线程通信

多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。

就是在一个线程进行了规定操作后,就进入等待状态(wait), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify);

当我们创建多个生产者和消费者时,无法直到到底要唤醒哪一个,所以这时候我们就用到了notifAll()方法。

为什么要线程通信

多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

当然如果我们没有使用线程通信来使用多线程共同操作同一份数据的话,虽然可以实现,但是在很大程度会造成多线程之间对同一共享变量的争夺,那样的话势必为造成很多错误和损失!

所以,我们才引出了线程之间的通信,多线程之间的通信能够避免对同一共享变量的争夺。

3、线程进程以及堆栈关系的总结

栈是线程独有的,保存其运行状态和局部自动变量的,栈在线程开始的时候初始化,每个线程的栈相互对立,因此,栈是线程安全的,栈空间有系统管理。栈被自动分配到进程的内存空间中。

堆在操作系统度进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完要返还,不然就是内存泄露。

iOS中的线程

iOS中提供了四套多线程方案、一种一种来看。

Pthreads (不做介绍)
NSThread
GCD
NSOperation & NSOperationQueue

1、NSThread

苹果封装、面向对象的、可以直接操控线程对象,非常直观和方便。但是,它的生命周期还是需要我们手动管理。

优缺点


优点:轻量级

缺点:一个NSThread对象代表一个线程,需要手动管理线程的生命周期,处理线程同步等问题,线程同步对数据的加锁会有一定的开销。

创建并启动

1、先创建线程类,再启动

//1 创建NSThread 并启动
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run) object:nil];
    
[thread start];

2、创建并自动启动

[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:nil];
 ////// 
__weak typeof(self) weakself = self;
[NSThread detachNewThreadWithBlock:^{
      [weakself run];
}];

3、使用 NSObject 的方法创建并自动启动

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

但是在Swift中没有这个方法:

Note: The performSelector: method and related selector-invoking methods are not imported in Swift because they are inherently unsafe.
//共同执行的方法\两种锁
- (void)run {
//    [lock lock];
//    NSLog(@"111111");
//    NSLog(@"---%@",NSThread.currentThread);
//    [lock unlock];
    
    @synchronized (self) {
        NSLog(@"111111%@",NSThread.currentThread);
        NSLog(@"---%@",NSThread.currentThread);
    }
}

执行结果:
2017-11-09 10:55:34.196040+0800 MultithreadingDemo[96041:6103178] 111111<NSThread: 0x604000273200>{number = 5, name = (null)}
2017-11-09 10:55:34.196457+0800 MultithreadingDemo[96041:6103178] ---<NSThread: 0x604000273200>{number = 5, name = (null)}
2017-11-09 10:55:34.197956+0800 MultithreadingDemo[96041:6103177] 111111<NSThread: 0x604000273100>{number = 3, name = (null)}
2017-11-09 10:55:34.198510+0800 MultithreadingDemo[96041:6103177] ---<NSThread: 0x604000273100>{number = 3, name = (null)}
2017-11-09 10:55:34.200726+0800 MultithreadingDemo[96041:6103179] 111111<NSThread: 0x604000273140>{number = 4, name = (null)}
2017-11-09 10:55:34.201170+0800 MultithreadingDemo[96041:6103179] ---<NSThread: 0x604000273140>{number = 4, name = (null)}

其他方法

除了创建启动外,NSThread 还以很多方法,下面是一些常见的方法

//取消线程
- (void)cancel;

//启动线程
- (void)start;

//判断某个线程的状态的属性
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;
@property (readonly, getter=isCancelled) BOOL cancelled;

//获取当前线程信息
+ (NSThread *)currentThread;

//获取主线程信息
+ (NSThread *)mainThread;

//使当前线程暂停一段时间,或者暂停到某个时刻
+ (void)sleepForTimeInterval:(NSTimeInterval)time;
+ (void)sleepUntilDate:(NSDate *)date;

2、GCD

Grand Central Dispatch,是libdispatch的市场名称,而libdispatch是Apple的一个库,其为并发代码在iOS和OS X的多核硬件上执行提供支持。确切地说GCD是一套低层级的C API,通过 GCD,开发者只需要向队列中添加一段代码块(block或C函数指针),而不需要直接和线程打交道。GCD在后端管理着一个线程池,它不仅决定着你的代码块将在哪个线程被执行,还根据可用的系统资源对这些线程进行管理。这样通过GCD来管理线程,从而解决线程生命周期(创建线程、调度任务、销毁线程)问题。同时自动合理地利用更多的CPU内核(比如双核、四核)。

GCD 优点

易用: GCD 提供一个易于使用的并发模型而不仅仅只是锁和线程,以帮助我们避开并发陷阱,而且因为基于block,它能极为简单得在不同代码作用域之间传递上下文。

灵活: GCD 具有在常见模式上(比如锁、单例),用更高性能的方法优化代码,而且GCD能提供更多的控制权力以及大量的底层函数。

性能: GCD 能自动根据系统负载来增减线程数量,这就减少了上下文切换以及增加了计算效率。

GCD 概念

1.Dispatch Object

GCD被组建成面向对象的风格。GCD对象被称为 dispatch object, 所有的 dispatch object 都是OC对象.,就如其他OC对象一样,当开启了 ARC 时,dispatch object 的retain和release都会自动执行。而如果是MRC的话,dispatch objects会使用dispatch_retain和dispatch_release这两个方法来控制引用计数。

在 iOS 6.0 dispatch_release 已被废弃。内部被改成对象释放(release)所以 arc 后都不再使用

2.Serial & Concurrent

串行任务就是每次只有一个任务被执行,并发任务就是在同一时间可以有多个任务被执行。

3.Synchronous & Asynchronous

Synchronous(同步函数)意思是在完成了它预定的任务后才返回,在任务执行时会阻塞当前线程。而 Asynchronous(异步函数)则是任务会完成但不会等它完成,所以异步函数不会阻塞当前线程,会继续去执行下去。

4.Concurrency & Parallelism

Concurrency (并发)的意思就是同时运行多个任务。这些任务可能是以在单核 CPU 上以分时(时间共享)的形式同时运行,也可能是在多核 CPU 上以真正的并行方式来运行。然后为了使单核设备也能实现这一点,并发任务必须先运行一个线程,执行一个上下文切换,然后运行另一个线程或进程。Parallelism(并行)则是真正意思上的多任务同时运行。

5.Context Switch

Context Switch即上下文切换,一个上下文切换指当你在单个进程里切换执行不同的线程时存储与恢复执行状态的过程。这个过程在编写多任务应用时很普遍,但会带来一些额外的开销。

6.Dispatch Queues

GCD dispatch queues 是一个强大的执行多任务的工具。Dispatch queue 是一个对象,它可以接受任务,并将任务以先进先出(FIFO)的顺序来执行。Dispatch queue 可以并发的或串行的执行任意一个代码块,而且并发任务会像 NSOperationQueue 那样基于系统负载来合适地并发进行,串行队列同一时间则只执行单一任务。Dispatch queues 内部使用的是线程,GCD 管理这些线程,并且使用 Dispatch queues 的时候,我们都不需要自己创建线程。Dispatch queues相对于和线程直接通信的代码优势是:使用起来特别方便,执行任务更加有效率。

7.Queue Types

  • main queue : 主队列 (主线程)
一般使用 main queue, 都是在该线程中操作 UI 相关的.也就是说, 在 main queue 中执行的任务会在主线程中执行.主线程只有一个, main queue 是与主线程相关的,所以 main queue 是串行队列.

//Returns the default queue that is bound to the main thread.
                                                                                                                                                                       dispatch_get_main_queue(void)
{
    return DISPATCH_GLOBAL_OBJECT(dispatch_queue_t, _dispatch_main_q);                                                                                   
}
  • global queue : 全局队列 (有多个线程)
dispatch_get_global_queue(long identifier, unsigned long flags);                                                        dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 
常写作 dispatch_get_global_queue(0, 0);

global queue 是并发队列.可以设置其优先级.  ???优先级问题
//@param identifier  优先级
- A quality of service class defined in qos_class_t or a priority defined in  
                                                                                                                                                   - dispatch_queue_priority_t.
 //@param flags  备用参数
- Reserved for future use. Passing any value other than zero may result in
- a NULL return value.
//@result  返回一个全局队列
- Returns the requested global queue or NULL if the requested global queue
- does not exist.
  • custom queue : 自定义队列 (串行:单线程 ,并行:有多个线程)
这些队列是可以是串行的, 也可以是并行的。默认是串行的.
dispatch_queue_attr_t设置成NULL的时候默认代表串行。
串行队列可以保证任务是串行的, 保证了执行顺序.类似锁机制.
   
dispatch_queue_create(const char *_Nullable label,dispatch_queue_attr_t _Nullable attr);
                                                                                           
//@param label  队列名称 尽量别重名
- A string label to attach to the queue.
- This parameter is optional and may be NULL.
//@param attr   队列类型  默认 DISPATCH_QUEUE_SERIAL
- A predefined attribute such as DISPATCH_QUEUE_SERIAL,
- DISPATCH_QUEUE_CONCURRENT, or the result of a call to
- a dispatch_queue_attr_make_with_* function.
//@result
- The newly created dispatch queue.

GCD的具体使用

1.添加任务到队列

GCD有两种方式来把任务添加到队列中:异步和同步。

异步方式添加任务到队列的情况:

1.自定义串行队列:按添加进队列的先后顺序 顺序执行(不管同步异步线程)

我们接着上面的run方法来写一个串行队列

第一步,写两个异步线程和一个同步线程加入队列执行:其中第一个线程执行任务之前睡眠1秒
[self run];
__weak typeof(self) weakself = self;
dispatch_queue_t queue = dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
    sleep(1);
    [weakself run];
});
dispatch_async(queue, ^{
    [weakself run];
});

dispatch_sync(queue, ^{
    [weakself run];
});

结果:
2017-11-09 14:00:22.642407+0800 MultithreadingDemo[97236:6195716] ---<NSThread: 0x604000069240>{number = 1, name = main}
2017-11-09 14:00:23.643340+0800 MultithreadingDemo[97236:6195829] ---<NSThread: 0x604000275300>{number = 3, name = (null)}
2017-11-09 14:00:23.643547+0800 MultithreadingDemo[97236:6195829] ---<NSThread: 0x604000275300>{number = 3, name = (null)}
2017-11-09 14:00:23.643776+0800 MultithreadingDemo[97236:6195716] ---<NSThread: 0x604000069240>{number = 1, name = main}

包括主线程在内,整个队列里面有两条线程,但是执行结果却被第一个sleep阻塞1秒。所以串行队列是一个个任务完成后再执行后面的任务

第二步,写一个异步线程包裹一个同步线程,并在同步线程中执行run
[self run];
__weak typeof(self) weakself = self;
dispatch_queue_t queue = dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
    [weakself run];
});
dispatch_async(queue, ^{  //1号任务
    [weakself run];
    dispatch_sync(queue, ^{  //2号任务
        [weakself run];
    });
});
结果:
2017-11-09 14:08:03.335534+0800 MultithreadingDemo[97291:6200400] ---<NSThread: 0x60000006f580>{number = 1, name = main}
2017-11-09 14:08:03.335846+0800 MultithreadingDemo[97291:6200500] ---<NSThread: 0x60000046a9c0>{number = 3, name = (null)}
2017-11-09 14:08:03.336308+0800 MultithreadingDemo[97291:6200500] ---<NSThread: 0x60000046a9c0>{number = 3, name = (null)}

崩溃在 dispatch_sync 这一行,所以我们只看到了一条主线程run的记录
分析一下:
1、我们使用了同步线程,而且是串行队列,
2、1号任务没有结束、2号任务是无法执行的
3、当任务走到同步线程开启的时候,线程会被阻塞,直到2号任务block内的任务执行完成才会释放
4、可是同步线程把任务加入queue队列之后才发现,自己要执行的这个任务前面还卡着一个1号任务
5、线程被阻塞,1号任务无法完成,1号任务没完成 2号任务就不能执行
6、造成死锁

所以改一下,只要把同步任务换个队列执行,就可以避免死锁了:
dispatch_async(queue, ^{
    [weakself run];
    dispatch_sync(dispatch_get_main_queue(), ^{
        [weakself run];
    });
});

2.主队列:顺序执行、串行队列 一般更新UI都在主线程。

//主队列中的任务一定会回到主线程去执行、如下方式去执行,同步任务在主线程、主队列执行,主队列是串行队列,又会出现死锁
dispatch_sync(dispatch_get_main_queue(), ^{
    [weakself run];
});
改成:
dispatch_async(dispatch_get_main_queue(), ^{
    [weakself run];
});

3.并发队列:非顺序执行,随机、同步执行并发队列一样会卡住主线程

如串行队列所写,在并行队列写相同代码执行结果会如何:
[self run];
__weak typeof(self) weakself = self;
dispatch_queue_t queue = dispatch_queue_create("并行队列", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    [weakself run];
});
dispatch_async(queue, ^{
    [weakself run];
    dispatch_sync(queue, ^{
        [weakself run];
    });
});

结果:
2017-11-09 14:34:15.075513+0800 MultithreadingDemo[97572:6219452] ---<NSThread: 0x60000007dbc0>{number = 1, name = main}
2017-11-09 14:34:15.075965+0800 MultithreadingDemo[97572:6219573] ---<NSThread: 0x60000046bdc0>{number = 4, name = (null)}
2017-11-09 14:34:15.075967+0800 MultithreadingDemo[97572:6219574] ---<NSThread: 0x60000046ae00>{number = 3, name = (null)}
2017-11-09 14:34:15.076703+0800 MultithreadingDemo[97572:6219573] ---<NSThread: 0x60000046bdc0>{number = 4, name = (null)}

可以看到执行结果是正常的,并未出现死锁,那是因为并行队列是可以多个任务并行执行的,正因为允许多个任务同时执行,所以执行结束时间并不是按着添加入队列的顺序来的。

4.全球队列:并行队列、异步线程常用队列

dispatch_get_global_queue(0, 0);

2.并发执行迭代循环

在开发中,并发队列能很好地提高效率,特别是当我们需要执行一个数据庞大的循环操作时。打个比方来说吧,我们需要执行一个for循环,每一次循环操作如下:

for (i = 0; i < count; i++) {
   NSLog("%d",i);
}

GCD提供了一个简化方法叫做dispatch_apply,当我们把这个方法放到并发队列中执行时,这个函数会调用单一block多次,并平行运算,然后等待所有运算结束。

代码示例:

但是dispatch_apply函数是没有异步版本的。只能将整个dispatch_apply 置于异步中。


dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_apply(count, queue, ^(size_t i) {
   NSLog("%d",i);
});

直接在主线程调用dispatch_apply 会阻塞主线程,如果使用了并发队列 队列任务会被放置在异步线程中执行,但是主线程依然被阻塞。只有整个放入异步线程才不会阻塞主线程。

3.挂起和恢复队列

有时候,我们不想让队列中的某些任务马上执行,这时我们可以通过挂起操作来阻止一个队列中将要执行的任务。当需要挂起队列时,使用dispatch_suspend方法;恢复队列时,使用dispatch_resume方法。调用dispatch_suspend会增加队列挂起的引用计数,而调用dispatch_resume则会减少引用计数,当引用计数大于0时,队列会保持挂起状态。因此,这队列的挂起和恢复中,我们需要小心使用以避免引用计数计算错误的出现。

执行挂起操作不会对已经开始执行的任务起作用,它仅仅只会阻止将要进行但是还未开始的任务。
dispatch_queue_t myQueue;

myQueue = dispatch_queue_create("队列", NULL);
//挂起队列
dispatch_suspend(myQueue);
//恢复队列
dispatch_resume(myQueue);

如下:

__weak typeof(self) weakself = self;
dispatch_queue_t queue = dispatch_queue_create("并行队列", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    [weakself run];
});
dispatch_async(queue, ^{
    dispatch_suspend(queue);
    [weakself run];
    dispatch_sync(queue, ^{
        [weakself run];
    });
});

结果 只有两条run语句,同步线程因为队列被挂起,所以并未执行
2017-11-09 14:43:22.593056+0800 MultithreadingDemo[97644:6225319] ---<NSThread: 0x60000027e0c0>{number = 9, name = (null)}
2017-11-09 14:43:22.592831+0800 MultithreadingDemo[97644:6226170] ---<NSThread: 0x600000271a40>{number = 8, name = (null)}

4.dispatch_after 的使用

延迟一段时间把一项任务提交到队列中执行,返回之后就不能取消

dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);

一般我们在做一些延时任务的时候使用的多

5.dispatch_once 的使用

保证在APP运行期间,block中的代码只执行一次

static Demo *demo;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    demo = [Demo new];
});

单例常用

6.Dispatch Groups 的使用

Dispatch groups是阻塞线程直到一个或多个任务完成的一种方式。在那些需要等待任务完成才能执行某个处理的时候,你可以使用这个方法。Group会在整个组的任务都完成时通知你,这些任务可以是同步的,也可以是异步的,即便在不同的队列也行。而且在整个组的任务都完成时, Group可以用同步的或者异步的方式通知你。当group中所有的任务都完成时,GCD 提供了两种通知方式。

dispatch_group_wait。它会阻塞当前线程,直到队列里面所有的任务都完成或者等到某个超时发生。

代码示例:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
// 添加队列到组中
dispatch_group_async(group, queue, ^{
// 一些异步操作 或者耗时操作
});

//如果在所有任务完成前超时了,该函数会返回一个非零值。
//你可以对此返回值做条件判断以确定是否超出等待周期;
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

NSLog(@"123"); //被阻塞,因为dispatch_group_wait  所以这一句代码只会在队列任务都完成后执行

dispatch_group_notify。它以异步的方式工作,当 Dispatch Group中没有任何任务时,它就会执行其代码,那么 completionBlock便会运行。可以用于在并行队列中待所有任务都完成之后再调起执行。

代码示例:

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

// 添加队列到组中
dispatch_group_async(group, queue, ^{
    NSLog(@"one---%@",NSThread.currentThread);
});
dispatch_group_async(group, queue, ^{
    // 一些延时操作
    sleep(2);
    NSLog(@"two---%@",NSThread.currentThread);
});
dispatch_group_async(group, queue, ^{
    // 一些延时操作
    sleep(3);
    NSLog(@"three---%@",NSThread.currentThread);
});
dispatch_group_async(group, queue, ^{
    NSLog(@"four---%@",NSThread.currentThread);
});

dispatch_group_notify(group, queue, ^{
    NSLog(@"我会一直等到现在");
});
NSLog(@"123");

结果
2017-11-09 15:21:48.480021+0800 MultithreadingDemo[98195:6255855] 123
2017-11-09 15:21:48.480192+0800 MultithreadingDemo[98195:6255916] one---<NSThread: 0x600000466800>{number = 3, name = (null)}
2017-11-09 15:21:48.480321+0800 MultithreadingDemo[98195:6255917] four---<NSThread: 0x600000466840>{number = 4, name = (null)}
2017-11-09 15:21:50.483266+0800 MultithreadingDemo[98195:6255918] two---<NSThread: 0x604000462e80>{number = 5, name = (null)}
2017-11-09 15:21:51.483851+0800 MultithreadingDemo[98195:6255922] three---<NSThread: 0x60400027dd40>{number = 6, name = (null)}
2017-11-09 15:21:51.484084+0800 MultithreadingDemo[98195:6255922] 我会一直等到现在

对这一段代码,并行队列执行,最后一行不会阻塞,其余加入group中的任务执行完成后才会执行notify中的任务。
常用于需要等待某些异步线程执行完成后统一处理的场景,比如多个接口数据拼装模型

7.dispatch_barrier_async 、dispatch_barrier_sync 的使用

在并行队列中,为了保持某些任务的顺序,需要等待一些任务完成后才能继续进行,使用 barrier 栅栏函数 来等待之前任务完成,避免数据竞争等问题。

同步,会拦截后面所有的代码执行,直到前面任务完成,并且完成栅栏函数中的任务。

异步,拦截并行队列中的后续任务,直到前面任务执行完,并且完成栅栏函数中的任务。不会影响主线程。

dispatch_barrier_async 函数会等待追加到并行队列中的操作全部执行完之后,然后再执行 dispatch_barrier_async 函数追加的处理,等 dispatch_barrier_async 追加的处理执行结束之后(同时只执行一个任务),Concurrent Dispatch Queue才恢复之前的动作继续执行。

注意:使用 dispatch_barrier_async,该函数只能搭配自定义并行队列 dispatch_queue_t 使用。不能使用: dispatch_get_global_queue ,否则 dispatch_barrier_async 的作用会和 dispatch_async 的作用一模一样。

__weak typeof(self) weakself = self;
dispatch_queue_t queue = dispatch_queue_create("并行队列", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    [weakself run];
});
dispatch_async(queue, ^{
    [weakself run];
});

//加入
dispatch_barrier_async(queue, ^{
    sleep(1);
    [weakself run2];
    sleep(1);
});
dispatch_barrier_async(queue, ^{
    [weakself run2];
    sleep(1);
});
dispatch_async(queue, ^{
    [weakself run];
});
dispatch_async(queue, ^{
    [weakself run];
});

结果
2017-11-09 16:50:54.226018+0800 MultithreadingDemo[99305:6326134] ---<NSThread: 0x6000002617c0>{number = 4, name = (null)}
2017-11-09 16:50:54.225967+0800 MultithreadingDemo[99305:6326323] ---<NSThread: 0x600000268700>{number = 3, name = (null)}
2017-11-09 16:50:55.227973+0800 MultithreadingDemo[99305:6326323] ++++<NSThread: 0x600000268700>{number = 3, name = (null)}
2017-11-09 16:50:56.228820+0800 MultithreadingDemo[99305:6326323] ++++<NSThread: 0x600000268700>{number = 3, name = (null)}
2017-11-09 16:50:57.230081+0800 MultithreadingDemo[99305:6326323] ---<NSThread: 0x600000268700>{number = 3, name = (null)}
2017-11-09 16:50:57.230082+0800 MultithreadingDemo[99305:6326134] ---<NSThread: 0x6000002617c0>{number = 4, name = (null)}

55\56秒 明显的三次停顿。说明执行 dispatch_barrier_async 插入的任务时 同时只执行了一个任务

3、NSOperation

NSOperation 是苹果公司对 GCD 的封装,完全面向对象,所以使用起来更好理解。 大家可以看到 NSOperation和 NSOperationQueue 分别对应 GCD 的 任务 和 队列 。

优缺点

与NSThread的区别:没有那么轻量级,但是不需要关心线程管理,数据同步的事情。

与GCD区别:NSOperationQueue可以方便的管理并发、NSOperation之间的优先级。GCD主要与block结合使用。代码简洁高效。

如果异步操作的过程需要更多的被交互和UI呈现出来,NSOperationQueue会是一个更好的选择。底层代码中,任务之间不太互相依赖,而需要更高的并发能力,GCD则更有优势

我们要做的就是:

1.将要执行的任务封装到一个NSOperation对象中

2.将此任务添加到一个NSOperationQueue对象中

创建添加

NSOperation有两个子类:NSBlockOperation 和 NSInvocationOperation (或者自行自定义Operation )

NSBlockOperation:(OC 代码、Swift也有)

+ (instancetype)blockOperationWithBlock:(void (^)(void))block;
- (void)addExecutionBlock:(void (^)(void))block;

__weak typeof(self) weakself = self;
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    [weakself run];
}];
[operation start];

结果
2017-11-09 15:53:29.765058+0800 MultithreadingDemo[98518:6280532] ---<NSThread: 0x604000073e80>{number = 1, name = main}

1、直接执行创建的operation 默认是当前线程
2、NSBlockOperation 还有一个添加执行block的方法,它会在当前线程和其他多个线程执行这些block中的任务
[operation addExecutionBlock:^{
    [weakself run];
}];

结果
2017-11-09 15:53:29.765058+0800 MultithreadingDemo[98518:6280532] ---<NSThread: 0x604000073e80>{number = 1, name = main}
2017-11-09 15:53:29.765055+0800 MultithreadingDemo[98518:6280642] ---<NSThread: 0x60400026ea40>{number = 3, name = (null)}

注意:当NSOperation开始执行后不能再添加任务

NSInvocationOperation: (Swift 不允许使用)

- (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;
- (instancetype)initWithInvocation:(NSInvocation *)inv NS_DESIGNATED_INITIALIZER;

//1.创建NSInvocationOperation对象
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];

//2.开始执行
[operation start];

队列

上面例子中的任务执行,不管是多线程还是单线程都必然会在当前线程执行一个任务

NSOperation的队列和GCD不同,不存在串行、并行之分,他们只有主队列和其他队列:

主队列:

NSOperationQueue *queue = [NSOperationQueue mainQueue];

其他队列:(注意:其他队列的任务会在其他线程并行执行)

所有的非主队列就是其他队列,也就是说不是通过 mainQueue 获取的队列都是其他队列

NSOperationQueue *queue = [[NSOperationQueue alloc]init];
__weak typeof(self) weakself = self;
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    [weakself run];
}];
[operation addExecutionBlock:^{
    [weakself run];
}];
//    [operation start];  只要加入队列,任务就会自动start
[queue addOperation:operation];

或者

[queue addOperationWithBlock:^{
    [weakself run];
}];

其实更多来看 NSOperation相当于一个任务组,里面可以装多个任务,然后任务组被加入队列去执行

那么问题来了:没有串行队列么?按前面说的,所有任务会在其他线程同步执行,那我希望一个个执行怎么办?

NSOperationQueue 有一个参数:maxConcurrentOperationCount

这个参数表示允许并发执行的任务数限制,当为1的时候其实也就是串行执行了

NSOperationQueue *queue = [[NSOperationQueue alloc]init];
__weak typeof(self) weakself = self;
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    [weakself run];
}];
[operation addExecutionBlock:^{
    sleep(1);
    [weakself run];
}];
queue.maxConcurrentOperationCount = 1;
[queue addOperation:operation];
[queue addOperationWithBlock:^{
    [weakself run];
}];

结果

2017-11-09 16:18:23.524428+0800 MultithreadingDemo[98831:6301089] ---<NSThread: 0x600000473840>{number = 3, name = (null)}
2017-11-09 16:18:24.524800+0800 MultithreadingDemo[98831:6301087] ---<NSThread: 0x60000046d640>{number = 4, name = (null)}
2017-11-09 16:18:24.525121+0800 MultithreadingDemo[98831:6301087] ---<NSThread: 0x60000046d640>{number = 4, name = (null)}

其他功能

依赖:NSOperation还有一个非常实用的功能,也就是添加依赖

NSBlockOperation *operationA = [NSBlockOperation blockOperationWithBlock:^{
    sleep(1);
    NSLog(@"拉取A接口--%@",NSThread.currentThread);
}];

NSBlockOperation *operationB = [NSBlockOperation blockOperationWithBlock:^{
    sleep(1);
    NSLog(@"通过A接口参数拉取B接口--%@",NSThread.currentThread);
}];

NSBlockOperation *operationC = [NSBlockOperation blockOperationWithBlock:^{
    sleep(1);
    NSLog(@"通过B接口参数拉取C接口--%@",NSThread.currentThread);
}];
[operationB addDependency:operationA];
[operationC addDependency:operationB];

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operationA, operationB, operationC] waitUntilFinished:NO];

队列允许多个任务同时执行,但因为三个任务之间的依赖,我们看一下结果:

2017-11-09 16:25:56.598192+0800 MultithreadingDemo[98972:6307395] 拉取A接口--<NSThread: 0x6040002748c0>{number = 3, name = (null)}
2017-11-09 16:25:57.599920+0800 MultithreadingDemo[98972:6307396] 通过A接口参数拉取B接口--<NSThread: 0x60000046d680>{number = 4, name = (null)}
2017-11-09 16:25:58.600665+0800 MultithreadingDemo[98972:6307395] 通过B接口参数拉取C接口--<NSThread: 0x6040002748c0>{number = 3, name = (null)}

注意:
使用依赖的时候,我们要注意一点,依赖不能产生循环依赖,不然会死锁
可以使用 removeDependency 来解除依赖关系。
不同的队列之间的任务也可以依赖

4、锁

NSLock

NSLock 遵循 NSLocking 协议,

lock 方法是加锁

unlock 是解锁

tryLock 是尝试加锁,如果失败的话返回 NO

lockBeforeDate: 是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO。

NSConditionLock 条件锁

@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;

NSConditionLock 和 NSLock 类似,都遵循 NSLocking 协议,方法都类似,只是多了一个 condition 属性,以及每个操作都多了一个关于 condition 属性的方法

NSConditionLock 可以称为条件锁:

tryLockWhenCondition:(NSInteger)condition; 只有 condition 参数与初始化时候的 condition 相等,lock 才能正确进行加锁操作。

unlockWithCondition:(NSInteger)condition; 解锁后 condition 的值更新为新的值

NSRecursiveLock 递归锁

NSRecursiveLock 是递归锁,他和 NSLock 的区别在于,NSRecursiveLock 可以在一个线程中重复加锁(反正单线程内任务是按顺序执行的,不会出现资源竞争问题),NSRecursiveLock 会记录上锁和解锁的次数,当二者平衡的时候,才会释放锁,其它线程才可以上锁成功。

如下递归操作,block中每次有加锁操作,再未解锁的时候再次进入递归,再次加锁,造成死锁。NSRecursiveLock就是用来解决这个问题的。
NSLock *normal_lock = [NSLock new];
NSRecursiveLock *recu_lock = [NSRecursiveLock new];
//线程1
dispatch_async(dispatch_get_main_queue(), ^{
    static void (^Block)(int);

    Block = ^(int value) {
        [normal_lock lock];
        if (value > 0) {
            NSLog(@"value:%d", value);
            Block(value - 1);
        }
        [normal_lock unlock];
    };
    Block(5);
});

NSCondition

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

NSCondition 的对象实际上作为一个锁和一个线程检查器,锁上之后其它线程也能上锁,而之后可以根据条件决定是否继续运行线程,即线程是否要进入 waiting 状态,经测试,NSCondition 并不会像上文的那些锁一样,先轮询,而是直接进入 waiting 状态,当其它线程中的该锁执行 signal 或者 broadcast 方法时,线程被唤醒,继续运行之后的方法。

用法如下:

    NSCondition *lock = [[NSCondition alloc] init];
    NSMutableArray *array = [[NSMutableArray alloc] init];
    //线程1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lock];
        while (!array.count) {
            [lock wait];
        }
        [array removeAllObjects];
        NSLog(@"array removeAllObjects");
        [lock unlock];
    });
    
    //线程2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);//以保证让线程2的代码后执行
        [lock lock];
        [array addObject:@1];
        NSLog(@"array addObject:@1");
        [lock signal];
        [lock unlock];
    });
也就是使用 NSCondition 的模型为:

锁定条件对象。

测试是否可以安全的履行接下来的任务。

如果布尔值是假的,调用条件对象的 wait 或 waitUntilDate: 方法来阻塞线程。 在从这些方法返回,则转到步骤 2 重新测试你的布尔值。 (继续等待信号和重新测试,直到可以安全的履行接下来的任务。waitUntilDate: 方法有个等待时间限制,指定的时间到了,则放回 NO,继续运行接下来的任务)

如果布尔值为真,执行接下来的任务。

当任务完成后,解锁条件对象。

而步骤 3 说的等待的信号,既线程 2 执行 [lock signal] 发送的信号。

其中 signal 和 broadcast 方法的区别在于,signal 只是一个信号量,只能唤醒一个等待的线程,想唤醒多个就得多次调用,而 broadcast 可以唤醒所有在等待的线程。如果没有等待的线程,这两个方法都没有作用。

@synchronized代码块

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    @synchronized(self) {
        sleep(2);
        NSLog(@"线程1");
    }
    NSLog(@"线程1解锁成功");
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(1);
    @synchronized(self) {
        NSLog(@"线程2");
    }
});

@synchronized(object) 指令使用的 object 为该锁的唯一标识,只有当标识相同时,才满足互斥,所以如果线程 2 中的 @synchronized(self) 改为@synchronized(self.view),则线程2就不会被阻塞。

@synchronized 指令实现锁的优点就是我们不需要在代码中显式的创建锁对象,便可以实现锁的机制,但作为一种预防措施,@synchronized 块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。
@synchronized 还有一个好处就是不用担心忘记解锁了。

如果在 @sychronized(object){} 内部 object 被释放或被设为 nil,从我做的测试的结果来看,的确没有问题,但如果 object 一开始就是 nil,则失去了锁的功能。不过虽然 nil 不行,但 @synchronized([NSNull null]) 是完全可以的。

条件信号量 dispatch_semaphore_t

dispatch_semaphore 是 GCD 用来同步的一种方式,与他相关的只有三个函数,一个是创建信号量,一个是等待信号,一个是发送信号。 有点和NSCondition类似,都是一种基于信号的同步方式。但 NSCondition 信号只能发送,不能保存(如果没有线程在等待,则发送的信号会失效)而 dispatch_semaphore 能保存发送的信号。dispatch_semaphore 的核心是 dispatch_semaphore_t 类型的信号量。

dispatch_semaphore_create(long value);

dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

dispatch_semaphore_signal(dispatch_semaphore_t dsema);


 
dispatch_semaphore_t signal = dispatch_semaphore_create(1);
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);


dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    dispatch_semaphore_wait(signal, overTime);
    sleep(1);
    NSLog(@"线程1");
    dispatch_semaphore_signal(signal);
    NSLog(@"%@",signal);
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    dispatch_semaphore_wait(signal, overTime);
    sleep(1);
    NSLog(@"线程2");
    dispatch_semaphore_signal(signal);
    NSLog(@"%@",signal);
});

dispatch_semaphore_wait(signal, overTime); 方法会判断 signal 的信号值是否大于 0。大于 0 不会阻塞线程,消耗掉一个信号,执行后续任务。如果信号值为 0,该线程会和 NSCondition 一样直接进入 waiting 状态,等待其他线程发送信号唤醒线程去执行后续任务,或者当 overTime  时限到了,也会执行后续任务。

dispatch_semaphore_signal(signal); 发送信号,如果没有等待的线程接受信号,则使 signal 信号值加一(做到对信号的保存)。

从上面的实例代码可以看到,一个 dispatch_semaphore_wait(signal, overTime); 方法会去对应一个 dispatch_semaphore_signal(signal); 看起来像 NSLock 的 lock 和 unlock,其实可以这样理解,区别只在于有信号量这个参数,lock unlock 只能同一时间,一个线程访问被保护的临界区,而如果 dispatch_semaphore 的信号量初始值为 x ,则可以有 x 个线程同时访问被保护的临界区。

OSSpinLock 自旋锁

OSSpinLock 是一种自旋锁,也只有加锁,解锁,尝试加锁三个方法。和 NSLock 不同的是 NSLock 请求加锁失败的话,会先轮询,但一秒过后便会使线程进入 waiting 状态,等待唤醒。而 OSSpinLock 会一直轮询,等待时会消耗大量 CPU 资源,不适用于较长时间的任务。


    __block OSSpinLock theLock = OS_SPINLOCK_INIT;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        OSSpinLockLock(&theLock);
        NSLog(@"线程1");
        sleep(10);
        OSSpinLockUnlock(&theLock);
        NSLog(@"线程1解锁成功");
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        OSSpinLockLock(&theLock);
        NSLog(@"线程2");
        OSSpinLockUnlock(&theLock);
    });

ThreadLockControlDemo[2856:316247] 线程1
ThreadLockControlDemo[2856:316247] 线程1解锁成功
ThreadLockControlDemo[2856:316260] 线程2

拿上面的输出结果和上文 NSLock 的输出结果做对比,会发现 sleep(10) 的情况,OSSpinLock 中的“线程 2”并没有和”线程 1解锁成功“在一个时间输出,而 NSLock 这里是同一时间输出,而是有一点时间间隔,所以 OSSpinLock 一直在做着轮询,而不是像 NSLock 一样先轮询,再 waiting 等唤醒。

5、常见问题

dispatch_release 已被废弃(6.0)dispatch_release在6.0以后内部被改成对象释放(release)所以 arc后都不再使用。

app启动,系统默认创建5个线程

NSTimer

 [self.timer setFireDate:[NSDate dateWithTimeIntervalSinceNow:5]];//暂停5s

6、捕获开发中子线程更新UI的逻辑

1.为什么UI要在主线程更新

因为UIKit不是线程安全的。试想下面这几种情况:

两个线程同时设置同一个背景图片,那么很有可能因为当前图片被释放了两次而导致应用崩溃。

两个线程同时设置同一个UIView的背景颜色,那么很有可能渲染显示的是颜色A,而此时在UIView逻辑树上的背景颜色属性为B。

两个线程同时操作view的树形结构:在线程A中for循环遍历并操作当前View的所有subView,然后此时线程B中将某个subView直接删除,这就导致了错乱还可能导致应用崩溃。

iOS4之后苹果将大部分绘图的方法和诸如 UIColor 和 UIFont 这样的类改写为了线程安全可用,但是仍然强烈建议讲UI操作保证在主线程中执行。

2.我的想法

View的更新操作 使用runtime 去替换 View 中实现 的方法 不变更实现。只是在中间插入 线程检查操作,发现子线程就必须打印线程调用栈并触发crash。

问题:替换哪些方法更合适? 都会涉及到哪些基础控件需要category?

3.例子

1.创建一个UIImage的category

@implementation UIImage (demo)

+(void)load
{
    Method  m1 = class_getClassMethod([UIImage class],@selector(imageNamed:));
    
    Method m2 = class_getClassMethod([UIImage class],@selector(ximageNamed:));
    
    // 开始交换方法实现
    method_exchangeImplementations(m1, m2);
}
+(UIImage *)ximageNamed:(NSString *)name
{
    NSLog(@"进入方法-开始检查线程");
    
    NSThread *thread = [NSThread currentThread];
    if (![thread isMainThread]) {
        NSLog(@" 当前线程不是主线程  %@",[NSThread callStackSymbols]);
    }
    return [UIImage ximageNamed:name];
}
@end

2.在一个视图内实现一段UIImage的异步赋予图片

UIImageView *img = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 300, 300)];
[self.view addSubview:img];
img.image = [UIImage imageNamed:@"networklosed"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    img.image = [UIImage imageNamed:@"mncg_search_nor"];
});
NSLog(@"测试线程是否异步");

7、参考

GCD使用三部曲之:基本用法

查看原文: iOS线程知识整理

  • purplesnake
  • orangemouse
  • goldenlion
  • brownfrog