多线程

进程

* 进程对应一块内存空间

线程

* 一个进程可以分为多个线程,一个iOS程序运行后,默认会开启一条线程,称为主线程或UI线程

  • 网络开发一般使用多线程

时间片

* 人的感知时有延迟的,CPU将时间分为人无法感知的碎片,称为时间片

串行与并行

* 线程是串行的,通过时间片实现伪并行   * 线程调度时无序的,线程启动后的调度顺序是由CPU决定的,程序员无法参与

线程的缺点

* 开启线程需要占用一定的内存空间,在iOS中,主线程栈区占用1MB,子线程占用512K

  • 线程太多,会占用大量的内存空间,降低程序性能,CPU在调度线程上的开销越大,负荷越大

线程的主要作用

* 显示和刷新UI界面

  • 处理UI事件(比如点击事件、滚动事件、拖拽事件等)

线程的使用注意

* 不要将耗时的操作放到主线程中,以避免主线程被阻塞,影响UI流畅度和用户体验  

  • 不要相信单次运行的结果

线程安全

* 多个线程同时进行操作同一块内存时,读写的数据可能会出现问题

互斥量(mutex)

* 为了避免多个线程同时进行操作同一块内存带来的问题,产生了互斥量的概念,也叫互斥锁、同步锁  

  • 使用互斥量会阻塞线程

  • 加锁的代码范围要尽可能小,只要锁住资源读写部分代码即可

信号量(semaphore)

* 信号量是一个计数器,用来控制访问共享资源的最大并行线程总数,这里的信号量也支持进程(Linux系统中目前不能用于进程)

* 当信号量的计数为1时,效果等价于互斥量

条件变量

线程间通信

XSI IPC

* 共享内存、消息队列和信号量集统称为XSI IPC,遵循相同的POSIX规范

* XSI IPC的同性

* 创建时 都需要提供一个外部key,类型`key_t`(整型)

 * key被用来参与 XSI IPC结构(共享内存、消息队列和信号量集)的创建,创建成功后每个IPC结构都有自己唯一的标识(非负整数)ID

* 在创建IPC结构时,都需要提供结构的访问权限

 * IPC结构由内核管理,如果不手工删除,重启机器后依然占据空间

* key的创建方式有三种

    * 宏 `IPC_PRIVATE`,但基本不用,因为私有别的进程无法获取

    * 可以把所有的key定义到一个头文件中,不会发生重复

    * 可以用 `ftok()`生成key,`ftok()`使用真实存在的路径 + 项目ID 生成key

    * IPC结构的创建/获取函数一般都是

        * `xxxget()` 参数中都有key,新建时一般都需要写上 `IPC_CREAT|权限`,返回值就是ID

* IPC结构一般都提供了一个控制函数

    * `xxxctl()`,都提供了以下功能

    * `IPC_STAT` - 查询

    * `IPC_Setter`  - 修改

    * `IPC_RMID` - 删除

 * IPC结构可以用命令查看、删除

    * `ipcs` - 查询当前有哪些IPC结构

    * `ipcrm` - 删除当前的IPC结构,需要提供ID

    * 选项

        * `-a` 所有

        * `-m` 共享内存

        * `-q` 消息队列

        * `-s` 信号量集

多线程方法编写建议

* 首先保证一条线程工作正常

* 多线程方法需要保证自己能够独立完成所有任务

* 一般开发不建议使用任何锁

pthread

* Unix/Linux/Windows通用,遵循PROSIX规范,在头文件中均已定义

创建进程

pthread_create(pthread_t, NULL, , )

* 成功返回0,返回错误编号

线程状态

join状态

 * join状态的线程会在函数返回时回收资源

detach状态

 * detach状态的线程会在线程结束时回收资源

* 一个线程最好处于joindetach之一,否则无法确定其回收资源的时间

取消线程

pthread_cancel()

* 要取消的线程必须支持取消操作

设置线程状态

pthread_setcancelstate()

设置取消方式

pthread_setcanceltype()

结束线程

return valuepthread_exit()

等待线程

pthread_join()

pthread.c

# include \<pthread.h\>
void* task(void* par){
    int *pi = (int *)par;
    printf("");
    if
    //pthread_exit();
    return 0;
}
int main(){
    pthread_t pt;
    pthread_create(&pt, NULL, task, );//成功返回0,返回错误编号
    pthread_join(pt);
    //pthread_cancel(pt);
    //pthread_setcancelstate();
    //pthread_setcanceltype();
}

互斥量

使用步骤

* 包含头文件

* 声明

* 初始化

* 加锁

* 执行可能造成数据冲突的代码

* 解锁

* 释放互斥量的资源 (销毁)

包含头文件

# include \<pthread.h\>

声明

pthread_mutex_t lock;

初始化

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
pthread_mutex_init(&lock,0);
// 或在声明时赋值:
// pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

加锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
pthread_mutex_lock(&lock);

解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_unlock(&lock);

释放互斥量的资源 (销毁)

int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_destroy(&lock);

信号量

使用步骤

* 包含头文件

* 定义信号量

* 初始化信号量  

* 获取信号量 (计数器减1)

* 访问资源  

* 释放信号量 (计数器加1)

* 回收信号量资源 (销毁)

包含头文件

# include \<semaphore.h\>

定义信号量

sem_t sem;

初始化信号量

sem_init(&sem,0,5);
sem_init(信号量指针,0代表用于线程非0用于进程,计数器最大值);

获取信号量 (计数器减1)

sem_wait(&sem);

访问资源

释放信号量 (计数器加1)

sem_post(&sem);

回收信号量资源 (销毁)

sem_destroy(&sem);

条件变量

NSThread

原生方法

* 需要start方法

number属性

* 1为主线程,否则为其他线程

name属性

* 标识线程

threadPriority属性

* 即线程优先级,仅表示CPU对该任务的调度会更加积极  

* 范围0.0 ~ 1.0

 默认值0.5   通常开发时,不要处理优先级

* 优先级反转

 * 低优先级任务阻塞CPU无法调度高优先级任务

alloc_init


NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:id];
thread.name = @"Thread A";
[thread start];

隐式方法

* 放在后台执行

performSelectorInBackGround:withObject:


[obj performSelectorInBackGround:@selector() withObject:nil];

派发任务

* 类方法指定selector方法在后台线程执行,方法会直接调度  

[NSThread detachNewThreadSelector:@selector() toTarget:self withObject:nil]

获取当前线程

[NSThread currentThread]

线程状态

* 新建(new)  

* start => 就绪状态(Runnable)

* CPU调度 => 运行(Running)  

* 休眠/同步锁 => 阻塞(Blocked)

* 线程任务结束/异常/强行退出 => 死亡(Dead)

线程休眠

[NSThread sleepForTimeInterval:1.0f]  

[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0f]]

结束线程

* 任务执行完毕,线程自然结束  

* 调用exit/return方法,手动强行结束

* 一旦结束线程,后续代码都不会执行   

* `NSThread exit]`

互斥量

* 要求是一个能够加锁的NSObject,所有的线程都能够访问到对象  

* 在实际开发中,如果只抢夺一个资源,就只需要一把锁,通常使用self

synchronized

@synchronized (self){
    // code here...
}

信号量

条件变量

原子锁

* 类似于互斥量,但性能略高  

* 在Obj-CC中定义属性时默认都是atomic

* 如果定义了原子属性,就不要重写gettersetter方法  

* 原子锁本质上是128位自旋锁,能够实现单写多读,保证只有一个线程写入,但允许多个线程读取

* 每一种技术,只要有额外的代码,就需要CPU付出额外的代价,原子属性同样代价很高,性能不好,耗电  

* 建议所有属性都声明为nonatomic

线程安全

* UIKit中几乎所有对象都不是线程安全的  

* 苹果约定,所有UI控件更新均在主线程中执行,从而保证UI正常显示

* 这也是为什么主线程也称为UI线程

  • 有些情况下在其他线程更新UI也是正确的,但一定不要尝试这样的做法

线程间通信

[obj performSelectorOnMainThread:@selector() : waitUntilDone:BOOL]

GCD

* GCD(Grand Central Dispatch,强大中枢调度器)  

* GCD是C语言框架,所有GCD函数均以dispatch开头

 GCD开发时,程序员不需要管线程,而是面对队列*开发,将需要的任务添加到不同的队列中

* GCD的优势

  • GCD是苹果公司为多核的并⾏行运算提出的解决⽅方案

  • GCD会⾃自动利⽤用更多的CPU内核(⽐比如双核、四核)

  • GCD会⾃自动管理线程的⽣生命周期(创建线程、调度任务、销毁线程)

  • 程序员只需要告诉GCD想要执⾏行什么任务,不需要编写任何线程管理代码

  • MRC的GCD开发中,遇到createcopyretain时需要dispatch_release(dispatch_queue_t p)函数释放

  • ARC的GCD开发中,不需要释放

任务

* 用block来定义  

* 任务决定是否开启新线程,同步不开启新线程,异步会开启新线程

同步任务

* 顺序执行任务  

* 同步任务只有一个用处,即阻塞后面的所有任务,等待同步任务完成后再并发执行

dispatch_sync(dispatch_queue_t queque,^{})

异步任务

* 要在别的线程并发执行,会开启新的线程  

* 异步是多线程的代名词

dispatch_async(dispatch_queue_t queque,^{})

队列

* 队列专门用来调度任务,安排不同的任务去不同的线程工作  

* 队列决定能够开启线程的数量,串行最多一条,并发最多线程数量由GCD决定

* 队列也可以不开启线程  

* 队列在调度任务时,如果正在执行的是同步任务,会等待同步任务执行完成后再调度后续的任务

串行队列

* 顺序的队列,所有任务顺序执行,最多开启一条线程

并发队列

* 能够开启多条线程

主队列

* 专门用来做线程间通讯,后台线程完成工作后,通知主线程更新UI  

* 主队列只能获取,不能创建

* 主队列调度任务在主线程工作,不能开启线程  

* 队列调度任务是先进先出,主队列不能开启线程,那么只能顺序执行任务

* 主队列中只能使用异步任务,否则会造成死锁

全局队列

* 全局队列在iOS 7和iOS 8中有区别  

* 全局队列是方便程序员使用,可以直接使用的整个系统全局共享的队列

* 全局队列只能获取,不能创建

全局队列与手动并发队列

* 全局队列本质上就是并发队列,系统全局的调度队列,可以方便程序员使用  

* 创建并发队列有一个好处,在大型商业应用中,通常需要跟踪具体的任务是由哪个队列调度的,如果程序闪退,会产生日志,发送给开发者,便于开发者解决问题,如果能够记录崩溃所在队列的名称,方便排错

dispatch_get_global_queue(long identifier,unsigned long flags)  

* 参数

* `long identifier`

    * iOS 8中为标识符,指定队列的服务质量,QOS与XPC框架联合使用,XPC是OS X开发中用于进程间通讯的框架

        * `QOS_CLASS_UNSPECIFIED`,即0,表示没有使用QOS

    * iOS 7中为优先级

        * `DISPATCH_QUEUE_PRIORITY_HIGH`,即2,高优先级

        * `DISPATCH_QUEUE_PRIORITY_DEFAULT`,即0,默认

        * `DISPATCH_QUEUE_PRIORITY_LOW`,即-2,低优先级

        * `DISPATCH_QUEUE_PRIORITY_BACKGROUND`,即INT16_MIN,后台优先级

        * 如果使用后台优先级,工作性能会慢的令人发指

    * 传入0就可以保证iOS 7与iOS 8全局队列的适配

* `unsigned long flags`,为未来使用保留,应该始终传入0

基本使用

使用步骤

* 定制任务

  • 确定想做的事情

  • 将任务添加到队列中

    • GCD会⾃自动将队列中的任务取出,放到对应的线程中执⾏行

    • 任务的取出遵循队列的FIFO原则,即先进先出,后进后出

串行队列+同步任务

* 不会开启新线程,顺序执行所有任务  

* 任务顺序执行的原因是同步任务

* 在开发中几乎不用

queue_serial_sync

// 队列
dispatch_queue_t que = dispatch_queue_create("meniny",DISPATCH_QUEUE_SERIAL);
// 让队列调度任务
for (int i = 0;i \< 10,i++){
    dispatch_sync(que, ^{
        // code here...
        NSLog(@"%@-%d",[NSThread currentThread],i);
    });
}

串行队列+异步任务

 开启一条新线程,顺序*执行所有任务  

* 任务顺序执行的原因是串行队列

queue_serial_async

// 队列
dispatch_queue_t que = dispatch_queue_create("meniny",DISPATCH_QUEUE_SERIAL);
// 让队列调度任务
for (int i = 0;i \< 10,i++){
    dispatch_async(que, ^{
        // code here...
        NSLog(@"%@-%d",[NSThread currentThread],i);
    });
}

并发队列+异步任务

 会开启多条线程,任务不是*顺序执行

queue_concurrent_async

// 队列
dispatch_queue_t que = dispatch_queue_create("meniny",DISPATCH_QUEUE_CONCURRENT);
// 让队列调度任务
for (int i = 0;i \< 10,i++){
    dispatch_async(que, ^{
        // code here...
        NSLog(@"%@-%d",[NSThread currentThread],i);
    });
}

并发队列+同步任务

* 不会开启新线程,顺序执行所有任务  

* 任务顺序执行的原因是没有开启新线程

queue_concurrent_sync

// 队列
dispatch_queue_t que = dispatch_queue_create("meniny",DISPATCH_QUEUE_CONCURRENT);
// 让队列调度任务
for (int i = 0;i \< 10,i++){
    dispatch_sync(que, ^{
        // code here...
        NSLog(@"%@-%d",[NSThread currentThread],i);
    });
}

主队列+异步任务

 不会开启新线程,顺序*执行所有任务  

* 任务顺序执行的原因是没有开启新线程

main_queue_async

// 队列
dispatch_queue_t que = dispatch_get_main_queue();
// 让队列调度任务
for (int i = 0;i \< 10,i++){
    dispatch_async(que, ^{
        // code here...
        NSLog(@"%@-%d",[NSThread currentThread],i);
    });
}

主队列+同步任务

 不会开启新线程,会造成死锁* 

main_queue_sync

// 队列
dispatch_queue_t que = dispatch_get_main_queue();
// 让队列调度任务
for (int i = 0;i \< 10,i++){
    dispatch_sync(que, ^{
        // code here...
        NSLog(@"%@-%d",[NSThread currentThread],i);
    });
}

全局队列+异步任务

  • 会开启多条线程,任务并发执行

global_queue_async

// 队列
dispatch_queue_t que = dispatch_get_global_queue(0, 0);
// 让队列调度任务
for (int i = 0;i \< 10,i++){
    dispatch_async(que, ^{
        // code here...
        NSLog(@"%@-%d",[NSThread currentThread],i);
    });
}

应用场景

同步任务

例:在线小说网站,需要用户登录,然后再下载小说

dispatch_queue_t que = dispatch_queue_create("meniny",DISPATCH_QUEUE_CONCURRENT);
// 要保证用户登录最先执行
dispatch_async(que, ^{
    dispatch_sync(que, ^{
        NSLog(@"用户登录-%@",[NSThread currentThread]);
    }
    dispatch_async(que, ^{
        NSLog(@"下载小说 A -%@",[NSThread currentThread]);
    }
    dispatch_async(que, ^{
        NSLog(@"下载小说 B -%@",[NSThread currentThread]);
    }
}

队列的选择取舍

* 串行队列最多开启一条线程,任务顺序执行,效率低,执行慢,但省电

 * 对执行顺序要求高,对并发要求不高,执行性能要求不高,兼顾电量,可以选择串行队列

* 并发队列可以开启多条线程,任务并发执行,效率高,执行快,但费电

 * 对执行效率要求高,对执行顺序要求不高,可以选择并发队列

常用GCD组合

最常见的GCD代码

// GCD是用来处理耗时的任务
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    // 耗时的任务
    // code here...
    // 与主线程通讯,更新UI
    dispatch_async(dispatch_get_main_queue(), ^{
        // 更新UI
        // code here...
    });
});

其他GCD用法

延时处理

dispatch_after(dispatch_time_t when,queue , ^{})  

when 延时

* 从`DISPATCH_TIME_NOW`起经过多少纳秒后在`queue`队列上执行`block`

 * dispatch_time(DISPATCH_TIME_NOW,延迟时间_纳秒)

queue 队列  

block 异步任务

dispatch_after

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"%@",[NSThread currentThread]);
});
// 全局队列会延时多少时间后新建线程执行block
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64)(1.0 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
    NSLog(@"%@",[NSThread currentThread]);
});
// 串行队列会延时多少时间后新建线程执行block
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64)(1.0 * NSEC_PER_SEC)), dispatch_queue_create("meniny", NULL), ^{
    NSLog(@"%@",[NSThread currentThread]);
});

成组工作

dispatch_group_t gup = dispatch_group_create();
dispatch_queue_t que = dispatch_get_global_queue(0, 0);
// 派发任务
dispatch_group_async(gup, que, ^{
    NSLog(@"任务一%@",[NSThread currentThread]);
});
dispatch_group_async(gup, que, ^{
    NSLog(@"任务二%@",[NSThread currentThread]);
});
dispatch_group_async(gup, que, ^{
    NSLog(@"任务三%@",[NSThread currentThread]);
});

dispatch_group_notify(gup, dispatch_get_main_queue(), ^{
    NSLog(@"任务全部完成%@",[NSThread currentThread]);
});

单次执行

* 有些代码只希望执行一次,应用最广泛的是单例设计模式  

dispatch_once()是线程安全的,在多线程执行的时候仍可以保证只执行一次,它同样使用了锁,而性能比互斥锁高很多,这是苹果推荐的技术,但目前已经达到了滥用的程度

dispatch_once

static dispatch_once_t onceToken;
NSLog(@"%ld",onceToken);
dispatch_once(&onceToken, ^{
    // 只执行一次的代码
    // code here...
});

NSOperation

  • NSOperation是抽象类,并不具备封装操作的能⼒力,必须使⽤用它的⼦子类

    * 使⽤用NSOperation子类的方式有3种

    • NSInvocationOperation

    • NSBlockOperation

    • 自定义⼦子类继承NSOperation,实现内部相应的⽅方法

  • 配合NSOperation和NSOperationQueue也可以实现多线程

  • NSOperation & GCD属于并发开发

  • 程序猿只需要面对队列,将操作添加到队列上即可,不用关心线程,也不用关心线程状态

  • NSOperation & GCD一般都不需要我们添加自动释放池

  • NSOperation是对GCD的封装

  • 历史

    • NSOperation是iOS 2出现,GCD在iOS 4出现

    • 苹果对NSOperation基于GCD面向对象封装

NSOperation和NSOperationQueue实现多线程的具体步骤

* 先将需要执⾏行的操作封装到⼀个NSOperation对象中

  • 然后将NSOperation对象添加到NSOperationQueue中

  • 系统会⾃自动将NSOperationQueue中的NSOperation取出来

NSOperationQueue

* 实例化的NSOperationQueue本质上就是GCD的全局队列  

* 添加到队列中的操作都是异步执行的

NSOperationQueue

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:op];

NSOperationQueue mainQueue

[[NSOperationQueue mainQueue] addOperationWithBlock:^{
    // upd UI...
}];
  • 通常开发中,会使用一个全局队列来管理

NSOperationQueue

@property (nonatomic, strong) NSOperationQueue *opQueue;

- (NSOperationQueue *)opQueue {
	if (!_opQueue) {
	    _opQueue = [[NSOperationQueue alloc] init];
	}
	return _opQueue;
	}

并发线程数

* 在iOS 7中,使用NSOperation或GCD一般只能开启10条左右的线程  

* 从iOS 8开始,如果任务很多,线程数量同样很大

* 开启线程是有消耗的,实际开发中有必要控制同时并发的线程数量,这个控制在GCD中较难实现  

maxConcurrentOperationCount:(NSInteger)最大并发线程数,设定最大并发线程数

* 在GCD底层为了提高程序并发性能,会维护一个线程池,供所有应用程序的多线程使用,每个线程上的任务执行后,会被GCD线程池回收,而不销毁  

* 在队列调度任务时,会出现任务执行完毕,但底层线程池还没有完全回收,如果在调度其他任务,系统会从线程池中取出一个空线程供程序使用

何时需要控制最大并发线程数?

* 下载网络资源,判断用户联网状态

 * WI-FI:不花钱,能够随时充电,可以让线程数5-6,并发性能好,速度快

* 3G:花钱,不容易充电,可以让线程数少一些,2-3,速度慢

队列暂停与继续调度任务

op.suspended = YES挂起,设定YESNO  

operationCount操作数,返回当前操作数

* 如果队列已经调度了任务,挂起操作不会对任务造成影响,任务仍然会继续执行  

* 如果任务没有执行完毕,会包含在operationCount中

* 任务执行完成后,队列中的任务技术才会变化

应用场景

* TableView滚动时需要暂停队列的下载,而停止滚动,需要让队列继续下载,这样可以避免很多无谓的网络请求

依赖

* 在GCD中可以通过同步任务要求任务执行的顺序,而NSOperation中只有异步任务  

[op2 addDependency:op1]指定任务之间的依赖关系,op2需要等op1执行完毕后才可以执行

 多个任务之间,可以跨队列依赖   注意,线程依赖关系不能循环,否则会造成死锁

应用场景

* 网络下载小说的压缩包  

* 解压缩保存到磁盘

* 通知用户可以阅读

NSInvocationOperation

[op start]

 * 调用start方法后会自动执行target的selector方法

* 不会创建新线程,默认在当前线程同步执行操作   

* 只有添加NSOperation到NSOperationQueue才会异步执行

NSInvocationOperation & start

NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector() object:@"hello"];
[op start];

NSInvocationOperation & NSOperationQueue

NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector() object:@"hello"];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:op];

NSBlockOperation

- (void)blockOperationWithBlock:(void (^)(void))block初始化并添加操作  

- (void)addExecutionBlock:(void (^)(void))block添加更多操作

NSBlockOperation

NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
    // code here...
}];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:op];

NSBlockOperation

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
    // code here...
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        // udp UI
    }];
}];

自定义子类继承NSOperation类,实现相应方法

GCD对比NSOperation

CGD

* GCD时C语言的  

* 两个队列,两种任务,需要排列组合

* 一次性、分组、延迟等功能时NSOperation不具备的

NSOperation

* NSOperation是Obj-C的  

* NSOperation是对GCD的封装

* 新创建的队列就是并发队列,添加到队列中的任务就是异步执行  

* 可以设置最大并发数、暂停与继续使用起来比GCD方便

* 可以指定队列之间的依赖关系,代码结构上直观

提示

* 国内大部分公司在使用GCD,很少使用NSOperation  

* 苹果强烈建议使用NSOperation,从iOS 8开始,GCD底层调度的线程数量从原有的10以内增加到70以上(测试结果)