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

版本记录

版本号 时间
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以及解决多线程数据安全锁的问题,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353

推荐阅读更多精彩内容

  • 1、简介:1.1 iOS有三种多线程编程的技术,分别是:1.、NSThread2、Cocoa NSOperatio...
    LuckTime阅读 1,345评论 0 1
  • 多线程基本概念 单核CPU,同一时间cpu只能处理1个线程,只有1个线程在执行 。多线程同时执行:是CPU快速的在...
    WeiHing阅读 706评论 1 5
  • 进程与线程 进程:计算机操作系统分配资源的单位,是指系统中正在运行的应用程序,进程之间相互独立,运行在受保护的内存...
    三十六_阅读 376评论 1 1
  • 在这篇文章中,我将为你整理一下 iOS 开发中几种多线程方案,以及其使用方法和注意事项。当然也会给出几种多线程的案...
    张战威ican阅读 603评论 0 0
  • 以前觉得生活应该光鲜亮丽,生活应该有趣自在等,在自己毕业了两年后,才懂得了原来生活是无味枯燥的,工作更是。今天我想...
    SDWISH阅读 347评论 0 0