atomic, nonatomic在多线程下的表现

49 篇文章 1 订阅

对于atomic的属性,系统生成的set/get方法,atomic原子性不能保证多线程安全,只是能保证数据的完整性,完整性体现在:使用者总能取到完整的值。

而nonatomic则没有这个保证,所以nonatomic的速度会比atomic快。

//@property(retain) UITextField *userName;
//系统生成的代码如下:

// atomic的属性,ARC下的实现
- (UITextField *) userName {
    UITextField *retval = nil;
    @synchronized(self) {
        retval = [[userName retain] autorelease];
    }
    return retval;
}

- (void) setUserName:(UITextField *)userName_ {
    @synchronized(self) {
      [userName release];
      userName = [userName_ retain];
    }
}

// MRC下的atomic实现
@synthesize userName = _userName;
//get
- (UITextField *)userName {
	UITextField temp = nil;	
	@synchronized(self) {
		temp = [[_userName retain] autorelease];
	}
	return temp;
}
// set
- (void)setUserName:(UITextField *)userName {
	@synchronized(self) {
		if (_userName != userName){
			[_userName release];	
			_username = [userName retain];
		}
	}
}

MRC下的nonatomic实现
@property (nonatomic, strong) UIImage *icon; 
@synthesize icon = _icon;
// set
- (void)setIcon:(UIImage *)icon {
	if(_icon != icon){
        [icon retain];
		[_icon release];        // 这里会因为多线程同时执行而崩溃
		_icon = icon;
	}
}
// get
- (UIImage *)icon {
	return _icon;
}

 

--------------------

需要注意的是, 使用@property (nonatomic, copy) NSString *myname; 这种会自动生成get和set方法 

而一旦在下面去手动写了下面这个方法,则意味着, 不会自动生成get方法,而只是自动生成set方法, 并生成一个变量_myname; 

- (NSString *)myname {    return theMyname;  }

而一旦手动实现了 set方法,则自动生成的变量就没有了, 这个时候就需要去使用@synthesize myname = theName; // 告之系统,将会使用theName变量来进行存储。

其set 方法也得使用theIvar变量来使用, 如下
- (void)setMyname:(NSString *)myname {    theName = myname; }

同时, 可以再继续实现其get方法, 如下:

- (NSString *)myname {    return theName;   }

@dynamic myname;  // 告诉系统, 这个变量的set和get方法, 由用户自己实现;如果用户不实现,调用时,会出现崩溃。

--------------------

Atomic/nonatomic: 对系统生成的 getter/setter 方法进行加锁操作。这个锁仅仅保证getter和setter存取线程安全, 并不保证访问上的线程安全。 也就是说在访问上, 其对应的变量仍然可能被其它线程所销毁。 这个锁只保证在调用getter和setter时,没有其它线程来同时调用getter和setter.

写属性时,系统默认为atomic;

 

对于atomic的代码,苹果实现如下, 使用了锁机制如下: (之前认为atomic是自旋锁,@synchronized是互斥锁,可能有一个错误,正确答案目前不知, 目前应该是能解释所有的情况下了。 2019.3.27 heqin)

 

当设置的为atomic时, 系统属性的定义如下: 

struct property_t {

    const char *name;       //名字

    const char *attributes; //特性

};

运行时发现, 在编译时就把属性特性考虑进去了,setter方法直接调用的是_setProperty的atomic版本。 

查了一下苹果的源码, 如下:

static inline void reallySetProperty(id self, SEL _cmd, 
    id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) {
    //偏移为0说明改的是isa
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);//获取原值
    //根据特性拷贝
    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }
    //判断原子性
    if (!atomic) {
        //非原子直接赋值
        oldValue = *slot;
        *slot = newValue;
    } else {
        //原子操作使用自旋锁
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    // 取isa
    if (offset == 0) {
        return object_getClass(self);
    }

    // 非原子操作直接返回
    id *slot = (id*) ((char*)self + offset);
    if (!atomic) return *slot;
        
    // 原子操作自旋锁
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
    
    // 出于性能考虑,在锁之外autorelease
    return objc_autoreleaseReturnValue(value);
}

如上面所知,这里使用的是自旋锁,所以我们才一直对外说, atomic使用的是自旋锁机制。

大家都知道自旋锁的机制是一直查询,会导致占用资源较高。 

所以在iOS 10后, 苹果因为一个缺陷弃用了OSSpinLock, 改用了os_unfair_lock。 

具体的缺陷是: 新版OS中, 系统维护了5个不同的线程优先级(QoS),background, utility, default, user-initiated, user-interactive. 高优线和始终会在低优线程前执行, 一个线程不会受到比它更低优先级线程的干扰。 这种线程 调度算法会产生潜在的优先级反转问题,从而会破坏spin block.  

 

再继续看spinlock_t的实现

using spinlock_t = mutex_tt<LOCKDEBUG>;
using mutex_t = mutex_tt<LOCKDEBUG>;

class mutex_tt : nocopy_t {
    os_unfair_lock mLock; //处理了优先级的互斥锁
    void lock() {
        lockdebug_mutex_lock(this);
        os_unfair_lock_lock_with_options_inline
            (&mLock, OS_UNFAIR_LOCK_DATA_SYNCHRONIZATION);
    }
    void unlock() {
        lockdebug_mutex_unlock(this);
        os_unfair_lock_unlock_inline(&mLock);
    }
}

结果就发现了上面的代码,原来这个spinlock_t 已经换成了互斥锁机制。 所以网上才有人说,atomic在后台的实现是使用的@synchronized(self)方式去实现的。 

 

 

网上一般的说法如下也没有错: (综合上述分析,也就是说atomic是自旋锁ios10之前, 在ios10之后采用了互斥锁)

苹果改用了互斥锁实现,只是名称没有修改。 估计是为了修复优先级反转的问题,苹果只好放弃使用自旋锁,采用了优化了性能的os_unfair_lock(不公平锁,据说是采用随机抢占的方式,来获得资源)。 这两种锁运行效率差不多。 

 

下面再说说Atomic不是线程安全的。

@property (atomic, assign) NSInteger intSource;

在clicked事件中去执行下面代码
    dispatch_queue_t queue1 = dispatch_queue_create([@"que1" UTF8String], DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue2 = dispatch_queue_create([@"que2" UTF8String], DISPATCH_QUEUE_SERIAL);

    self.intSource = 0;
    
    dispatch_async(queue1, ^{
        for (int i = 0; i < 10000; i++) {
            	self.intSource = self.intSource + 1;			// 这一行的代码会分成两行执行。 int temp = self.intSource +1,  然后再是self.intSource = temp;
		//self.intSource++;		// 与上一行代码效果相同
        }
    });
    
    dispatch_async(queue2, ^{
        for (int i = 0; i < 10000; i++) {
            	self.intSource = self.intSource + 1;
		//self.intSource++;		// 效果相同
        }
    });

在其它按钮事件中,再去输出这个值。 

输出的结果并不会是20000, 而是可能为12660, 13007之类的数据, 而且每次执行结果不相同。解决办法是增加颗粒度,将两个操作合并为原子操作,从而解决写入过期数据的问题。

 

这是因为, atmoic修饰的intSource会在底层在使用时, 可能多个线程都执行到self.intSource=xxxx, 这时候, 会出现多个线程给这个值进行赋值操作。 从而导致赋值不一致的问题。 

举例来说, 一个线程正在设置值为intSource = 10000+1, 另一个线程也正好在设置intSource = 10000+1, 这就导致中间某些值被覆盖,所以计算的总值会小于20000;

 

这里,如果使用self.intSource++; 也是同样问题。 

self.intSource++会转化为

int temp = self.intSource + 1; // 这一行的temp变量存在线程不安全的问题。 所以解决方案就是增加锁的颗粒度

self.intSource = temp; // 下面是加锁的, 但上面这行int temp = self.intSource+1是没有加锁的。

 

 

Atomic不能保证对象多线程的安全,它只是能保证你访问的时候给你返回一个完好无损的Value而已。atomic:系统生成的 getter/setter 会保证 get、set 操作的完整性,不受其他线程影响。getter 还是能得到一个完好无损的对象(可以保证数据的完整性),但这个对象在多线程的情况下是不能确定的

使用atomic后, 会变成下面的代码,以保证在set和get时的数据完整性。

{lock}

    if (property != newValue) { 

        [property release]; 

         property = [newValue retain]; 

    }

{unlock}

 

用atomic修饰后,这个属性的setter、getter方法是线程安全的,但是对于整个对象来说不一定是线程安全的。

 

由于上面提到优先级反转。下面解释一下什么是优先级反转。

假设现在有三个线程, Thread 1(高优先级), Thread 2(中估先级), Thread3(低优先级)

T0时刻: Thread 3运行,获得同步资源source;

T1时刻:Thread 2运行,由于优于Thread 3, 所以Thread 3被迫停下(由于Thread 3被迫停下, 所以它使用的资源source也无法释放出来)。

T2时刻:Thread 1运行,由于优于Thread 2, 所以Thread 1运行起来, 但是Thread1需要使用同步资源source,而这正被Thread 3所持有, 所以Thread1只好挂起以等待资源source的释放。

T3时刻:此时Thread 2可调度运行。 Thread1, Thread3均无法调度运行。 

这种现象就优先级反转:

高优先级任务被低优先级任务阻塞,导致高优先级任务迟迟得不到调度。但其他中等优先级的任务却能抢到CPU资源。-- 从现象上来看,好像是中优先级的任务比高优先级任务具有更高的优先权。

如何解决优先级反转:

1。 优先级继承;

仍然是上面情况,

当系统有检测到低优先级的线程持有了高优先级线程所需要使用的同步资源时,系统临时提升低优先级线程的优先级,以便让低优先级线程先执行, 执行完毕后, 待低优先级线程释放出资源后, 再恢复低优先级的优先级。 这样高优先级就能正常执行了。

2。 不公平锁抢占;

首先:在对同一资源的抢占处理上, 一般采用的是队列的方式,也就是说对同一资源的抢占,一般是队首的先抢到该资源;而不公平锁的抢占法就是,这个队列中所有的等待线程都可能会抢到这个资源。而不是完全按队列的顺序来获取。

当高优先级发现资源被低优先级持有时,系统采用随机资源抢占法,去获得同步资源。 以便高优先级的任务能够进行。

不公平锁这块的描述更多是我的推测,仅作参考。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值