【问题标题】:How do I use an NSRunLoop on an NSOperationQueue?如何在 NSOperationQueue 上使用 NSRunLoop?
【发布时间】:2016-04-01 19:14:57
【问题描述】:

我有一个通过蓝牙与 ExternalAccessory 通信的应用,响应有一些延迟,所以我希望 IO 发生在后台线程上。

我为单线程操作设置了一个 NSOperationQueue 以将我的请求排入队列:

self.sessionQueue = [NSOperationQueue new];
self.sessionQueue.maxConcurrentOperationCount = 1;

如果我安排从该队列读取和写入EAAccessory 流,我的应用程序会崩溃,因为如果队列正在使用的线程上没有NSRunLoop,来自套接字的数据将无法传递。初始化队列后,我立即创建了一个带有空 NSMachPort 的运行循环,以保持它运行并启动它:

[self.sessionQueue addOperationWithBlock:^{
    NSRunLoop* queueLoop = [NSRunLoop currentRunLoop];
    [queueLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
    [queueLoop run]; // <- this blocks
}];

这会阻塞队列,因为运行循环永远不会退出,但我不确定如何正确管理运行循环,以便我可以成功地从附属流中读取。

【问题讨论】:

  • "如果运行循环是因为您想要获得通知或类似类型的 API 承诺而运行,那么您需要小心。您不能只是消除该循环的运行并拥有事情继续进行。但同样不要将所有线程代码移动到 NSOperation(运行循环完好无损)并将其放入操作队列;让运行循环的 NSOperation 块不是对 NSOperationQueues 的明智使用。 -lists.apple.com/archives/cocoa-dev/2009/Sep/msg01145.html

标签: ios macos concurrency foundation nsrunloop


【解决方案1】:

您不应该尝试在NSOperation 中运行运行循环。 Grand Central Dispatch 拥有运行该操作的线程。您应该启动自己的线程并将其运行循环用于会话流。

However, you need to be aware that NSRunLoop is not generally thread safe, but CFRunLoop is. 这意味着当您想在会话处理线程上运行一个块时,您需要下拉到CFRunLoop 级别。

此外,获得对后台线程运行循环的引用的唯一方法是在该后台线程上运行某些东西。所以第一步是创建你自己的NSThread 子类来导出它自己的运行循环:

typedef void (^MyThreadStartCallback)(CFRunLoopRef runLoop);

@interface MyThread: NSThread

/// After I'm started, I dispatch to the main queue to call `callback`,
// passing my runloop. Then I destroy my reference to `callback`.
- (instancetype)initWithCallback:(MyThreadStartCallback)callback;

@end

@implementation MyThread {
    MyThreadStartCallback _callback;
}

- (instancetype)initWithCallback:(MyThreadStartCallback)callback {
    if (self = [super init]) {
        _callback = callback;
    }
    return self;
}

- (void)main {
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    dispatch_async(dispatch_get_main_queue(), ^{
        _callback(runLoop);
    });
    _callback = nil;
    CFRunLoopRun();
}

@end

现在您可以创建MyThread 的实例,并传入一个回调。当您启动MyThread 时,它将使回调在主线程上运行,并将其自己的(MyThread's)运行循环传递给回调。所以你可以使用MyThread 作为你的会话处理线程,像这样:

@implementation Thing {
    CFRunLoopRef _sessionRunLoop;
}

- (void)scheduleStreamsOfSession:(EASession *)session {
    MyThread *thread = [[MyThread alloc] initWithCallback:^(CFRunLoopRef runLoop) {
        // Here I'm on the main thread, but the session-handling thread has
        // started running and its run loop is `runLoop`.
        [self scheduleStreamsOfSession:session inRunLoop:runLoop];
    }];
    [thread start];
}

- (void)scheduleStreamsOfSession:(EASession *)session inRunLoop:(CFRunLoopRef)runLoop {

    // Here I'm on the main thread. I'll save away the session-handling run loop
    // so I can run more blocks on it later, perhaps to queue data for writing
    // to the output stream.
    _sessionRunLoop = runLoop;

    NSInputStream *inputStream = session.inputStream;
    NSOutputStream *outputStream = session.outputStream;

    // Here I'm on the main thread, where it's not safe to use the
    // session-handling thread's NSRunLoop, so I'll send a block to
    // the session-handling thread.
    CFRunLoopPerformBlock(runLoop, kCFRunLoopCommonModes, ^{

        // Here I'm on the session-handling thread, where it's safe to
        // use NSRunLoop to schedule the streams.
        NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
        [inputStream scheduleInRunLoop:currentRunLoop forMode:NSRunLoopCommonModes];
        [outputStream scheduleInRunLoop:currentRunLoop forMode:NSRunLoopCommonModes];

    });

    // CFRunLoopPerformBlock does **not** wake up the run loop. Since I want
    // to make sure the block runs as soon as possible, I have to wake up the
    // run loop manually:
    CFRunLoopWakeUp(_sessionRunLoop);
}

@end

【讨论】:

【解决方案2】:

如果需要,任何线程都可以为其创建一个NSRunLoop,任何Cocoa 或AppKit 应用程序的main 线程默认运行一个,并且任何辅助线程必须以编程方式运行它们。如果您生成NSThread,线程主体将负责启动NSRunLoop,但NSOperationQueue 创建它自己的一个或多个线程并将操作分派给它们。

当使用期望NSRunLoop 向后台线程(无论是您自己创建的还是libdispatch 创建的后台线程)传递事件的API 时,您有责任确保NSRunLoop 运行.通常你会想要运行循环,直到你的每个NSBlockOperation 任务都满足某些条件,我在NSRunLoop 上写了一个类别来简化这一点:

#import <Foundation/Foundation.h>

@interface NSRunLoop (Conditional)
-(BOOL)runWhileCondition:(BOOL *)condition inMode:(NSString *)mode inIntervals:(NSTimeInterval) quantum;
@end

#pragma mark -

@implementation NSRunLoop (Conditional)

-(BOOL)runWhileCondition:(BOOL *)condition inMode:(NSString *)mode inIntervals:(NSTimeInterval) quantum {
    BOOL didRun = NO;
    BOOL shouldRun = YES;
    NSPort *dummyPort = [NSMachPort port];
    [self addPort:dummyPort forMode:NSDefaultRunLoopMode];
    while (shouldRun) {
        @autoreleasepool {
            didRun = [self runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:quantum]];
            shouldRun = (didRun ? *condition : NO);
        }
    }
    [self removePort:dummyPort forMode:NSDefaultRunLoopMode];
    return didRun;
}

@end

有了这个条件,你可以安排一个NSBlockOperation,它将启动运行循环并一直运行到指定条件为NO

__block BOOL streamOperationInProgress = YES;
[self.sessionQueue addOperationWithBlock:^{
    NSRunLoop *queueLoop = [NSRunLoop currentRunLoop];
    NSStream *someStream = // from somewhere...
    [someStream setDelegate:self];
    [someStream scheduleInRunLoop:queueLoop forMode:NSDefaultRunLoopMode]:

    // the delegate implementation of stream:handleEvent:
    // sets streamOperationInProgress = NO;

    [queueLoop
        runWhileCondition:&streamOperationInProgress 
        inMode:NSDefaultRunLoopMode 
        inIntervals:0.001];
}];

上面例子中的问题是把 BOOL 放在某个地方,当操作完成时,委托人可以将其设置为NO

这是NSRunLoop+Condition 类别的要点。

【讨论】:

  • 这不是使用 100% CPU 吗?
  • 不,在 IO 出现之前它一直在一个 mach 事件处理程序中
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-08-30
  • 2017-02-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-22
相关资源
最近更新 更多