iOS之多线程相关问题

一、进程、线程及关系

一、什么是进程?

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

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

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

二、什么是线程?

  • 线程是CPU调度的最小单位。

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

三、进程和线程的关系

进程和线程的关系,可以简单的形象的做个比喻:进程=火车,线程=车厢

  • 线程须在进程下才能运行的(单纯的车厢无法运行)

  • 一个进程可以包含多个线程(一辆火车可以有多个车厢)

  • 不同进程间的数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)

  • 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)

  • 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)

  • 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车中一节车厢着火了,将影响到所有车厢)

  • 进程可以拓展到多机,线程最多适合多核不同火车(可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)

  • 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁"

  • 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”

二、任务、队列及执行任务的方式

一、任务

  • 任务就是要执行的操作,也就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。

二、队列

一、队列有两种:

  1. 串行队列:

    • 每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
  2. 并行队列:

    • 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)

二、执行队列的方式

  1. 同步执行(sync):

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

  • 异步添加任务到指定的队列中,它不会造成主线程的等待,可以继续执行任务.

  • 可以在新的线程中执行任务,具备开启新线程的能力。

队列和任务的关系

任务和队列的关系可以打一个形象的比喻,比如小时后玩的四驱车,队列=跑道,串行队列=一条跑道,并行队列=多条跑道,四驱车=任务)。

基本规则就是每条跑道上只能有一辆车在上面跑。串行队列由于只有一条跑道,所以每次只能跑一辆车(一个任务),等这辆车跑完,别的车(任务)才能跑。并发队列由于有多个跑道,所以可以供多辆车(多个任务)一起跑。

  • 执行队列的方式就相当于我们如何把车(任务)放到跑道(队列)里

  • 同步执行不开启新线程:

    • 就相当于我们只有主线程一只手,每次只能把一辆车(任务)放到跑道上跑,等车跑完,把车收了以后,才能把下一辆车(任务)放到跑道上跑

    • 所以不管我们是把车(任务)放到单条跑道(串行队列)还是把车(任务)放到多条跑道(并发队列),每次都只能控制一辆车。并且由于主线程在玩车(执行任务),也就干不了别的事,所以同步执行会造成主线程等待。

  • 异步执行可以开启新线程:

    • 就相当于我们除了有主线程这只手外还邀请了很多一起玩的小伙伴(分线程),我们每个人都能拿起一辆车(任务)放到对应的跑道(队列)上,然后一起跑。所以在多条跑道(并发队列)上多辆车(多个任务)可以一起跑。

    • 如果车(任务)很多,跑道只有一个(串行队列),那么还是得排队玩,每次只能跑一辆车(任务)。但是,由于邀请了小伙伴(开启了线程),主线程这只手就可以让小伙伴(分线程)先玩车,自己去处理别的事情。所以异步执行不会造成主线程等待。

四、多线程引起的死锁

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

产生死锁的情况:

  • 一个比较常见的死锁例子:主队列同步
- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_sync(dispatch_get_main_queue(), ^{
       
        NSLog(@"deallock");
    });
}

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

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

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

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

  • 二、在同串行队列下异步任务里调用同步任务
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

dispatch_async(serialQueue, ^{
       
    dispatch_sync(serialQueue, ^{
            
        NSLog(@"deadlock");
    });
});

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

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

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

二、在不同的串行队列,异步block调用同步不会引起死锁

 dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t serialQueue2 = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    
dispatch_async(serialQueue, ^{
        
  dispatch_sync(serialQueue2, ^{
            
    NSLog(@"serialQueue2");
    });
    NSLog(@"serialQueue");
});

serialQueue、serialQueue2是两个不同队列,所以不存在队列引起的循环等待。

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

打印结果:

serialQueue2
serialQueue

五、GCD任务执行顺序

  • 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");

代码打印结果为:1 3 2 4 5

首先先打印1,

接下来将任务2其添加至串行队列上,由于任务2是异步,不会阻塞线程,继续向下执行,打印3

然后是任务4,将任务4添加到串行队列上,因为任务4和任务2在同一串行队列,根据队列先进先出原则,任务4必须等任务2执行后才能执行。所以先打印2

又因为任务4是同步任务,会阻塞线程,所以再打印4.

最后打印5

  • 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的,所有这个方法就会失效。

解决方法:

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

  2. 或者将dispatch_async改成同步,因为同步是在当前线程执行,那么如果当前线程是主线程,test方法也是会去执行的,如果不是就还是不执行)。

  3. 在就是开启子线程的run,在perfomselector下开发runloop就行代码如下:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        [self performSelector:@selector(test:) withObject:nil afterDelay:0];
        [[NSRunLoop currentRunLoop] run];
    });

开启子线程的runloop就会执行了。

六、自旋锁与互斥锁

一、自旋锁

  • 自旋锁是一种用于保护多线程共享资源的锁,与一般互斥锁(mutex)不同之处在于当自旋锁尝试获取锁时以忙等待(busy waiting)的形式不断地循环检查锁是否可用。

  • 当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠),当上一个线程的任务执行完毕,下一个线程会立即执行。

使用场景:对持有锁较短的程序

在多CPU的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。

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

二、互斥锁

  • 互斥锁:当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕。

  • 当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务。

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

三、各自优缺点:

自旋锁优点:

  • 自旋锁的优点在于,因为自旋锁不会引起调用者睡眠,所以不会进行线程调度,CPU时间片轮转等耗时操作。

  • 所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁。

自旋锁缺点:

  • 自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。
  • 自旋锁不能实现递归调用。

自旋锁:

  • atomic
  • OSSpinLock
  • dispatch_semaphore_t 等

互斥锁如:

  • pthread_mutex
  • @ synchronized
  • NSLock、NSConditionLock
  • NSCondition
  • NSRecursiveLock等

六、NSThread+runloop实现常驻线程

一、为什么去实现常驻线程,常驻线程使用背景?

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

  • 那么如何创建一个线程可以让它可以再次工作呢?也就是创建一个常驻线程。

一、实现常驻线程的步骤

  1. 首先常驻线程既然是常驻,那么可以用GCD实现一个单例来保存NSThread 代码如下:
+ (NSThread *)shareThread {
    
    static NSThread *_shareThread = nil;
    
    static dispatch_once_t oncePredicate;
    
    dispatch_once(&oncePredicate, ^{
        
        _shareThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadTest) object:nil];

        [_shareThread setName:@"threadTest"];
        
        [_shareThread start];
    });
    
    return _shareThread;
}

这样创建的thread就不会销毁了吗?

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

- (void)test {
    NSLog(@"test:%@", [NSThread currentThread]);
}

执行结果并没有打印,说明test方法没有被调用。

当创建的NSThread没有开启runloop的话,常驻线是不会有效的。

因为新建的子线程默认没有开启runloop

因此需要给这个线程添加了一个runloop,并且加了一个NSMachPort端口监听,防止新建的线程由于没有活动直接退出。

  1. 其次给线程开启runloop。

需要给这个线程添加了一个runloop,并且加了一个NSMachPort端口监听,防止新建的线程由于没有活动直接退出。。

- (void)threadTest
{
    @autoreleasepool {
        
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        
        [runLoop run];
    }
}

二、常驻线程的使用

performSelector是封装在NSObject的NSThreadPerformAdditions类别里,只要是NSObject直接调用

[self performSelector: onThread: withObject: waitUntilDone:]
 

三、常驻线程退出

只有从runloop中移除我们之前添加的端口,这样runloop没有任何事件,所以直接退出。方法如下:

[NSRunLoop currentRunLoop]removePort:<#(nonnull NSPort *)#> forMode:<#(nonnull NSRunLoopMode)#>

你可能感兴趣的:(iOS之多线程相关问题)