iOS线程知识整理

什么是线程

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

进程

进程:(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中,线程是能独立运行的基本单位,于是也是独立调度和分派的基本单位。因为线程很“轻”,故线程的切换很是迅速且开销小。

③可并发执行。

在一个进程中的多个线程之间,能够并发执行,甚至容许在一个进程中全部线程都能并发执行;一样,不一样进程中的线程也能并发执行。

④共享进程资源。

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

二、线程之间的通讯

什么是线程通讯

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

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

当咱们建立多个生产者和消费者时,没法直到到底要唤醒哪个,因此这时候咱们就用到了notifAll()方法。

为何要线程通讯

多个线程并发执行时, 在默认状况下CPU是随机切换线程的,当咱们须要多个线程来共同完成一件任务,而且咱们但愿他们有规律的执行, 那么多线程之间须要一些协调通讯,以此来帮咱们达到多线程共同操做一份数据。

固然若是咱们没有使用线程通讯来使用多线程共同操做同一份数据的话,虽然能够实现,可是在很大程度会形成多线程之间对同一共享变量的争夺,那样的话势必为形成不少错误和损失!

因此,咱们才引出了线程之间的通讯,多线程之间的通讯可以避免对同一共享变量的争夺。

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

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

堆在操做系统度进程初始化的时候分配,运行过程当中也能够向系统要额外的堆,可是用完要返还,否则就是内存泄露。安全

iOS中的线程

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

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

一、NSThread

苹果封装、面向对象的、能够直接操控线程对象,很是直观和方便。可是,它的生命周期仍是须要咱们手动管理。

优缺点

优势:轻量级

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

建立并启动

一、先建立线程类,再启动并发

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

二、建立并自动启动app

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

三、使用 NSObject 的方法建立并自动启动异步

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

可是在Swift中没有这个方法:async

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 还以不少方法,下面是一些常见的方法ide

//取消线程
- (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;

二、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号任务是没法执行的
三、当任务走到同步线程开启的时候,线程会被阻塞,直到2号任务block内的任务执行完成才会释放
四、但是同步线程把任务加入queue队列以后才发现,本身要执行的这个任务前面还卡着一个1号任务
五、线程被阻塞,1号任务没法完成,1号任务没完成 2号任务就不能执行
六、形成死锁

因此改一下,只要把同步任务换个队列执行,就能够避免死锁了:
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 插入的任务时 同时只执行了一个任务

三、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}

一、直接执行建立的operation 默认是当前线程
二、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 来解除依赖关系。
不一样的队列之间的任务也能够依赖

四、锁

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 等唤醒。

五、常见问题

dispatch_release 已被废弃(6.0)dispatch_release在6.0之后内部被改为对象释放(release)因此 arc后都再也不使用。

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

NSTimer

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

六、捕获开发中子线程更新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(@"测试线程是否异步");

七、参考

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

相关文章
相关标签/搜索