문제

The documentation on NSSet objectEnumeration says:

When this method is used with mutable subclasses of NSSet, your code shouldn’t modify the set during enumeration. If you intend to modify the set, use the allObjects method to create a “snapshot” of the set’s members. Enumerate the snapshot, but make your modifications to the original set.

Now my question is: Is the allObjects method itself thread safe?

I have implemented an operation set like so:

@interface OperationSet : NSObject
@end
@implementation OperationSet
{
    NSMutableSet *_set;
}
- (instancetype)init
{
    self = [super init];
    if (self)
    {
        _set = [[NSMutableSet alloc] init];
    }
    return self;
}
- (void)addOperation:(Operation *)operation
{
    if (operation)
    {
        [_set addObject:operation];
    }
}
- (void)removeOperation:(Operation *)operation
{
    if (operation)
    {
        [_set removeObject:operation];
    }
}
- (void)removeAllOperations
{
    [_set removeAllObjects];
}
- (void)enumerateWithOperationBlock:(OperationBlock)block
{
    NSArray *allObjects = [_set allObjects];
    [allObjects enumerateObjectsUsingBlock:^(Operation *o, NSUInteger idx, BOOL *stop) {
        block(o);
    }];
}
- (void)flushCompletedOperations
{
    NSArray *allObjects = [_set allObjects];
    NSSet *safeSet = [NSSet setWithArray:allObjects];
    NSSet *completed = [safeSet objectsPassingTest:^BOOL(Operation *o, BOOL *stop){
        return o.completed;
    }];
    [_set minusSet:completed];
}
- (NSUInteger)count
{
    return [_set count];
}
- (BOOL)any:(OperationAnyBlock)block
{
    NSArray *allObjects = [_set allObjects];
    NSUInteger index = [allObjects indexOfObjectPassingTest:^BOOL(Operation *o, NSUInteger idx, BOOL *stop) {
        return block(o);
    }];
    return (index != NSNotFound);
}
- (Operation *)getOperationWithMatchingData:(NSDictionary *)data
{
    NSArray *allObjects = [_set allObjects];
    NSUInteger index = [allObjects indexOfObjectPassingTest:^BOOL(Operation *o, NSUInteger idx, BOOL *stop) {
        return [o matchesData:data];
    }];
    return (index == NSNotFound ? nil : allObjects[index]);
}
@end

This all works fine. But I have got a crash via Crashlytics, which is rare (two out of hundreds), but is there:

EXC_BAD_ACCESS KERN_INVALID_ADDRESS at 0x0000000000000008
Thread : Crashed: com.apple.main-thread
0  CoreFoundation                 0x000000018772c438 -[__NSSetM addObject:] + 448
1  CoreFoundation                 0x000000018772c430 -[__NSSetM addObject:] + 440

The OperationSet is accessed from multiple threads.

Any help is greatly appreciated.

EDIT

Thanks dasblinkenlight for enlighting the allObjects usage. I have edited my implementation like so:

@interface OperationSet : NSObject
@end
@implementation OperationSet
{
    NSMutableSet *_set;
    dispatch_queue_t _queue;
}
- (instancetype)init
{
    self = [super init];
    if (self)
    {
        _set = [[NSMutableSet alloc] init];
        _queue = dispatch_queue_create("OperationQueue", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}
- (void)addOperation:(Operation *)operation
{
    if (operation)
    {
        dispatch_async(_queue, ^{
            [_set addObject:operation];
        });
    }
}
- (void)removeOperation:(Operation *)operation
{
    if (operation)
    {
        dispatch_async(_queue, ^{
            [_set removeObject:operation];
        });
    }
}
- (void)removeAllOperations
{
    dispatch_async(_queue, ^{
        [_set removeAllObjects];
    });
}
- (void)enumerateWithOperationBlock:(OperationBlock)block
{
    __block NSArray *allObjects;
    dispatch_sync(_queue, ^{
        allObjects = [_set allObjects];
    });
    [allObjects enumerateObjectsUsingBlock:^(Operation *o, NSUInteger idx, BOOL *stop) {
        block(o);
    }];
}
- (void)flushCompletedOperations
{
    __block NSArray *allObjects;
    dispatch_sync(_queue, ^{
        allObjects = [_set allObjects];
    });
    NSSet *safeSet = [NSSet setWithArray:allObjects];
    NSSet *completed = [safeSet objectsPassingTest:^BOOL(Operation *o, BOOL *stop){
        return o.completed;
    }];
    [_set minusSet:completed];
}
- (NSUInteger)count
{
    return [_set count];
}
- (BOOL)any:(OperationAnyBlock)block
{
    __block NSArray *allObjects;
    dispatch_sync(_queue, ^{
        allObjects = [_set allObjects];
    });
    NSUInteger index = [allObjects indexOfObjectPassingTest:^BOOL(Operation *o, NSUInteger idx, BOOL *stop) {
        return block(o);
    }];
    return (index != NSNotFound);
}
- (Operation *)getOperationWithMatchingData:(NSDictionary *)data
{
    __block NSArray *allObjects;
    dispatch_sync(_queue, ^{
        allObjects = [_set allObjects];
    });
    NSUInteger index = [allObjects indexOfObjectPassingTest:^BOOL(Operation *o, NSUInteger idx, BOOL *stop) {
        return [o matchesData:data];
    }];
    return (index == NSNotFound ? nil : allObjects[index]);
}
@end

The code works! Which is a good sign, but can you please review it?

And there is another question: Is there any difference in using allObjects versus making a set copy?

That is using this code:

- (void)enumerateWithOperationBlock:(OperationBlock)block
{
    __block NSArray *allObjects;
    dispatch_sync(_queue, ^{
        allObjects = [_set allObjects];
    });
    [allObjects enumerateObjectsUsingBlock:^(Operation *o, NSUInteger idx, BOOL *stop) {
        block(o);
    }];
}

over this code:

- (void)enumerateWithOperationBlock:(OperationBlock)block
{
    __block NSSet *safeSet;
    dispatch_sync(_queue, ^{
        safeSet = [_set copy];
    });
    [safeSet enumerateObjectsUsingBlock:^(Operation *o, BOOL *stop) {
        block(o);
    }];
}

Thanks for your help.

도움이 되었습니까?

해결책

NSMutableSet is not thread-safe. If you wish to access one from multiple threads, you must enforce one-at-a-time access yourself.

This is documented in “Thread Safety Summary” in the Threading Programming Guide.

The typical way to enforce one-at-a-time access is by creating one GCD queue (for each set) and accessing the set only from that queue (using dispatch_sync or, if possible, dispatch_async). In your example, you would add a dispatch_queue_t instance variable to your class, initialize it ininit, and use it in each of your other instance methods.

다른 팁

NSMutableSet is listed among the classes that are not thread-safe, so its methods should be considered non-thread safe unless explicitly documented otherwise (none of the NSMutableSet methods are documented as thread-safe at this time).

I think that by

use the allObjects method to create a “snapshot”

they meant creating a snapshot behind a lock, to avoid holding a lock on the entire set during the entire time that it takes you to enumerate its objects, and perform operations on them.

Your other question: [mySet allObjects] returns an NSArray containing all the objects in the set, while [mySet copy] returns an NSSet. If you don't need the properties of a set (very fast test for membership), an NSArray will probably be a bit quicker.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top