iOS多线程和线程安全

作者: 鲲鹏DP | 来源:发表于2019-08-09 16:28 被阅读6次

1.基本概念

  • 进程:
    操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体,是操作系统分配资源的最小单位。可以理解为一个车间。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。
  • 线程:
    程序执行的最小单位。可以理解为车间中的一条生产线。一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线。线程上下文切换比进程上下文切换要快得多(最开始只有进程,但是进程之间的切换开销较大,所以出现了线程)。
  • 多线程:
    一个进程中开启多条线程执行不同的任务,多条线程并发执行。
  • 主线程:
    程序启动,默认就会开启的一条线程就是主线程。一般用来处理UI的显示和UI事件。开发中耗时操作一般都需要放到子线程中执行,完了后如果需要刷新UI就要回到主线程操作。
  • 队列:
    队列是有序集合,先进先出(FIFO)。将任务放在队列中先进先出,有序执行。
  • 并发:
    开启多条线程,同时执行耗时任务。并发的本质只是CPU在多条线程中来回的切换而已,能在一定程度上提高任务的执行效率。
  • 串行:
    任务在一条线程上,按先后顺序依次执行。
  • 线程安全:
    当开启多条线程执行任务时,不通线程之间可能操作相同数据,由于是并发执行,容易造成数据错乱(银行存钱取钱问题)。线程安全,就是要避免这样的错误。可以通过加锁的形式实现,@synchronized,NSLock.

2.实现方式对比

  • pthread:
    跨平台,适用于多种操作系统,可移植性强,是一套纯C语言的通用API,且线程的生命周期需要程序员自己管理,使用难度较大,所以在实际开发中通常不使用。
  • NSThread:
    基于OC语言的API,使得其简单易用,面向对象操作。线程的声明周期由程序员管理,在实际开发中偶尔使用。
  • GCD:
    GCD是Apple公司为多核并行运算提出的解决方案,能够自动利用更多的CPU内核,可以自动管理线程的生命周期,我们只需要告诉他要执行什么任务即可。需要开几条线程执行任务都是系统操作的。
  • NSOperation:
    是对GCD的进一步封装,更加面向对象。提供了设置最大并发数的API。使用简单,先创建队列,再封装任务,最后把任务加到队列中即可。

3.具体介绍

3.1 pthread

1.引入头文件
#import <pthread.h>
2.开启线程
-(void)pthread{
    pthread_t p = NULL;
    id str = @"i'm pthread param";
    int result=  pthread_create(&p, NULL, demo, (__bridge void *)(str));
    if (result == 0) {
        NSLog(@"创建线程 OK");
    } else {
        NSLog(@"创建线程失败 %d", result);
    }
    // pthread_detach:设置子线程的状态设置为detached,则该线程运行结束后会自动释放所有资源。
    pthread_detach(p);
    
}
3.执行任务
// 后台线程调用函数
void *demo(void *params) {
    NSString *str = (__bridge NSString *)(params);
    
    NSLog(@"%@ - %@", [NSThread currentThread], str);
    
    return NULL;
}

3.2 NSThread

  • 手动开启
   //1.block
        NSThread *thread = [[NSThread alloc]initWithBlock:^{
            //要执行的任务
        }];
        //设置线程名称
        thread.name=[NSString stringWithFormat:@"thread%d",i];
         //开启线程
        [thread start];
 
   //2.target
        NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(getMoney) object:nil];
      //设置线程名称
        thread.name=[NSString stringWithFormat:@"thread%d",i];
       //开启线程
        [thread start];

  • 默认开启
  [NSThread detachNewThreadWithBlock:^{
             //要执行的任务
            [self getMoney];
   }];
//或则
   [NSThread detachNewThreadSelector:@selector(getMoney) toTarget:self withObject:nil]
  • 隐式创建

[self performSelectorInBackground:@selector(doSomething3:) withObject:@"NSThread3"];

Apple给NSObject专门提供了一个分类(NSThreadPerformAdditions),用来实现线程间的通讯跳转。


622045CD-D9A9-408A-A123-F088ECA3441C.png
  • 部分方法介绍:
// 当前线程
[NSThread currentThread];
//退出线程(一旦执行退出线程,就不能再执行任务,后面的代码不会再走)
[NSThread exit];
//判断当前线程是否为主线程
[NSThread isMainThread];
//判断当前线程是否是多线程
[NSThread isMultiThreaded];
//主线程的对象
NSThread *mainThread = [NSThread mainThread];
//阻塞线程
//休眠多久
[NSThread sleepForTimeInterval:2];
//休眠到指定时间
[NSThread sleepUntilDate:[NSDate date]];

68962028-EBA0-4A22-B143-A9C3DFBB5A49.png
阻塞线程,thread1变成最后执行(还有些其他的阻塞线程的方法,见后面总结)
3.3 GCD
  • 同步函数+串行队列:
    不会开启新的线程,任务都在主线程中串行执行,不会出现资源抢夺(Data Race)
   dispatch_queue_t  serialQueue = dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL);
  for (int i = 0; i<100; i++) {
        //同步函数+串行队列:不会开启新的线程,任务都在主线程中串行执行,不会出现资源抢夺(Data Race)
        dispatch_sync(serialQueue, DISPATCH_QUEUE_SERIAL), ^{
            [self getMoney];
        }) ;
 }
791BC37E-DD82-4BAD-9E43-01B81F62FB4D.png
  • 同步函数+并发队列:
    不会开启新的线程,任务都在主线程中串行执行,不会出现资源抢夺(Data Race)
  dispatch_queue_t  concurentQueue = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i<100; i++) {
         //同步函数+并发队列:不会开启新的线程,任务都在主线程中串行执行,不会出现资源抢夺(Data Race)
            dispatch_sync(concurentQueue, ^{
                         [self getMoney];
            });
    }

3E185D4F-843C-43A5-9583-B4C4EB10C167.png
  • 异步函数+串行队列:
    开启一条子线程,任务在子线程中串行执行,不会出现资源抢夺(Data Race)
   dispatch_queue_t  serialQueue = dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL);
//    dispatch_queue_t  concurentQueue = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i<100; i++) {
         //异步函数+串行队列:开启一条子线程,任务在子线程中串行执行,不会出现资源抢夺(Data Race)
            dispatch_async(serialQueue, ^{
                         [self getMoney];
            });
    }
9961B10E-0CAC-4A15-8E4F-F86A3EA7D60C.png
  • 异步函数+并发队列:
    开启多条新的线程,任务在子线程中并发执行,会出现资源抢夺(Data Race)
   //dispatch_queue_t  serialQueue = dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL);
  dispatch_queue_t  concurentQueue = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i<100; i++) {
         //异步函数+串行队列:开启一条子线程,任务在子线程中串行执行,不会出现资源抢夺(Data Race)
            dispatch_async(concurentQueue, ^{
                         [self getMoney];
            });
    }
F582BB3A-6FE6-4B96-B6A5-43A79DEC62D5.png
  • 同步函数+主队列:
    • 同步函数+主队列 在主线程中,出现死锁;
    • 同步函数+主队列在子线程中,不会出现死锁,任务在主线程串行执行,不会Data Race
      //同步函数+主队列 在主线程中,出现死锁
          dispatch_sync(dispatch_get_main_queue(), ^{
              [self getMoney];
           }) ;
        
        //    //同步函数+主队列在子线程中,不会出现死锁,任务在主线程串行执行,不会Data Race
         dispatch_async(dispatch_queue_create("异步函数+串行队列", DISPATCH_QUEUE_SERIAL), ^{
                 dispatch_sync(dispatch_get_main_queue(), ^{
                      [self getMoney];
                   }) ;
           });
  • 异步函数+主队列:
    不会开启新的线程,任务都在主线程中串行执行,不会出现资源抢夺(Data Race)
  dispatch_sync(dispatch_get_main_queue(), ^{
              [self getMoney];
   }) ;
  • 同步函数+全局并发队列:
    不会开启新的线程,任务都在主线程中串行执行,不会出现资源抢夺(Data Race)
  for (int i = 0; i<100; i++) {
       //同步函数+全局队列:不会开启新的线程,任务都在主线程中串行执行,不会出现资源抢夺(Data Race)
                dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                    [self getMoney];
                }) ;
        
    }
7586113B-B407-4E42-BE18-E00AAFE526B0.png
  • 异步函数+全局并发队列:
    开启多条新的线程,任务再子线程中并发执行,会出现资源抢夺(Data Race)
    for (int i = 0; i<100; i++) {
//        异步函数+全局队列:开启多条新的线程,任务再子线程中并发执行,会出现资源抢夺(Data Race)
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                    [self getMoney];
                }) ;
        
    }
75502ECF-B428-4F07-AF63-148EF98C82F4.png

GCD的其他常用函数

  • 延时函数
 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
    });

  • 一次性函数(可用于单例)
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
    });
  • 栅栏函数

只有当栅栏函数执行完毕后才能执行后面的函数

   dispatch_barrier_sync(concurentQueue, ^{
          //同步执行
           [self getMoney];
    });
 dispatch_barrier_async(concurentQueue, ^{
          //异步执行
           [self getMoney];
    });
8B1532A3-5399-4D46-A486-072A523BCF1E.png

栅栏函数注意点:

很多人都说,栅栏函数不能使用全局并发队列。经查阅官方文档,和代码验证发现该观点是错误的。

D38832D7-00B5-431F-A09A-61E5FE2CA944.png

上面主要意思:

  • 栅栏函数API和GCD异步/同步函数类似,可以提交任务到执行队列,能够有效的实现读写操作;
  • 栅栏函数当被加入通过 DISPATCH_QUEUE_CONCURRENT 创建的队列时才表现的特殊。此时,比栅栏函数先加入的任务都执行完了才会执行栅栏函数中的任务,在栅栏函数提交的任务完成前,不会执行后面的任务。
  • 栅栏函数被加入到全局并发队列或则其他队列中时,表现的就和使用GCD异步/同步函数一样。(起不到栅栏作用)

因此,栅栏函数只能使用通过DISPATCH_QUEUE_CONCURRENT创建的并发队列,才能真正发挥作用。

  • GCD定时器
  • GCD的信号量机制(dispatch_semaphore)

    信号量是一个整型值,有初始计数值;可以接收通知信号和等待信号。当信号量收到通知信号时,计数+1;当信号量收到等待信号时,计数-1;如果信号量为0,线程会被阻塞,直到信号量大于0,才会继续下去。
    使用信号量机制可以实现线程的同步,也可以控制最大并发数。以下是如何控制最大并发数的代码。

    dispatch_queue_t workConcurrentQueue = dispatch_queue_create("cccccccc", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t serialQueue = dispatch_queue_create("sssssssss",DISPATCH_QUEUE_SERIAL);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);

for (NSInteger i = 0; i < 10; i++) {
  dispatch_async(serialQueue, ^{
      dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
      dispatch_async(workConcurrentQueue, ^{
          NSLog(@"thread-info:%@开始执行任务%d",[NSThread currentThread],(int)i);
          sleep(1);
          NSLog(@"thread-info:%@结束执行任务%d",[NSThread currentThread],(int)i);
          dispatch_semaphore_signal(semaphore);});
  });
}
NSLog(@"主线程...!");

小结

65D7D71D-5DA9-462C-AD1E-1746B52E25A2.png
  • 同步和异步体现的是,是否具有开启线程的能力;
  • 串行和并发描述的是,任务在线程中执行的方式;
  • 同步函数没有开启线程的能力,不管遇上什么队列,都不会开启新的线程,只能在主线程串行执行,线程安全。(在主线程中,同步函数遇上主线程死锁除外)
  • 异步函数具有开启新线程的能力,但只是有,并非一定开,即使开了也不一定开多条。遇上串行队列开一条子线程,遇上并发队列开多条,遇上主队列不开。
  • 线程并不是开的越多越好,因为开启线程会有CPU的开销,线程越多,CPU线程之间的调度的开销也越多。

注意点:

B400D74B-6C2A-4A21-83B0-2C819F87F2F5.png 563562BC-08EE-46F7-A701-B103AE1F5A18.png

上面两个例子发现,异步函数不管是遇上串行还是并发队列,都开启了很多线程。根据上面的分析,异步函数遇到串行队列只会开启一条线程,遇上并发队列会开启多条线程,线程多多少系统会自行控制,存在线程的重用,不会开启这么多的子线程。

问题就出在:每次循环都是重新创建的新队列,任务都不在一个队列中了,自然也就不串行了。开发中切忌不能这么干,队列尽量重用。不然会开启很多的线程,CPU会累死。

3.4 NSOperation

NSOperation 是苹果公司对 GCD 的封装,完全面向对象,并比GCD多了一些更简单实用的功能,所以使用起来更加方便易于理解。NSOperation 和NSOperationQueue 分别对应 GCD 的 任务 和 队列。

  • 创建队列

主队列:不开子线程,任务在主线程中串行执行

 //主队列
    NSOperationQueue *mainQ = [NSOperationQueue mainQueue];
 // 其他队列
    NSOperationQueue *q = [[NSOperationQueue alloc]init];

其他队列:开启子线程,任务执行方式依赖于最大并发数maxConcurrentOperationCount

  • 最大并发数默认为-1,任务在多条子线程并发执行;
  • 设置为0;不会执行任务;
  • 设置为1;任务会在子线程串行执行,但不是在某一条子线程执行,而是在多条子线程会来回切换;
  • 设置为>1; 任务在多条子线程并发执行。
    最大并发数不等于开启的线程数,即使将最大并发数设置的很大,也不会开启很多线程,开启的线程数量还是和GCD一样,系统自动控制
    2688E86D-5CAD-4683-B5D2-D477C947D960.png
  • 封装操作三种方式

    NSOperation本身不具备封装操作的能力,需要使用其子类。

    NSInvocationOperation

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

    NSBlockOperation

    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
              [self getMoney];
          }];
    

    自定义operation继承自NSOperation

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN
typedef void(^DKPOperationBlock)(void);
@interface DKPOperation : NSOperation
-(instancetype)initWithBlock:(DKPOperationBlock)block;
@end

NS_ASSUME_NONNULL_END

#import "DKPOperation.h"

@interface DKPOperation()
@property(nonatomic,copy)DKPOperationBlock block;

@end

@implementation DKPOperation
-(instancetype)initWithBlock:(DKPOperationBlock)block{
    if (self =[super init]) {
        self.block = block;
    }
    return self;
}
-(void)main{
    if (self.block) {
        self.block();
    }
}
@end

   
    NSOperationQueue *q = [[NSOperationQueue alloc]init];
    for (int i = 0; i < 100; i ++) {
        DKPOperation *operation = [[DKPOperation alloc]initWithBlock:^{
            [self getMoney];
        }];
        [q addOperation:operation];
    }
   
  • 设置依赖

通过设置依赖,能够限制任务执行的顺序。任务A依赖于任务B,就要等任务B完成了才会执行任务A。A依赖B,B依赖C,结果C-->B-->A

1337241F-32A3-456C-AA37-9BFBAFF341BA.png

4.线程安全

多线程并发执行,如果同时操作同一个数据,就会出现Data Race。

如何检测?
17245E24-3B34-46DB-8AFA-5BB5F61A31DD.png
开启Thread Sanitizer,运行代码 99D86313-5F25-4003-BCDA-3732E191A52F.png
解决办法:加锁,避免Data Race
  • NLock
@property(nonatomic,strong) NSLock *lock;
- (void)viewDidLoad {
    [super viewDidLoad];
    _lock = [[NSLock alloc]init];
     [self concurentNSOperation];
}
 -(void)concurentNSOperation{
    //创建队列
    //    NSOperationQueue *mainQ = [NSOperationQueue mainQueue];
    NSOperationQueue *q = [[NSOperationQueue alloc]init];
    q.maxConcurrentOperationCount=2;
    for (int i = 0; i< 100; i++) {
        NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
           
            [self getMoney];
            
        }];
        
        [q addOperation:operation];
    }
-(void)getMoney{
    
    [_lock lock];
     money--;
    NSLog(@"money:%d,thread:%@",money,[NSThread currentThread]);
    [_lock unlock];
 
}
  • @synchronized(同步锁)
-(void)getMoney{
    
    @synchronized (self) {
        money--;
        NSLog(@"money:%d,thread:%@",money,[NSThread currentThread]);
    }
}
  • NSConditionLock (条件锁)
  • NSRecursiveLock(递归锁)
  • OSSpinLock(自旋锁)
  • os_unfair_lock(互斥锁)
  • 信号量

阻塞线程的几种方式:

  • 加锁
  • NSThread:两个sleep方法
  • GCD:栅栏函数
  • NSOperation:设置依赖

注意点:

  • 1.栅栏函数只能使用自己创建的并发队列(DISPATCH_QUEUE_CONCURRENT)才能起作用,使用其他队列和同步(异步)函数效果一样的。
  • 2.在使用队列时,注意队列的重用,不要盲目创建新队列。

相关文章

网友评论

    本文标题:iOS多线程和线程安全

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