前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >锁的使用以及底层原理

锁的使用以及底层原理

作者头像
拉维
发布2021-04-16 16:28:08
6090
发布2021-04-16 16:28:08
举报
文章被收录于专栏:iOS小生活iOS小生活

之前在多线程——锁?中我有总结过锁,看本文之前可以先看看那篇文章。


@synchronized

我们知道,@synchronized是一把互斥锁(互斥锁保证在任何时刻都只能有一个线程访问该对象),它通过大括号来作为加锁、解锁的标识。

来看下面这个例子:

代码语言:javascript
复制
- (void)viewDidLoad {
    [super viewDidLoad];

    self.ticketCount = 20;
    [self lv_testSaleTicket];
}

//开4条多线程同时卖票
- (void)lv_testSaleTicket {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 3; i++) {
            [self saleTicket];
        }
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 10; i++) {
            [self saleTicket];
        }
    });
}

- (void)saleTicket {
    if (self.ticketCount > 0) {
        self.ticketCount--;
        sleep(0.1);
        NSLog(@"当前余票还剩:%ld张", self.ticketCount);
    } else {
        NSLog(@"当前车票已售罄");
    }
}

打印结果如下:

通过打印结果我们可以看出,这样是有问题的,线程不安全。我们加一把锁保证线程安全:

代码语言:javascript
复制
- (void)saleTicket {
    @synchronized (self) {
        if (self.ticketCount > 0) {
            self.ticketCount--;
            sleep(0.1);
            NSLog(@"当前余票还剩:%ld张", self.ticketCount);
        } else {
            NSLog(@"当前车票已售罄");
        }
    }
}

此时再打印,结果如下:

这时打印结果就正常了。

我们上例是通过@synchronized锁保证了线程的安全。接下来我们就来分析下@synchronized锁。

我们有两种方式开启研究。

第一种比较简单,就是汇编。首先在加锁的地方打个断点,如下图:

然后打开汇编:

就可以找到对应的关键字眼:

第二种方式就是使用如下指令将OC文件编译成C++代码:

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m

这种方式这里就不做演示了。

通过上面两种方式的分析,我们可以知道,@synchronized锁在底层是通过objc_sync_enter和objc_sync_exit函数来分别进行加锁和解锁的。

然后我下一个objc_sync_enter的符号断点:

发现objc_sync_enter是在objc库中,因此我就在libobjc源码中去查找objc_sync_enter:

代码语言:javascript
复制
// Begin synchronizing on 'obj'. 
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.  
int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        SyncData* data = id2data(obj, ACQUIRE);
        assert(data);
        data->mutex.lock();
    } else {
        // @synchronized(nil) does nothing
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

    return result;
}
  • 通过注释我们知道,@synchronized锁是一把递归锁,而递归锁是特殊的互斥锁(互斥锁分为递归锁和非递归锁)。
  • 如果@synchronized后面的对象传的是nil,那么将不会做任何事情,也就是说,@synchronized大括号里面包含的内容将不会被加锁。
  • 根据上面两点,递归锁和nil进行搭配,可以防止死锁

好,接下来我们就来看一下@synchronized后面的对象不为空的情况:

这是正常情况下都会进行的操作,我们看到,首先是通过id2data函数来生成一个SyncData类型的对象。所以,我们先来看一下SyncData的定义:

代码语言:javascript
复制
typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;
} SyncData;

再来看一下id2data这个函数的实现:

我们知道,无论是在Controller、View还是Model中,@synchronized锁都可以使用,可以说,@synchronized锁可以在程序的任何地方使用,因此@synchronized锁具备全局性

而通过上面SyncData类型以及id2data函数源码,我们可以猜到,实际上在底层会将@synchronized锁住的那部分内容封装成一个SyncData类型的节点对象

那么这些节点对象是怎么存储的呢?实际上它就是通过哈希表的形式进行存储SyncData类型的节点的,这张哈希表是一个全局的哈希表

接下来我们看个例子:

该例中运行会崩溃。在Setter方法里面,会首先对_testArray进行release(可以参考内存管理之MRC来了解setter的自定义),然后才会再对_testArray进行赋值。如果是多条线程同时进行,那么就会有可能出现这样一种情形:_testArray指向的对象被销毁了,并且原对象的内存空间被复用了,但是_testArray还没有指向新的对象,此时呢又对_testArray执行了一次release,这个时候就会导致野指针调用。这就是上面?程序崩溃的原因所在。

那么如何完善呢?答案是加一把锁。

下面我们加一把@synchronized锁

我们知道@synchronized锁后面跟的是一个对象,这个对象是作为锁的标识,我们这里将self.testArray作为@synchronized锁的唯一标识传递进去,然后运行:

程序也崩了!这是为啥呢?

上文中我有提到,如果@synchronized后面的对象传的是nil,那么将不会做任何事情,也就是说,@synchronized大括号里面包含的内容将不会被加锁。

根据前面的分析我们应该知道,self.testArray有可能是为nil,此时就不会加锁,因此问题就又回到了没有加锁的情形,所以在此崩溃也就顺理成章了。

通过这个问题呢,我其实是想说明一点,就是@synchronized这种加锁的形式实际上是不太靠谱的,它虽然使用简单,但是不太安全,一旦传入的对象为空,就会出现问题

上面的情形,我们直接使用NSLock来处理:

此时就没什么问题了。

NSLock

上面?我们已经演示了NSLock的使用场景,接下来我们看一下NSLock的实现源码。

我点进NSLock这个类,发现它是Foundation框架下面的,而OC的Foundation没有开源,但是Swift的Foundation开源了一部分, 我们下载好swift-corelibs-foundation-master的源码,然后找到NSLock的实现:

代码语言:javascript
复制
open class NSLock: NSObject, NSLocking {
    internal var mutex = _MutexPointer.allocate(capacity: 1)
#if os(macOS) || os(iOS) || os(Windows)
    private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
    private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
#endif

    public override init() {
#if os(Windows)
        InitializeSRWLock(mutex)
        InitializeConditionVariable(timeoutCond)
        InitializeSRWLock(timeoutMutex)
#else
        pthread_mutex_init(mutex, nil)
#if os(macOS) || os(iOS)
        pthread_cond_init(timeoutCond, nil)
        pthread_mutex_init(timeoutMutex, nil)
#endif
#endif
    }

    deinit {
#if os(Windows)
        // SRWLocks do not need to be explicitly destroyed
#else
        pthread_mutex_destroy(mutex)
#endif
        mutex.deinitialize(count: 1)
        mutex.deallocate()
#if os(macOS) || os(iOS) || os(Windows)
        deallocateTimedLockData(cond: timeoutCond, mutex: timeoutMutex)
#endif
    }

    open func lock() {
#if os(Windows)
        AcquireSRWLockExclusive(mutex)
#else
        pthread_mutex_lock(mutex)
#endif
    }

    open func unlock() {
#if os(Windows)
        ReleaseSRWLockExclusive(mutex)
        AcquireSRWLockExclusive(timeoutMutex)
        WakeAllConditionVariable(timeoutCond)
        ReleaseSRWLockExclusive(timeoutMutex)
#else
        pthread_mutex_unlock(mutex)
#if os(macOS) || os(iOS)
        // Wakeup any threads waiting in lock(before:)
        pthread_mutex_lock(timeoutMutex)
        pthread_cond_broadcast(timeoutCond)
        pthread_mutex_unlock(timeoutMutex)
#endif
#endif
    }

    open func `try`() -> Bool {
#if os(Windows)
        return TryAcquireSRWLockExclusive(mutex) != 0
#else
        return pthread_mutex_trylock(mutex) == 0
#endif
    }

    open func lock(before limit: Date) -> Bool {
#if os(Windows)
        if TryAcquireSRWLockExclusive(mutex) != 0 {
          return true
        }
#else
        if pthread_mutex_trylock(mutex) == 0 {
            return true
        }
#endif

#if os(macOS) || os(iOS) || os(Windows)
        return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)
#else
        guard var endTime = timeSpecFrom(date: limit) else {
            return false
        }
        return pthread_mutex_timedlock(mutex, &endTime) == 0
#endif
    }

    open var name: String?
}

可以看到,NSLock实际上就是对系统底层互斥锁的简单封装

接下来看个例子:

代码语言:javascript
复制
NSLock *lock = [[NSLock alloc] init];

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    static void (^testMethod)(int);

    testMethod = ^(int value){
        [lock lock];
        if (value > 0) {
          NSLog(@"current value = %d",value);
          testMethod(value - 1);
        }
        [lock unlock];
    };

    testMethod(10);
});

运行之后打印结果如下:

2021-03-24 13:58:04.888859+0800 003-NSLock分析[7265:1263518] current value = 10

testMethod是一个递归函数,testMethod(10)应该打印十次才对,这里才打印了一次,这明显是不对的。

我们在递归函数中,应该使用递归锁

NSRecursiveLock

紧接着上面那个例子,我们将NSLock替换为NSRecursiveLock:

代码语言:javascript
复制
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    static void (^testMethod)(int);

    testMethod = ^(int value){
        [lock lock];
        if (value > 0) {
          NSLog(@"current value = %d",value);
          testMethod(value - 1);
        }
        [lock unlock];
    };

    testMethod(10);
});

然后再运行,结果如下:

2021-03-24 14:02:11.464604+0800 003-NSLock分析[7298:1270869] current value = 10

2021-03-24 14:02:11.464812+0800 003-NSLock分析[7298:1270869] current value = 9

2021-03-24 14:02:11.465040+0800 003-NSLock分析[7298:1270869] current value = 8

2021-03-24 14:02:11.465184+0800 003-NSLock分析[7298:1270869] current value = 7

2021-03-24 14:02:11.465322+0800 003-NSLock分析[7298:1270869] current value = 6

2021-03-24 14:02:11.465426+0800 003-NSLock分析[7298:1270869] current value = 5

2021-03-24 14:02:11.465581+0800 003-NSLock分析[7298:1270869] current value = 4

2021-03-24 14:02:11.465804+0800 003-NSLock分析[7298:1270869] current value = 3

2021-03-24 14:02:11.466708+0800 003-NSLock分析[7298:1270869] current value = 2

2021-03-24 14:02:11.467291+0800 003-NSLock分析[7298:1270869] current value = 1

此时打印正常了。

我们接下来再对上面的例子做一个改造:

代码语言:javascript
复制

NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];

for (int i = 0; i < 10; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);

        testMethod = ^(int value){
            [lock lock];
            if (value > 0) {
              NSLog(@"current value = %d",value);
              testMethod(value - 1);
            }
            [lock unlock];
        };

        testMethod(10);
    });
}

实际上就是在外层加了一个for循环,然后我们运行发现崩溃了:

崩溃的原因就在于,死锁了!那么这个死锁是怎么产生的呢?且听我下面慢慢分析。

for循环执行了10次,因此会有10个异步添加到全局队列的操作,也就是会产生10条多线程来执行任务。

每一条多线程中都会有一个递归函数,递归函数中会有加锁操作。此时就很可能会发生下面的场景:线程1中通过lock加了锁,但是还没有unlock,此时线程2也通过lock加了锁,那么线程1和线程2就同时持有了某一块内存资源,并且他们两个线程对于自己已经持有的资源都不会主动放弃,也不会强势抢夺对方的资源,只能是互相等待,也就是说,线程1的unlock需要等待线程2的unlock,而线程2中的unlock又需要等待线程1 中的unlock执行完毕才能执行,这就导致了死锁

那么针对这种情况应该怎么办呢?答案就是使用@synchronized

代码语言:javascript
复制
for (int i = 0; i < 10; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);

        testMethod = ^(int value){
            @synchronized (self) {
                if (value > 0) {
                  NSLog(@"current value = %d",value);
                  testMethod(value - 1);
                }
            }
        };

        testMethod(10);
    });
}

将NSRecursiveLock替换成了@synchronized之后,就没啥问题了,程序执行正常,结果也令人满意。

那么,为什么将NSRecursiveLock替换成了@synchronized之后就可以了呢?这就要对比一下NSRecursiveLock和@synchronized了。

虽然NSRecursiveLock和@synchronized都是递归锁,但是@synchronized当其后面的对象标识相同的时候,它就不会在没有解锁的情况下再次进入进行加锁,因此会从根本上杜绝死锁的情况发生

根据上面?对于@synchronized、NSLock和NSRecursiveLock的分析,我们总结了一些小观点:

  • 对于一些普通的线程安全操作,使用NSLock就差不多了
  • 在需要递归调用的方法或者函数中使用的锁,一定要使用递归锁,比如NSRecursiveLock
  • 在循环多线程的情形下,一定要注意死锁的情形,必要的话使用@synchronized来代替其他的互斥锁

NSCondition

详见多线程——锁?

NSConditionLock

先来看下其声明:

代码语言:javascript
复制
@interface NSConditionLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end

NSConditionLock是一把锁,一旦一个线程获得了锁,其他的线程就必须要等待。

[conditionLock lock]:表示期待获得锁,如果没有其他线程获得该锁(不需要判断其内部的condition),那么将执行它下面的代码;如果已经有其他的线程获得了该锁(可能是条件锁,也可能是无条件锁),那么就会等待,直到其他线程解锁。

[conditionLock lockWhenCondition:1]:如果没有其他的线程获得该锁,但是该锁内部的condition不等于1,此时依然不能获得该锁,依然需要等待;如果没有其他的线程获得该锁,并且该锁内部的condition等于1,则会进入代码区,同时设置它获得该锁,其他的线程将等待它代码的完成,直到他解锁。

[conditionLock unlockWithCondition:1]:表示释放锁,同时把内部condition设置为1条件。

[conditionLock lockWhenCondition:1 beforeDate:limitDate]:表示如果被锁定(注意,并没有获得该锁!),则超过时间limitDate后将不再阻塞该线程。

下面来看个例子:

代码语言:javascript
复制
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
   [conditionLock lockWhenCondition:1];
   NSLog(@"线程 1");
   [conditionLock unlockWithCondition:0];
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
   [conditionLock lockWhenCondition:2];
   NSLog(@"线程 2");
   [conditionLock unlockWithCondition:1];
});

dispatch_async(dispatch_get_global_queue(0, 0), ^{
   [conditionLock lock];
   NSLog(@"线程 3");
   [conditionLock unlock];
});

执行后打印结果为:

2021-03-24 17:23:12.962065+0800 004-NSCondition[8242:1558008] 线程 3

2021-03-24 17:23:12.977179+0800 004-NSCondition[8242:1558008] 线程 2

2021-03-24 17:23:13.040493+0800 004-NSCondition[8242:1558006] 线程 1

分析如下:

  • 首先在主线程依次添加了三个任务到全局队列,这三个任务的优先级由高到低依次为:线程1>线程3>线程2。
  • 当首先执行线程1 的时候,由于条件不符合(初始条件是2,而线程1中的条件是1),因此线程1会进入到waiting状态,并且释放当前的条件锁。
  • 然后执行线程3,线程3中是直接调用的lock,这里是不需要对比条件值的,所以会直接运行打印。
  • 再然后是执行线程2,由于满足条件值,所以线程2会打印,打印完成后会调用[conditionLock unlockWithCondition:1]将条件值设置为1,并且发送broadcast,此时线程1接收到信号,因此被唤醒并打印。

自旋锁

最基本的锁,实际上就只有三类:自旋锁、互斥锁和读写锁。其他的比如条件锁、递归锁、信号量等都是基于三类基本锁在上层的一种封装。

前面我们讲了互斥锁,NSLock是最基本的互斥锁。互斥锁又分为递归锁和非递归锁,NSRecursive、@synchronized都是递归锁。

接下来我们就来聊聊自旋锁。

自旋锁(spinlock)是指当一个线程在获取自旋锁的时候,如果该自旋锁已经被其他的线程获取,那么当前线程将会循环等待,并且会持续不断地判断锁是否能够被成功获取,直到成功获得了锁之后才会退出循环。

等待获取锁的线程会一直处于活跃状态,虽然它并没有执行任何有效的任务,因此使用自旋锁会造成忙等待(busy-waiting)

自旋锁能够避免进程上线文之间的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。

我们平常在开发中接触到的最多的自旋锁就是atomic。关于atomic,我之前写过一篇文章atomic和nonatomic

我们知道,atomic和nonatomic是声明属性的时候的原子修饰符,它们控制了在编译器自动为属性生成的setter和getter中是否进行加锁控制。

我接下来想看下其源码,因此我就到setter/getter的源码中去查看atomic和nonatomic的源码。

在libobjc源码中搜索“objc_setProperty”:就可以找到对应的setter源码:

它们都调用了reallySetProperty函数:

代码语言:javascript
复制
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    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);
}

使用到atomic的地方:

我们发现,如果不是atomic,那么就会直接修改值;如果是atomic,那么就加一把spinlock(自旋锁),然后才修改值。

同样的道理,我们看一下getter的源码:

如果不是atomic,那么就会直接返回值;如果是atomic,那么就加一把spinlock(自旋锁),然后才返回值。

所以说,atomic就是给系统自动生成的setter/getter方法内部自动加锁

atomic虽然保证了对象的setter/getter方法的线程安全,但是并不能保证整个对象是线程安全的

比如下面这个例子:

代码语言:javascript
复制
@interface ViewController ()

@property (atomic, strong) NSArray *array;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self lv_test_atomic2];
}

- (void)lv_test_atomic2{
    //Thread A
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 100000; i ++) {
            if (i % 2 == 0) {
                self.array = @[@"Norman", @"Lavie", @"Lily"];
            }
            else {
                self.array = @[@"Gaoying"];
            }
            NSLog(@"Thread A: %@\n", self.array);
        }
    });

    //Thread B
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 100000; i ++) {
            if (self.array.count >= 2) {
                NSString* str = [self.array objectAtIndex:1];
            }
            NSLog(@"Thread B: %@\n",self.array);
        }
    });
}

@end

A线程里面会不停的变换self.array里面的元素,有时是一个有时是三个。

B线程里面会在self.array里面的元素个数大于等于两个的时候获取第2个元素。

这样貌似没啥问题,但是运行之后你会发现,崩了:

2021-03-25 11:39:34.254859+0800 005-atomic分析[9944:1946565] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSSingleObjectArrayI objectAtIndex:]: index 1 beyond bounds [0 .. 0]'

数组越界了!为啥呢?我明明在获取之前判断数组的元素个数了啊。

这就是多线程不安全导致的。线程B中判断完了之后,线程A中将self.array里面的元素替换了,此时再获取[self.array objectAtIndex:1],自然就崩掉了。

该例也进一步验证了,atomic并不能保证对象是绝对线程安全的

接下来再来看个例子:

代码语言:javascript
复制
@interface ViewController ()

@property (atomic, assign) NSInteger num;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self lv_test_atomic1];
}

- (void)lv_test_atomic1 {
    //Thread A
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 10000; i ++) {
            self.num += 1;
            NSLog(@"%ld", (long)self.num);
        }
    });

    //Thread B
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 10000; i ++) {
            self.num += 1;
            NSLog(@"%ld", (long)self.num);
        }
    });
}

@end

打印结果如下:

2021-03-25 11:50:49.352481+0800 005-atomic分析[10010:1967719] 1

2021-03-25 11:50:49.352522+0800 005-atomic分析[10010:1967717] 2

2021-03-25 11:50:49.353013+0800 005-atomic分析[10010:1967719] 3

2021-03-25 11:50:49.353015+0800 005-atomic分析[10010:1967717] 4

2021-03-25 11:50:49.353361+0800 005-atomic分析[10010:1967719] 5

2021-03-25 11:50:49.353444+0800 005-atomic分析[10010:1967717] 6

2021-03-25 11:50:49.353616+0800 005-atomic分析[10010:1967717] 7

......

2021-03-25 11:50:57.806527+0800 005-atomic分析[10010:1967719] 19920

2021-03-25 11:50:57.806857+0800 005-atomic分析[10010:1967717] 19921

2021-03-25 11:50:57.807141+0800 005-atomic分析[10010:1967719] 19922

2021-03-25 11:50:57.807497+0800 005-atomic分析[10010:1967717] 19923

2021-03-25 11:50:57.807818+0800 005-atomic分析[10010:1967719] 19924

2021-03-25 11:50:57.808150+0800 005-atomic分析[10010:1967717] 19925

2021-03-25 11:50:57.808510+0800 005-atomic分析[10010:1967719] 19926

2021-03-25 11:50:57.808799+0800 005-atomic分析[10010:1967717] 19927

2021-03-25 11:50:57.809163+0800 005-atomic分析[10010:1967719] 19928

2021-03-25 11:50:57.809406+0800 005-atomic分析[10010:1967717] 19929

2021-03-25 11:50:57.809727+0800 005-atomic分析[10010:1967719] 19930

2021-03-25 11:50:57.810282+0800 005-atomic分析[10010:1967719] 19931

2021-03-25 11:50:57.810548+0800 005-atomic分析[10010:1967719] 19932

2021-03-25 11:50:57.810913+0800 005-atomic分析[10010:1967719] 19933

2021-03-25 11:50:57.811225+0800 005-atomic分析[10010:1967719] 19934

2021-03-25 11:50:57.811585+0800 005-atomic分析[10010:1967719] 19935

2021-03-25 11:50:57.811849+0800 005-atomic分析[10010:1967719] 19936

按道理,两个线程一共执行了20000次,所以最后的结果应该是20000才对啊。为啥这里最后才到19936呢?

原因还是出在多线程上面。

self.num += 1实际上就是self.num = self.num + 1,这里包含了属性的getter和setter,它们都是atomic的,因此其内部都是线程安全的。现在有可能会出现这样一种情形:

线程A中的self.num的getter完了之后,线程B中的getter获取到锁了,然后在线程B中的getter完成之后,线程A中的setter和线程B中的setter在赋值的时候都是获取到的同一个值大小。按道理的话,两次setter应该加2,但是这种情形下这两次setter只加了1。

所以,就会出现最终的打印结果没有到20000的情况。

所以,atomic只是保证了getter/setter存取方法的线程安全,并不能保证整个对象是线程安全的,也就是说,是否使用atomic跟属性的多线程安全并没有什么直接的联系,在编程的时候,线程安全还是需要开发者自己来处理。

而在效率层面,atomic需要锁住资源,因此比nonatomic慢了20倍,所以综合考虑,我们在iOS开发过程中要尽量使用nonatomic

读写锁

读写锁是一种特殊的自旋锁,它把共享资源的访问者划分为读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。

写锁相对于自旋锁而言,能够提高并发性。因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读写数为实际的逻辑CPU数。

写者是具有排他性的,一个读写锁同时只能有一个写者或者多个读者,但是不能同时既有读者又有写者。在读写锁保持期间也是抢占失效的

代码语言:javascript
复制
如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。

一次只有一个线程可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁. 正是因为这个特性,当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞。

当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进行加锁, 它必须直到所有的线程释放锁。

通常, 当读写锁处于读模式锁住状态时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁⻓期占用, 而等待的写模式锁请求⻓期阻塞。

读写锁适合于对数据结构的读次数比写次数多得多的情况。因为读模式锁定时可以共享,以写模式锁住时意味着独占,所以读写锁又叫共享-独占锁。

总结

本文介绍了iOS开发过程中的各个锁,有些是着重讲的,有些一带而过。下面附带各个锁的性能对比,诸位在开发过程中可以根据具体业务场景,再结合性能,去选择最合适的锁。

以上。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-03-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 iOS小生活 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档