线程安全和“收集在被枚举时被突变”

时间:2013-05-21 18:58:07

标签: ios objective-c multithreading performance crash

我有一个“Ad”类,我在Ad.m中设置了一个静态变量:

static NSDictionary *citiesDict = nil; // class variable 

并且在同一个文件中我实现了一个类方法,它基本上加载了城市名称的plist文件及其索引号(如果尚未加载),最后将作为参数传递的数值转换为城市名称:

+(NSString *) cityFromNumberValue:(NSString *)cityNumberValue
{   
    // load the cities from the plist file named "cities" if it's not already loaded
    if (!citiesDict)
    {  
        NSString *path = [[NSBundle mainBundle] pathForResource:@"cities" ofType:@"plist"];

       citiesDict = [[NSDictionary alloc ]initWithContentsOfFile:path];
       NSLog(@"loading plist");

    }

NSLog(@"will return value");
NSArray *temp = [ citiesDict allKeysForObject:cityNumberValue];  
NSString *key = [temp lastObject]; 
return key ;
}

并且总是在同一个文件中我实现了一个init方法将Dictionary转换为Ad对象,其中它使用Class方法+ cityFromNumberValue:cityNumberValue:

-(id) initWithDictionary: (NSDictionary *) dictionay
{
    self = [super init];
    if (self)
    {
    // convert the number to name of city
    self.departureCity= [Ad cityFromNumberValue:[dictionay objectForKey:@"ad_villedepart"]]; 
    self.arrivalCity=   [Ad cityFromNumberValue:[dictionay objectForKey:@"ad_villearrivee"]]; 
    }
return self;
}

我在同一个文件中也有一个从Web服务获取广告的方法,它调用方法+ cityFromNumberValue:cityNumberValue:在for循环中:

+(NSDictionary *) fetchAdOfPage : (NSInteger) page PerPage : (NSInteger) perPage
{
    NSMutableArray *  adsArray = [[NSMutableArray alloc] init];
   ...... 
    NSMutableArray *array = [NSArray arrayWithContentsOfURL:url];

// convert the new fetchted dictionnaries of Ads to an Ad objects

for (NSMutableDictionary *dictAd in array) 
{ 
  // convertion using the designated initializer

    Ad *ad = [[Ad alloc]initWithDictionary:dictAd]; 
    [adsArray addObject:ad]; 

    }
      ....
    return dictFinal ;
}

以及来自mu控制器的其他地方我称之为这样的获取方法:

 // do request on async thread
        dispatch_queue_t fetchQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_async(fetchQ, ^{

            NSDictionary * dict = [Ad fetchAdOfPage:currentPage PerPage:kAdsPerPage];

            dispatch_sync(dispatch_get_main_queue(), ^{
                .....
            });
        });

        dispatch_release(fetchQ);

现在我的问题是,上次我在模拟器中运行应用程序时出现错误“收集在枚举时发生变异”并且它指向该行:

 NSArray *temp = [ citiesDict allKeysForObject:cityNumberValue];

但我第一次遇到这个错误,我之前没有得到它,我使用相同的代码超过三个月,一切都很好,我甚至无法再次重现同样的错误!在放置一个NSLog后看看发生了什么我总是得到:

2013-05-21 19:39:20.141 myApp[744:3b03] loading plist
2013-05-21 19:39:20.151 myApp[744:6303] will return value
2013-05-21 19:39:20.151 myApp[744:3b03] will return value
2013-05-21 19:39:20.152 myApp[744:6303] will return value
2013-05-21 19:39:20.153 myApp[744:6303] will return value
2013-05-21 19:39:20.154 myApp[744:6303] will return value
2013-05-21 19:39:20.153 myApp[744:3b03] will return value
2013-05-21 19:39:20.155 myApp[744:6303] will return value

但有一次我得到了:

2013-05-21 19:39:20.141 myApp[744:3b03] loading plist
2013-05-21 19:39:20.142 myApp[744:6303] loading plist
2013-05-21 19:39:20.151 myApp[744:6303] will return value
2013-05-21 19:39:20.151 myApp[744:3b03] will return value
2013-05-21 19:39:20.152 myApp[744:6303] will return value
2013-05-21 19:39:20.153 myApp[744:6303] will return value
2013-05-21 19:39:20.154 myApp[744:6303] will return value
2013-05-21 19:39:20.153 myApp[744:3b03] will return value
2013-05-21 19:39:20.155 myApp[744:6303] will return value

应用程序没有崩溃,但是因为我使用if语句检查了,所以有两次奇怪的“加载plist”!所以我猜有两个线程已进入:

if (!citiesDict)

同时,然后他们两个都设置了citiesDict字典,因为这个字典可以用在

 [ citiesDict allKeysForObject:cityNumberValue]; 

就在可能导致崩溃的if语句之后“集合在被枚举时被突变”,这可能是真正的Senario吗?

因为我无法再次重现错误我想知道如果添加:

   @synchronized(citiesDict)
    {
        citiesDict = [[NSDictionary alloc ]initWithContentsOfFile:path];
        NSLog(@"loading plist");
    }

可以解决这个问题吗?你有任何建议,以更好地实现更安全的实现,以及当我们不得不从不同的线程使用相同的数组时,我们如何避免“收集在被枚举时突变”错误,并且只能从中读取数组的内容不同的线程会导致问题或问题是在同时写入时?提前感谢您的帮助

3 个答案:

答案 0 :(得分:2)

@synchronized(citiesDist)不是最好的,但应该解决你的问题...无论如何,在我看来,在线程安全环境中初始化一个变量的最佳方法就是以这种方式使用GCD:

static dispatch_once_t once;
dispatch_once(&once, ^{
    citiesDict = [[NSDictionary alloc ]initWithContentsOfFile:path];
    NSLog(@"loading plist");
});

答案 1 :(得分:2)

使用静态对象时,最佳做法是使用dispatch_once,这将确保代码只执行一次,为您提供完整的线程安全性

为静态对象设置getter方法也是一种很好的做法

+ (NSDictionary *)getCitiesDict {
    static dispatch_once_t pred;
    static NSDictionary *citiesDict = nil;

    dispatch_once(&pred, ^{
        NSString *path = [[NSBundle mainBundle] pathForResource:@"cities" ofType:@"plist"];

        citiesDict = [[NSDictionary alloc ]initWithContentsOfFile:path];
    });

    return citiesDict;
}

要访问它,就这样去

[[OwnerClass getCitiesDict] valueForKey:@"key"]; // OwnerClass is the name of the class where this is defined

答案 2 :(得分:1)

你走在正确的轨道上。为了在同一个方法中实现该数组的线程安全加载,请同步它并在其中进行空检查:

@synchronized(citiesDict)
{
    if (!citiesDict)
    {
        citiesDict = [[NSDictionary alloc] initWithContentsOfFile:path];
        NSLog(@"loading plist");
    }
}

将上面的代码放入您在读取数组之前调用的函数中。基本上你想避免在加载时读取数组。