Objective-C中的线程安全单例对象?

3
如何在Objective-C中创建一个线程安全的单例对象?例如,如果我有一个共享数据控制器作为单例对象,如果两个或多个线程同时访问它会发生什么?或者这些对象是否默认具有线程安全性?更新:在这种情况下,数据控制器属性内的对象是否决定其是否线程安全?例如,我的数据控制器具有NSMutableArray并且将其设置为非原子,则它将不是线程安全的。在这种情况下,它的值会发生什么?更新2:正如@synchronized(self)所做的那样?
2个回答

5
如果数据控制器不是线程安全的,则可能发生未定义行为--不惜一切代价避免它 =)
NSObjects默认情况下绝对不是线程安全的。使用原子属性并不能使一个类变得线程安全(因为这是一个常见的误解)。
典型的解决方案涉及确保所有可变状态都受到适当锁定的保护(例如互斥锁或@synchronized)。
当我说可变状态时,我指的是可以在外部或内部发生变化的对象。如果您不确定,请锁定以确保类型从多个线程中读取或写入。这必须在读取和写入时发生-始终如此。如果您有很多阅读,那么读写锁可能是更好的、更专业的锁。
要进行更详细的回答,您需要发布一些代码。
更新
请将其视为传递性。您的NSMutableArray、它所持有的对象以及所有对它们的外部引用必须以线程安全的方式使用,并且您必须跟踪所有这些内容。通常,您首先通过减少共享的可变状态来开始。而不是给客户端一个数组的引用,给他们数组中持有的元素的副本。同时,用锁保护所有读取、写入和元素复制。
为了简单起见,我将使用@synchronize进行演示:
@interface MONCookie : NSObject <NSCopying>

- (NSString *)name;

@end

@interface MONDataController : NSObject
{
@private
  NSMutableArray * cookies; // << MONCookie[]
}

- (void)addCookie:(MONCookie *)cookie;

- (MONCookie *)cookieWithName:(NSString *)name;

@end

@implementation MONDataController

- (id)init
{
  // no lock required here
  self = [super init];
  if (nil != self) {
    cookies = [NSMutableArray new];
  }
  return self;
}

- (void)dealloc
{
  // no lock required here
  [cookies release], cookies = nil;
  [super dealloc];
}

- (void)addCookie:(MONCookie *)cookie
{
  @synchronized(self) { // now accessing cookies - lock required
    [cookies addObject:cookie];
  }
}

- (MONCookie *)cookieWithName:(NSString *)name
{
  MONCookie * ret = nil;
  @synchronized(self) { // now accessing cookies - lock required
    for (MONCookie * at in cookies) {
      if ([at.name isEqualToString:name]) {
        ret = [at copy]; // << give them a copy if cookie is not threadsafe
      }
    }
  }
  return [ret autorelease];
}

@end

更新2

@synchronized设置了一个对象级别的锁。您可以将其视为递归(或可重入)锁,专属于您的实例。与其他锁定方法相比,它也非常慢。上面的代码使用了它,并且是线程安全的,并等同于持有递归锁,在@synchronized边界上进行锁定和解锁。

@interface MONCookie : NSObject <NSCopying>
{
@private
    NSRecursiveLock * lock;
}

@end

@implementation MONCookie

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

- (void)temperatureDidIncrease
{
    // ...  
}

- (void)bake
{
    // use the same lock for everything
    // do not mix @synchronized in some places, and use of the lock 
    // in others. what you use to protect the data must remain consistent
    //
    // These are equivalent approaches to protecting your data:

    {   // @synchronized:
        @synchronized(self) {
            [self temperatureDidIncrease];  
        }
    }

    {   // using the lock:
        [lock lock];
        [self temperatureDidIncrease];  
        [lock unlock];
    }
}

@end

我没有任何代码...我只是进入理论部分...您能指导我一些示例代码,展示如何使数据控制器线程安全吗? - Ankit Srivastava
该系统提供了pthread - 有许多示例可供参考。您还可以参考iOS和OS X示例以使用NSLock。pthread示例会更好,因为它们实际上会比示例代码更好地解释锁定和线程安全性。 - justin
还有一件事..你说“NSObjects默认情况下绝对不是线程安全的。使用原子属性并不能使一个类变得线程安全(因为这是一个常见的误解)。”那么非原子属性又是做什么用的呢..?或者默认情况下是原子的吗? - Ankit Srivastava
即使是单个属性也可能出现问题的一个简单例子是:if ([self.stringIvar isEqualToString:self.stringIvar])。这可能会在具有或不具有原子属性的情况下失败。对于更多的实例变量和更复杂的程序,这种情况就更加棘手了。 - justin
另一种方法是使用dispatch_sync在getter中提交读取和写入到并发GCD队列中,在setter中使用dispatch_barrier_async。在What's New in GCD中有一个示例。 - Jano
@Jano 你可以这样做...但我不认为这会比正确的锁定方法(针对该场景)更好。你是否与pthread_rwlock进行了比较? - justin

-5
我以前没有用过Objective C编程,但是从操作系统课程中了解到,你应该在单例对象中放置一个共享布尔变量,比如isLocked,每当一个线程尝试访问该类时,它应该执行以下操作。据我记得,这被称为严格交替:)好老的学校日子。
线程A:
if(!isLocked) {
isLocked = True;
Do the stuff
isLocked = False;
}

1
这不是正确的做法。检查锁是无用的。你需要先锁定,执行操作,然后再解锁。 - justin

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