NSOperation和Singleton:正确的并发设计

3
我需要向大家请教一下我的应用程序设计,基本上我想知道它是否能按照我的预期工作?由于多线程是一个比较棘手的问题,我想听听你们的意见。
基本上,我的任务非常简单 - 我有一个名为SomeBigSingletonClass的大型单例类,它有两个方法someMethodOne和someMethodTwo。这些方法应该定期(基于定时器)在不同的线程中调用。但是,在任何时候都只应该有一个实例的每个线程,例如,每次只应该运行一个someMethodOne方法,对于someMethodTwo也是如此。
我尝试过使用GCD进行实现,但它缺少非常重要的功能,即它不能提供检查当前是否有正在运行的任务的手段,例如,我无法检查是否只有一个running instance of let say someMethodOne方法。
NSThread提供了良好的功能,但我相信像NSOperation和GCD这样的新高级技术将使维护我的代码更加简单。所以我决定放弃NSThread。
我的解决方案是使用NSOperation: 我计划如何实现这两个线程调用。
@implementation SomeBigSingletonClass

- (id)init
{
    ...
    // queue is an iVar
    queue = [[NSOperationQueue alloc] init];

    // As I'll have maximum two running threads 
    [queue setMaxConcurrentOperationCount:2];
    ...
}

+ (SomeBigSingletonClass *)sharedInstance
{
    static SomeBigSingletonClass *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[SomeBigSingletonClass alloc] init];
    });
    return sharedInstance;
}

- (void)someMethodOne
{
    SomeMethodOneOperation *one = [[SomeMethodOneOperation alloc] init];
    [queue addOperation:one];
}

- (void)someMethodTwo
{
    SomeMethodTwoOperation *two = [[SomeMethodOneOperation alloc] init];
    [queue addOperation:two];
}
@end 

最后,我的NSOperation继承类将如下所示。
@implementation SomeMethodOneOperation

- (id)init
{
    if (![super init]) return nil;
    return self;
}

- (void)main {
    // Check if the operation is not running
    if (![self isExecuting]) {
        [[SomeBigSingletonClass sharedInstance] doMethodOneStuff];
    }
}

@end

同样适用于 SomeMethodTwoOperation 操作类。

@Priya 代码是最新的。请跟进讨论,希望你能找到你要找的东西。 - deimus
@Priya 抱歉,不行。如果您有特定的问题,请随时开启另一个问题线程。 - deimus
@deimus....好的,谢谢。 - Priya
2个回答

2
如果您正在使用NSOperation,您可以通过创建自己的NSOperationQueue并将numberOfConcurrentOperations设置为1来实现您想要的效果。
您也可以使用您的类作为锁对象,在@synchronized范围内执行操作。
编辑:澄清---
我提出的是:
队列A(1个并发操作--用于一次执行SomeMethodOneOperationSomeMethodTwoOperation
队列B(n个并发操作--用于一般后台操作执行)
编辑2:更新代码,说明如何运行最大操作一和操作二,每次只执行一个操作一和操作二中的最多一个。
-(void)enqueueMethodOne
{
    static NSOperationQueue * methodOneQueue = nil ;
    static dispatch_once_t onceToken ;    
    dispatch_once(&onceToken, ^{
        queue = [ [ NSOperationQueue alloc ] init ] ;
        queue = 1 ;
    });

    [ queue addOperation:[ NSBlockOperation blockOperationWithBlock:^{
        ... do method one ...
    } ] ];
}

-(void)enqueueMethodTwo
{
    static NSOperationQueue * queue = nil ;
    static dispatch_once_t onceToken ;    
    dispatch_once(&onceToken, ^{
        queue = [ [ NSOperationQueue alloc ] init ] ;
        queue = 1 ;
    });

    [ queue addOperation:[ NSBlockOperation blockOperationWithBlock:^{
        ... do method two ...
    } ] ];
}

编辑3:

根据我们的讨论:

我指出isExecuting是一个成员变量,仅指正在查询的操作的状态,而不是该类的任何实例是否正在执行

因此,Deimus的解决方案不能保持多个操作实例同时运行


刚刚检查过了,可以在操作的“main”中使用“isExecuting”,因为它会在操作被推入队列时运行。 - deimus
1
你可以在主函数中检查isExecuting,但我不明白这如何帮助你决定是否安全运行,因为你正在创建单独的操作对象。 - nielsbot
从苹果的指南中 - main:(可选)此方法通常用于实现与操作对象相关联的任务。尽管您可以在start方法中执行任务,但使用此方法实现任务可以使设置和任务代码更清晰地分离。 - deimus
看来我需要将我的 main 函数改成 start 函数? - deimus
1
好吧,我可以告诉你,你的解决方案不会起作用。每次想要运行任务一或任务二时,你需要创建一个新的操作对象--在你的代码中isExecuting将始终返回YES - nielsbot
显示剩余14条评论

0

抱歉,我来晚了。如果你的方法是基于定时器调用的,并且想要它们在彼此之间并发执行,但是自身是同步的,那我建议使用GCD定时器。

基本上,你有两个定时器,一个执行methodOne方法,另一个执行methodTwo方法。由于你将块传递给GCD定时器,因此你甚至不必使用方法,特别是如果你想确保其他代码在它们不应该运行时不会调用那些方法。

如果你将定时器安排到并发队列中,则两个定时器可能正在不同的线程上同时运行。但是,定时器本身只会在其计划运行时运行。这是我刚刚编写的一个示例... 你可以很容易地将其与单例一起使用...

首先,一个辅助函数用于创建一个带有块的定时器,在定时器触发时调用该块。该块传递对象,因此可以通过块引用它而不创建保留周期。如果我们将self作为参数名称,块中的代码看起来就像其他代码一样...

static dispatch_source_t setupTimer(Foo *fooIn, NSTimeInterval timeout, void (^block)(Foo * self)) {
    // Create a timer that uses the default concurrent queue.
    // Thus, we can create multiple timers that can run concurrently.
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    uint64_t timeoutNanoSeconds = timeout * NSEC_PER_SEC;
    dispatch_source_set_timer(timer,
                              dispatch_time(DISPATCH_TIME_NOW, timeoutNanoSeconds),
                              timeoutNanoSeconds,
                              0);
    // Prevent reference cycle
    __weak Foo *weakFoo = fooIn;
    dispatch_source_set_event_handler(timer, ^{
        // It is possible that the timer is running in another thread while Foo is being
        // destroyed, so make sure it is still there.
        Foo *strongFoo = weakFoo;
        if (strongFoo) block(strongFoo);
    });
    return timer;
}

现在讲解基本类的实现。如果您不想暴露 methodOne 和 methodTwo,那么就没有必要创建它们,尤其是它们比较简单的情况下,可以直接将代码放在块中。
@implementation Foo {
    dispatch_source_t timer1_;
    dispatch_source_t timer2_;
}

- (void)methodOne {
    NSLog(@"methodOne");
}

- (void)methodTwo {
    NSLog(@"methodTwo");
}

- (id)initWithTimeout1:(NSTimeInterval)timeout1 timeout2:(NSTimeInterval)timeout2 {
    if (self = [super init]) {
        timer1_ = setupTimer(self, timeout1, ^(Foo *self) {
            // Do "methodOne" work in this block... or call it.
            [self methodOne];
        });
        timer2_ = setupTimer(self, timeout2, ^(Foo *self) {
            // Do "methodOne" work in this block... or call it.
            [self methodTwo];
        });
        dispatch_resume(timer1_);
        dispatch_resume(timer2_);
    }
    return self;
}

- (void)dealloc {
    dispatch_source_cancel(timer2_);
    dispatch_release(timer2_);
    dispatch_source_cancel(timer1_);
    dispatch_release(timer1_);
}
@end

编辑 针对评论(更详细的解释为什么该块不会同时执行,以及为什么错过的计时器会合并成一个)。

您无需检查它是否被多次运行。直接从文档中获取...

调度源不可重入。在调度源暂停或事件处理程序块正在执行时收到的任何事件都将合并,并在调度源恢复或事件处理程序块返回后传递。

这意味着当GCD dispatch_source计时器块被分派时,它将不会再次分派,直到已经运行的计时器完成。您什么也不用做,库本身将确保不会同时执行该块。

如果该块所需时间超过计时器间隔,则“下一个”计时器调用将等待正在运行的计时器完成。此外,所有将要传递的事件都合并为一个单一事件。

您可以调用

unsigned numEventsFired = dispatch_source_get_data(timer);

从您的处理程序内获取自上次执行处理程序以来触发的事件数量(例如,如果您的处理程序通过4个计时器触发,则这将是4,但您仍将在此一个事件中获得所有这些触发 - 您不会为它们接收单独的事件)。

例如,假设您的间隔计时器为1秒,并且您的计时器需要5秒才能运行。该计时器直到当前块完成后才会再次触发。此外,所有这些计时器将合并成一个,因此您将获得一个对您的块的调用,而不是5个。

现在,说了这么多,我应该警告您可能存在的错误。现在,我很少在库代码中遇到错误,但这个错误是可以重复的,并且似乎违反了文档。因此,如果这不是一个错误,则是一个未记录的功能。但是,可以轻松地解决这个问题。

在使用计时器时,我注意到合并的计时器肯定会被合并。这意味着,如果您的计时器处理程序正在运行,并且在其运行时触发了5个计时器,则该块将立即被调用,表示那些错过的5个事件。然而,一旦完成了这个任务,该块将再次执行,仅执行一次,无论之前错过了多少计时器事件。

不过,很容易识别这些情况,因为dispatch_source_get_data(timer)将返回0,这意味着自上次调用该块以来没有计时器事件被触发。

因此,我已经习惯于将此代码添加为我的计时器处理程序的第一行...

if (dispatch_source_get_data(timer) == 0) return;

嗨@Jodi,感谢回复,基本上我已经按照这种方式做了,但是找不到你指定methodOne同步性的片段。 - deimus
我的意思是,您在哪里检查只有一个实例正在运行methodOne - deimus
@Jody Hagins...你能否在这里逐步分享那个示例代码的实现呢? - Priya

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接