NSOperation

概述

iOS并發(fā)編程中,把每個(gè)并發(fā)任務(wù)定義為一個(gè)Operation。NSOperation是一個(gè)抽象類,無(wú)法直接使用,它只定義了Operation的一些基本方法。我們需要?jiǎng)?chuàng)建一個(gè)繼承于它的子類或者使用系統(tǒng)預(yù)定義的子類。

方法結(jié)構(gòu)

NSOperation

把邏輯代碼寫在NSOperation中,就是把邏輯代碼添加一層殼,執(zhí)行NSOperation就是間接的執(zhí)行邏輯代碼。

  • 用來(lái)定義操作對(duì)象的基礎(chǔ)(抽象)類。處理并發(fā)任務(wù)時(shí),具體子類通常要重寫main、isConcurrent、isExecuting 、isFinished方法。
  • Operation默認(rèn)都是串行操作(FIFO),默認(rèn)情況下Operation并不額外創(chuàng)建線程。
  • 啟動(dòng)一個(gè)Operation任務(wù)
    如果希望擁有更多的控制權(quán),以及在一個(gè)操作中可以執(zhí)行異步任務(wù),那么就重寫 start 方法。
    如果重寫 start 方法,你必須手動(dòng)管理操作的狀態(tài)。 為了讓操作隊(duì)列能夠捕獲到操作的改變,需要將狀態(tài)的屬性以配合 KVO 的方式進(jìn)行實(shí)現(xiàn)。
- (void)start;
  • 取消一個(gè)Operation
    如果你在main方法中沒(méi)有對(duì)cancel進(jìn)行任何處理的話,發(fā)送cancel消息是沒(méi)有任何效果的。為了讓Operation響應(yīng)cancel消息,那么你就要在main方法中一些適當(dāng)?shù)牡胤绞謩?dòng)的判斷isCancelled屬性,如果返回YES的話,應(yīng)釋放相關(guān)資源并立刻停止繼續(xù)執(zhí)行。
@property (readonly, getter=isCancelled) BOOL cancelled;
- (void)cancel;
  • 用來(lái)執(zhí)行你所想要執(zhí)行的任務(wù)
    可以通過(guò)重寫 main 方法 來(lái)定義自己的 operations,開(kāi)發(fā)者不需要管理一些狀態(tài)屬性(例如 isExecuting 和 isFinished),當(dāng) main 方法返回的時(shí)候,這個(gè) operation 就結(jié)束了。
- (void)main;
  • 判斷Operation是否是可并發(fā)的
@property (readonly, getter=isConcurrent) BOOL concurrent; // To be deprecated; use and override 'asynchronous' below
@property (readonly, getter=isAsynchronous) BOOL asynchronous API_AVAILABLE(macos(10.8), ios(7.0), watchos(2.0), tvos(9.0));
  • 查看、添加、刪除 操作依賴
@property (readonly, copy) NSArray<NSOperation *> *dependencies;
- (void)addDependency:(NSOperation *)op;
- (void)removeDependency:(NSOperation *)op;

下面就是intermediateOperation操作必須等到operation1、operation2 完成后才能執(zhí)行。

[intermediateOperation addDependency:operation1];
[intermediateOperation addDependency:operation2];
  • Operation執(zhí)行完成時(shí)自動(dòng)執(zhí)行completionBlock。可以在此進(jìn)行一些完成的處理。
    每個(gè)Operation都可以設(shè)置一個(gè)completionBlock。
@property (nullable, copy) void (^completionBlock)(void) API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
  • 線程優(yōu)先級(jí)
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};

@property NSOperationQueuePriority queuePriority;
  • 可以通過(guò)KVO監(jiān)聽(tīng)Operation的一下?tīng)顟B(tài)改變的Key
isCancelled
isConcurrent
isExecuting
isFinished
isReady
dependencies
queuePriority
completionBlock

NSInvocationOperation

  • 初始化:
- (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;

- (instancetype)initWithInvocation:(NSInvocation *)inv;
  • 變量
@property (readonly, retain) NSInvocation *invocation;
//個(gè)Operation完成后返回結(jié)果
@property (nullable, readonly, retain) id result;

NSBlockOperation

  • 初始化:
+ (instancetype)blockOperationWithBlock:(void (^)(void))block;
- (void)addExecutionBlock:(void (^)(void))block;
@property (readonly, copy) NSArray<void (^)(void)> *executionBlocks;

Operation操作流程

一些公共方法:

-(void)operationComplete {
    NSLog(@"All task finished.");
}

-(void)logOperation:(NSOperation *)op keyPathes:(NSArray *)keyPathes {
    [keyPathes enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSLog(@"%@ %@ = %@",op.name,obj,[[op valueForKey:obj] boolValue]?@"YES":@"NO");
    }];
}

-(void)addObserverForOperation:(id)op keyPathes:(NSArray *)keyPathes {
    [keyPathes enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        [op addObserver:self forKeyPath:obj options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) context:NULL];
    }];
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([object isKindOfClass:[NSOperation class]]) {
        NSLog(@"observeValueForKeyPath:%@---%@---%@",[object name],keyPath,change);
    }
}
  • 先執(zhí)行start,后執(zhí)行cancel
-(void)operationKeysChange{
    TestBlockOperation * op = [TestBlockOperation blockOperationWithBlock:^{
        NSLog(@"enter op");
        [NSThread sleepForTimeInterval:3];
        NSLog(@"leave op");
    }];
    op.name = @"op";
    NSArray * keyPathes = @[@"isReady",@"isCancelled",@"isExecuting",@"isFinished"];
    [self logOperation:op keyPathes:keyPathes];
    [self addObserverForOperation:op keyPathes:keyPathes];
op.completionBlock = ^{
        [self operationComplete];
    };
    [op start];
    [op cancel];
}

初始狀態(tài)下,ready為YES,其他均為NO。
當(dāng)我們調(diào)用 -start 后,執(zhí)行 -main 之前 isExecuting 屬性從NO被置為YES。
調(diào)用 -main 之后開(kāi)始執(zhí)行提交到Operation中的任務(wù)。
任務(wù)完成后 isExecuting 屬性從YES被置為NO,isFinished 屬性從NO被置為YES。

op isReady = YES
op isCancelled = NO
op isExecuting = NO
op isFinished = NO
op before start
observeValueForKeyPath:op---isExecuting---{kind = 1;new = 1;old = 0;}
op before main
enter bp1
leave bp1
/*這里執(zhí)行業(yè)務(wù)代碼,完成后才進(jìn)行下面的操作*/
op after main
All task finished.
observeValueForKeyPath:op---isExecuting---{kind = 1;new = 0;old = 1;}
observeValueForKeyPath:op---isFinished---{kind = 1;new = 1;old = 0;}
op after start
op before cancel
op after cancel
  • 先執(zhí)行cancel,后執(zhí)行start
-(void)operationKeysChange{
    TestBlockOperation * op = [TestBlockOperation blockOperationWithBlock:^{
        NSLog(@"enter op");
        [NSThread sleepForTimeInterval:3];
        NSLog(@"leave op");
    }];
    op.name = @"op";
    NSArray * keyPathes = @[@"isReady",@"isCancelled",@"isExecuting",@"isFinished"];
    [self logOperation:op keyPathes:keyPathes];
    [self addObserverForOperation:op keyPathes:keyPathes];
op.completionBlock = ^{
        [self operationComplete];
    };
    [op cancel];
    [op start];
}

先調(diào)用 -start ,后調(diào)用 -cancel ,isCancelled 屬性從NO被置為YES,isReady 屬性無(wú)論什么狀態(tài)都會(huì)被置為YES。
先調(diào)用 -start ,后調(diào)用 -cancel ,會(huì)將 isFinished 屬性從NO被置為YES,然后并不調(diào)用 -main 方法。

op isReady = YES
op isCancelled = NO
op isExecuting = NO
op isFinished = NO
op before cancel
observeValueForKeyPath:op---isCancelled---{kind = 1;new = 1;old = 0;}
observeValueForKeyPath:op---isReady---{kind = 1;new = 1;old = 1;}
op after cancel
op before start
All task finished.
observeValueForKeyPath:op---isFinished---{kind = 1;new = 1;old = 0;}
op after start
  • NSOperationQueue 但是沒(méi)有依賴
-(void) operationKeysChange {
    TestBlockOperation * bp1 = [TestBlockOperation blockOperationWithBlock:^{
        NSLog(@"enter bp1");
        [NSThread sleepForTimeInterval:3];
        NSLog(@"leave bp1");
    }];
    bp1.name = @"bp1";
    bp1.completionBlock = ^{
        NSLog(@"bp1 complete");
    };
    
    TestBlockOperation * bp2 = [TestBlockOperation blockOperationWithBlock:^{
        NSLog(@"enter bp2");
        [NSThread sleepForTimeInterval:3];
        NSLog(@"leave bp2");
    }];
    bp2.name = @"bp2";
    bp2.completionBlock = ^{
        NSLog(@"bp2 complete");
    };
    
    NSArray * keyPathes = @[@"isReady",@"isCancelled",@"isExecuting",@"isFinished"];
    
    [self logOperation:bp1 keyPathes:keyPathes];
    [self logOperation:bp2 keyPathes:keyPathes];
    [self addObserverForOperation:bp1 keyPathes:keyPathes];
    [self addObserverForOperation:bp2 keyPathes:keyPathes];
    
    NSOperationQueue * q = [NSOperationQueue new];
    [q addOperation:bp1];
    [q addOperation:bp2];
}

NSOperationQueue 執(zhí)行順序和NSOperation一樣


bp1 isReady = YES
bp1 isCancelled = NO
bp1 isExecuting = NO
bp1 isFinished = NO

bp2 isReady = YES
bp2 isCancelled = NO
bp2 isExecuting = NO
bp2 isFinished = NO

bp1 before start
bp2 before start

observeValueForKeyPath:bp1---isExecuting---{kind = 1;new = 1;old = 0;}
observeValueForKeyPath:bp2---isExecuting---{kind = 1;new = 1;old = 0;}

bp1 before main
bp2 before main

enter bp1
enter bp2

leave bp1
leave bp2

bp2 after main
bp1 after main

bp2 complete
bp1 complete

observeValueForKeyPath:bp1---isExecuting---{kind = 1;new = 0;old = 1;}
observeValueForKeyPath:bp2---isExecuting---{kind = 1;new = 0;old = 1;}

observeValueForKeyPath:bp1---isFinished---{kind = 1;new = 1;old = 0;}
observeValueForKeyPath:bp2---isFinished---{kind = 1;new = 1;old = 0;}

bp1 after start
bp2 after start
  • NSOperationQueue 但是有依賴
-(void) operationKeysChange {
    TestBlockOperation * bp1 = [TestBlockOperation blockOperationWithBlock:^{
        NSLog(@"enter bp1");
        [NSThread sleepForTimeInterval:3];
        NSLog(@"leave bp1");
    }];
    bp1.name = @"bp1";
    bp1.completionBlock = ^{
        NSLog(@"bp1 complete");
    };
    
    TestBlockOperation * bp2 = [TestBlockOperation blockOperationWithBlock:^{
        NSLog(@"enter bp2");
        [NSThread sleepForTimeInterval:3];
        NSLog(@"leave bp2");
    }];
    bp2.name = @"bp2";
    bp2.completionBlock = ^{
        NSLog(@"bp2 complete");
    };
    
    NSArray * keyPathes = @[@"isReady",@"isCancelled",@"isExecuting",@"isFinished"];
    
    [self logOperation:bp1 keyPathes:keyPathes];
    [self logOperation:bp2 keyPathes:keyPathes];
    [self addObserverForOperation:bp1 keyPathes:keyPathes];
    [self addObserverForOperation:bp2 keyPathes:keyPathes];
    
    NSOperationQueue * q = [NSOperationQueue new];
    [bp1 addDependency:bp2];
    [q addOperation:bp1];
    [q addOperation:bp2];
}

當(dāng)為bp1添加bp2作為依賴以后,bp1的 isReady 屬性從YES置為NO。
由于bp2是bp1的依賴,所以優(yōu)先執(zhí)行bp2。
在bp2中任務(wù)完成之后,-main 方法調(diào)用結(jié)束之后, -start 方法調(diào)用結(jié)束之前,bp1調(diào)用 -start 并將 isReady 屬性置為YES。

bp1 isReady = YES
bp1 isCancelled = NO
bp1 isExecuting = NO
bp1 isFinished = NO

bp2 isReady = YES
bp2 isCancelled = NO
bp2 isExecuting = NO
bp2 isFinished = NO

bp1 before addDependency:
observeValueForKeyPath:bp1---isReady---{kind = 1;new = 0;old = 1;}
bp1 after addDependency:

bp2 before start
observeValueForKeyPath:bp2---isExecuting---{kind = 1;new = 1;old = 0;}
bp2 before main
enter bp2
leave bp2
bp2 after main
bp1 before start
observeValueForKeyPath:bp1---isReady---{kind = 1;new = 1;old = 0;}
observeValueForKeyPath:bp1---isExecuting---{kind = 1;new = 1;old = 0;}
bp2 complete
observeValueForKeyPath:bp2---isExecuting---{kind = 1;new = 0;old = 1;}
bp1 before main
enter bp1
observeValueForKeyPath:bp2---isFinished---{kind = 1;new = 1;old = 0;}
bp2 after start
leave bp1
bp1 after main
bp1 complete
observeValueForKeyPath:bp1---isExecuting---{kind = 1;new = 0;old = 1;}
observeValueForKeyPath:bp1---isFinished---{kind = 1;new = 1;old = 0;}
bp1 after start
NSOperation流程
  • 通過(guò)上面的流程圖自定義實(shí)現(xiàn)簡(jiǎn)單的并發(fā)操作
@interface SAMOperation : NSOperation
@end
@implementation SAMOperation{
    BOOL isFinished;//監(jiān)聽(tīng)是否執(zhí)行結(jié)束
    BOOL isExecuting;//監(jiān)聽(tīng)是否正在執(zhí)行
}

/*1.自定義初始化方法*/
-(instancetype)init{
    if (self == [super init]) {
        isExecuting = NO;
        isFinished = NO;
    }
    return self;
}
-(void)start{
    //如果被取消了就直接返回結(jié)果,不會(huì)執(zhí)行main方法
    if ([self isCancelled]) {
        [self willChangeValueForKey:@"isFinished"];
        isFinished = NO;
        [self didChangeValueForKey:@"isFinished"];
        return;
    }

    //沒(méi)有被取消,使用獨(dú)立線程執(zhí)行main方法中的操作
    [self willChangeValueForKey:@"isExecuting"];
    [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
    isExecuting = YES;
    [self didChangeValueForKey:@"isExecuting"];
    
}

/*2.自定義輔助方法*/
-(void)main{
    @autoreleasepool{//使用獨(dú)立的內(nèi)存釋放池,不然會(huì)內(nèi)存泄漏
        @try {
            if (![self isCancelled]) {
                NSLog(@"Begin%@",[NSThread currentThread]);
                [NSThread sleepForTimeInterval:1.0];
                NSLog(@"End%@",[NSThread currentThread]);
                //任務(wù)結(jié)束,修改狀態(tài)值
                [self willChangeValueForKey:@"isFinished"];
                [self willChangeValueForKey:@"isExecuting"];
                isExecuting = NO;
                isFinished = YES;
                [self didChangeValueForKey:@"isExecuting"];
                [self willChangeValueForKey:@"isFinished"];

            }
        } @catch (NSException *exception) {
        } @finally {
        }
    }
}

-(BOOL)isConcurrent{
    return YES;
}

-(BOOL)isExecuting{
    return isExecuting;
}

-(BOOL)isFinished{
    return isFinished;
}
@end

使用:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"begin func");
    SAMOperation *op1 = [SAMOperation new];
    [op1 start];
    NSLog(@"end func");
}

NSOperationQueue

  • 兩種不同類型的隊(duì)列:主隊(duì)列和自定義隊(duì)列。主隊(duì)列運(yùn)行在主線程之上,而自定義隊(duì)列在后臺(tái)執(zhí)行。在兩種類型中,這些隊(duì)列所處理的任務(wù)都使用 NSOperation 的子類來(lái)表述。
  • NSOperationQueue是一個(gè)Operation執(zhí)行隊(duì)列,你可以將任何你想要執(zhí)行的Operation添加到Operation Queue中,以在隊(duì)列中執(zhí)行。
  • NSOperationQueue 可以動(dòng)態(tài)的創(chuàng)建多個(gè)線程來(lái)完成相應(yīng)Operation,總線程數(shù)量通過(guò)maxConcurrentOperationCount屬性來(lái)控制。
  • 當(dāng)操作添加到隊(duì)列中,它會(huì)待在隊(duì)列中,直到被顯式取消或者執(zhí)行完為止。
  • 創(chuàng)建隊(duì)列
// 主隊(duì)列隊(duì)列
NSOperationQueue *queue = [NSOperationQueue mainQueue];
// 自定義隊(duì)列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
  • 設(shè)置最大并發(fā)數(shù)
    maxConcurrentOperationCount 為1時(shí),隊(duì)列為串行隊(duì)列。只能串行執(zhí)行。
    maxConcurrentOperationCount 默認(rèn)情況下為-1,表示不進(jìn)行限制,可進(jìn)行并發(fā)執(zhí)行。
    maxConcurrentOperationCount 大于1時(shí),隊(duì)列為并發(fā)隊(duì)列。操作并發(fā)執(zhí)行,當(dāng)然這個(gè)值不應(yīng)超過(guò)系統(tǒng)限制,即使自己設(shè)置一個(gè)很大的值,系統(tǒng)也會(huì)自動(dòng)調(diào)整為 min{自己設(shè)定的值,系統(tǒng)設(shè)定的默認(rèn)最大值}。
@property NSInteger maxConcurrentOperationCount;
  • 取消操作
- (void)cancelAllOperations;

NSOperationQueue 就相當(dāng)于管道,Operation以FIFO的形式通過(guò)管道,maxConcurrentOperationCount 就是管道數(shù)量。下面的demo就是限制總管道數(shù)量為1,也就是所有的Operation必須以FIFO形式通過(guò)管道,也就是串行。

- (void)demo {
    NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"operation1-%@",[NSThread currentThread]);
    }];

    NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"operation2-%@",[NSThread currentThread]);
    }];
    operation2.completionBlock = ^{NSLog(@"operation2-completionBlock-%@",[NSThread currentThread]);};
    
    NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"operation3-%@",[NSThread currentThread]);
    }];

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    queue.maxConcurrentOperationCount = 1;
    [queue addOperation:operation1];
    [queue addOperation:operation2];
    [queue addOperation:operation3];
    [queue waitUntilAllOperationsAreFinished];
}

執(zhí)行結(jié)果:從結(jié)果可以看出,maxConcurrentOperationCount = 1 相當(dāng)于在addOperation的時(shí)候就設(shè)置了操作之間的依賴。

operation1-<NSThread: 0x604000270100>{number = 3, name = (null)}
operation2-<NSThread: 0x604000270540>{number = 4, name = (null)}
operation2-completionBlock-<NSThread: 0x604000270100>{number = 3, name = (null)}
operation3-<NSThread: 0x604000270540>{number = 4, name = (null)}

設(shè)置queue.maxConcurrentOperationCount = 2;的執(zhí)行結(jié)果

operation2-<NSThread: 0x60400027be80>{number = 4, name = (null)}
operation1-<NSThread: 0x600000474140>{number = 3, name = (null)}
operation3-<NSThread: 0x60400027be80>{number = 4, name = (null)}
operation2-completionBlock-<NSThread: 0x600000474340>{number = 5, name = (null)}
  • 添加Operation的三種方式
- (void)addOperation:(NSOperation *)op;
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
- (void)addOperationWithBlock:(void (^)(void))block API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
  • 簡(jiǎn)單的自定義Operation
@interface SAMOperation : NSOperation
@end
@implementation SAMOperation
-(void)main{
    @autoreleasepool{
        @try {
            if (![self isCancelled]) {
                NSLog(@"Begin");
                [NSThread sleepForTimeInterval:1.0];
                NSLog(@"End");
            }
        } @catch (NSException *exception) {
        } @finally {
        }
    }
}
@end

使用:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"begin func");
    SAMOperation *op1 = [SAMOperation new];
    NSOperationQueue *queue = [NSOperationQueue new];
    [queue addOperation:op1];
    //[queue waitUntilAllOperationsAreFinished];
    NSLog(@"end func");
}

打印

begin func
end func
Begin
End

//把[queue waitUntilAllOperationsAreFinished];注釋打開(kāi)的打印
begin func
Begin
End
end func

線程安全和 線程同步

@interface ViewController ()
@property (nonatomic, assign) NSInteger ticketSurplusCount;
@property (nonatomic, strong)NSLock *lock;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.ticketSurplusCount = 50;
    self.lock = [[NSLock alloc] init];
    
    [self initTicketStatusNotSave];
}

/**
 * 非線程安全:不使用 semaphore
 * 初始化火車票數(shù)量、賣票窗口(非線程安全)、并開(kāi)始賣票
 */
- (void)initTicketStatusNotSave {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印當(dāng)前線程
    NSLog(@"semaphore---begin");
    
    self.ticketSurplusCount = 50;
    
    __weak typeof(self) weakSelf = self;
    
    // queue1 代表北京火車票售賣窗口
    NSOperationQueue *queueO1 = [[NSOperationQueue alloc]init];
    queueO1.maxConcurrentOperationCount = 1;
    
    [queueO1 addOperationWithBlock:^{
        [weakSelf saleTicketNotSafe:@"北京"];
    }];
    // queue2 代表上海火車票售賣窗口
    NSOperationQueue *queueO2 = [[NSOperationQueue alloc]init];
    queueO2.maxConcurrentOperationCount = 1;
    [queueO2 addOperationWithBlock:^{
        [weakSelf saleTicketNotSafe:@"上海"];
    }];
    
    // queue2 代表上?;疖嚻笔圪u窗口
    NSOperationQueue *queueO3 = [[NSOperationQueue alloc]init];
    queueO3.maxConcurrentOperationCount = 1;
    [queueO3 addOperationWithBlock:^{
        [weakSelf saleTicketNotSafe:@"深圳"];
    }];
}


/**
 * 售賣火車票(非線程安全)
 */
- (void)saleTicketNotSafe:(NSString*)who{
    while (1) {
        [self.lock lock];
        if (self.ticketSurplusCount > 0) {  //如果還有票,繼續(xù)售賣
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票數(shù):%ld 窗口:%@====%@", (long)self.ticketSurplusCount,who, [NSThread currentThread]]);
            //[NSThread sleepForTimeInterval:0.2];
            [self.lock unlock];
        } else { //如果已賣完,關(guān)閉售票窗口
            NSLog(@"所有火車票均已售完");
             [self.lock unlock];
            break;
        }
    }
}
@end
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容