美文网首页
iOS 面试 -- 多线程

iOS 面试 -- 多线程

作者: Crics | 来源:发表于2019-06-27 22:18 被阅读0次

    1、进程

            1)进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动,它是操作系统分配资源的基本单元。

            2)进程是指在系统中正在运行的一个应用程序,就是一段程序的执行过程,可以理解为手机上的一个app。

            3)每个进程之间是独立的,每个进程均运行在某专用且受保护的内存空间内,拥有独立运行所需的全部资源。

    2、线程

            1)程序执行流的最小单元,线程是进程中的一个实体。

            2)一个进程要想执行任务,必须至少有一条线程,应用程序启动的时候,系统会默认开启一条进程,也就是主线程。

    3、进程和线程的关系

            1)线程是进程的执行单元,进程的所有任务都在线程中执行。

            2)线程是CPU分配资源和调度的最小单位

            3)一个程序可以对应多个进程(多进程),一个进程中可有多个线程,但至少要有一条线程。

            4)同一个进程内的线程共享进程资源。

    4、多进程

            打开Mac的活动监视器,可以卡到很多个进程同时运行。

            1)进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然,程序是死的(静态的),进程是活的(动态的)。

            2)进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们是处于运行状态下的操作系统本身。所有由用户启动的进程都是用户进程,进程是操作系统进行资源分配的单位。

            3)进程又被细化为线程,也就是一个进程下有多个能独立运行的更小的单位。在同一个时间里,同一台计算机系统中如果允许两个或两个以上的进程处于运行状态,这便是多进程。

    5、多线程

            1)同一时间,CPU只能处理1条线程,只有1条线程在执行。多线程并发执行,其实就是CPU快速地在多条线程之间调度(切换)。如果CPU调度线程的实践足够快,就造成了多线程并发执行的假象。

            2)如果线程非常非常多,CPU会在N多线程之间调度,消耗大量CPU资源,每条线程被调度执行的频次会降低(线程的执行效率降低)。

            3)多线程的优点:

                    能适当提高程序的执行效率。

                    能适当提高资源利用率(CPU、内存利用率)。

            4)多线程的缺点:

                    开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能。线程越多,CPU在调度线程上的开销就越大。

                    程序设计更加复杂:比如线程之间的通信、多线程的数据共享等。

    6、任务

            就是执行操作的意思,也就是在线程中执行的那段代码。在GCD中是放在block中的。执行任务有两种方式:同步执行(sync)和异步执行(async)。

            1)同步(Sync):同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行,即会阻塞线程。只能在当前线程中执行任务(是当前线程,不一定是主线程),不具备开启新线程的能力。    

            2)异步(Async):线程会立即返回,无需等待就会继续执行下面的任务,不阻塞当前线程。可以在新的线程中执行任务,具备开启新线程的negligence(并不一定开启新线程)。如果不是添加到主队列上,异步会在子线程中执行任务。

    7、队列

            队列(Dispatch Queue):这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。

            在GCD中有两个队列:串行队列和并发队列。两者都符合FIFO(先进先出)的原则。

            两者的主要区别是:执行顺序不同,以及开启线程数不同。

            1)串行队列:(Serial Despatch Queue):

                    同一时间,队列中只能执行一个任务,只有当前的任务执行完成之后,才能执行下一个任务。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)。主队列是主线程上的一个串行队列,是系统自动为我们创建的。

            2)并发队列(Concurrent Dispatch Queue):

                    同时允许多个任务并发执行。(可以开启多个线程,并且同时执行任务)。并发队列的并发功能只有在异步(dispatch_async)函数下才有效


    IOS多线程:NSThread、NSOperationQueue、GCD

    1)NSThread:轻量级别的多线程技术

    是我们自己手动开辟的子线程,如果使用的是初始化方式就需要我们自己启动,如果使用的是构造方式它就会自动启动。只需要我们手动开辟的线程,都需要我们自己管理该线程,不只是启动,还有该线程使用完毕后的资源回收。

    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(testThread:) object@"手动创建"];

    // 当使用初始化方法出来的线程需要start启动

    [thread start];

    // 可以为开辟的线程起名字

    thread.name = @"自己的线程";

    // 调整Thread的权限,线程权限的范围是0~1。越大权限越高,先执行的概率就会越高,由于概率高,所以并不能很准确的实现我们想要的执行顺序,默认值时0.5

    thread.threadPriority = 1;

    // 取消当前已经启动的线程

    [thread cancel];

    // 通过遍历构造器开辟线程

    [NSThread detachNewThreadSelector:@selector(testThread:) toTarget:self withObject:@"构造器方法"];

    performSelector...只要是NSObject的子类或者对象都可以通过调用此方法进入子线程和主线程,其实这些方法所开辟的子线程也是NSThread的另一种体现方式。

    在编译阶段并不会去检查方法是否有效存在,如果不存在,只会给出警告。

    // 在当前线程,延迟1s执行。响应了OC语言的动态性:延迟到运行时才绑定方法

    [self performSelector:@selector(test) withObject:nil afterDelay:1];

    // 回到主线程。waitUntilDone:是否将该回调方法执行完再执行后面的代码。如果是YES:就必须等回调方法执行完成之后才能执行后面的代码,会阻塞当前的线程;如果是NO:就是不等回调方法结束,不会阻塞当前线程

    [self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:YES];

    // 开辟子线程

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

    // 在指定线程执行

    [self performSelector:@selector(test) onThread:[NSThread currentThread] withObject:nil waitUntilDone:YES];

    需要注意的是:如果是带有afterDelay的延时函数,会在内部创建一个Timer,然后添加到当前线程的RunLoop中,也就是如果当前线程没有开启runloop,该方法会失效。在子线程中,需要启动runloop(注意调用顺序)。

    [self performSelector:@selector(test) withObject:nil afterDelay:1];

    [[NSRunLoop currentRunLoop] run];

    而performSelector: withObject: 只是一个单纯的消息发送,和时间没有一点关系。所以不需要添加到子线程的RunLoop中也能执行。

    2)GCD和NSOperationQueue

    GCD是面向底层的C语言的API,NSOperationQueue是用GCD构建封装的,是GCD的高级抽象。

    1、GCD执行效率更高,而却由于队列中执行的是block构成的任务,这是一个轻量级的数据结构,写起来更方便。

    2、GCD只支持FIFI的队列,而NSOperationQueue可以通过设置最大并发数,设置优先级,添加依赖关系等调整执行顺序。

    3、NSOperationQueue甚至可以跨队列设置依赖关系,但是GCD只能通过设置串行队列,或者在队列内添加barrier(dispatch_barrier_async)任务,才能控制执行顺序,较为复杂。

    4、NSOperationQueue是面向对象,所以支持KVO,可以监听operation是否正在执行(isExecuted)、是否结束(isFinished)、是否取消(isCancelled)。

            1)实际项目开发中,很多时候只会用到异步操作,不会有特别复杂的线程关系管理,所以苹果推崇的且优化完善、运行快速的GCD是首选。

            2)如果考虑异步操作之间的事务性,顺序性,依赖关系,比如多线程并发下载,GCD需要自己写更多的代码来实现,而NSOperationQueue已经内建了这些支持

            3)不论是GCD还是NSOperationQueue,我么接触的都是任务和队列,都没有直接接触到线程,事实上线程管理也的确不需要我么操心,系统对于线程的创建,调度管理和释放都做得很好。而NSThread需要我们自己去管理线程的生命周期,还要考虑线程头部、加锁问题,造成一些性能上的开销。

    3)死锁

    死锁就是队列引起的循环等待

    (1)一个比较常见的死锁:主队列同步   

     - (void)viewDidLoad {

            [super viewDidLoad];

            dispatch_sync(dispatch_get_main_queue(), ^{  

                    NSLog(@"1");

            });

            NSLog(@"2");

    }

    在主线程中运用主队列同步,也就是把任务放到了主线程的队列中。

    同步对于任务是立刻执行的,那么当吧任务放到主队列时,它就会立马执行,只有执行完这个任务,viewDidLoad才会继续向下执行。

    而viewDidLoad和任务都是在主队列上的,由于队列的FIFO(先进先出)原则,任务又需等待viewDidLoad执行完毕后才能继续执行,viewDidLoad和这个任务就形成了相互循环等待,就造成了死锁。

    想避免这种死锁,可以将同步改成异步dispatch_async或者将dispatch_get_main_queue换成其他串行或并行队列,都可以解决

    (2)下面的代码也会造成死锁:

    dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

    dispatch_async(serialQueue, ^{

            dispatch_sync(serialQueue, ^{

                    NSLog(@"111");

            });

            NSLog(@"222");

    });

    外面的函数无论是同步还是异步都会造成死锁。

    这是因为里面的任务和外面的任务都是在同一个serialQueue队列内,又是同步,这就和上面主队列同步的例子一样造成了死锁。

    解决方案和上面的一样,将里面的同步改成异步dispatch_async,或者将serialQueue换成其他串行或者并行队列,都可以解决。

    dispatch_queue_t serialQueue1 = dispatch_queue_create("test",  DISPATCH_QUEUE_SERIAL);

    dispatch_queue_t serialQueue2 = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

    dispatch_async(serialQueue1, ^{

            dispatch_sync(serialQueue2, ^{

                    NSLog(@"111");

            }) ;

            NSLog(@"222");

    });

    这样是不会死锁的,并且serialQueue1和serialQueue2是在同一个线程中。

    4)GCD —— 队列

    GCD有三种队列类型:

    1、main queue:通过dispatch_get_main_queue()获得,这是一个与主线程相关的串行队列。

    2、global queue:全局队列,是并发队列,由整个进程共享。存放着高、中、低三种优先级的全局队列。调用dispatch_get_global_queue并传入优先级来访问队列。

    3、自定义队列:通过函数dispatch_queue_create创建的队列。

    (1)串行队列先异步后同步

    dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

    NSLog(@"1");

    dispatch_async(serialQueue, ^{

            NSLog(@"2");

    });

    NSLog(@"3");

    dispatch_sync(serialQueue, ^{

            NSLog(@"4");

    });

    NSLog(@"5");

    打印顺序:13245。

    原因:首先先打印1;接下来将任务2添加到串行队列上,由于任务2是异步,不会阻塞线程,继续向下执行,打印3;然后是任务4,将任务4添加到串行队列上,因为任务4和任务2在同一个串行队列,根据队列FIFI原则,任务4必须等待任务2执行完后才能执行,有因为任务4是同步任务,会阻塞线程,只有执行完任务4才能继续向下执行打印5。

    这里的任务4在主线程中执行,而任务2在子线程中执行。如果任务4是添加到另一个串行队列或者并行队列,则任务2和任务4无序执行。

    (2)performSelector

    dispatch_async(dispatch_get_global_queue(0 ,0 ) ^{

                [self performSelector:@selector(test) withObject:nil afterDelay:0];

    });

    这里的test方法不会去执行,原因是

    - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;

    这个方法要创建任务提交到runloop上,而gcd框架继承的线程默认没有开启对应runloop的,所以这个方法会失效。

    而如果将dispatch_get_global_queue改成主队列,由于主队列所在的主线程是默认开启了runloop的,就会去执行。

    将dispatch_async改成同步,因为同步是在当前线程执行,那么如果当前线程是主线程,test方法也会去执行。

    在performSelector 下面添加[[NSRunLoop currentRunLoop] run]; 开启当前线程,test也会去执行

    (3)dispatch_barrier_async

    1> 怎么用GCD实现多读单写?

    多度单写的意思就是:可以多个读者同时读取数据,而在读的时候,不能取写入数据。并且,在写的过程中,不能有其他写者去写,即读者之间是并发的,写者与读者或者其他者时互斥的。

     处理

    这里的写处理可以用 dispatch_barrier_sync(栅栏函数)去实现

    2> dispatch_barrier_async

    dispatch_queue_t concurrentQueue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);

    for (NSInteger i = 0; i < 10; I++) {

            dispatch_sync(concurrentQueue, ^{

                    NSLog(@"%zd", i);

            });

    }

    dispatch_barrier_sync(concurrentQueue, ^{

             NSLog(@"barrier");

    });

    for (NSInteger i = 10; i < 20; i++) {

            dispatch_sync(concurrentQueue, ^{

                    NSLog(@"%zd", i);

            });

    }

    这里的dispatch_barrier_sync上的队列要和需要阻塞的任务在同一队列上,否则是无效的。

    从打印上看,任务0-9和热舞10-19因为是异步并发的原因,彼此是无序的。而由于栅栏函数的存在,导致顺序必然是先执行任务0-9,再执行栅栏函数,再执行任务10-19.

    dispatch_barrier_sync和dispatch_barrier_async的区别在于会不会阻塞当前的线程。

    比如,上述代码如果在dispatch_barrier_async后随便加一条打印,则会先去执行该打印,再去执行任务0-9和栅栏函数;额如果是dispatch_barrier_sync,则会在任务0-9和栅栏函数执行完后再去执行这条打印。

    3> 设计多读单写

    - (id)readDataForKey:(NSString *)key {

            __block id result;

            dispatch_sync(_concurrentQueue, ^ {

                    result = [self valueForKey:key];

            });

            return result;

    }

    - (void)writeData:(id)data forKey:(NSString *)key {

            dispatch_barrier_async(_concurrentQueue, ^{

                    [self setValue:data forKey:key];

            });

    }

    (4)dispatch_group_async

    场景:在n个耗时并发任务都完成后再去执行接下来的任务。比如,在n个网络请求完成后刷新UI页面。

    dispatch_queue_t concurrentQueue = dispatch_queue_create("test", DISPATCH_QUQUE_CONCURRENT);

    dispatch_group_t group  = dispatch_group_create();

    for (NSInteger i = 0; i < 10; i++) {

            dispatch_group_async(group, concurrentQueue, ^{

                    sleep(1);

                    NSLog(@"%zd:网络请求", i);

            });

    }

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{

            NSLog(@"刷新页面");

    });

    (5)Dispatch Semaphore

    GCD中的信号量是指:Dispatch Semaphore,是持有计数的信号。

    Dispatch Semaphore提供了三个函数

    1、dispatch_semaphore_create:创建一个Semaphore并初始化信号的总量。

    2、dispatch_semaphore_signal:发送一个信号,让信号总量加1。

    3、dispatch_semaphore_wait:可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行。

    Dispatch Semaphore在开发中主要用于:

    1> 保证线程同步,将异步执行的任务转换成同步执行的任务。

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    __block NSInteger number = 0;

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

            number = 100;

            dispatch_semaphore_signal(semaphore);

    });

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    NSLog(@"semaphore-- end, number = %zd", number);

    dispatch_semaphore_wait加锁阻塞了当前线程,dispatch_semaphore_signal解锁后,当前线程才能继续执行。

            2> 保证线程安全,为线程加锁。

    在线程安全中可以将dispatch_semaphore_wait看作是加锁,而dispatch_semaphore_signal看作是解锁。

    // 首先创建全局变量

    _semaphore = dispatch_semaphore_create(1);

    __block NSInteger count = 0;

    // 这里信号量是1。

    - (void)asyncTask {

            dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);

            count++;

            sleep(1);

            NSLog(@"执行任务:%zd", count);

            dispatch_semaphore_signal(_semaphore);

    }

    // 异步并发调用asyncTask

    for (NSInteger i = 0; i < 100; i++) {       

            dispatch_async(dispatch_get_global_queue(0, 0), ^{                  

                  [self asyncTask];         

            });

    }

    打印的是任务从1顺序执行到100,没有发生两个任务同时执行的情况。

    原因:在子线程中并发执行asyncTask,那么第一个添加到并发队列里的,会将信号量减1,此时信号量等于0,可以执行接下来的任务。而并发队列中的其他任务,由于此时信号量不等于0,必须等当前正在执行的任务执行完毕后调用dispatch_semaphore_signal将信号量加1,才可以继续执行接下来的任务,以此类推,从而达到线程加锁的目的。

    (6)延时函数(dispatch_after)

    dispatch_after能让我么添加进队列的任务延时执行,该函数并不是在指定时间后执行处理,而只是在指定时间追加处理到dispatch_queue。

    // 第一个参数是time,第二个参数是dispatch_queue,第三个参数是要执行的block

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

    NSLog(@"dispatch_after");

    });

    由于其内部使用的是dispatch_time_t管理时间,而不是NSTimer。所有如果在子线程中调用,相比于performSelector: afterDelay,不用关心runloop是否开启。

    7)单例(dispatch_once)

    static id _instance;

    + (instancetype)shareInstance {

    static dispatch_once_t  onceToken;

            dispatch_once(&onceToken, ^{

                    _instance = [[self alloc] init];

            });

            return _instance;

    }

    + (id)allocWithZone:(struct _NSZone *)zone {

    static dispatch_one_t onceToken;

            dispatch_once(&onceToken, ^{

                    _instance = [super allocWithZone:zone];

            });

            return _instance;

    }

    // 如果遵守了NSCopying协议

    - (id)copyWithZone:(nullable NSZone *)zone {

            return _instance;

    }

    5)NSOperationQueue

    NSOperation、NSOperationQueue是苹果提供给我们的一套多线程解决方案,实际上NSOperation、NSOperationQueue是基于GCD更高一层的封装,完全面向对象。但是比GCD更简单易用、代码可读性也更高。

    1、可添加完成的代码块,在操作完成后执行。

    2、可以添加操作之间的依赖,方便控制执行顺序。

    3、可以设置操作执行的优先级。

    4、可以很方便的取消一个操作的执行。

    5、使用KVO观察对操作执行状态的更改:isExecuting、isFinished、isCancelled。

        1)如果只是重写NSOperation的main方法,由底层控制变更任务执行及完成状态,以及任务退出。

        2)如果重写了NSOperation的start方法,自行控制任务状态。

        3)系统通过KVO的方式移出isFinished == NSOperation。

    6、可以设置最大并发数量

    既然是基于GCD的更高一层的封装。那么,GCD中的一些概念同样适用于NSOperation、NSOperationQueue。在NSOperation、NSOperationQueue中也有类似的任务和队列。

    操作(Operation):

    1、执行操作的意思,换句话说就是你在线程中执行的那段代码。

    2、在GCD中是放在block里面的。在NSOperation中,我们使用NSOperation子类NSInvocationOperation、NSBlockOperation,或者自定义子类来封装操作。

    操作队列(OperationQueue)

    1、用来存放操作的队列。不同于GCD中的调度队列FIFO(先进先出)原则。NSOperationQueue对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序),由操作之间相对的优先级决定的(优先级是操作对象自身的属性)。

    2、操作队列通过设置最大并发数(maxConcurrentOperationCount)来控制并发、串行数量。

    3、NSOperationQueue为我们提供了两种不同类型的队列:主队列和自定义队列。主队列运行在主线程上,自定义队列在后台执行。

    NSOperation、NSOperationQueue使用步骤:

    NSOperation需要配合NSOperationQueue来实现多线程。因为默认情况下,NSOperation单独使用时,系统同步执行操作,配合NSOperationQueue才能更好实现异步执行。

    1、创建操作:先将需要执行的操作封装到一个NSOperation对象中。

    2、创建队列:创建NSOperationQueue对象

    3、将操作加入到队列中:将NSOperation对象添加到NSOperationQueue对象中。

    创建操作

    NSOperation是个抽象类,不能用来封装操作。我们只能使用它的子类来封装操作。

    1、NSInvocationOperation。

    2、NSBlockOperation。

    3、自定义继承自NSOperation的子类,通过实现内部相应的方法来封装操作。

    -- NSInvocationOperation

    - (void)useInvocationOperation {

            // 1、创建 NSInvocationOperation对象

            NSInvocationOperation * op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task) object:nil];

            // 2、调用 start 方法开始执行操作

            [op start];

    }

    - (void)task {

            for (int i = 0; i < 10; i++) {

                    [NSThread sleepForTimeInterval:0.5] // 模拟耗时操作

                    NSLog(@"111 ------ %@", [NSThread currentThread]); / / 打印当前线程

            }

    }

    在没有使用NSOperationQueue,在主线程中单独使用子类NSInvocationOperation执行一个操作时,操作是在当前线程执行的,并没有开启新线程。

    切换到其他线程

    // 在其他线程使用子类NSInvocationOperation

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

    在其他线程中单独使用子类NSInvocationOperation,操作是在当前调用的其他线程上执行的,并没有开启新线程。

    -- NSBlockOperation

    - (void)useBlockOperation {

    // 1、创建 NSBlockOperation对象

            NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{

                    for (int i = 0; i < 10; i ++ ) {

                            [NSThread sleepForTimeInterval:0.5];

                            NSLog(@"111 --- %@", [NSThread currentThread]);

                    }

            }];

            // 2、调用 start 方法开始执行操作

            [op start];

    }

    和上面的NSInvocationOperation使用一样。因为代码是在主线程中调用的,所有打印结果为主线程。如果在其他线程中执行操作,则打印结果为其他线程。

    但是,NSBlockOperation还提供了一个方法 addExecutionBlock:,通过 addExecutionBlock:就可以为NSBlockOperation添加额外的操作。这些操作(包括blockOperationWithBlock中的操作)可以在不同的线程中同时(并发)执行。只有当所有相关的操作已经完全执行时,才视为完成。

    如果添加的操作过多的话,blockOperationWithBlock:中的操作也可能会在其他线程(非当前线程)中执行,这是由系统决定的,并不是说添加到 blockOperationWithBlock:中的操作一定会在当前线程中执行。

    - (void)useBlockOperationAndExecutionBlock {

            NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{

                    for (int i = 0; i < 10; i++) {

                            [NSThread sleepForTimeInterval:0.2];

                            NSLog(@"1 --- %@", [NSThread currentThread]);

                    }

    }];

            [op addExecutionBlock:^{

                    for (int i = 0; i < 10; i++) {

                            [NSThread sleepForTimeInterval:0.2];

                            [NSLog(@"2 ---- %@", [NSThread currentThread])];

                    }

            }];

            // ......

    }

    使用子类NSBlockOperation,并调用方法 AddExecutionBlock:的情况下,blockOperationWithBlock:方法中的操作和 addExecutionBlock:中的操作是在不同的线程中异步执行的。

    一般情况,如果一个NSBlockOperation对象封装了多个操作。NSBlockOperation是否开启新线程,取决于操作的个数。如果操作个数多,就可能会自动开启新线程。开启的线程数是由系统决定的。

    -- 自定义继承自NSOperation 的子类

    通过重写main或者start方法来定义自己的NSOperation对象。

    // KKOperation.h

    #import <Foundation/Foundation.h>

    @interface KKOperation : NSOperation

    @end

    #import "KKOperation.h"

    @implementation KKOperation

    - (void)main {

            if (self.isCancelled) {

                    if (int i = 0 ; i < 10; i++) {

                            [NSThread sleepForTimeInterval:0.2];

                           NSLog(@"1 ---- %@", [NSThread currentThread]);

                    }

            }

    }

    @end

    #import "KKOperation.h"

    - (void)useCustomOperation {

    KKOperation *op = [[KKOperation alloc] init];

            [op start];

    }

    在没有使用NSOperationQueue,在主线程单独使用自定义继承自NSOperation的子类的去年高考下,是在主线程执行的操作,并没有开启新线程

    创建队列

    NSOperationQueue一共有两种队列:主队列、自定义队列。其中自定义队列公司包含了串行、并发功能。

    1、主队列

    凡是添加到主队列中的操作,都会放到主线程中执行(注:不包括操作使用addExecutionBlock:添加的额外操作,额外操作可能在其他线程执行)。

    NSOperationQueue *queue = [NSOperationQueue mainQueue];

    2、自定义队列

        1)添加到这种队列中的操作,会自动放到子线程中执行

        2)同时包含了:串行、并发功能

                NSOperationQueue *queue = [[NSOperationQueue alloc] init];    

    -- 将操作放入队列中

    (1) - (void)addOperation:(NSOperation *)op;

    需要先创建操作。再将创建好的操作放入到建好的队列中去

    - (void)addOperationToQueue {

    NSOperationQueue *queue = [[NSOperation alloc] init];

            NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];

            NSInvocationOperation *op2 = [[NSInvocation alloc] initWithTarget:self selector:@selector(task2) withObject:nil];

            NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{

                    for (int i = 0; i < 10; i++)  {

                            [NSThread sleepForTimeInterval:0.2];

                            NSLog(@"3 --- %@", [NSThread currentThread]);

                    }

            }];

            [op3 addExecutionBlock:^{

                    for (int i = 0; i < 10; i++) {

                            [NSThread sleepTimeInterval:0.2];

                            NSLog(@"4 --- %@", [NSThread currentThread]);

                    }

            }];

            [queue addOperation:op1];

            [queue addOperation:op2];

            [queue addOperation:op3];

    }

    使用NSOperation子类创建的操作,并使用addOperation:将操作加入到操作队列后能够开启新线程,进行并发执行。

    (2)- (void)addOperationWithBlock:(void (^)(void))block;

    无需先创建操作,在block中直接添加操作,直接将包含操作的block加入到队列中。

    - (void)addOperationWithBlockToQueue {

    NSOperationQueue *queue = [[NSOperation alloc] init];

            [queue addOperationWithBlock:^{

                    for (int i = 0; i < 10; i++) {

                            [NSThread sleepForTimeInterval:0.2];

                            NSLog(@"1 --- %@", [NSThread currentThread]);

                    }

            }];

            // ....

    }

    使用addOperationWithBlock:将操作加操作队列后能够开启新线程,进行并发执行

    -- NSOperationQueue控制串行,并发执行

    maxConcurrentOperationCount:最大并发操作数,用来控制一个特定队列中可以有多少个操作同时参与并发执行。(不是指并发线程的数量,而是一个队列中同时能并发执行的最大操作数。而且一个操作也并非只能在一个线程中运行)。

    maxConcurrentOperationCount:默认情况为-1,表示不进行限制,可进行并发执行。为1时,队列为串行队列,只能串行执行。大于1是,队列为并发队列,操作并发执行,当然这个值不能超过系统限制,即使自己设置一个很大的值,系统也会自动调整为min(自己设定的值,系统设定的最大值)。

    -- NSOperation操作依赖

    NSOperation、NSOperationQueue最吸引人的地方是它能添加操作之间的依赖关系。通过操作依赖,我们可以很方便的控制操作之间的执行顺序。

    // 添加依赖,使当前操作依赖于操作op的完成

    1、-(void)addDependency:(NSOperation *)op; 

    // 移出依赖,取消当前操作对操作op的依赖

    2、- (void)removeDependency:(NSOperation *)op;

    // 在当前操作开始执行之前,完成执行的所有操作对象数组

    3、@property (readonly, copy) NSArray<NSOperation *> *dependencies;

    A执行完操作,B才能执行操作

    - (void)addDependency {

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

            NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{

                    // .....

            }];

            NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{

                    // .....

            }];

            [op2 addDependency:op1];

            [queue addOperation:op1];

            [queue addOperation:op2];

    }

    -- NSOperation优先级

    queuePriority属性适用于同一操作队列中的操作,不适用与不同操作队列中的操作。默认所有新创建的操作对象优先级都是 NSOperationQueuePriorityNormal;

    typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {

            NSOperationQueuePriorityVerLow = -8L,

            NSOperationQueuePriorityLow = -4L,

            NSOperationQueuePriorityNormal = 0,

            NSOperationQueuePriorityHigh = 4,

            NSOperationQueuePriorityVeryHigh = 8,

    }

    对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作开始执行,顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。

    当一个操作的所有依赖都已经完成时,操作对象通常会进入准备就绪状态,等待执行。

    例如:

    现在有4个优先级都是 NSOperationQueuePriorityNormal(默认级别)的操作:op1、op2、op3、op4.其中op3依赖op2,op2依赖op1。现在讲将者4个操作添加到队列中并发执行。

    1、因为op1和op4都没有需要依赖的操作,所以在op1、op4执行之前,就处于就绪状态的操作、

    2、op3和op2都有依赖的操作,所以op3和op2都不是准备就绪状态下的操作。

    queuePriority的作用:

    1、queuePriority属性决定了进入准备就绪状态下的操作之间的开始执行顺序,并且,优先级不能取代依赖关系。

    2、如果一个队列中既包含高优先级操作,又包含低优先级操作,并且两个操作都已经准备就绪,那么列表先执行高优先级操作。比如上例,如果op1和op4是不同优先级操作,那么就会先执行优先级高的操作。

    3、如果一个队列中既包含了准备就绪状态的操作,又包含了未准备就绪的操作,未准备就绪的操作优先级比准备就绪的操作优先级高。那么,虽然准备就绪的操作优先级低,也会优先执行。优先级不能取代依赖关系。如果要控制操作间的启动顺序,则必须使用依赖关系。

    -- NSOperation、NSOperationQueue线程间的通信

    在iOS开发过程中,我们一般在主线程进行UI刷新,例如:点击、滚动、拖拽等事件。我们常把一些耗时的操作放在其他线程,如网络图片加载、文件上传等耗时操作。当我们在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通信。

    - (void)communication {

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

            [queue addOperationWithBlock:^{

                    // 耗时操作

            }];

            [[NSOperationQueue mainQueue] addOperationWithBlock:^{

                    // 更新UI显示

            }];

    }

    -- NSOperation、NSOperationQueue线程同步和线程安全

    1、线程安全:如果代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。

        1)如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样,就是线程安全的。

        2)若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话可能影响线程安全。

    2、线程同步:

        1)例如:线程A和线程B一块合作,A执行到一定程度时要依靠线程B的某个结果,于是停下来,示意B运行;B以言执行,再讲结果给A;A再继续操作。

        2)例如:两个人在一起聊天,两个人不能同时说话,避免听不清(操作冲突)。等一个人说完(一个线程结束操作),另一个再说(另一个线程开始操作。)

    ## 模拟火车票售卖:总共有100张火车票,有两个售卖窗口,一个在北京,一个再广州。两窗口同时售卖,直到卖完为止。

    非线程安全:

    /*

        非线程安全:

        初始化火车票数量,卖票窗口(非线程安全),并开始卖票

    */

    - (void)initTicketStatusNotSave {

            NSLog(@"currentThread --- %@", [NSThread currentThread]);

            self.ticketSurplusCount = 100;

            // 北京窗口

            NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];

            queue1.maxConcurrentOperationCount = 1;

            // 广州窗口

            NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];

            queue2.maxConcurrentOperationCount = 1;

            // 售卖操作 op

            NSBlockOperation *op1  = [NSBlockOperation blockOperationWithBlock:^{

                    [self saleTicketNotSafe];

            }];

            NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{

                    [self saleTicketNotSafe];

            }];

            // 添加操作,开始买票

            [queue1 addOperation:op1];

            [queue2 addOperation:op2];

    }

    // 售卖火车票(非线程安全)

    - (void)saleTicketNotSafe{

            while(1) {

                    if (self.ticketSurplusCount > 0 ) {

                            self.ticketSurplusCount--;

                            NSLog(@"剩余票数:%zd,窗口:%@", self.ticketSurplusCount, [NSThread currentThread]);

                    }else {

                            NSLog(@"火车票已售完!");

                            break;

                    }

            }

    }

    结果:非线程安全下,即不使用NSLock情况下,得到的票数错乱无章的。

    线程安全:给线程加锁,在一个线程执行高操作的时候,不允许其他线程进行操作

    iOS线程加锁方式:

    @synchronized、NSLock、NSRecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock、atomic(property) set/get的等等

    /*

    线程安全:使用NSLocj加锁

        初始化火车票数量,卖票窗口(线程安全),并开始卖票

    */

    -(void)initTicketStatusSave {

    NSLog(@"currentThread -- %@", [NSThread currentThread]);

            self.ticketSurplusCount = 100; // 总票数

            self.luck = [[NSLock alloc] init]; // 初始化锁

            // 初始化售卖窗口

            NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];

            queue1.maxConcurrentOperationCount = 1;

            NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];

            queue2.maxConcurrentOperationCount = 1;

            // 初始化售卖操作

            NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{

                    [self saleTicketSafe];

            }];

            NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{

                    [self saleTicketSafe];

            }];

            // 添加操作,开始卖票

            [queue1 addOperation:op1];

            [queue2 addOperation:op2];

    }

    // 售卖火车票(线程安全)

    - (void)saleTicketSafe {

            while(1) {

                    [self.lock lock]; // 加锁

                   if (self.ticketSurplusCount > 0) {

                            self.ticketSurplusCount--;

                            NSLog(@"剩余票数:%zd,窗口:%@", self.ticketSurplusCount, [NSThread currentThread]);

                    }

                     [self.lock unlock];  // 解锁

                    if (self.ticketSurplusCount <= 0) {

                            NSLog(@"所有火车票已售完!");    

                            bread;

                    }    

            }

    }

    在考虑了线程安全,使用了NSLock加载、解锁机制下,得到了票数是正确的,没有出现混乱的情况。解决了多线程同步问题。

    -- NSOperation常用属性及方法

    1、取消操作:-(void)cancel;

    2、判断操作状态的方法

            1)-(BOOL)isFinished; // 判断操作是否已经结束

            2)-(BOOL)isCancelled; // 判断操作是否已经标记为取消

            3)-(BOOL)isExecuting; // 判断操作是否正在运行

            4)-(BOOL)isReady; // 判断操作是否处于准备就绪状态,这个值和操作的依赖关系相关

    3、操作同步

            1)-(void)waitUntilFinished;阻塞当前线程,直到该操作结束。可用于线程执行顺序的同步。

            2)-(void)setCompletionBlock:(void (^)(void))block;completionBlock会在当前操作执行完毕时执行completionBlock。

            3)-(void)addDependency:(NSOperation *)op;添加依赖,使当前操作依赖于操作op的完成。

            4)-(void)removeDependency:(NSOperation *)op;移出依赖,取消当前操作对操作ap的依赖。

            5)@property (readonly, copy) NSArray<NSOperation *> *dependencies;在当前操作开始执行之前完成执行的所有操作的数组。

    -- NSOperationQueue的常用属性和方法

    1、取消、暂停、恢复操作

            1)-(void)cancelAllOperation;可以取消队列的所有操作。

            2)-(BOOL)isSuspended;判断队列是否处于暂停状态。YES:暂停,NO为恢复。

            3)-(void)setSuspended:(BOOL)b;可设置操作的暂停和恢复,YES代表暂停,NO代表恢复。

    2、操作同步

    -(void)waitUntilAllOperationsAreFinished;阻塞当前线程,知道队列中的操作全部执行完毕。

    3、添加、获取操作

            1)-(void)addOperationWithBlock:(void (^)(void))block;向队列中添加一个NSBlockOperation类型的操作对象。

            2)-(void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait;向队列中添加数组,wait标志是否阻塞当前线程直到所有操作结束。

            3)-(NSArray *)operations;当前在队列中的操作数组(某个操作执行结束后会自动从数组清除)。

            4)-(NSInteger)operationCount;当前队列中的操作数。

    4、获取队列

            1)+(id)currentQueue;获取当前队列,如果当前线程不是在NSOperationQueue上运行则返回bil。

            2)+(id)mianQueue:获取主队列。

    warning:

    1、这里的暂停和取消(包括操作的取消和队列的取消)并不代表可以将当前的操作立即取消,而是当当前的操作执行完毕忠厚不再执行新的操作。

    2、暂停和取消的区别就在于:暂停操作之后还可以恢复操作,继续向下执行;而取消操作之后,所有的操作就情况了,无法再接着执行剩下的操作。

    -- NSThread + runloop实现常驻线程

    NSThread在实际开发中比较常用到的场景就是去实现常驻线程。

    由于每次开辟子线程都会消耗cpu,在需要频繁使用子线程的情况下,频繁开辟子线程会消耗大量的cpu资源,而且创建线程都是任务执行完之后就释放了,不能再次利用。此时就需要创建一个常驻线程。

    GCD实现以单例来保存NSThread

    + (NSThread *)shareThread {

    static id shareThread;

            static dispatch_once_t onceToken;

            dispatch_once(&onceToken, ^{

                    shareThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadTest) object:nil];

                    [shareThread setName:@"shareThread"];

                    [shareThread start];

            });

            return shareThread;

    }

    + (void)threadTest {

            @autoreleasepool {

                    NSRunLoop *runloop = [NSRunLoop currentRunLoop];

                    [runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

                    [runloop run];

            }

    }

    [self performSelector:@selector(test) onThread:[[self class] shareThread] withObject:nil waitUntilDone:NO];

    - (void)test {

            NSLog(@"test thread:%@", [NSThread currentThread]);

    }

    27、锁

    1、自旋锁:

    是一种用于保护多线程共享资源的锁,与一般互斥锁(mutex)不同之处在于当自旋锁尝试获取锁时,以忙等待(busy waiting)的形式不断地循环检查锁是否可用。当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠),当上一个线程的任务执行完毕,下一个线程会立即执行。在多CPU的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序性能。

    2、互斥锁:

    让上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕,当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务。

    总结:

    自旋锁会忙等:忙等就是在访问被锁资源时,调用者线程不会休眠,而且不停的在那里循环,直到被锁资源释放锁。

    互斥锁会休眠:休眠就是在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其他线程工作。直到被锁资源释放锁。此时会唤醒休眠线程。

    优缺点:

    自旋锁:

            优点:因为自旋锁不会引起调用者休眠,所以不会进行线程调度,CPU时间片轮转等耗时操作。所以如果能在很短时间内获得锁,自旋锁的效率远高于互斥锁。

            缺点:自旋锁一直占着CPU,它在未获得锁的情况下,一直自旋,如果不能在很短的实践内获得锁,无疑会浪费CPU资源,是CPU效率降低。自旋锁不能实现递归调用。        

    自旋锁:atomic、CSSPinLock、dispatch_semaphore_t

    互斥锁:pthread_mutex、@synchronized、NSLock、NSConditionLock、NSCondition、NSRecursiveLock。

    自旋锁实现思路:

    bool lock = false; // 一开始没上锁,任何线程都可以申请解锁

    do {

            while (lock); // 如果lock为true就一直死循环,相当于申请锁

            lock = true;    // 上锁,这样别的线程就无法获得锁

            // 临界区

            lock = false;     // 相当于释放锁,这样别的线程可以进入临界区

            // 不需要锁保护的代码

    }

    @synchronized

    NSObject *objc = [[NSObject alloc]  init];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

            @synchronized(obj) {

                    NSLog(@"线程1 开始");

                    sleep(2);

                    NSLog(@"线程1 结束");

            }

    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    sleep(2);

            @synchronized(obj){

                    NSLog(@"线程 2");

            }

    });

    // 线程1 开始 ——》线程1 结束 ——》 线程2

    @synchronized(obj)指令是用obj为该锁的唯一标识,只有当标识相同时,才能满足互斥,如果线程2中的obj为其他值,则线程2就不会被阻塞。

    @synchronized指令实现锁的优点:我们不需要在代码中显示的创建锁对象,便可以实现锁的机制,但作为一种防御措施,@synchronized块会隐士的添加一个异常处理来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。如果不想让隐士的异常处理例程带来额外的开销,可以考虑使用锁对象。

    dispatch_semaphore

    dispatch_semaphore_t signal  = dispatch_semaphore_create(1);

    dispatch_time_t waitTime = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    dispatch_semaphore_wait(signal, waitTime);

            NSLog(@"线程 1 开始");

            sleep(2);

            NSLog(@"线程 1 结束");

            dispatch_semaphore_signal(signal);

    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    sleep(1);

            dispatch_semaphore_wait(signal, waitTime);

            NSLog(@"线程 2");

            dispatch_semaphore_signal(signal);

    });

    // waitTime > 2:同步操作。 线程1 开始    ——》 线程1 结束    ——》线程2

    //waitTime < 2:线程1 开始    ——》线程2    ——》线程1 结束

    dispatch_semaphore是GCD用来同步的一种方式。

    1、dispatch_semaphore_create的声明:

    dispatch_semaphore_t dispatch_semaphore_create(long value);传入的参数为long,输出是一个dispatch_semaphore_t类型且值为value的信号量。

    参数value必须 大于或等于0,否认在dispatch_semaphore_create会返回NULL。

    2、dispatch_semaphore_signal声明:

    long dispatch_semaphore_signal(dispatch_semaphore_t dsema);这个函数会使传入的信号量dsema的值加1。

    3、dispatch_semaphore_wait声明:

    long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);设个函数会使传入的信号量dsema的值减1。

    函数作用:如果dsema信号量的值大于0,该函数所处的线程就继续执行下面的语句,并且将信号量的值减1;如果dsema的值为0;那么这个函数就阻塞当前线程等待timeout(注意timeout的类型为dispatch_time_t,不能直接传入整型或float型数),如果等待期dsema的值被dispatch_semaphore)signal函数加1了,且该函数(dispatch_semaphore_wait)所处线程获得了信号量,那么就继续向下执行并将信号量减1.如果等待期没有获得信号量或者信号量的值一直未0,那么等到timeout时,其所处线程自动执行其后语句。

    dispatch_semaphore是信号量,但当信号总量为1时,也可以当作锁来用。在没有等待情况出现时,它的性能比 pthread_mutext还要高,但一旦有等待情况出现,性能就会下降很多。相对于OSSpinLock来说,它的优势在于等待时不会消耗CPU资源。

    NSLock

    NSLock *lock = [[NSLock alloc] init];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    // [lock lock];

            [lock lockBeforeDate:[NSDate date]];

            NSLog(@"线程 1 开始");

            sleep(2);

            NSLog(@"线程1 结束");

            [lock unlock];

    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    sleep(1);

            if ([lock tryLock]) {  // 尝试获取锁,如果获取不到返回NO,不会阻塞该线程

                    NSLog(@"锁可以操作");

                    [lock unlock];    

            }else {    

                    NSLog(@"锁不可操作");

            }

            NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:3];

            if ([lock lockBeforeDate:date]) { // 尝试在未来的3s内获取锁,并阻塞该线程,如果3s内获取不到,恢复线程,返回NO,不会阻塞该线程

                    NSLog(@"没有超时,获得锁");

                    [lock unlock];

            }else {

                    NSLog(@"超时,没有获得锁");

            }

    });

    // 线程1 开始 ——》 锁不可操作 ——》线程1 结束 ——》没有超时,获得锁

    NSLock 是Cocoa提供给我们最基本的锁对象,也是最经常使用的

    源码:

    @protocol NSLocking

    - (void)lock;

    - (void)unlock;

    @end

    @interface NSLock: NSObject<NSLocking> {

    @private

            void *_priv;

    }

    - (BOOL)tryLock; // 尝试加锁,如果锁不可用(已经被锁住),则不会阻塞线程,并返回NO

    -(BOOL)lockBeforeDate:(NSDate *)limit; // 在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO

    @property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

    @end

    NSRecursiveLock递归锁:

    NSRecursiveLock实际上定义的是一个递归锁,这个锁可以被同一个线程多次请求,而不会引起死锁。主要是用在循环或递归操作中。

    NSLock *lock = [[NSLock alloc] init];

    // NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    static void (^RecursiveMethod)(int);

            RecursiveMethod = ^(int value) {

                    [lock lock];

                    if (value > 0) {

                            NSLog(@"value = %d", value);

                            sleep(1);

                            RecursiveMethod(value-1);

                    }

                    [lock unlock];

            }

            RecursiveMethod(5);

    });

    这段代码是一个典型的死锁情况。在我们的线程中,RecursiveMethod是递归调用的。所以每次进入这个block时,都会去加一次锁,而从第二次开始,有锁已经被使用了且没有解锁,所以它需要等待锁被解除,这样就导致了死锁,线程被阻塞了。

    在这种情况下,我们就可以使用NSRecursiveLock。它允许同一个线程多次加锁,而不会造成死锁。递归锁会跟踪它被lock的次数。每次成功的lock都必须平衡调用unlock操作。只有所有达到这种平衡,锁最后才能被释放。

    将NSLock替换为NSRecursiveLock,代码才能正常工作

    @interface NSRecursiveLock : NSObject <NSLocking> {

    @private

            void *_priv;

    }

    - (BOOL)tryLock;

    - (BOOL)lockBeforDate:(Date *)limit;

    @property (nullable, copt) NSString *name NS_AVAILABEL(10_5,, 2_0);

    @end

    NSConditionLock条件锁NSMutableArray *books = [NSMutableArray array];

    NSInteger HAS_BOOK = 1;

    NSInteger NO_BOOK = 0;

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

            while (1) {

                    [lock lockWhenCondition:NO_BOOK];

                    [books addObject:[[Book alloc] initWithName:@"围城"]];

                    NSLog(@"1 total books:%zd", books.count);

                    [lock unlockWithCondition:HAS_BOOK];

                    sleep(1);

            }

    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

            while(1) {

                    NSLog(@"2 wait for lend a book");

                   [lock lockWhenCondition:HAS_BOOK];

                    [books removeObjectAtIndex:0];

                    NSLog(@"3 lend a book");

                    [lock unlockWithCondition:NO_BOOK];

            }

    });

    // 2 -> 1 -> 3 -> 2 -> 1 -> 3 -> 2 -> 1 -> 3

    在线程1中的加锁使用了lock,是不需要条件的,所以顺利的就锁住了,但在unlock时,使用了一个整型条件,它可以开启其他线程中正在等待这把钥匙的锁。而线程2则只需要一把标识为2的钥匙,所以当线程1循环到最后一次的时候,才最终打开可2中的阻塞。NSConditionLock跟其他锁一样,是需要lock与unlock对应的,只是lock。lockWhenCondition与unlock、unlockWithCondition是可以随意组合的。

    当使用多线程的时候,有时一把只会lock和unlock的锁,不能完全满足我们的需求。因为普通的锁只关心锁与不锁,而不在乎用什么钥匙才能开锁,而我们在处理资源共享的时候,多数请求是只满足一定的条件下才能打开这把锁。

    NSCondition

    NSCondition *condition = [[NSCondition alloc] init];

    NSMutableArray *books = [NSMutableArray array];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

            while(1) {

                    [condition lock];

                    if [books count] == 0) {

                            NSLog(@"1  have no book ");

                            [condition wait];

                    }

                    [books removeObjectAtIndex:0];

                    NSLog(@"2  lend a book ");

                    [condition unlock];

    }

    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

            while(1) {

                    [condition lock];

                    [books addObject:[[Books alloc] initWithName:@"四季"] ];

                    NSLog(@"3  borrow a book, total books :%zd ", books.count);

                    [condition signal];

                    [condition unlock];

                   sleep(1);

    }

    });

    // 1 -> 3 -> 2 -> 1 -> 3 -> 2 -> 1 -> 3 -> 2

    一种最基本的条件锁,受控控制线程wait和signal

    1、[condition lock]:一般用于多线程同时访问、修改同一个数据源时,保证在同一时间内数据只被访问、修改一次,其他线程的命令需要lock外等待,直到unlock,才可访问。

    2、[condition unlock]:与lock同理。

    3、[condition wait]:让当前线程处于等待状态。

    4、[condition signal]:CPU发信号告诉线程不用等待,可以继续执行

    pthread_mutex

    __block pthread_mutex_t pLock;

    pthread_mutex_init(&pLock, NULL);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

            pthread_mutex_lock(&pLock);

            NSLog(@"线程 1 开始");

            sleep(2);

            NSLog(@"线程 1 结束");

            pthread_mutex_unlock(&pLock);

    }),

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

            sleep(1);

            pthread_mutex_lock(&pLock);

            NSLog(@"线程 2");

            pthread_mutex_unlock(&pLock);

    });

    // 线程1 开始 ——》 线程 1 结束    ——》线程 2

    c语言下多线程加锁方式。

    1、pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);初始化锁变量mutex,attr为锁属性,NULL值为默认属性。

    2、pthread_mutex_look(pthread_mutex_t *mutex):加锁

    3、pthread_mutex_tylock(pthread_mutex_t *mutex);加锁,但与2不同的是:当锁已经在使用的使用,返回EBUSY,而不是挂机等待。

    4、pthread_mutex_unlock(pthread_mutex_t *mutex);释放锁

    5、pthread_mutex_destroy(pthread_mutex_t ** mutex);使用完后释放

    pthread_mutex(recursive)

    __block pthread_mutex_t pLock;

    // pthread_mutex_init(&pLock, NULL); 初始化会出现死锁

    pthread_mutexattr_t attr;

    pthread_mutexattr_init(&attr);

    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);

    pthread_mutex_init(&pLock, &attr);

    pthread_mutexattr_destroy(&attr);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

            static void (^RecursiveMethod)(int);

            RecursiveMethod = ^(int value) {

                    pthread_mutex_lock(&pLock);

                    if (value > 0 ) {

                            NSLog(@"value = %zd", value);

                            sleep(1)    ;

                            RecursiveMethod(value - 1);

                    }

                    pthread_mutex_unlock(&pLock);

            };

            RecursiveMethod(5);

    });

    这是pthread_mutex为了防止在递归的情况下出现死锁而出现的递归锁。作用和NSRecursiveLock递归锁类似。

    OSSpinLock

    __block OSSpinLock spLock = OS_SPINLOCK_INIT;

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

            OSSpinLockLock(&spLock);

            NSLog(@"线程 1 开始");

            sleep(1);

            NSLog(@"线程 1 结束");

            OSSpinLockUnlock(&spLock);

    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

            OSSpinLockLock(&spLock);

            sleep(1);

            NSLog("线程 2");

            QSSpinLockUnlock(&spLock);

    });

    OSSpinLock:自旋锁,性能最高的锁。原理很简单,就是一直do while 忙等。它的缺点是当等待时会消耗大量的CPU资源,所以它不适合较长时间的事务。而且OSSpinLock目前已经不安全,慎用。

    锁之间性能对比:

    OSSpinLock和dispatch_semaphore的效率远远高于其他。

    @synchronized和NSCondition效率较差

    OSSpinLock现在已经不安全。

    相关文章

      网友评论

          本文标题:iOS 面试 -- 多线程

          本文链接:https://www.haomeiwen.com/subject/sqmrcctx.html