并发编程 - 概述

引言

为了提升程序的性能和效率,并发编程在开发过程中扮演着至关重要的角色。iOS设备通常配备多个CPU核心,这意味着即使应用的主线程忙于处理UI事件,应用仍然可以在后台执行更多的计算任务,而不需要频繁的上下文切换,从而提高整体的执行效率。

在本篇博客中,我们将讨论线程的基本概念,并探讨iOS中并发编程的实现方案。此外,我们还将深入探讨如何确保并发编程的安全性,避免常见的线程安全问题。

线程

线程是运行时执行的一组指令序列。

每个进程都至少包含一个线程,在iOS中进程启动时的主要线程通常被称为主线程负责处理UI事件,又称为UI线程。与用户交互相关的所有中断时间最终都会被分发到UI线程。Cocoa编程不允许在其它线程更新UI,这就意味着无论何时应用在后台线程执行了耗时操作比如网络请求,数据处理或者其他操作,代码都必须将上下文切换到主线程再进行UI更新。

线程开销

虽然多个线程可以提升应用对事件的处理效率,但是每个线程也都有一定的开销,它也会影响到程序的性能,线程不仅仅有创建时的时间开销,还会消耗内核的内存,即应用的内存空间。

每个线程大约消耗1KB的内核内存空间,这块内存将用于存储于线程有关的数据结构和属性,且属于联动内存,无法被分页。

对于主线程而言它的栈空间大小为1M,而且无法修改,所有二级线程默认分配521KB的栈空间。但是需要注意完整的栈并不会立即被创建出来,实际的栈空间大小会随着使用而增长。因此即使主线程有1MB的栈空间,某个时间点的实际栈空间可能要小很多。

在线程启动前,栈空间的大小是可以改变的,栈空间的最小值是16KB,而且其数值必须是4KB的整数倍。

并发编程方案

iOS中有多种并发方案可供我们选择,通常情况下只使用GCD就可以很好的实现并发编程的需求,但是有些特定的场景我们仍然需要考虑使用NSTread或者是NSOperationQueue,下面我们来分别简单介绍一下这些方案。

GCD

GCD提供的内容很多,通常来讲它可以:

  1. 创建任务和分发队列,并且允许我们在主线程执行,并发执行,串行执行任务。
  2. 创建线程组,实现对一组任务的执行情况进行跟踪,而无需关心这些任务基于的队列和线程。
  3. 创建信号量,通过信号量来控制最大的并发数,或使用它保证并发异步的线程安全。
  4. 创建屏障,允许我们在并发的队列中创建同步的点。
  5. 分发对象和管理源,实现更为底层的管理和控制。

但是它有一个硬性的限制就是最多只能创建64个线程,虽然这个数值对于移动应用来讲是一个很高的合理值,但是如果我们不加以控制,在代码执行时间过程时,也是有可能达到GCD现场池的上线的。所以我们应该避免浪费地使用dispatch_async和dispatch_sync。

NSOperation&NSOperationQueue

操作和操作队列是并发编程中又一个重要概念。

NSOperation封装了一个任务以及和任务相关的数据和代码,而NSOperationQueue以先入先出的顺序控制了一个或者多个这类任务的执行。

NSOperation和NSOperationQueue都提供控制线程个数的能力。可用maxConcurrentOperationCount属性控制队列的个数,也可以控制每个队列的线程个数。

在使用NSOperationQueue时,我们需要自己管理创建的队列,没有默认队列,可以设置队列的优先级。

而且NSOperationQueue是多核安全的,我们可以放心地分享队列,从不同的线程中提交任务,而不用担心随还队列。

NSTread

NSThread 是 iOS 中最基本的多线程实现方式。

NSThread 允许我们直接管理线程的生命周期,包括创建、启动、暂停、恢复和销毁线程。相比于 GCD 和 NSOperationQueue,NSThread 提供了更底层的控制,因此适用于一些需要手动管理线程的特殊场景。

虽然NSTread为我们提供了更底层的控制,但是在使用起来复杂性也比较高,容易出错。因此大多数场景我们仍然推荐使用GCD或者是NSOperationQueue。

线程安全

并发编程中往往伴随的就是线程安全问题,贯穿整个软件开发的职业生涯,我们总被要求要编写线程安全的代码,也就是说,如果有多个线程并行地执行同一组指令,不能产生任何副作用。

下面两个技术方案可以实现这一点:

  1. 不要使用可修改的共享状态。
  2. 如果无法避免使用可修改的共享状态,那么确保代码是线程安全的。

这说起来容易,但是在开发中做起来却非常难。

原子属性

iOS中的属性默认为非原子性的,如果一个属性设置为原子属性(atomic)那么它的修改和读取肯定都是原子的,这是实现应用线程安全的一个良好开端。

@property(atomic)NSString * firstName;//原子属性

这一点很重要,因为这可以组织两个线程同时更新一个值,否则的话可能导致错误的状态,因为原子属性保证了正在修改属性的线程必须处理完毕后,其它线程才能开始处理。

但是原子属性存在性能开销,所以过渡使用它们并不明智,另外在Swift中我们并没有办法像OC一样设置属性的为原子性,所以我们还需要考虑其它保证线程安全的方案。

同步块 @synchronized()

使用@synchronized()指令可以创建一个信号量,并且进入临界区,在临界区内的代码在任何时刻都只能被一个线程执行,从而保证线程安全。

- (void)updateUser:(HPUser *)user properties:(NSDictionary *)properties{

    @synchronized(user){//取得针对user对象的锁。一切相关的修改都会被一同处理,而不会发生竞争状态

        NSString * fn = [properties objectForKey:@“firstName”];

        if(fn != nil){

            user.firstName = fn;

        }

        NSString * ln = [properties objectForKey:@“lastName”];

        if(fn != nil){

            user.lastName = ln;

        }

    }

}

但是过渡使用@synchronized()指令也会拖慢应用的运行速度,所以我们需要合理的使用它,将保证线程安全的最少的代码放到临界区内。

另外@synchronized()括号内设置的对象也至关重要,每一个不同的对象表示一个不同的临界区。

即使原子属性和同步块已经可以解决大部分线程安全的问题,但是在并发编程中我们仍然避免不了和锁打交道。

锁是进入临界区的基础结构,atomic属性和@synchrinized块是为了实现更便捷实用的高级别抽象。

以下是三种可用的锁:

NSLock

这是中低级别的锁,一旦获取了锁,执行则进入临界区,并且不会超过一个线程并行执行,释放锁则标记着临界区的结束。

- (instanceType)init{

    if(self = [super init]){

        self->lock = [NSLock new];//初始化锁

    }

    return self;

}

- (void)safeMethod{

    [self->lock lock];//获取锁,进入临界区

    //线程安全的代码,在临界区任意时刻最多只允许一个线程执行

    [self->lock unlock];//释放锁标记着临界区的结束。其他线程现在能够获取锁了。

}

 NSLock在再次进行加锁前必须先解锁,NSLock必须在锁定的线程中进行解锁。

NSRecursiveLock

递归锁,与NSLock不同的时,它允许在解锁前多次锁定,如果解锁的次数与锁定的次数匹配上了,那么就意味着锁释放了,其它线程可以获取锁。

当类中有多个方法使用同一个锁进行同步,且其中一个方法调用另一个方法时,NSRecursiveLock非常有用。

- (instancetype)init{

    if(self = [super init]){

        self->lock = [NSRecursiveLock new];

    }

    return self;

}

- (void)safeMethod1{

    [self->lock lock];//safeMethod1方法获取锁。

    [self safeMethod2];//它调用了safeMethod2方法。

    [self->lock unlock];//safeMehod1释放了锁。因为每个锁定操作都有一个相应的解锁操作与之匹配,所以锁现在被释放,并可以被其他线程所获取。

}

- (void)safeMethod2{

    [self->lock lock];//safeMethod2从已经获取到的锁再次获取了锁。

    //线程安全的代码

    [self->lock unlock];//safeMethod2释放了锁

}

NSCondition

有些情况需要协调线程之间的执行。例如,一个线程可能需要等待其他线程返回结果。NSCondition可以原子性地释放锁,从而使得其他等待的线程可以获取锁,而初始的线程继续等待。

一个线程会等待释放锁的条件变量。另一个线程会通知条件变量释放该锁,并唤醒等待中的线程。

在生产者-消费者问题上NSCondition锁就非常适用。

结语

任何应用的开发都会涉及到并发编程,选择正确的方案来确保代码的线程安全至关重要,使用信号量同步访问代码块非常重要;使用读-写锁实现高吞吐量的读和保护的写同样重要。

在本篇博客中我们介绍了线程的概念和线程的开销,讨论了多线程的多种实现方案,以及实现线程安全的多种方案。

在后续的博客中,我们将会详细讨论每个多线程的实现方案以及保证线程安全的具体案例。

  • 13
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值