iOS - 线程中常见的几种锁

2023-10-27

线程锁主要是用来解决“共享资源”的问题,实际开发中或多或少的都会用到各类线程锁,为了线程的安全我们有必要了解常见的几种锁,下面是本人查看一些大牛的博客然后整理的内容,加上自己的一些见解,水平有限,如果不慎有误,欢迎交流指正。

常见锁列举

  • 自旋锁(OSSPinLockos_unfair_lock
  • 互斥锁(pthread_mutex_tNSLock@synthronized
  • 递归锁(pthread_mutex_tNSRecursiveLock
  • 条件锁(pthread_cond_tNSConditionNSConditionLock
  • 信号量(dispatch_semaphore_tpthread_mutex_t等)
  • 读写锁(pthread_rwlock_t
  • 栅栏(dispatch_barrier_syncdispatch_barrier_async
  • atomic

一、自旋锁

自旋锁的意思就是当资源被占有时,自旋锁不会引起其他调用者休眠,而是让其他调用者自旋,不停的循环访问自旋锁导致调用者处于busy-wait(忙等状态),直到自旋锁的保持者释放锁。自旋锁是为了实现保护共享资源一种锁机制,在任何时刻只能有一个保持者,也就是说在任何时刻只能有一个可执行单元获得锁。也正是因为其他调用者会保持自旋状态,使得在锁的保持者释放锁时能够即刻获得锁,效率非常高。但我们说调用者时刻自旋也是消耗CPU资源的,所以如果自旋锁的使用者保持锁的时间比较短的话,使用自旋锁是非常合适的,因为在锁释放之后省去了唤醒调用者的时间。

在iOS10之前,苹果使用自旋锁是OSSPinLock

// 头文件
// #import <libkern/OSAtomic.h>
// 初始化自旋锁
static OSSpinLock spinLock = OS_SPINLOCK_INIT;
// 自旋锁的使用
-(void)spinLockTest {
    OSSpinLockLock(&spinLock);
    // Critical section , to do something (临界区,在这里访问共享资源)
    OSSpinLockUnlock(&spinLock);
}

但是YYKit的作者 ibireme《不再安全的 OSSpinLock》一文中指出OSSpinLock存在潜在的优先级反转问题,意思就是如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。

如果开发者能够保证访问锁的线程全部都处于同一优先级,OSSpinLock也是可用的。

所以在 iOS 10/macOS 10.12 发布时,苹果提供了新的 os_unfair_lock 作为 OSSpinLock 的替代,并且将 OSSpinLock 标记为了 Deprecated。使用 os_unfair_lock

// 头文件
// #import <os/lock.h>
static os_unfair_lock_t unfairLock = &OS_UNFAIR_LOCK_INIT;

-(void)unfairLockTest {
    os_unfair_lock_lock(unfairLock);
    // Critical section , to do something (临界区,在这里访问共享资源)
    os_unfair_lock_unlock(unfairLock);
}

二、互斥锁

互斥锁和自旋锁类似,都是为了解决对某项资源的互斥使用,并且在任意时刻最多只能有一个执行单元获得锁,与自旋锁不同的是,互斥锁在被持有的状态下,其他资源申请者只能进入休眠状态,当锁被释放后,CPU会唤醒资源申请者,然后获得锁并访问资源。

常见的互斥锁常见有pthread_mutex_tNSLock@synthronized三种:

1、 @synthronized

@synchronized指令是在Objective-C代码中动态创建互斥锁的便捷方法,和其他互斥锁不同的是我们不必直接创建互斥锁定对象,直接通过@synchronized指令来完成:

- (void)myMethod:(id)anObj
{
    @synchronized(anObj)
    {
        //大括号之间的所有内容均受@synchronized指令保护。
    }
}

虽然看起来很简洁,但是@synchronized是以牺牲性能来换取语法上的简洁和可读性的。官方文档指出在内部隐式地将异常处理程序添加到受保护的代码中,如果抛出异常,此处理程序将自动释放互斥量。这意味着,当我们使用@synchronized指令时,还免费的得到异常处理的功能,但这也造成了额外的开销。如果不介意由隐式异常处理程序引起的额外开销的话,可以考虑使用该锁,否则就使用其他锁类。

具体的实现原理可以参考这篇文章: 关于 @synchronized,这儿比你想知道的还要多

2、 NSLock

NSLock是Object-C以对象的形式暴露给开发者的一种锁,并遵守NSLocking协议,实现起来很简单:

- (void)mutexLockTest {
    NSLock *lock = [[NSLock alloc] init];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lock];
        NSLog(@"---thread1");
        sleep(3);
        [lock unlock];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lock];;
        NSLog(@"---thread2");
        sleep(3);
        [lock unlock];
    });  
}

输出:

LockTest[60255:2555431] ---thread1
LockTest[60255:2555429] ---thread2

上例中的lockunlock都是协议NSLocking中的方法,NSLock类中还有两个方法:

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

我们知道lock方法是会阻塞线程的,而tryLock方法不会阻塞线程,如果返回YES则加锁成功,否则返回NO,加锁失败。方法lockBeforeDate:意思是一段时间之内都会尝试加锁并阻塞线程,如果这段时间能加锁成功则返回YES,否则返回NO。

注意:lock–unlock 、tryLuck—unLock 必须成对存在,不能迭代(或递归)加锁,如果发生两次lock,而未unlock过,则会产生死锁问题。

3、 pthread_mutex_t

pthread_mutex_t是纯C语言的函数,效率更高,先来看下几个函数:

// 初始化互斥锁函数
int pthread_mutex_init(pthread_mutex_t * __restrict,
		const pthread_mutexattr_t * _Nullable __restrict);
// 加锁函数		
int pthread_mutex_lock(pthread_mutex_t *);
// 解锁函数
int pthread_mutex_unlock(pthread_mutex_t *);
// 互斥锁属性初始化函数
int pthread_mutexattr_init(pthread_mutexattr_t *);
/**
 * 互斥锁属性设置函数
 * 
 * 锁类型:
 * PTHREAD_MUTEX_NORMAL            0   普通互斥锁
 * PTHREAD_MUTEX_ERRORCHECK	       1   带有错误检查互斥锁
 * PTHREAD_MUTEX_RECURSIVE		   2   递归锁
*/ 
int pthread_mutexattr_settype(pthread_mutexattr_t *, int);

具体使用,创建一个简单的互斥锁:

- (void)mutexLockTest {
  // 初始化互斥锁
    __block pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    // 或者
    // __block pthread_mutex_t mutex;
    // pthread_mutex_init(&mutex, NULL);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        pthread_mutex_lock(&mutex);
        sleep(2);
        NSLog(@"----- thread1 %@",[NSThread currentThread]);
        pthread_mutex_unlock(&mutex);
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        pthread_mutex_lock(&mutex);
        sleep(2);
        NSLog(@"----- thread2 %@",[NSThread currentThread]);
        pthread_mutex_unlock(&mutex);
    });
}

输出结果:

LockTest[65150:2630192] ----- thread1 <NSThread: 0x600002b25700>{number = 8, name = (null)}
LockTest[65150:2630190] ----- thread2 <NSThread: 0x600002bf0280>{number = 4, name = (null)}

对于NSLock就是在内部封装一个pthread_mutex,属性为 PTHREAD_MUTEX_ERRORCHECK,它会损失一定性能换来错误提示。

这三种互斥锁的性能比较: pthread_mutex > NSLock > @synchronized

三、递归锁

递归锁可以被同一线程多次请求,而不会引起死锁,即在多次被同一个线程进行加锁时,不会造成死锁。这主要是用在循环或递归操作中。

1、NSRecursiveLock

NSRecursiveLock也是Object-C为我们提供锁类,其遵守NSLocking协议,递归锁也是通过 pthread_mutex_lock 函数来实现,在函数内部会判断锁的类型,如果显示是递归锁,就允许递归调用,仅仅将一个计数器加一,等到递归完毕之后,所有锁都会释放。举例:

- (void)recursiveLock {
    NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        static void (^RecursiveBlock)(int);
        RecursiveBlock = ^(int  value) {
            [recursiveLock lock];
            // 这里要设置递归结束的条件或次数,不然会无限递归下去
            if (value > 0) {
                NSLog(@"处理中...");
                sleep(1);
                RecursiveBlock(--value);
            }
            NSLog(@"处理完成!");
            [recursiveLock unlock];
        };
        RecursiveBlock(4);
    });
}

2、pthread_mutex_t

NSRecursiveLockNSLock 的区别在于内部封装的 pthread_mutex_t 的类型不同,前者的类型为 PTHREAD_MUTEX_RECURSIVE,举例:

- (void)recursiveLock {
    __block pthread_mutex_t recursiveMutex;
    pthread_mutexattr_t recursiveMutexattr;
    pthread_mutexattr_init(&recursiveMutexattr);
    pthread_mutexattr_settype(&recursiveMutexattr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&recursiveMutex, &recursiveMutexattr);
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        static void (^RecursiveBlock)(int);
        RecursiveBlock = ^(int  value) {
            pthread_mutex_lock(&recursiveMutex);
            // 这里要设置递归结束的条件或次数,不然会无限递归下去
            if (value > 0) {
                NSLog(@"处理中...");
                sleep(1);
                RecursiveBlock(--value);
            }
            NSLog(@"处理完成!");
            pthread_mutex_unlock(&recursiveMutex);
        };    
        RecursiveBlock(4);
    });
}

四、条件锁

条件是信号量的另一种类型,当某个条件为true时,它允许线程相互发信号。条件通常用于指示资源的可用性或确保任务以特定顺序执行。当线程测试条件时,除非该条件已经为真,否则它将阻塞。它保持阻塞状态,直到其他线程显式更改并发出条件信号为止。条件和互斥锁之间的区别在于,可以允许多个线程同时访问该条件。

1、NSCondition

NSCondition 的底层是通过条件变量(condition variable) pthread_cond_t 来实现的。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待某个数据就绪,随后唤醒线程,比如常见的生产者-消费者模式。具体举例:

- (void)conditionLock {
    NSCondition *conditionLock = [[NSCondition alloc] init];
    __block NSString *food;
    // 消费者1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [conditionLock lock];
        if (!food) {// 没有现成的菜(判断是否满足线程阻塞条件)
            NSLog(@"等待上菜");
            [conditionLock wait];// 没有菜,等着吧!(满足条件,阻塞线程)
        }
        // 菜做好了,可以用餐!(线程畅通,继续执行)
        NSLog(@"开始用餐:%@",food);
        [conditionLock unlock];
    });
    // 消费者2
    //    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//        [conditionLock lock];
//        if (!food) {
//            NSLog(@"等待上菜2");
//            [conditionLock wait];
//        }
//        NSLog(@"开始用餐2:%@",food);
//        [conditionLock unlock];
//    });

    // 生产者
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [conditionLock lock];
        NSLog(@"厨师做菜中...");
        sleep(5);
        food = @"四菜一汤";
        NSLog(@"厨师做好了菜:%@",food);
        [conditionLock signal];
//        [conditionLock broadcast];
        [conditionLock unlock];
    });
}

上面的例子可以看到,不仅用到了条件锁(signalwait),还用到了互斥锁(lockunlock),也就说条件锁是配合互斥锁一起使用的,这也是出于线程的安全考虑,防止其他线程修改数据,保证消费者拿到的是正确的数据。

NSCondition中还有一个方法:

- (void)broadcast;

broadcast方法是以广播的形式通知所有满足条件且等待中的线程可以继续执行任务了,如上例中把注释的部分代码解开,会有两份输出结果。

2、NSConditionLock

NSConditionLock也给一种条件锁,具体例子:

- (void)conditionLock {
    NSConditionLock *conditionLock = [[NSConditionLock alloc] init];
    NSMutableArray *arrayM = [NSMutableArray array];
    NSInteger condition = 4;// 条件
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [conditionLock lock];
        for (int i = 0; i < 6; i++) {
            sleep(1);
            [arrayM addObject:@(i)];
            NSLog(@"异步下载第 %d 张图片",i);
            if (arrayM.count == 4) {// 当下载四张图片就回到主线程刷新
                [conditionLock unlockWithCondition:condition];
            }
        }
    });
    
    dispatch_async(dispatch_get_main_queue(), ^{
        [conditionLock lockWhenCondition:condition];
        NSLog(@"已经下载4张图片%@",arrayM);
        [conditionLock unlock];
    });
}

NSConditionLockNSCondition有点相似,都是当条件满足时,唤醒另外一线程然后执行任务,只不过前者直接将条件作为了函数的参数。

3、pthread_cond_t

- (void)conditionLock3 {
    __block pthread_mutex_t mutex;
    __block pthread_cond_t condition;
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&condition, NULL);    
    NSMutableArray *arrayM = [NSMutableArray array];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        pthread_mutex_lock(&mutex);
        for (int i = 0; i < 6; i++) {
            sleep(1);
            [arrayM addObject:@(i)];
            NSLog(@"异步下载第 %d 张图片",i);            
            if (arrayM.count == 4) {
                //pthread_cond_broadcast(&condition);
                pthread_cond_signal(&condition);
            }
        }
    });
    
    dispatch_async(dispatch_get_main_queue(), ^{
        pthread_cond_wait(&condition, &mutex);
        NSLog(@"已经获取到4张图片");
        pthread_mutex_unlock(&mutex);
    });
}

上例中的pthread_cond_waitpthread_cond_signal两个函数的本质是锁的转移,以生产者-消费者模式来说, pthread_cond_wait 方法是消费者放弃锁,然后生产者获得锁,pthread_cond_signal 则是一个锁从生产者到消费者转移的过程。

三种锁的性能比较:pthread_mutex > NSCondition > NSConditionLock

五、信号量

信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量VI,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端。确认这些信号量VI引用的是初始创建的信号。

使用信号量采用GCD中的dispatch_semaphore_t对象,实际开发中通常用于控制最大并发量控制资源的同步访问网络同步加载等,函数介绍:

// 创建信号量,并给信号量初始化值
dispatch_semaphore_t dispatch_semaphore_create(long value);
// 信号量减1,当信号量小于0时阻塞线程
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
// 信号量加1
long dispatch_semaphore_signal(dispatch_semaphore_t dsema);

当我们需要控制两个或多个网络同步执行的时候,该如何做?我们会有多种方案(dispatch_async、锁等),这里我们使用信号量来实现:

   // 创建信号量并初始化信号量的值为0
    dispatch_semaphore_t semaphone = dispatch_semaphore_create(0);
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{     // 线程1
        sleep(2);
        NSLog(@"async1.... %@",[NSThread currentThread]);
        dispatch_semaphore_signal(semaphone);//信号量+1
    });
    
    dispatch_semaphore_wait(semaphone, DISPATCH_TIME_FOREVER);//信号量减1

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 线程2
        sleep(2);
        NSLog(@"async2.... %@",[NSThread currentThread]);
        dispatch_semaphore_signal(semaphone);//信号量+1
    });

输出:

LockTest[26691:3553853] async1.... <NSThread: 0x60000379c300>{number = 3, name = (null)}
LockTest[26691:3553853] async2.... <NSThread: 0x60000379c300>{number = 3, name = (null)}

分析:上例代码在主线程中执行,信号量初始化为0,执行到线程1时,因为是异步函数,所以会开辟分线程,然后会立即返回函数随后才会执行block中的任务,这时主线程中执行到dispatch_semaphore_wait函数(下文简称wait),信号量减1,变为-1,这时主线程会阻塞,导致线程2无法执行。当线程1中的执行完dispatch_semaphore_signal(下文简称signal)函数之后,信号量加1,变为0,这时主线程会解除阻塞状态,然后执行线程2中的任务。由此达到两个网络同步执行请求,如果是多个网络请求同步执行也是依次类推。

上面说到阻塞主线程的状况,如果我们把线程1中的任务执行完之后回到主线程调用signal函数,结局会怎样呢?

        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"回到主线程");
            dispatch_semaphore_signal(semaphone);//信号量+1
        });

输出:

LockTest[27663:3570710] async1.... <NSThread: 0x6000020f4a00>{number = 5, name = (null)}

分析:根据上面的结果可以看出,线程1任务执行完成,而线程2却一直没有执行,什么原因?因为主线程死锁,当主线程执行到wait函数时,信号量减1,变为-1,阻塞主线程,然后线程1中的任务执行完毕,想要回到主线程调用signal函数,但是主线程已经处于阻塞状态了,导致根部不会执行到signal函数,导致主线程死锁。

再举个例子,当我们需要下载100张图片时,因为网络下载属于耗时操作,所以我们会使用并发队列(concurrent queue)和异步函数(dispatch_async),目的时开辟分线程,把耗时操作放在分线程中执行,但过多的线程数与项目的性能是成反比的,所以控制并发、提高性能则尤为重要,如下例:

   // 创建信号量并初始值为5,最大并发量5
   dispatch_semaphore_t semaphore =  dispatch_semaphore_create(5);
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    for (int i = 0;i < 100 ; i ++) {        
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);       
        dispatch_async(queue, ^{
            NSLog(@"i = %d  %@",i,[NSThread currentThread]);
            //此处模拟一个 异步下载图片的操作
            sleep(arc4random()%6);
            dispatch_semaphore_signal(semaphore);
        });
    }

分析:上例初始信号量为5,即最多允许同时有5个资源可以进入临界区域,解释上面的功能就是最多只允许5张图片同时下载,因为图片大小不同,所以耗时也不同,谁先执行完就会调用signal函数,使信号量加1,然后再允许一个资源进入临界区域,依次达到控制最大并发量的目的。

下载图片是分线程中执行,虽然最大并发量是5,但是不一定就开辟5个线程,实际需要开辟多少线程和当前CPU资源情况、内存状况、线程池线程数量等决定的。

六、读写锁

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。

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

具体实例:

    __block pthread_rwlock_t rwlock;
    pthread_rwlock_init(&rwlock, NULL);
    NSMutableArray *arrayM = [NSMutableArray array];
    
    void(^WrightBlock)(NSString *) = ^ (NSString *str) {
        pthread_rwlock_wrlock(&rwlock);
        NSLog(@"开启写操作");
        [arrayM addObject:str];
        sleep(2);
        pthread_rwlock_unlock(&rwlock);
    };
    
    void(^ReadBlock)(void) = ^ {
        pthread_rwlock_rdlock(&rwlock);
        NSLog(@"开启读操作");
        sleep(1);
        NSLog(@"读取数据:%@",arrayM);
        pthread_rwlock_unlock(&rwlock);
    };
    
    for (int i = 0; i < 5; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            WrightBlock([NSString stringWithFormat:@"%d",i]);
        });
    }
    
    for (int i = 0; i < 5; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            ReadBlock();
        });
    }

七、栅栏

栅栏函数在GCD中常用来控制线程同步,在队列中它总是等栅栏之前的任务执行完,然后执行栅栏自己的任务,执行完自己的任务后,再继续执行栅栏后的任务。常用函数有同步栅栏函数(dispatch_barrier_sync)和异步栅栏函数(dispatch_barrier_async)。

具体实例:

    // 并发队列
    dispatch_queue_t queue = dispatch_queue_create("com.gcd.brrier", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        sleep(1);
        NSLog(@"任务1 -- %@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"任务2 -- %@",[NSThread currentThread]);
    });
    
    // 栅栏函数,修改同步栅栏和异步栅栏,观察“栅栏结束”的打印位置
    dispatch_barrier_sync(queue, ^{
        for (int i = 0; i < 4; i++) {
            NSLog(@"任务3 --- log:%d -- %@",i,[NSThread currentThread]);
        }
    });
    
    // 在这里执行一个输出
    NSLog(@"栅栏结束");
    
    dispatch_async(queue, ^{
        sleep(1);
        NSLog(@"任务4 -- %@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"任务5 -- %@",[NSThread currentThread]);
    });

上例中的代码是在主线程中执行的,当我们使用同步栅栏函数时,NSLog(@"栅栏结束")会在栅栏任务执行完之后输出,当使用异步栅栏函数时,NSLog(@"栅栏结束")会在刚开始就输出,说明同步栅栏函数会阻塞当前线程,功能类似于同步函数(dispatch_sync)。

注意:在使用栅栏函数时,只有使用自定义并发队列才有意义,如果用的是串行队列或者系统提供的全局并发队列,这个栅栏函数的作用等同于一个同步函数的作用。

八、原子操作atomic

原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch(换到另一个线程)。

属性的默认是atomic的,对于atomic的属性,系统会生成setter和getter方法时会确保set、get操作的完整性,据说setter和getter内部是通过添加自旋锁的方式保证其操作安全的。例如当线程A对属性进行getter操作时,线程B对属性进行setter操作,那么线程A仍能获取到完整无损的属性值。但是是不是说atomic就是多线程安全的呢?答案是不能,看下面例子:

// 添加一个atomic属性
@property (atomic, copy) NSString *name;
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.gcd.concurrent", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(concurrentQueue, ^{// 线程1
        self.name = @"张三";//setter操作
    });
    dispatch_async(concurrentQueue, ^{// 线程2
        self.name = @"李四";//setter操作
    });
    dispatch_async(concurrentQueue, ^{// 线程3
        self.name = @"王二";//setter操作
        // 这里有一个稍微耗时的操作,完事后想利用name的值,因为模拟需求是要把名字叫王二的人叫来
        sleep(2);
        NSLog(@"叫王二来一趟,name = %@",self.name);//getter操作
        
    });
    dispatch_async(concurrentQueue, ^{// 线程4
        self.name = @"麻子";//setter操作
    });

输出:

LockTest[18309:3431300] 叫王二来一趟,name = 麻子

上例中我们模拟需求是希望在线程3中修改属性name的值为“王二”,在执行完耗时操作时会用到这值“王二”,同时还有线程1、2、3在对属性name进行setter操作,最后打印的name却是“麻子”。说明在多线程操作时,atomic并不能保证多线程安全,能够保证的是读/写操作原子性,得到一个完整无损的数据。更多可以看看薯妹的文章

有atomic就有nonatomic(非原子操作),对于nonatomic的属性,系统为其生成的setter、getter方法并不是安全的,它允许多个线程同时对属性进行读/写操作。例如线程A对属性进行getter操作,同时又有多个线程对属性进行setter操作,那么get到的数据可能是一个我们自己都不认识的垃圾数据。

结论:atomic是读/写操作安全的(操作的原子性),但不能保证多线程安全;nonatomic不能保证读写操作的安全(操作非原子性),也不能保证多线程安全;但是nonatomic的性能比atomic高,如果不涉及多线程操作,使用nonatomic是不错的选择,因为他可以保证性能的同时还能确保数据安全;如果开发中涉及到大量多线程操作,那么务必使用atomic,因为相对于性能而言,数据的正确安全更为重要。能在性能和安全之间找到平衡是最考验程序员的!!!

以上是常见几种锁的介绍,YY大神曾作出过几种锁的性能测试,但是只是测试了单线程的情况,不能反映多线程下的实际性能,所以这个结果只能当作一个定性分析:
在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

iOS - 线程中常见的几种锁 的相关文章

  • MySQL 中的字段递增是原子操作吗?

    我正在制作一个网站 我想在标准 MyISAM 表中增加一个计数器 简化示例 UPDATE votes SET num num 1 如果多个连接执行相同的查询 这会导致问题吗 或者 MySQL 会处理它并锁定表或其他措施以确保不存在冲突吗 写
  • C++ std::atomic 变量的线程同步问题

    当下面的程序偶尔打印 坏 输出时 它会给我带来意想不到的行为 两个线程应该使用两个 std atomic 变量 s lock1 和 s lock2 进行同步 在 func2 中 为了将 s var 变量设置为 1 它必须以原子方式在 s l
  • Linux 上 C 语言的原子操作

    我正在尝试将我编写的一些代码从 Mac OS X 移植到 Linux 并且正在努力寻找仅适用于 OSX 的合适替代品OSAtomic h 我找到了海湾合作委员会 sync 系列 但我不确定它是否与我拥有的旧编译器 内核兼容 我需要代码在 G
  • 如何初始化静态 std::atomic 数据成员

    我想为名为的类生成标识符order以线程安全的方式 下面的代码无法编译 我知道原子类型没有复制构造函数 我认为这解释了为什么这段代码不起作用 有人知道如何让这段代码真正发挥作用吗 有替代方法吗 include
  • 为什么atomic.StoreUint32比sync.Once中的普通分配更受欢迎?

    在阅读Go源码时 我对src sync once go中的代码有一个疑问 func o Once Do f func Note Here is an incorrect implementation of Do if atomic Comp
  • 中断安全 FIFO 中的 DMB 指令

    相关这个线程 https stackoverflow com q 50800118 1488067 我有一个 FIFO 它应该可以跨 Cortex M4 上的不同中断工作 头部索引必须是 由以下人员原子编写 修改 多个中断 不是线程 通过单
  • 哪里可以找到 __sync_add_and_fetch_8?

    尝试使用 sync add and fetch 时出现错误 test8 cpp text 0x90e undefined reference to sync add and fetch 8 collect2 ld returned 1 ex
  • gcc 中的线程安全原子操作

    在我编写的一个程序中 我有很多代码如下 pthread mutex lock frame gt mutex frame gt variable variable pthread mutex unlock frame gt mutex 如果中
  • 为什么原子操作被认为是线程安全的?

    原子操作如何成为线程安全的 我读过有关该主题的内容维基百科关于线程安全的文章 http en wikipedia org wiki Thread safety 但文章并没有真正解释幕后的过程 换句话说 为什么线程A执行的 原子 操作不能被线
  • Objective-C:标量属性默认为原子?

    一位朋友告诉我 标量属性 BOOL NSInteger 等 的 property 默认值是非原子的 IE property BOOL followVenmo 默认为 property nonatomic BOOL followVenmo 但
  • 如何自动更新最大值?

    在串行代码中 更新最大值可以简单地通过以下方式完成 template
  • C++11内存_顺序_获取和内存_顺序_释放语义?

    http en cppreference com w cpp atomic memory order http en cppreference com w cpp atomic memory order以及其他 C 11 在线参考 将 me
  • AtomicReference.compareAndSet() 使用什么来确定?

    假设你有以下课程 public class AccessStatistics private final int noPages noErrors public AccessStatistics int noPages int noErro
  • 在使用 libstdc++ 进行调试期间强制使用 std::atomic 内部的锁

    我用谷歌搜索了一下 似乎找不到GCC选项或libstdc 库宏为此 是否可以强制在所有的内部使用锁定std atomic模板专业化 在某些平台上 某些专业化无论如何都会锁定 因此这似乎是一个可行的选择 过去我发现使用std atomic使用
  • CPU Relax 指令和 C++11 原语

    我注意到许多使用特定于操作系统的原语实现的无锁算法 例如所描述的自旋锁here http locklessinc com articles locks 使用 Linux 特定的原子原语 经常使用 cpurelax 指令 使用 GCC 可以通
  • Objective-C 2.0中的多线程问题

    我有我的主应用程序委托 其中包含一个返回对象的方法 该应用程序委托在主线程上运行 我还有一个在不同线程上运行的 NSOperation 除了希望有时能够在主线程上调用我的应用程序委托方法之外 我还需要从 NSOperation 线程中调用它
  • 对原子变量的非原子操作,反之亦然[重复]

    这个问题在这里已经有答案了 给出以下代码 static int x static void f for int i 0 i lt 100 i atomic fetch add x 3 进一步 假设f由两个线程同时调用 C C 内存模型是否保
  • 何时在多线程中使用 易失性?

    如果有两个线程访问全局变量 那么许多教程都说使该变量成为易失性的 以防止编译器将变量缓存在寄存器中 从而无法正确更新 然而 两个线程都访问共享变量需要通过互斥体进行保护 不是吗 但在这种情况下 在线程锁定和释放互斥体之间 代码位于关键部分
  • 在 x86-64 多核机器上以 C++ Atomic 读取和写入 int

    我读了this https stackoverflow com questions 5002046 atomicity in c myth or reality 我的问题很相似但又有些不同 请注意 我知道 C 0x 不能保证这一点 但我特别
  • std::atomic::load 的内存排序行为

    我是否错误地假设atomic load也应该充当内存屏障 确保所有以前的非原子的写入将对其他线程可见吗 为了显示 volatile bool arm1 false std atomic bool arm2 false bool trigge

随机推荐

  • ndk-build配置、Android Studio jni的配置以及jni常见问题的解决

    最近项目用到了jni比较频繁 android studio 配置jni也是必须的 但不知道是不是运气问题 我在自己电脑使用jni一点问题都没有 可以说是无障碍 但是 一使用公司电脑配置就出现了一大片编译报错 编译不通过的问题 抱着不怕搞事情
  • 268道Go语言面试真题及详解+100例代码实例+DDD实践

    Go最近动静挺大的 刚刚发布的1 18包含以下几大特性 1 泛型 2 模糊测试 Fuzzing 3 工作空间 Workspaces 4 20 性能提升 Apple M1 ARM64 和 PowerPC64 用户开心了 由于 Go 1 17
  • SSH(ssh: connect to host localhost port 22: Connection refused)问题的解决

    centos默认并没有安装ssh服务 如果通过ssh链接centos 需要自己手动安装openssh server 判断是否安装ssh服务 可以通过如下命令进行 输入 ssh localhost 如果 输出 ssh connect to h
  • 如何使用计算机查询本机网卡信息,本机mac地址查询的三种方法

    现在电脑非常流行 大部分的学生以及白领或者说每一个家庭几乎都有一台电脑 不过大家对于电脑的认识却没有这么高的普及度 很多人对于它的了解仅仅停留在使用电脑看视频用软件的层面 对于电脑自身的认识不是很多 例如本机mac地址查询这个问题就难倒了很
  • ubuntu 12.04安装OpenGL

    安装 建立基本编译环境 首先不可或缺的 就是编译器与基本的函式库 如果系统没有安装的话 请依照下面的方式安装 sudo apt get install build essential 安装OpenGL Library 接下来要把我们会用到的
  • torchvision详细介绍

    前言 深度学习道路漫漫 唯有不断总结 脚踏实地才能造就一番就成 也不断勉励自己 不要放弃 相信自己可以的 共勉 torchvision简介 torchvision是pytorch的一个图形库 它服务于PyTorch深度学习框架的 主要用来构
  • 基础不牢地动山摇之IO流1(File、FilelnputStream、FileOutputStream)

    目录 文件与文件流理解 创建文件常用的三种方式 File构造方法 获取文件信息 目录的操作和文件删除 1 删除文件 2 删除目录 3 创建多级目录 IO流原理及流的分类 原理 分类 IO流体系图 常用的类 InputStream 字节输入流
  • matlab最小二乘法_最小二乘法原理详解

    本文是 Least squares approximation 的学习笔记 这个视频从线性代数的角度 对最小二乘法的原理讲解的通俗易懂 1 提出问题 如上图所示 A 是一个n行k列的矩阵 每行可以看作是一个观测数据 或者一个训练样本 的输入
  • 包区别 版本_Lerna-如何优雅地管理多个npm包

    关于 Lerna Lerna A tool for managing JavaScript projects with multiple packages lerna js org 对于 lerna 的两段描述 A tool for man
  • 锐捷路由技术系列

    1 锐捷路由技术 锐捷路由器基本功能的初始化配置 主机名 推荐配置 Ruijie config hostnameNAME txt 将设备命名为NAME txt 接口描述 推荐配置 XWRJ config interfaceinterface
  • Linux less命令和Linux head命令

    less 工具也是对文件或其它输出进行分页显示的工具 应该说是linux正统查看文件内容的工具 功能极其强大 less 的用法比起 more 更加的有弹性 在 more 的时候 我们并没有办法向前面翻 只能往后面看 但若使用了 less 时
  • python之类、对象详解,实例化代码示例,构造函数与析构函数,私有属性和方法

    世界万物节皆可分类 世界万物皆可对象 只要对象 肯定属于某种类 只要对象 肯定有属性 类 具有相同属性 方法对象的抽象 对象 类的实例化 每个对象可有不同属性 类的三大特性 封装 将数据方法放到类里 类就变成了一个胶囊或者容器 继承 一个类
  • 安装Cpython解释器(day02)

    安装Cpython解释器 Python解释器目前已支持所有主流操作系统 在Linux Unix Mac系统上自带Python解释器 在Windows系统上需要安装一下 具体步骤如下 1 1 下载python解释器 打开官网 https ww
  • Sql执行平时都很快但是偶尔就会很慢

    Sql执行平时都很快但是偶尔就会很慢 记录一下在翻看MySQL技术文章的资料 觉得很不错就自己记录一下 大部分来源于网络 SQL执行变慢的原因 一条Sql执行很慢 那是每次执行都慢还是偶尔慢 简单的总结一下 一 针对偶尔慢的原因 数据库在刷
  • 蓝桥杯零基础冲过赛-第22天

    注意 因为蓝桥杯大部分题目都会涉及到数据规模过大问题 所以大整数是解决数据规模过大的问题的其中一种最简便的方式 核心 竖式个位对齐原理 文章目录 大整数加法 大整数减法 大整数乘法 大整数除法 大整数余数 大整数加法 意义 因为数据类型有s
  • 摸鱼时间少? 是时候学会用Vue自定义指令进行业务开发了

    文章目录 前言 一 博主用Vue自定义指令在业务中实现了什么需求 1 首屏Loading切换指令 用来占位 支持调节Loading样式 2 复制指令 3 文件流形式下载后端数据 转blob下载 4 防抖 支持设置延迟时间 5 按钮或菜单权限
  • 永远怀念左耳朵耗子陈皓——IT界的失去

    2023年 中国IT界遭遇了一次巨大的损失 左耳朵耗子陈皓先生的离世让人震惊和悲伤 作为一位杰出的技术专家和开源倡导者 他为IT界做出了卓越贡献 本文将回顾他的职业生涯和他对IT界的重要影响 以及他离世后的深远意义 第一部分 IT界的璀璨明
  • Opencv

    Opencv 检测框线 模糊判断 计算图片相似度 开操作检测横竖线 拉普拉斯方差判断模糊度 直方图统计判断图片相似性 开操作检测横竖线 开操作是先选定合适结构元对图像进行腐蚀处理再用相同结构元对图像进行膨胀处理 开操作可以平滑物体轮廓 断开
  • C++并发编程(5):std::unique_lock、互斥量所有权传递、锁的粒度

    std unique lock lt gt 灵活加锁 参考博客 线程间共享数据 使用互斥量保护共享数据 C 多线程unique lock详解 多线程编程 五 unique lock 相较于std lock guard std unqiue
  • iOS - 线程中常见的几种锁

    线程锁主要是用来解决 共享资源 的问题 实际开发中或多或少的都会用到各类线程锁 为了线程的安全我们有必要了解常见的几种锁 下面是本人查看一些大牛的博客然后整理的内容 加上自己的一些见解 水平有限 如果不慎有误 欢迎交流指正 常见锁列举 自旋