iOS多线程整理

一.进程与线程

进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元。
一个程序至少要有进程,一个进程至少要有一个线程。想在进程中执行任务就必须开启线程,一条线程就代表一个任务。一个进程中允许开启多条线程。
在网上看到了一个很好的类比:

  • 1.计算机的核心是CPU,它承担了所有的计算任务:假设CPU是一个工厂,时刻在运行。
  • 2.单个CPU一次只能运行一个任务:工厂里有很多车间,但是工厂电力有限,同一时间只能有一个车间工作。
  • 3.进程代表CPU所能处理的单个任务:进程就好比车间,同一时刻CPU只能运行一个进程。
  • 4.一个进程可以包括多个线程:线程就好比车间里的工人,可以多人协同完成一个任务。
  • 5.一个进程的内存空间是所有线程共享的:车间的空间是工人共享的。

二、主线程(UI线程)

主线程的主要作用
1、显示\刷新UI界面
2、处理UI事件(比如点击事件、滚动事件、拖拽事件等)
主线程的使用注意
别将比较耗时的操作放到主线程中
耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的坏体验

三、多线程

同一时间,CPU只能处理一条线程,多线程并发,其实是CPU快速的在多条线程之间快速调度,从而造成的多线程并发的假象。
多线程编程是为了防止主线程被堵塞,提高运行效率。
虽然多线程能提高运行效率,但是线程也不是开的越多越好,太多的话反而会消耗大量的CPU资源,每条线程被调度执行的效率降低。
一般CPU*2条线程能有最好的CPU使用率,不过具体还是要看实际需求。
线程的优先级不是决定线程调用顺序的,他是决定线程被CPU调用的频率的。

多线程的优点:
能适当提高程序的执行效率和资源利用率(CPU、内存利用率)
多线程的缺点:
开启线程需要占用一定的内存空间(iOS中默认情况下主线程和子线程都占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能
线程越多,CPU在调度线程上的开销就越大
程序设计更加复杂:比如线程之间的通信、多线程的数据共享

四、线程加锁

信号量

在多线程环境下用来确保代码不会被并发调用,在进入一段代码前,必须获得一个信号量,在结束代码前,必须释放这个信号量,其他想要执行该代码的线程必须等待直到前者释放了该信号量。
就性能而言OSSpinLock和dispatch_semaphore最好,NSConditionLock、NSLock、@synchronized较差
但是由于OSSpinLock不安全,所以如果考虑性能的话建议使用dispatch_semaphore

1.互斥锁@synchronized(self) {}

互斥锁是给操作全局对象的代码加锁,不是给整条线程加锁。
Self:锁对象,任何继承自NSObkect的对象都可以是锁对象,因为内部都有一把锁,默认是开着的。
锁对象:一定要是全局的锁对象,要保证所有的线程都能够访问,self是最方便使用的锁对象。
互斥锁锁定的范围应该尽量小,但是一定要锁住资源的读写部分。
加锁后程序执行的效率要低一些,因为线程要等待解锁,就是牺牲了性能保证了安全性。

2.线程锁NSLock

NSLock实现了最基本的互斥锁,遵循了NSLocking协议。如果连续使用了两次就会造成死锁问题。如果要在递归中使用锁就要用到NSRecursiveLock递归锁。

    NSLock *lock = [[NSLock alloc] init];
    [lock lock];
    //加锁代码,同样只放抢占资源的读取和修改代码,不要讲其他操作放在里面
    [lock unlock];

3.原子属性atomic

iOS开发中一般所有属性都声明为nonatomic,性能更高,尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力
Nonatomic非原子属性:非线程安全,适合内存小的移动设备
Atomic原子属性:线程安全,需要消耗大量的资源,性能比非原子属性差
Atomic:
表示线程是安全的,是针对多线程设计的属性修饰符,是默认值
保证同一时间只有一个线程能够写入,但是同一时间多个线程都可以读取
Atomic本身就有一个锁,自旋锁

互斥锁和自旋锁都能保证同一时间,只有一条线程执行锁定范围的代码,但是方法不同。
互斥锁:如果发现有其他线程正在执行锁定的代码,线程会进入休眠状态,等待其他线程执行完毕,打开锁之后,线程会重新进入就绪状态,等待被CPU重新调度
自旋锁:如果发现有其他线程正在执行锁定的代码,线程会以死循环的方式,一直等待锁定代码执行完成

4.dispatch_semaphore_t

dispatch_semaphore_t是GCD中的信号量,支持信号通信和信号等待。每当发送一个信号通知,则信号量+1;每当发送一个等待信号时信号量-1;如果信号量为0,则信号会处于等待状态,直到信号量大于0开始执行。

#import "MYDispatchSemaphoreViewController.h"

@interface MYDispatchSemaphoreViewController (){
    dispatch_semaphore_t semaphore;
}
@end
@implementation MYDispatchSemaphoreViewController

- (void)viewDidLoad {
    [super viewDidLoad];
//创建一个信号量为1的信号
    semaphore = dispatch_semaphore_create(1);
}

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    /**
     *  semaphore:等待信号
     DISPATCH_TIME_FOREVER:等待时间
     wait之后信号量-1,为0
     */
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObject:imageName];
    }
    //发送一个信号通知,这时候信号量+1,为1
    dispatch_semaphore_signal(semaphore);
}

@end

5.条件锁NSConditionLock

//初始化一个NSConditionLock对象
- (id)initWithCondition:(NSInteger)condition

//返回一个Condition
- (NSInteger)condition

//获取和释放锁
1、– (BOOL)lockBeforeDate:(NSDate *)limit
//在指定时间前尝试获取锁,若成功则返回YES 否则返回NO
2、– (void)lockWhenCondition:(NSInteger)condition
//尝试获取锁。在加锁成功前接收对象的Condition必须与参数Condition 相同

3、– (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit
//同上,只是又加上了一个时间

4、– (BOOL)tryLock //尝试着获取锁
5、– (BOOL)tryLockWhenCondition:(NSInteger)condition
//如果接收对象的condition与给定的condition相等,则尝试获取锁
6、– (void)unlockWithCondition:(NSInteger)condition
//解锁并设置接收对象的condition

6.递归锁NSRecursiveLock

有时候“加锁代码”中存在递归调用,递归开始前加锁,递归调用开始后会重复执行此方法以至于反复执行加锁代码最终造成死锁,这个时候可以使用递归锁来解决。使用递归锁可以在一个线程中反复获取锁而不造成死锁,这个过程中会记录获取锁和释放锁的次数,只有最后两者平衡锁才被最终释放。

7.GCD线程阻断dispatch_barrier_async/dispatch_barrier_sync

@interface MYdispatch_barrier_syncViewController ()
{
        __block double then, now;
}
@property (nonatomic, assign)dispatch_queue_t queue;
@end

@implementation MYdispatch_barrier_syncViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    _queue = dispatch_queue_create(“myQueue", DISPATCH_QUEUE_CONCURRENT);
}
- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    if (imageNames.count>0) {
        imageName = [imageNames firstObject];
        [imageNames removeObjectAtIndex:0];
    }else{
        now = CFAbsoluteTimeGetCurrent();
        printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
    }
}

- (void)getImageNameWithMultiThread{
    NSMutableArray *imageNames = [NSMutableArray new];
    int count = 1024*11;
    for (int i=0; i<count; i++) {
        [imageNames addObject:[NSString stringWithFormat:@"%d",i]];
    }
    then = CFAbsoluteTimeGetCurrent();
    for (int i=0; i<count+1; i++) {
        //100来测试锁有没有正确的执行
        dispatch_barrier_async(_queue, ^{
             [self getIamgeName:imageNames];
        });
    }
}

8.自旋锁OSSpinLock

性能最高的锁。原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量CPU资源,所以它不适用于较长时间的任务。对于内存缓存的存取来说,它非常合适。
如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。这并不只是理论上的问题,libobjc 已经遇到了很多次这个问题了,于是苹果的工程师停用了 OSSpinLock。

总结:

@synchronized:适用线程不多,任务量不大的多线程加锁
NSLock:其实NSLock并没有想象中的那么差,不知道大家为什么不推荐使用
dispatch_semaphore_t:使用信号来做加锁,性能提升显著
NSCondition:使用其做多线程之间的通信调用不是线程安全的
NSConditionLock:单纯加锁性能非常低,比NSLock低很多,但是可以用来做多线程处理不同任务的通信调用
NSRecursiveLock:递归锁的性能出奇的高,但是只能作为递归使用,所以限制了使用场景
OSSpinLock:性能也非常高,可惜出现了线程问题
dispatch_barrier_async/dispatch_barrier_sync:测试中发现dispatch_barrier_sync比dispatch_barrier_async性能要高,真是大出意外

五、iOS开发中多线程编程的三种方式

NSThread最轻量级,但需要手动管理线程的生命周期,线程同步也会对有一定的系统开销
GCD性能最优,使用起来最灵活
NSOperation是由GCD封装而来,性能跟GCD比相差不大,但是可以自己控制最大并发数,添加依赖关系,业务逻辑比较复杂的情况用此方法较好

1.NSThread

这套方案是经过苹果封装的, 完全面向对象,可以直接控制线程对象。
优点:NSThread是最轻量级的方案,能更直观地控制线程对象
缺点:需要自己管理线程的生命周期,线程同步。协调多个线程对同一数据的访问时一般是在访问前加锁,这会导致一定的性能开销

属性:
name - 线程名称
给线程起名字,可以方便运行调试,定位BUG
threadPriority - 线程优先级
为浮点数整形,范围在0~1之间,1最高,默认0.5,不建议修改线程优先级
线程的"优先级"不是决定线程调用顺序的,他是决定线程备CPU调用的频率的
在开发的时候,不要修改优先级
多线程开发的原则是越简单越好

//使用对象方法创建新线程需要手动开启
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(demo:) object:nil];
//类方法和隐式创建新线程时,不需要手动开启,会直接执行doSth方法
[NSThread detachNewThreadSelector:@selector(doSth) toTarget:self withObject:nil];
[self performSelectorInBackground:@selector(doSth) withObject:nil];
//如果子线程结束后需要刷新UI要回到主线程中刷
[self performSelectorOnMainThread:@selector(updateUI:) withObject:data waitUntilDone:YES];
+ (void)sleepUntilDate:(NSDate *)date;休眠到指定日期
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;休眠指定时长
- (void)cancel NS_AVAILABLE(10_5, 2_0);取消线程
- (void)start NS_AVAILABLE(10_5, 2_0); 启动线程
+ (void)exit; 退出当前线程, 不能在主线程中调用

2.GCD

Grand Central Dispatch (GCD)是Apple开发的一个多核编程的解决方案。
GCD是一套底层的C语言构成的API,在GCD中我们要向队列中添加一段代码块,而不需要直接和线程打交道。GCD在后端管理着一个线程池,它不仅决定着我们的代码将在哪个线程被执行,还可以根据可用的系统资源对这些线程进行管理,它会自动管理线程的生命周期(创建线程、调度任务、销毁线程)。

任务:要执行的操作,其实就是一段代码,分为同步任务和异步任务
同步任务:会阻塞当前线程并等待Block中的任务执行完毕,然后当前线程才会继续往下运行
异步任务:当前操作会直接往下执行,不会阻塞当前线程
(注:1.用是否开辟新线程来解释同步和异步是不准确的 2.要回到主线程中刷新UI 3.主队列调用同步任务会卡死)

队列:用于存放任务
串行队列:GCD会FIFO(先进先出)的取出来一个,执行一个,然后取下一个,这样一个一个执行
并行队列:GCD也会FIFO的取出,但是取出一个就放到别的线程,然后取出一个又放到另一个的线程。这样由于取的快,看起来所有的任务都是一起执行的。不过需要注意,GCD会根据系统资源控制并行的数量,所以如果任务很多,它不会让所有任务同时执行。

主队列:这是一个特殊的串行队列,用于刷新UI,任何需要刷新UI的工作都要在主队列执行,所以一般耗时的任务都要放到别的线程执行。
dispatch_queue_t queue = dispatch_get_main_queue();
自定义队列:使用dispatch_queue_create创建,第一个参数是标识符,第二个参数表示串行还是并行。

  //串行队列
  dispatch_queue_t queue = dispatch_queue_create("tk.bourne.testQueue", NULL);
  dispatch_queue_t queue = dispatch_queue_create("tk.bourne.testQueue", DISPATCH_QUEUE_SERIAL);
  //并行队列
  dispatch_queue_t queue = dispatch_queue_create("tk.bourne.testQueue", DISPATCH_QUEUE_CONCURRENT);
  //全局并发队列:并行任务一般都加入这个系统提供的并发队列,
  dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

创建任务,第一个参数是任务所在的队列,block中写要执行的操作

同步任务:
dispatch_sync(queue, ^{
    });
异步任务:
dispatch_async(queue, ^{
    });

串行同步:不会开线程,顺序执行
串行异步:会开线程,顺序执行
并行同步:不开线程,顺序执行
并行异步:开线程,乱序执行

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    NSLog(@"只执行一次");
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"延迟5S");
});

屏障队列
dispatch_barrier_async(_ queue: dispatch_queue_t, _ block: dispatch_block_t)
不管传入的是串行队列还是并行队列,这个方法会阻塞这个queue(不是阻塞当前线程),直到这个queue中排在它前面的任务都执行完成后才会开始执行自己,自己执行完毕后,再会取消阻塞,使这个queue中排在它后面的任务继续执行
dispatch_barrier_sync(_ queue: dispatch_queue_t, _ block: dispatch_block_t)
会阻塞传入的queue和当前线程,其他的和上面一样

队列分组

    //组内的任务执行完后可以执行方法通知我们
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //第一个参数:任务所在的分组
    //第二个参数:任务所在的队列
    dispatch_group_async(group, globalQueue, ^{
        NSLog(@"分组任务1");
    });
    dispatch_group_async(group, globalQueue, ^{
        NSLog(@"分组任务2");
    });
    dispatch_group_notify(group, globalQueue, ^{
        //当上面两个任务都完成以后,会执行这个方法,我们在这里处理我们的需求
    });

3.NSOperation

NSOperation是苹果对GCD的封装,完全面向对象,用起来更好理解
NSOperation对应GCD的任务,NSOperationQueue对应GCD的队列
使用:
1.将要执行的任务封装到一个NSOperation对象中
2.将此任务添加到一个NSOperationQueue对象中
注:NSOperation本身是抽象基类,因此必须使用它的子类,Foundation框架提供了两个具体子类直接供我们使用:NSInvocationOperation和NSBlockOperation
NSOperation调用start方法即可开始执行操作,NSOperation对象默认在当前队列同步执行,中途可以用cancel取消任务

1.创建NSInvocationOperation对象
  NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
  NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
      NSLog(@"%@", [NSThread currentThread]);
  }];
2.开始执行
  [operation start];

NSBlockOperation使用这个方法可以给Operation添加多个要执行的Block,此时会在主线程和其他线程中并发执行

[operation addExecutionBlock:^{}];

NSOperation 有一个添加依赖关系的功能。比如有 3 个任务:A: 从服务器上下载一张图片,B:给这张图片加个水印,C:把图片返回给服务器。

//1.任务一:下载图片
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"下载图片 - %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1.0];
}];

//2.任务二:打水印
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"打水印   - %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1.0];
}];

//3.任务三:上传图片
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"上传图片 - %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1.0];
}];

//4.设置依赖
[operation2 addDependency:operation1];      //任务二依赖任务一
[operation3 addDependency:operation2];      //任务三依赖任务二

//5.创建队列并加入任务
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];

注:1.相互依赖会死锁  2.可以使用removeDependency来解除依赖关系 3、可以在不同的队列之间依赖,依赖是添加在任务上的,与队列无关

NSOperationQueue - 队列
添加到队列后会自动调用任务的start()方法

 NSOperationQueue *queue = [NSOperationQueue mainQueue]; //主队列
 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; //其他队列
 queue.maxConcurrentOperationCount  //队列最大并发数
//1.创建一个其他队列    
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

设置队列的最大并发数,设置成1这个队列就成了串行队列
queue.maxConcurrentOperationCount = 1;

//2.创建NSBlockOperation对象
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"%@", [NSThread currentThread]);
}];

//3.添加多个Block
for (NSInteger i = 0; i < 5; i++) {
    [operation addExecutionBlock:^{
        NSLog(@"第%ld次:%@", i, [NSThread currentThread]);
    }];
}

//4.队列添加任务
[queue addOperation:operation];

//5.直接给队列添加Block
[queue addOperationWithBlock:^{
}];

其他:

BOOL executing; //判断任务是否正在执行

BOOL finished; //判断任务是否完成

void (^completionBlock)(void); //用来设置完成后需要执行的操作

- (void)cancel; //取消任务

- (void)waitUntilFinished; //阻塞当前线程直到此任务执行完毕
NSOperationQueue

NSUInteger operationCount; //获取队列的任务数

- (void)cancelAllOperations; //取消队列中所有的任务

- (void)waitUntilAllOperationsAreFinished; //阻塞当前线程直到此队列中的所有任务执行完毕

[queue setSuspended:YES]; // 暂停queue

[queue setSuspended:NO]; // 继续queue

最后附一张图

660EEC5945983421074FDBF8A658BFF8.jpg
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容