将 NSOperation 子类化为并发且可取消

发布于 2024-09-26 05:58:37 字数 1343 浏览 8 评论 0原文

我无法找到有关如何将 NSOperation 子类化以实现并发并支持取消的良好文档。我阅读了苹果文档,但找不到“官方”示例。

这是我的源代码:

@synthesize isExecuting = _isExecuting;
@synthesize isFinished = _isFinished;
@synthesize isCancelled = _isCancelled;

- (BOOL)isConcurrent
{
    return YES;
}

- (void)start
{
/* WHY SHOULD I PUT THIS ?
    if (![NSThread isMainThread])
    {
        [self performSelectorOnMainThread:@selector(start) withObject:nil waitUntilDone:NO];
        return;
    }
*/

    [self willChangeValueForKey:@"isExecuting"];
    _isExecuting = YES;
    [self didChangeValueForKey:@"isExecuting"];


    if (_isCancelled == YES)
    {
        NSLog(@"** OPERATION CANCELED **");
    }
    else
    {
        NSLog(@"Operation started.");
        sleep(1);
        [self finish];
    }
}

- (void)finish
{
    NSLog(@"operationfinished.");

    [self willChangeValueForKey:@"isExecuting"];
    [self willChangeValueForKey:@"isFinished"];

    _isExecuting = NO;
    _isFinished = YES;

    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];

    if (_isCancelled == YES)
    {
        NSLog(@"** OPERATION CANCELED **");
    }
}

在我发现的示例中,我不明白为什么使用performSelectorOnMainThread:。它会阻止我的操作同时运行。

另外,当我注释掉该行时,我的操作会同时运行。但是,即使我已调用 cancelAllOperationsisCancelled 标志也不会被修改。

I am unable to find good documentation about how to subclass NSOperation to be concurrent and also to support cancellation. I read the Apple docs, but I am unable to find an "official" example.

Here is my source code :

@synthesize isExecuting = _isExecuting;
@synthesize isFinished = _isFinished;
@synthesize isCancelled = _isCancelled;

- (BOOL)isConcurrent
{
    return YES;
}

- (void)start
{
/* WHY SHOULD I PUT THIS ?
    if (![NSThread isMainThread])
    {
        [self performSelectorOnMainThread:@selector(start) withObject:nil waitUntilDone:NO];
        return;
    }
*/

    [self willChangeValueForKey:@"isExecuting"];
    _isExecuting = YES;
    [self didChangeValueForKey:@"isExecuting"];


    if (_isCancelled == YES)
    {
        NSLog(@"** OPERATION CANCELED **");
    }
    else
    {
        NSLog(@"Operation started.");
        sleep(1);
        [self finish];
    }
}

- (void)finish
{
    NSLog(@"operationfinished.");

    [self willChangeValueForKey:@"isExecuting"];
    [self willChangeValueForKey:@"isFinished"];

    _isExecuting = NO;
    _isFinished = YES;

    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];

    if (_isCancelled == YES)
    {
        NSLog(@"** OPERATION CANCELED **");
    }
}

In the example I found, I don't understand why performSelectorOnMainThread: is used. It would prevent my operation from running concurrently.

Also, when I comment out that line, I get my operations running concurrently. However, the isCancelled flag is not modified, even though I have called cancelAllOperations.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

活雷疯 2024-10-03 05:58:37

好的,据我了解,您有两个问题:

  1. 您是否需要代码注释中出现的 performSelectorOnMainThread: 段?该代码的作用是什么?

  2. 当您在包含此操作的 NSOperationQueue 上调用 cancelAllOperations 时,为什么 _isCancelled 标志未修改?

让我们按顺序处理这些。为了便于解释,我假设您的 NSOperation 子类名为 MyOperation。我会解释你的误解,然后给出一个正确的例子。

1. 同时运行 NSOperations

大多数时候,您会将 NSOperationNSOperationQueue 结合使用,从您的代码来看,这听起来像是您正在做的事情。在这种情况下,您的 MyOperation 将始终在后台线程上运行,无论 -(BOOL)isConcurrent 方法返回什么,因为 NSOperationQueue被明确设计为在后台运行操作。

因此,您通常不需要重写 -[NSOperation start] 方法,因为默认情况下它只是调用 -main 方法。这是您应该重写的方法。默认的 -start 方法已在适当的时间为您处理设置 isExecutingisFinished

因此,如果您希望 NSOperation 在后台运行,只需覆盖 -main 方法并将其放入 NSOperationQueue 中即可。

代码中的 performSelectorOnMainThread: 会导致 MyOperation 的每个实例始终在主线程上执行其任务。由于一个线程上一次只能运行一段代码,这意味着不能运行其他 MyOperationNSOperationNSOperationQueue 的全部目的是在后台执行某些操作。

唯一一次你想要将事情强制到主线程上是当你更新用户界面时。如果您需要在 MyOperation 完成时更新 UI,那么您应该使用 performSelectorOnMainThread:。我将在下面的示例中展示如何做到这一点。

2. 取消 NSOperation

-[NSOperationQueue cancelAllOperations] 会调用 -[NSOperation cancel] 方法,这会导致后续调用 -[NSOperation isCancelled]返回YES但是,您做了两件事使其无效。

  1. 您正在使用@synthesize isCancelled来覆盖NSOperation的-isCancelled方法。没有理由这样做。 NSOperation 已经以完全可接受的方式实现了 -isCancelled

  2. 您正在检查自己的_isCancelled实例变量以确定操作是否已被取消。 NSOperation 保证如果操作已被取消,[self isCancelled] 将返回 YES。它保证您的自定义 setter 方法将被调用,也不保证您自己的实例变量是最新的。您应该检查[self isCancelled]

您应该做什么

标题:

// MyOperation.h
@interface MyOperation : NSOperation {
}
@end

和实现:

// MyOperation.m
@implementation MyOperation

- (void)main {
    if ([self isCancelled]) {
        NSLog(@"** operation cancelled **");
    }

    // Do some work here
    NSLog(@"Working... working....")

    if ([self isCancelled]) {
        NSLog(@"** operation cancelled **");
    }
    // Do any clean-up work here...

    // If you need to update some UI when the operation is complete, do this:
    [self performSelectorOnMainThread:@selector(updateButton) withObject:nil waitUntilDone:NO];

    NSLog(@"Operation finished");
}

- (void)updateButton {
    // Update the button here
}
@end

请注意,您不需要对 isExecutingisCancelled 执行任何操作isFinished。这些都是自动为您处理的。只需重写 -main 方法即可。就是这么简单。

(注意:从技术上讲,这不是“并发”NSOperation,因为 -[MyOperation isConcurrent] 在实现时会返回 NO但是,它在后台线程上运行。isConcurrent 方法确实应该命名为-willCreateOwnThread,因为这是一个更准确的名称。该方法意图的描述。)

Okay, so as I understand it, you have two questions:

  1. Do you need the performSelectorOnMainThread: segment that appears in comments in your code? What does that code do?

  2. Why is the _isCancelled flag is not modified when you call cancelAllOperations on the NSOperationQueue that contains this operation?

Let's deal with these in order. I'm going to assume that your subclass of NSOperation is called MyOperation, just for ease of explanation. I'll explain what you're misunderstanding and then give a corrected example.

1. Running NSOperations concurrently

Most of the time, you'll use NSOperations with an NSOperationQueue, and from your code, it sounds like that's what you're doing. In that case, your MyOperation will always be run on a background thread, regardless of what the -(BOOL)isConcurrent method returns, since NSOperationQueues are explicitly designed to run operations in the background.

As such, you generally do not need to override the -[NSOperation start] method, since by default it simply invokes the -main method. That is the method you should be overriding. The default -start method already handles setting isExecuting and isFinished for you at the appropriate times.

So if you want an NSOperation to run in the background, simply override the -main method and put it on an NSOperationQueue.

The performSelectorOnMainThread: in your code would cause every instance of MyOperation to always perform its task on the main thread. Since only one piece of code can be running on a thread at a time, this means that no other MyOperations could be running. The whole purpose of NSOperation and NSOperationQueue is to do something in the background.

The only time you want to force things onto the main thread is when you're updating the user interface. If you need to update the UI when your MyOperation finishes, that is when you should use performSelectorOnMainThread:. I'll show how to do that in my example below.

2. Cancelling an NSOperation

-[NSOperationQueue cancelAllOperations] calls the -[NSOperation cancel] method, which causes subsequent calls to -[NSOperation isCancelled] to return YES. However, you have done two things to make this ineffective.

  1. You are using @synthesize isCancelled to override NSOperation's -isCancelled method. There is no reason to do this. NSOperation already implements -isCancelled in a perfectly acceptable manner.

  2. You are checking your own _isCancelled instance variable to determine whether the operation has been cancelled. NSOperation guarantees that [self isCancelled] will return YES if the operation has been cancelled. It does not guarantee that your custom setter method will be called, nor that your own instance variable is up to date. You should be checking [self isCancelled]

What you should be doing

The header:

// MyOperation.h
@interface MyOperation : NSOperation {
}
@end

And the implementation:

// MyOperation.m
@implementation MyOperation

- (void)main {
    if ([self isCancelled]) {
        NSLog(@"** operation cancelled **");
    }

    // Do some work here
    NSLog(@"Working... working....")

    if ([self isCancelled]) {
        NSLog(@"** operation cancelled **");
    }
    // Do any clean-up work here...

    // If you need to update some UI when the operation is complete, do this:
    [self performSelectorOnMainThread:@selector(updateButton) withObject:nil waitUntilDone:NO];

    NSLog(@"Operation finished");
}

- (void)updateButton {
    // Update the button here
}
@end

Note that you do not need to do anything with isExecuting, isCancelled, or isFinished. Those are all handled automatically for you. Simply override the -main method. It's that easy.

(A note: technically, this is not a "concurrent" NSOperation, in the sense that -[MyOperation isConcurrent] would return NO as implemented above. However, it will be run on a background thread. The isConcurrent method really should be named -willCreateOwnThread, as that is a more accurate description of the method's intention.)

☆獨立☆ 2024-10-03 05:58:37

@BJHomer 的出色答案值得更新。

并发操作应该重写 start 方法而不是 main

正如 Apple 文档中所述:

如果您正在创建并发操作,您至少需要重写以下方法和属性:

  • start
  • asynchronous
  • executing
  • finished

A正确的实现还需要覆盖cancel。使子类线程安全并获得正确的所需语义也相当棘手。

因此,我将一个完整且工作的子类作为 在代码审查中用 Swift 实现的提案。欢迎提出意见和建议。

该类可以轻松用作自定义操作类的基类。

The excellent answer of @BJHomer deserves an update.

Concurrent operations should override the start method instead of main.

As stated in the Apple Documentation:

If you are creating a concurrent operation, you need to override the following methods and properties at a minimum:

  • start
  • asynchronous
  • executing
  • finished

A proper implementation also requires to override cancel as well. Making a subclass thread-safe and getting the required semantics right is also quite tricky.

Thus, I've put a complete and working subclass as a proposal implemented in Swift in Code Review. Comments and suggestion are welcome.

This class can be easily used as a base class for your custom operation class.

摘星┃星的人 2024-10-03 05:58:37

我知道这是一个老问题,但我最近一直在调查这个问题,遇到了同样的例子,也有同样的疑问。

如果您的所有工作都可以在 main 方法内同步运行,则不需要并发操作,也不需要覆盖 start,只需完成您的工作并在完成后从 main 返回。

但是,如果您的工作负载本质上是异步的 - 即加载 NSURLConnection,则必须子类化 start。当您的 start 方法返回时,操作尚未完成。仅当您手动向 isFinished 和 isExecuting 标志发送 KVO 通知时(例如,一旦异步 URL 加载完成或失败),NSOperationQueue 才会认为已完成。

最后,当您要启动的异步工作负载需要在主线程上进行运行循环侦听时,您可能希望将启动分派到主线程。由于工作本身是异步的,因此它不会限制并发性,但在工作线程中启动工作可能没有准备好适当的运行循环。

I know this is an old question, but I've been investigating this lately and encountered the same examples and had the same doubts.

If all your work can be run synchronously inside the main method, you don't need a concurrent operation , neither overriding start, just do your work and return from main when done.

However, if your workload is asynchronous by nature - i.e loading a NSURLConnection, you must subclass start. When your start method returns, the operation isn't finished yet. It will only be considered finished by the NSOperationQueue when you manually send KVO notifications to the isFinished and isExecuting flags (for instance, once the async URL loading finishes or fails).

Lastly, one might want to dispatch start to the main thread when the async workload you want to start require a run-loop listening on the main thread. As the work itself is asynchronous, it won't limit your concurrency, but starting the work in a worker thread might not have a proper runloop ready.

怼怹恏 2024-10-03 05:58:37

看一下 ASIHTTPRequest。它是一个 HTTP 包装类,作为子类构建在 NSOperation 之上,并且似乎实现了这些。请注意,截至 2011 年中期,开发人员建议不要在新项目中使用 ASI。

Take a look at ASIHTTPRequest. It is an HTTP wrapper class built on top of NSOperation as a subclass and seems to implement these. Note that as of mid-2011, the developer recommends not using ASI for new projects.

Bonjour°[大白 2024-10-03 05:58:37

关于在 NSOperation 子类中定义“cancelled”属性(或定义“_cancelled”iVAR),通常这是不必要的。仅仅是因为当 USER 触发取消时,自定义代码应始终通知 KVO 观察者您的操作现在已完成。换句话说,isCancelled =>

特别是,当 NSOperation 对象依赖于其他操作对象的完成时,它会监视这些对象的 isFinished 键路径。因此,未能生成完成通知(发生取消情况)可能会阻止应用程序中其他操作的执行。


顺便说一句,@BJ Homer 的回答:“isConcurrent 方法确实应该命名为 -willCreateOwnThread”,很有道理

因为如果你不重写start-method,只需手动调用NSOperation-Object的default-start-method,调用线程本身默认是同步的;所以,NSOperation-Object只是一个非并发操作。

但是,如果您确实重写了启动方法,在启动方法实现中,自定义代码应该生成一个单独的线程...等等,那么您就成功地打破了“调用线程默认是同步的”的限制,因此,使 NSOperation-Object 成为并发操作,之后可以异步运行。

Regarding define "cancelled" property (or define "_cancelled" iVAR) inside NSOperation subclass, normally that is NOT necessary. Simply because when USER triggers the cancellation, custom code should always notify KVO observers that your operation is now finished with its work. In other words, isCancelled => isFinished.

Particularly, when NSOperation object is dependent on the completion of other operation objects, it monitors the isFinished key path for those objects. Failing to generate a finish notification (in case of cancellation happens) can therefore prevent the execution of other operations in your application.


BTW, @BJ Homer's answer: "The isConcurrent method really should be named -willCreateOwnThread" makes A LOT sense!

Because if you do NOT override start-method,simply manually call NSOperation-Object's default-start-method, calling-thread itself is, by default, synchronous; so, NSOperation-Object is only a non-concurrent operation.

However, if you DO override start-method, inside start-method implementation, custom-code should spawn a separate thread…etc,then you successfully break the restriction of "calling-thread default being synchronous", therefore making NSOperation-Object becoming a concurrent-operation, it can run asynchronously afterwards.

听闻余生 2024-10-03 05:58:37

此博客文章:

http://www.dribin.org/ dave/blog/archives/2009/09/13/snowy_concurrent_operations/

解释了为什么您可能需要:

if (![NSThread isMainThread])
{
    [self performSelectorOnMainThread:@selector(start) withObject:nil waitUntilDone:NO];
    return;
}

在您的 start 方法中。

This blog post:

http://www.dribin.org/dave/blog/archives/2009/09/13/snowy_concurrent_operations/

explains why you might need:

if (![NSThread isMainThread])
{
    [self performSelectorOnMainThread:@selector(start) withObject:nil waitUntilDone:NO];
    return;
}

in your start method.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文