美文网首页iOS
iOS与多线程(十) —— NSThread的使用以及锁(一)

iOS与多线程(十) —— NSThread的使用以及锁(一)

作者: 刀客传奇 | 来源:发表于2019-06-26 15:03 被阅读66次

    版本记录

    版本号 时间
    V1.0 2019.06.26 星期三

    前言

    信号量机制是多线程通信中的比较重要的一部分,对于NSOperation可以设置并发数,但是对于GCD就不能设置并发数了,那么就只能靠信号量机制了。接下来这几篇就会详细的说一下并发机制。感兴趣的可以看这几篇文章。
    1. iOS与多线程(一) —— GCD中的信号量及几个重要函数
    2. iOS与多线程(二) —— NSOperation实现多并发之创建任务
    3. iOS与多线程(三) —— NSOperation实现多并发之创建队列和开启线程
    4. iOS与多线程(四) —— NSOperation的串并行和操作依赖
    5. iOS与多线程(五) —— GCD之一个简单应用示例(一)
    6. iOS与多线程(六) —— GCD之一个简单应用示例(二)
    7. iOS与多线程(七) —— GCD之一个简单应用示例源码(三)
    8. iOS与多线程(八) —— 多线程技术概览与总结(一)
    9. iOS与多线程(九) —— pthread的使用(一)

    NSThread API详细说明

    首先看下写作环境

    Xcode 10.2.1,iOS 12.2

    NSThread位于Foundation库中,是对pthread对象化的封装,首先看一下苹果给的API,后续会根据这些进行详细的说明。

    #import <Foundation/NSObject.h>
    #import <Foundation/NSDate.h>
    #import <Foundation/NSNotification.h>
    
    @class NSArray<ObjectType>, NSMutableDictionary, NSDate, NSNumber, NSString;
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface NSThread : NSObject  {
    @private
        id _private;
        uint8_t _bytes[44];
    }
    
    @property (class, readonly, strong) NSThread *currentThread;
    
    + (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
    + (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
    
    + (BOOL)isMultiThreaded;
    
    @property (readonly, retain) NSMutableDictionary *threadDictionary;
    
    + (void)sleepUntilDate:(NSDate *)date;
    + (void)sleepForTimeInterval:(NSTimeInterval)ti;
    
    + (void)exit;
    
    + (double)threadPriority;
    + (BOOL)setThreadPriority:(double)p;
    
    @property double threadPriority API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0)); // To be deprecated; use qualityOfService below
    
    @property NSQualityOfService qualityOfService API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0)); // read-only after the thread is started
    
    @property (class, readonly, copy) NSArray<NSNumber *> *callStackReturnAddresses API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    @property (class, readonly, copy) NSArray<NSString *> *callStackSymbols API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
    
    @property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    
    @property NSUInteger stackSize API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    
    @property (readonly) BOOL isMainThread API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    @property (class, readonly) BOOL isMainThread API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); // reports whether current thread is main
    @property (class, readonly, strong) NSThread *mainThread API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    
    - (instancetype)init API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) NS_DESIGNATED_INITIALIZER;
    - (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    - (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
    
    @property (readonly, getter=isExecuting) BOOL executing API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    @property (readonly, getter=isFinished) BOOL finished API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    @property (readonly, getter=isCancelled) BOOL cancelled API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    
    - (void)cancel API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    
    - (void)start API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    
    - (void)main API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); // thread body method
    
    @end
    
    FOUNDATION_EXPORT NSNotificationName const NSWillBecomeMultiThreadedNotification;
    FOUNDATION_EXPORT NSNotificationName const NSDidBecomeSingleThreadedNotification;
    FOUNDATION_EXPORT NSNotificationName const NSThreadWillExitNotification;
    
    @interface NSObject (NSThreadPerformAdditions)
    
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
        // equivalent to the first method with kCFRunLoopCommonModes
    
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
        // equivalent to the first method with kCFRunLoopCommonModes
    - (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    1. 获取当前线程

    这个是类可以直接访问的属性,使用的使用直接类方法调用就可以[NSThread currentThread]

    //只读属性,获取当前所在的线程
    @property (class, readonly, strong) NSThread *currentThread;
    

    使用示例

    @implementation ViewController
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        NSLog(@"[NSThread currentThread] = %@", [NSThread currentThread]);
    }
    
    @end
    
    2019-06-25 14:59:43.984004+0800 JJNSThread[35308:10555610] [NSThread currentThread] = <NSThread: 0x280a7eec0>{number = 1, name = main}
    

    2. 判断是否是多线程

    + (BOOL)isMultiThreaded;
    

    用于判断是否是多线程

    使用示例

    @implementation ViewController
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
         NSLog(@"[NSThread currentThread] isMultiThreaded = %d", [NSThread isMultiThreaded]);
    }
    
    @end
    

    下面看一下输出

    2019-06-25 15:09:40.341807+0800 JJNSThread[35329:10557532] [NSThread currentThread] isMultiThreaded = 1
    

    3. 线程字典信息

    @property (readonly, retain) NSMutableDictionary *threadDictionary;
    

    每个线程都维护了一个键-值的字典,它可以在线程里面的任何地方被访问。你可以使用该字典来保存一些信息,这些信息在整个线程的执行过程中都保持不变。比如你可以使用它来存储在你的整个线程过程中 Run loop 里面多次迭代的状态信息。

    使用示例

    @implementation ViewController
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        NSThread *newThread = [[NSThread alloc] init];
        NSMutableDictionary *threadDict = [newThread threadDictionary];
        [threadDict setObject:@"threadDict" forKey:@"key"];
        NSLog(@"threadDict = %@", threadDict);
    }
    
    @end
    

    下面看一下输出

    2019-06-25 15:25:46.417378+0800 JJNSThread[35341:10559633] threadDict = {
        key = threadDict;
    }
    

    4. 线程休眠

    //当前代码所在线程睡到指定时间
    + (void)sleepUntilDate:(NSDate *)date;
    
    //当前线程睡多长时间
    + (void)sleepForTimeInterval:(NSTimeInterval)ti;
    

    5. 线程优先级

    下面看一下线程优先级的设置

    + (double)threadPriority;
    
    //给当前线程设定优先级,调度优先级的取值范围是0.0 ~ 1.0,默认0.5,值越大,优先级越高。
    + (BOOL)setThreadPriority:(double)p;
    
    @property double threadPriority API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0)); // To be deprecated; use qualityOfService below
    
    @property NSQualityOfService qualityOfService API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0)); // read-only after the thread is started
    

    这里,iOS 8.0以后threadPriority已经被废弃了,用qualityOfService替换,这个是一个枚举,在线程start以后,就变为只读属性了。

    typedef NS_ENUM(NSInteger, NSQualityOfService) {
        NSQualityOfServiceUserInteractive = 0x21,
        NSQualityOfServiceUserInitiated = 0x19,
        NSQualityOfServiceUtility = 0x11,
        NSQualityOfServiceBackground = 0x09,
        NSQualityOfServiceDefault = -1
    } API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));
    

    使用示例

    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        NSLog(@"threadPriority = %lf", [NSThread threadPriority]);
    }
    
    2019-06-25 15:32:47.899996+0800 JJNSThread[35343:10560382] threadPriority = 0.500000
    

    6. 线程名称

    @property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    

    主线程名字默认为main,子线程如果不指定就为空。

    7. 栈空间

    @property NSUInteger stackSize API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    

    使用示例

    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        NSThread *thread = [[NSThread alloc] init];
        NSLog(@"thread.stackSize = %ld", thread.stackSize);
        
        NSLog(@"[NSThread currentThread].stackSize = %ld", [NSThread currentThread].stackSize);
    }
    
    2019-06-25 16:43:43.588894+0800 JJNSThread[35348:10567022] thread.stackSize = 524288
    2019-06-25 16:43:43.588926+0800 JJNSThread[35348:10567022] [NSThread currentThread].stackSize = 524288
    

    8. 栈空间地址和符号

    callStackReturnAddresses线程的调用都会有函数的调用函数的调用就会有栈返回地址的记录,在这里返回的是函 数调用返回的虚拟地址,说白了就是在该线程中函数调用的虚拟地址的数组。

    @property (class, readonly, copy) NSArray<NSNumber *> *callStackReturnAddresses API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    

    使用示例

    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        NSLog(@"%@", [NSThread callStackReturnAddresses]);
    }
    
    2019-06-25 17:14:16.570190+0800 JJNSThread[35362:10570460] (0x104c267f4 0x1c5b828f8 0x1c5b82cfc 0x1c6160b3c 0x1c61610e4 0x1c61719c0 0x1c6124838 0x1c6129f10 0x1c59ee104 0x1c59f669c 0x1c59edd88 0x1c59ee678 0x1c59ec9c4 0x1c59ec68c 0x1c59f11cc 0x1c59f1fb0 0x1c59f1084 0x1c59f5d84 0x1c6128518 0x1c5d24f0c 0x19c562d44 0x19c56c754 0x19c56bf5c 0x104fb4c74 0x104fb8840 0x19c59d0bc 0x19c59cd58 0x19c59d310 0x199b7a2bc 0x199b7a23c 0x199b79b24 0x199b74a60 0x199b74354 0x19bd7479c 0x1c612bb68 0x104c268ac 0x19963a8e0)
    

    同上面的方法一样,只不过返回的是该线程调用函数的名字数字。

    @property (class, readonly, copy) NSArray<NSString *> *callStackSymbols API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
    

    使用示例

    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        NSLog(@"%@", [NSThread callStackSymbols]);
    }
    
    2019-06-25 17:21:03.620362+0800 JJNSThread[35364:10571215] (
        0   JJNSThread                          0x0000000102f067fc -[ViewController viewDidLoad] + 96
        1   UIKitCore                           0x00000001c5b828f8 <redacted> + 1012
        2   UIKitCore                           0x00000001c5b82cfc <redacted> + 28
        3   UIKitCore                           0x00000001c6160b3c <redacted> + 136
        4   UIKitCore                           0x00000001c61610e4 <redacted> + 272
        5   UIKitCore                           0x00000001c61719c0 <redacted> + 48
        6   UIKitCore                           0x00000001c6124838 <redacted> + 3532
        7   UIKitCore                           0x00000001c6129f10 <redacted> + 1540
        8   UIKitCore                           0x00000001c59ee104 <redacted> + 776
        9   UIKitCore                           0x00000001c59f669c <redacted> + 160
        10  UIKitCore                           0x00000001c59edd88 <redacted> + 236
        11  UIKitCore                           0x00000001c59ee678 <redacted> + 1064
        12  UIKitCore                           0x00000001c59ec9c4 <redacted> + 744
        13  UIKitCore                           0x00000001c59ec68c <redacted> + 428
        14  UIKitCore                           0x00000001c59f11cc <redacted> + 220
        15  UIKitCore                           0x00000001c59f1fb0 _performActionsWithDelayForTransitionContext + 112
        16  UIKitCore                           0x00000001c59f1084 <redacted> + 244
        17  UIKitCore                           0x00000001c59f5d84 <redacted> + 360
        18  UIKitCore                           0x00000001c6128518 <redacted> + 540
        19  UIKitCore                           0x00000001c5d24f0c <redacted> + 360
        20  FrontBoardServices                  0x000000019c562d44 <redacted> + 440
        21  FrontBoardServices                  0x000000019c56c754 <redacted> + 256
        22  FrontBoardServices                  0x000000019c56bf5c <redacted> + 64
        23  libdispatch.dylib                   0x0000000102f9cc74 _dispatch_client_callout + 16
        24  libdispatch.dylib                   0x0000000102fa0840 _dispatch_block_invoke_direct + 232
        25  FrontBoardServices                  0x000000019c59d0bc <redacted> + 40
        26  FrontBoardServices                  0x000000019c59cd58 <redacted> + 408
        27  FrontBoardServices                  0x000000019c59d310 <redacted> + 52
        28  CoreFoundation                      0x0000000199b7a2bc <redacted> + 24
        29  CoreFoundation                      0x0000000199b7a23c <redacted> + 88
        30  CoreFoundation                      0x0000000199b79b24 <redacted> + 176
        31  CoreFoundation                      0x0000000199b74a60 <redacted> + 1004
        32  CoreFoundation                      0x0000000199b74354 CFRunLoopRunSpecific + 436
        33  GraphicsServices                    0x000000019bd7479c GSEventRunModal + 104
        34  UIKitCore                           0x00000001c612bb68 UIApplicationMain + 212
        35  JJNSThread                          0x0000000102f068b4 main + 124
        36  libdyld.dylib                       0x000000019963a8e0 <redacted> + 4
    )
    

    9. 是否是主线程

    @property (readonly) BOOL isMainThread API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    @property (class, readonly) BOOL isMainThread API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); // reports whether current thread is main
    @property (class, readonly, strong) NSThread *mainThread API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    

    isMainThread用来判断该线程是否是主线程,而mainThread用来获取当前的主线程。

    使用示例

    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        NSLog(@"%d", [NSThread isMainThread]);
        NSLog(@"%@", [NSThread mainThread]);
    }
    
    2019-06-26 09:43:06.894754+0800 JJNSThread[35492:10650199] 1
    2019-06-26 09:43:06.894846+0800 JJNSThread[35492:10650199] <NSThread: 0x281686e80>{number = 1, name = main}
    

    10. 线程创建

    对象方法创建

    下面看一下线程的初始化

    - (instancetype)init API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) NS_DESIGNATED_INITIALIZER;
    - (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    - (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
    

    类方法创建

    //block方式
    + (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
    
    //SEL方式
    + (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
    

    注意:创建子线程并开始,注意以下两个类方法创建后就可执行,不需手动开启

    下面就是使用示例

    @implementation ViewController
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        [NSThread detachNewThreadWithBlock:^{
            NSLog(@"block [NSThread currentThread] = %@", [NSThread currentThread]);
        }];
        
        [NSThread detachNewThreadSelector:@selector(detachNewThread) toTarget:self withObject:nil];
    }
    
    - (void)detachNewThread
    {
        NSLog(@"sel [NSThread currentThread] = %@", [NSThread currentThread]);
    }
    
    @end
    

    下面是输出

    2019-06-25 15:06:13.270482+0800 JJNSThread[35325:10556932] block [NSThread currentThread] = <NSThread: 0x280d74d80>{number = 4, name = (null)}
    2019-06-25 15:06:13.270702+0800 JJNSThread[35325:10556933] sel [NSThread currentThread] = <NSThread: 0x280d42340>{number = 3, name = (null)}
    

    隐式创建

    @interface NSObject (NSThreadPerformAdditions)
    
    /**
      指定方法在主线程中执行
      参数 1. SEL 方法
            2. 方法参数
            3. 是否等待当前执行完毕
            4. 指定的Runloop model
    */
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
        // equivalent to the first method with kCFRunLoopCommonModes
    
    /**
      指定方法在某个线程中执行
      参数  1. SEL 方法
             2. 方法参数
             3. 是否等待当前执行完毕
             4. 指定的Runloop model
    */
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
        // equivalent to the first method with kCFRunLoopCommonModes
    
    /**
      指定方法在开启的子线程中执行
      参数 1. SEL 方法
           2. 方法参数
    */
    - (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    
    @end
    

    11. 线程状态控制

    下面就是线程的状态控制

    @property (readonly, getter=isExecuting) BOOL executing API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    @property (readonly, getter=isFinished) BOOL finished API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    @property (readonly, getter=isCancelled) BOOL cancelled API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    
    //取消线程
    - (void)cancel API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    
    //开启线程
    - (void)start API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    
    //退出当前线程
    + (void)exit;
    

    isExecuting判断线程是否正在执行,isFinished判断线程是否已经结束,isCancelled判断线程是否撤销。

    12. 线程入口函数

    - (void)main API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); // thread body method
    

    这个是线程的入口函数。


    NSThread与锁

    在程序运行过程中,如果存在多线程,那么各个线程读写资源就会存在先后、同时读写资源的操作,因为是在不同线程,CPU调度过程中我们无法保证哪个线程会先读写资源,哪个线程后读写资源。因此为了防止数据读写混乱和错误的发生,我们要将线程在读写数据时加锁,这样就能保证操作同一个数据对象的线程只有一个,当这个线程执行完成之后解锁。

    常用的锁有下面几种

    • @synchronized
    • NSLock
    • NSConditionLock
    • NSRecursiveLock
    • 自旋锁

    1. @synchronized

    下面就是一个卖票的示例,用来说明互斥锁的作用

    #import "ViewController.h"
    
    @interface ViewController ()
    
    @property (nonatomic, assign) NSInteger tickets;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        self.tickets = 10;
        
        NSThread *thread1 = [[NSThread alloc]initWithTarget:self selector:@selector(sellTickets) object:nil];
        thread1.name = @"售票员A";
        [thread1 start];
        
        NSThread *thread2 = [[NSThread alloc]initWithTarget:self selector:@selector(sellTickets) object:nil];
        thread2.name = @"售票员B";
        [thread2 start];
    }
    
    - (void)sellTickets
    {
        while (1) {
            [NSThread sleepForTimeInterval:0.1];
            //互斥锁 -- 保证锁内的代码在同一时间内只有一个线程在执行
            @synchronized (self){
                //1.判断是否有票
                if (self.tickets > 0) {
                    //2.如果有就卖一张
                    self.tickets --;
                    NSLog(@"还剩%ld张票, 线程 = %@",self.tickets,[NSThread currentThread]);
                }
                else{
                    //3.没有票了提示
                    NSLog(@"已经没票了 %@",[NSThread currentThread]);
                    break;
                }
            }
        }
    }
    
    @end
    

    下面看一下输出

    2019-06-26 11:16:50.776862+0800 JJNSThread[35572:10662615] 还剩9张票, 线程 = <NSThread: 0x283a15380>{number = 3, name = 售票员A}
    2019-06-26 11:16:50.776974+0800 JJNSThread[35572:10662616] 还剩8张票, 线程 = <NSThread: 0x283a15280>{number = 4, name = 售票员B}
    2019-06-26 11:16:50.882257+0800 JJNSThread[35572:10662616] 还剩7张票, 线程 = <NSThread: 0x283a15280>{number = 4, name = 售票员B}
    2019-06-26 11:16:50.882487+0800 JJNSThread[35572:10662615] 还剩6张票, 线程 = <NSThread: 0x283a15380>{number = 3, name = 售票员A}
    2019-06-26 11:16:50.991518+0800 JJNSThread[35572:10662616] 还剩5张票, 线程 = <NSThread: 0x283a15280>{number = 4, name = 售票员B}
    2019-06-26 11:16:50.991764+0800 JJNSThread[35572:10662615] 还剩4张票, 线程 = <NSThread: 0x283a15380>{number = 3, name = 售票员A}
    2019-06-26 11:16:51.096391+0800 JJNSThread[35572:10662616] 还剩3张票, 线程 = <NSThread: 0x283a15280>{number = 4, name = 售票员B}
    2019-06-26 11:16:51.096712+0800 JJNSThread[35572:10662615] 还剩2张票, 线程 = <NSThread: 0x283a15380>{number = 3, name = 售票员A}
    2019-06-26 11:16:51.201245+0800 JJNSThread[35572:10662616] 还剩1张票, 线程 = <NSThread: 0x283a15280>{number = 4, name = 售票员B}
    2019-06-26 11:16:51.201890+0800 JJNSThread[35572:10662615] 还剩0张票, 线程 = <NSThread: 0x283a15380>{number = 3, name = 售票员A}
    2019-06-26 11:16:51.306646+0800 JJNSThread[35572:10662615] 已经没票了 <NSThread: 0x283a15380>{number = 3, name = 售票员A}
    2019-06-26 11:16:51.306956+0800 JJNSThread[35572:10662616] 已经没票了 <NSThread: 0x283a15280>{number = 4, name = 售票员B}
    

    2. NSLock

    首先看一下API

    @interface NSLock : NSObject <NSLocking> {
    @private
        void *_priv;
    }
    
    //尝试加锁,成功返回YES ;失败返回NO ,但不会阻塞线程的运行
    - (BOOL)tryLock;
    
    //在指定的时间以前得到锁。
    //YES:在指定时间之前获得了锁;NO:在指定时间之前没有获得锁。
    //该线程将被阻塞,直到获得了锁,或者指定时间过期。
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    
    @property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    
    @end
    

    下面我们换成NSLock看一下

    #import "ViewController.h"
    
    @interface ViewController ()
    
    @property (nonatomic, assign) NSInteger tickets;
    @property (nonatomic, strong) NSLock *lock;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        self.tickets = 10;
        
        self.lock = [[NSLock alloc] init];
        
        NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(sellTickets) object:nil];
        thread1.name = @"售票员A";
        [thread1 start];
    
        NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(sellTickets) object:nil];
        thread2.name = @"售票员B";
        [thread2 start];
    }
    
    - (void)sellTickets
    {
        while (1) {
            [NSThread sleepForTimeInterval:0.1];
        
            [self.lock lock];
            //1.判断是否有票
            if (self.tickets > 0) {
                //2.如果有就卖一张
                self.tickets --;
                NSLog(@"卖一张,还剩%ld张票, 线程 = %@",self.tickets, [NSThread currentThread]);
            }
            else{
                //3.没有票了提示
                NSLog(@"已经没票了 %@",[NSThread currentThread]);
                break;
            }
            [self.lock unlock];
        }
    }
    
    @end
    

    接着看一下输出

    2019-06-26 12:34:42.890668+0800 JJNSThread[35618:10673688] 卖一张,还剩9张票, 线程 = <NSThread: 0x2824f85c0>{number = 3, name = 售票员A}
    2019-06-26 12:34:42.890926+0800 JJNSThread[35618:10673689] 卖一张,还剩8张票, 线程 = <NSThread: 0x2824f84c0>{number = 4, name = 售票员B}
    2019-06-26 12:34:42.995899+0800 JJNSThread[35618:10673688] 卖一张,还剩7张票, 线程 = <NSThread: 0x2824f85c0>{number = 3, name = 售票员A}
    2019-06-26 12:34:42.996128+0800 JJNSThread[35618:10673689] 卖一张,还剩6张票, 线程 = <NSThread: 0x2824f84c0>{number = 4, name = 售票员B}
    2019-06-26 12:34:43.098488+0800 JJNSThread[35618:10673689] 卖一张,还剩5张票, 线程 = <NSThread: 0x2824f84c0>{number = 4, name = 售票员B}
    2019-06-26 12:34:43.101237+0800 JJNSThread[35618:10673688] 卖一张,还剩4张票, 线程 = <NSThread: 0x2824f85c0>{number = 3, name = 售票员A}
    2019-06-26 12:34:43.203755+0800 JJNSThread[35618:10673689] 卖一张,还剩3张票, 线程 = <NSThread: 0x2824f84c0>{number = 4, name = 售票员B}
    2019-06-26 12:34:43.206475+0800 JJNSThread[35618:10673688] 卖一张,还剩2张票, 线程 = <NSThread: 0x2824f85c0>{number = 3, name = 售票员A}
    2019-06-26 12:34:43.308982+0800 JJNSThread[35618:10673688] 卖一张,还剩1张票, 线程 = <NSThread: 0x2824f85c0>{number = 3, name = 售票员A}
    2019-06-26 12:34:43.309270+0800 JJNSThread[35618:10673689] 卖一张,还剩0张票, 线程 = <NSThread: 0x2824f84c0>{number = 4, name = 售票员B}
    2019-06-26 12:34:43.414239+0800 JJNSThread[35618:10673688] 已经没票了 <NSThread: 0x2824f85c0>{number = 3, name = 售票员A}
    

    3. NSConditionLock

    NSConditionLock用于需要根据一定条件满足后进行 加锁/解锁.

    首先看下API

    @interface NSConditionLock : NSObject <NSLocking> {
    @private
        void *_priv;
    }
    
    //初始化条件锁
    - (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
    
    @property (readonly) NSInteger condition;
    
    //加锁 (条件是:锁空闲,即没被占用)
    - (void)lockWhenCondition:(NSInteger)condition;
    
    //尝试加锁,成功返回TRUE,失败返回FALSE
    - (BOOL)tryLock;
    
    //在指定条件成立的情况下尝试加锁,成功返回TRUE,失败返回FALSE
    - (BOOL)tryLockWhenCondition:(NSInteger)condition;
    
    //在指定的条件成立时,解锁
    - (void)unlockWithCondition:(NSInteger)condition;
    
    //在指定时间前加锁,成功返回TRUE,失败返回FALSE,
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    
    //条件成立的情况下,在指定时间前加锁,成功返回TRUE,失败返回FALSE
    - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
    
    @property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    
    @end
    

    下面就看一下适用场景

    static NSInteger CONDITION_NO_DATA        //条件一: 没有数据
    static NSInteger CONDITION_HAS_DATA       //条件二: 有数据
    
    //初始化锁时,指定一个默认的条件
    NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:CONDITION_NO_DATA];
    
    //生产者,加锁与解锁的过程
    while (YES) {
        
        //1. 当满足 【没有数据的条件时】进行加锁
        [lock lockWhenCondition:CONDITION_NO_DATA];
    
        //2. 生产者生成数据 
        //.....
    
        //3. 解锁,并设置新的条件,已经有数据了
        [locker unlockWithCondition:CONDITION_HAS_DATA];
    }
    
    
    //消费者,加锁与解锁的过程 
    while (YES) {
        
        //1. 当满足 【有数据的条件时】进行加锁
        [lock lockWhenCondition:CONDITION_HAS_DATA];
    
        //2. 消费者消费数据 
        //.....
    
        //3. 解锁,并设置新的条件,没有数据了
        [locker unlockWithCondition:CONDITION_NO_DATA];
    }
    

    4. NSRecursiveLock

    此锁可以在同一线程中多次被使用,但要保证加锁与解锁使用平衡,多用于递归函数,防止死锁。

    首先看下API文档

    @interface NSRecursiveLock : NSObject <NSLocking> {
    @private
        void *_priv;
    }
    
    //尝试加锁,成功返回TRUE,失败返回FALSE
    - (BOOL)tryLock;
    
    //在指定时间前尝试加锁,成功返回TRUE,失败返回FALSE
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    
    //线程锁名称
    @property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    
    @end
    

    下面看这个示例,其实就是递归使用这个锁

    - (void)initRecycle:(int)value
    {
        NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
        [recursiveLock lock];
        if(value > 0)
        {
            NSLog(@"value = %d",value);
            sleep(1);
            [self initRecycle:value - 1];
        }
        [recursiveLock unlock];
    }
    

    看一下输出

    2019-06-26 14:27:53.553329+0800 JJNSThread[35682:10690663] value = 5
    2019-06-26 14:27:54.558623+0800 JJNSThread[35682:10690663] value = 4
    2019-06-26 14:27:55.563893+0800 JJNSThread[35682:10690663] value = 3
    2019-06-26 14:27:56.569331+0800 JJNSThread[35682:10690663] value = 2
    2019-06-26 14:27:57.570493+0800 JJNSThread[35682:10690663] value = 1
    

    5. 自旋锁

    这里有必要说一下属性修饰符atomic和nonatomic,前者安全,性能低;后者不安全,但是性能高,在我们确认安全的情况下我们一般都使用nonatomicatomic内部其实有一个自旋锁。

    前面四种都是互斥锁,这里和自旋锁有什么区别和联系呢?

    共同点

    • 都能够保证线程安全

    不同点

    • 互斥锁:如果其他线程正在执行锁定的代码,此线程就会进入休眠状态,等待锁打开,然后被唤醒
    • 自旋锁:如果线程被锁在外面,就会用死循环的方式一直等待锁打开!

    后记

    本篇主要讲述了NSThread以及解决多线程数据安全锁的问题,感兴趣的给个赞或者关注~~~

    相关文章

      网友评论

        本文标题:iOS与多线程(十) —— NSThread的使用以及锁(一)

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