iOS——锁与死锁问题

iOS中的锁

  • 什么是锁
  • 锁的分类
    • 互斥锁
      • [1. @synchronized](#1. @synchronized)
      • [2. NSLock](#2. NSLock)
      • [3. pthread](#3. pthread)
    • 递归锁
      • [1. NSRecursiveLock](#1. NSRecursiveLock)
      • [2. pthread](#2. pthread)
    • 信号量Semaphore
      • [1. dispatch_semaphore_t](#1. dispatch_semaphore_t)
      • [2. pthread](#2. pthread)
    • 条件锁
      • [1. NSCodition](#1. NSCodition)
      • [2. NSCoditionLock](#2. NSCoditionLock)
      • [3. POSIX Conditions](#3. POSIX Conditions)
    • 分布式锁
    • 读写锁
      • [1. dispatch_barrier_async / dispatch_barrier_sync](#1. dispatch_barrier_async / dispatch_barrier_sync)
      • [2. pthread](#2. pthread)
    • 自旋锁
      • [1. OSSpinLock](#1. OSSpinLock)
      • [2. os_unfair_lock](#2. os_unfair_lock)
    • [atomic(property) set / get](#atomic(property) set / get)
    • ONCE
  • 死锁问题

在iOS开发里面,锁是为了保护共享资源的访问确保线程安全性和避免竞争条件。iOS的应用通常在多线程的环境下运行,之前学习的多线程GCDOperation Queue都是执行并发任务的,多个线程可能同时访问某个对象,所以锁可以确保每次只有一个线程能够修改或访问共享资源,保护了数据的安全,避免了资源冲突。

什么是锁

在过去几十年并发研究领域的出版物中,锁总是扮演着坏人的角色,锁背负的指控包括引起死锁、锁封护(luyang注:lock convoying,多个同优先级的线程重复竞争同一把锁,此时大量虽然被唤醒而得不到锁的线程被迫进行调度切换,这种频繁的调度切换相当影响系统性能)、饥饿、不公平、data races以及其他许多并发带来的罪孽。有趣的是,在共享内存并行软件中真正承担重担的是------锁。

在计算机科学中,锁是一种同步机制,用于多线程环境中对资源访问的限制。你可以理解成它用于排除并发的一种策略。

objectivec 复制代码
	if (lock == 0) {
		lock = myPID;
	}

上面这段代码并不能保证这个任务有锁,因此它可以在同一时间被多个任务执行。这个时候就有可能多个任务都检测到lock是空闲的,因此两个或者多个任务都将尝试设置lock,而不知道其他的任务也在尝试设置lock。这个时候就会出问题了。再看看下面这段代码:

swift 复制代码
	class Account {
    private(set) var val: Int = 0
    public func add(x: Int) {
        objc_sync_enter(self)
        defer {
            objc_sync_exit(self)
        }
        val += x
    }
    
    public func minus(x: Int) {
        objc_sync_enter(self)
        defer {
            objc_sync_exit(self)
        }
        val -= x;
    }
}

这样就能防止多个任务去修改val了。

锁的分类

锁根据不同的性质可以分成不同的类。

在WiKiPedia介绍中,一般的锁都是建议锁,也就四每个任务去访问公共资源的时候,都需要取得锁的资讯,再根据锁资讯来确定是否可以存取。若存取对应资讯,锁的状态会改变为锁定,因此其他线程不会访问该资源,当结束访问时,锁会释放,允许其他任务访问。有些系统有强制锁,若未经授权的锁访问锁定的资料,在访问时就会产生异常。

在iOS中,锁分为互斥锁、递归锁、信号量、条件锁、自旋锁、读写锁(一种特所的自旋锁)、分布式锁

对于数据库的锁分类:

分类方式 分类
按锁的粒度划分 表级锁、行级锁、页级锁
按锁的级别划分 共享锁、排他锁
按加锁的方式划分 自动锁、显示锁
按锁的使用方式划分 乐观锁、悲观锁
按操作划分 DML锁、DDL锁

下面是各种锁性能的图表

互斥锁

在编程中,引入对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为"互斥锁"的标记,这个标记用来保证在任一时刻,只能有一个线程访问对象。

1. @synchronized

  • @synchronized要一个参数,这个参数相当于信号量
objectivec 复制代码
// 用在防止多线程访问属性上比较多
- (void)setTestInt:(NSInteger)testInt {
	@synchronized (self) {
		_testInt = testInt;
	}
}

2. NSLock

  • block及宏定义
objectivec 复制代码
// 定义block类型
typedef void(^MMBlock)(void);

// 定义获取全局队列方法
#define MM_GLOBAL_QUEUE(block) \
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ \
    while (1) { \
        block();\
    }\
})
  • 测试代码
objectivec 复制代码
NSLock *lock = [[NSLock alloc] init];
MMBlock block = ^{
	[lock lock];
	NSLog(@"执行操作");
	sleep(1);
	[lock unlock];
};
MM_GLOBAL_QUEUE(block);

隔一秒输出一次。

3. pthread

pthread除了创建互斥锁,还可以创建递归锁、读写锁、once等锁。稍后会介绍一下如何使用。如果想要深入学习pthread请查阅相关文档、资料单独学习。

  • 静态初始化: pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

  • 动态初始化: pthread_mutex_init() 函数是以动态方式创建互斥锁的,参数 attr 指定了新建互斥锁的属性。如果参数 attr 为 NULL ,使用默认的属性,返回0代表初始化成功。这种方式可以初始化普通锁、递归锁(同 ** NSRecursiveLock** ), 初始化方式有些复杂。

  • 此类初始化方法可设置锁的类型,PTHREAD_MUTEX_ERRORCHECK 互斥锁不会检测死锁, PTHREAD_MUTEX_ERRORCHECK 互斥锁可提供错误检查, PTHREAD_MUTEX_RECURSIVE 递归锁, PTHREAD_PROCESS_DEFAULT 映射到 PTHREAD_PROCESS_NORMAL .

下面源自YYKitcopy:

objectivec 复制代码
#import <pthread.h>

//YYKit
static inline void pthread_mutex_init_recursive(pthread_mutex_t *mutex, bool recursive) {
#define YYMUTEX_ASSERT_ON_ERROR(x_) do { \
__unused volatile int res = (x_); \
assert(res == 0); \
} while (0)
    assert(mutex != NULL);
    if (!recursive) {
        //普通锁
        YYMUTEX_ASSERT_ON_ERROR(pthread_mutex_init(mutex, NULL));
    } else {
        //递归锁
        pthread_mutexattr_t attr;
        YYMUTEX_ASSERT_ON_ERROR(pthread_mutexattr_init (&attr));
        YYMUTEX_ASSERT_ON_ERROR(pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE));
        YYMUTEX_ASSERT_ON_ERROR(pthread_mutex_init (mutex, &attr));
        YYMUTEX_ASSERT_ON_ERROR(pthread_mutexattr_destroy (&attr));
    }
#undef YYMUTEX_ASSERT_ON_ERROR
}
  • 测试代码
objectivec 复制代码
__block pthread_mutex_t lock;
    pthread_mutex_init_recursive(&lock,false);

    MMBlock block0=^{
        NSLog(@"线程 0:加锁");
        pthread_mutex_lock(&lock);
        NSLog(@"线程 0:睡眠 1 秒");
        sleep(1);
        pthread_mutex_unlock(&lock);
        NSLog(@"线程 0:解锁");
    };
    MM_GLOBAL_QUEUE(block0);

    MMBlock block1=^(){
        NSLog(@"线程 1:加锁");
        pthread_mutex_lock(&lock);
        NSLog(@"线程 1:睡眠 2 秒");
        sleep(2);
        pthread_mutex_unlock(&lock);
        NSLog(@"线程 1:解锁");
    };
    MM_GLOBAL_QUEUE(block1);

    MMBlock block2=^{
        NSLog(@"线程 2:加锁");
        pthread_mutex_lock(&lock);
        NSLog(@"线程 2:睡眠 3 秒");
        sleep(3);
        pthread_mutex_unlock(&lock);
        NSLog(@"线程 2:解锁");
    };
    MM_GLOBAL_QUEUE(block2);
  • 运行结果
objectivec 复制代码
 线程 2:加锁
 线程 0:加锁
 线程 1:加锁
 线程 2:睡眠 3 秒
 线程 2:解锁
 线程 0:睡眠 1 秒
 线程 2:加锁
 线程 0:解锁
 线程 1:睡眠 2 秒
 线程 0:加锁

递归锁

同一个线程可以多次加锁,不会造成死锁

举例:

objectivec 复制代码
NSLock *lock = [[NSLock alloc] init];
 
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    static void (^RecursiveMethod)(int);
 
    RecursiveMethod = ^(int value) {
 
        [lock lock];
        if (value > 0) {
 
            NSLog(@"value = %d", value);
            sleep(2);
            RecursiveMethod(value - 1);
        }
        [lock unlock];
    };
 
    RecursiveMethod(5);
});

这段代码是一个典型的死锁情况。在我们的线程中,RecursiveMethod是递归调用的。所有每次进入这个block时,都会去加一次锁,而从第二次开始,由于锁已经被使用了且没有解锁,所有它需要等待锁被解除,这样就导致了死锁,线程被阻塞住了。控制台会输出如下信息:

value = 5

*** -[NSLock lock]: deadlock ( '(null)') *** Break on _NSLockError() to debug.

1. NSRecursiveLock

  • 实现代码
objectivec 复制代码
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    MM_GLOBAL_QUEUE(^{
        static void (^RecursiveBlock)(int);
        RecursiveBlock = ^(int value) {
            [lock lock];
            if (value > 0) {
                NSLog(@"加锁层数 %d", value);
                sleep(1);
                RecursiveBlock(--value);
            }
            [lock unlock];
        };
        RecursiveBlock(3);
    });
  • 输出结果(从输出结果可以看出并未发生死锁):
objectivec 复制代码
加锁层数 3
加锁层数 2
加锁层数 1
加锁层数 3
加锁层数 2
加锁层数 1
加锁层数 3
加锁层数 2

2. pthread

  • 代码实现
objectivec 复制代码
__block pthread_mutex_t lock;
    //第二个参数为true生成递归锁
    pthread_mutex_init_recursive(&lock,true);

    MM_GLOBAL_QUEUE(^{
        static void (^RecursiveBlock)(int);
        RecursiveBlock = ^(int value) {
            pthread_mutex_lock(&lock);
            if (value > 0) {
                NSLog(@"加锁层数 %d", value);
                sleep(1);
                RecursiveBlock(--value);
            }
            pthread_mutex_unlock(&lock);
        };
        RecursiveBlock(3);
    });
  • 输出结果(同样,结果显示并未发生死锁):
objectivec 复制代码
加锁层数 3
加锁层数 2
加锁层数 1
加锁层数 3
加锁层数 2
加锁层数 1
加锁层数 3
加锁层数 2

信号量Semaphore

信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量

1. dispatch_semaphore_t

同步实现

objectivec 复制代码
// 参数可以理解为信号的总量,传入的值必须大于或等于0,否则,返回NULL
// dispatch_semaphore_signal + 1
// dispatch_semaphore_wait等待信号,当 <= 0会进入等待状态
__block dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
MM_GLOBAL_QUEUE(^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"这里简单写一下用法,可自行实现生产者、消费者");
        sleep(1);
        dispatch_semaphore_signal(semaphore);
    });

2. pthread

objectivec 复制代码
__block pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
    __block pthread_cond_t cond=PTHREAD_COND_INITIALIZER;

    MM_GLOBAL_QUEUE(^{
        //NSLog(@"线程 0:加锁");
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex);
        NSLog(@"线程 0:wait");
        pthread_mutex_unlock(&mutex);
        //NSLog(@"线程 0:解锁");
    });

    MM_GLOBAL_QUEUE(^{
        //NSLog(@"线程 1:加锁");
        sleep(3);//3秒发一次信号
        pthread_mutex_lock(&mutex);
        NSLog(@"线程 1:signal");
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(&mutex);
        //NSLog(@"线程 1:加锁");
    });

条件锁

1. NSCodition

NSCondition 的对象实际上作为一个锁和一个线程检查器:锁主要为了当检测条件时保护数据源,执行条件引发的任务;线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。

  • NSCondition同样实现了NSLocking协议,所以它和NSLock一样,也有NSLocking协议的lock和unlock方法,可以当做NSLock来使用解决线程同步问题,用法完全一样。
objectivec 复制代码
- (void)getIamgeName:(NSMutableArray *)imageNames{
 	 NSCondition *lock = [[NSCondition alloc] init];
    NSString *imageName;
    [lock lock];
    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObject:imageName];
    }
    [lock unlock];
}
  • 同时,NSCondition提供更高级的用法。wait和signal,和条件信号量类似。比如我们要监听imageNames数组的个数,当imageNames的个数大于0的时候就执行清空操作。思路是这样的,当imageNames个数大于0时执行清空操作,否则,wait等待执行清空操作。当imageNames个数增加的时候发生signal信号,让等待的线程唤醒继续执行。
  • NSConditionNSLock@synchronized等是不同的是,NSCondition可以给每个线程分别加锁,加锁后不影响其他线程进入临界区。这是非常强大。 但是正是因为这种分别加锁的方式,NSCondition使用wait并使用加锁后并不能真正的解决资源的竞争。比如我们有个需求:不能让m<0。假设当前m=0,线程A要判断到m>0为假,执行等待;线程B执行了m=1操作,并唤醒线程A执行m-1操作的同时线程C判断到m>0,因为他们在不同的线程锁里面,同样判断为真也执行了m-1,这个时候线程A和线程C都会执行m-1,但是m=1,结果就会造成m=-1.
  • 当我用数组做删除试验时,做增删操作并不是每次都会出现,大概3-4次后会出现。单纯的使用lockunlock是没有问题的。
objectivec 复制代码
- (void)executeNSCondition {
    NSCondition* lock = [[NSCondition alloc] init];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (NSUInteger i=0; i<3; i++) {
            sleep(2);
            if (i == 2) {
                [lock lock];
                [lock broadcast];
                [lock unlock];
            }
            
        }
    });
    
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        [self threadMethodOfNSCodition:lock];
    });
    
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        [self threadMethodOfNSCodition:lock];
    });
    
    
}

-(void)threadMethodOfNSCodition:(NSCondition*)lock{
    [lock lock];
    [lock wait];
    [lock unlock];
    
}

2. NSCoditionLock

  • lock不分条件,如果锁没被申请,直接执行代码
  • unlock不会清空条件,之后满足条件的锁还会执行
  • unlockWithCondition 我的理解就是设置解锁条件(同一时刻只有一个条件,如果已经设置条件,相当于修改条件)
  • lockWhenCondition满足特定条件,执行相应代码
  • NSConditionLock同样实现了NSLocking协议,试验过程中发现性能很低。
  • NSConditionLock也可以像NSCondition一样做多线程之间的任务等待调用,而且是线程安全的。
objectivec 复制代码
- (void)executeNSConditionLock {
    NSConditionLock* lock = [[NSConditionLock alloc] init];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (NSUInteger i=0; i<3; i++) {
            sleep(2);
            if (i == 2) {
                [lock lock];
                [lock unlockWithCondition:i];
            }
        }
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        [self threadMethodOfNSCoditionLock:lock];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        [self threadMethodOfNSCoditionLock:lock];
    });   
}
-(void)threadMethodOfNSCoditionLock:(NSConditionLock*)lock{
    [lock lockWhenCondition:2];
    [lock unlock];
}

3. POSIX Conditions

  • POSIX条件锁需要互斥锁和条件两项来实现,虽然看起来没有什么关系,但在运行时中,互斥锁将会与条件结合起来。线程将被一个互斥和条件结合的信号来唤醒。
  • 首先初始化条件和互斥锁,当ready_to_gofalse的时候,进入循环,然后线程将会被挂起,直到另一个线程将ready_to_go设置为true的时候,并且发送信号的时候,该线程才会被唤醒。
objectivec 复制代码
pthread_mutex_t mutex;
pthread_cond_t condition;
Boolean     ready_to_go = true;
void MyCondInitFunction()
{
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&condition, NULL);
}
void MyWaitOnConditionFunction()
{
    // Lock the mutex.
    pthread_mutex_lock(&mutex);
    // If the predicate is already set, then the while loop is bypassed;
    // otherwise, the thread sleeps until the predicate is set.
    while(ready_to_go == false)
    {
        pthread_cond_wait(&condition, &mutex);
    }
    // Do work. (The mutex should stay locked.)
    // Reset the predicate and release the mutex.
    ready_to_go = false;
    pthread_mutex_unlock(&mutex);
}
void SignalThreadUsingCondition()
{
    // At this point, there should be work for the other thread to do.
    pthread_mutex_lock(&mutex);
    ready_to_go = true;
    // Signal the other thread to begin work.
    pthread_cond_signal(&condition);
    pthread_mutex_unlock(&mutex);
}

分布式锁

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

NSDistributedLock

  • 处理多个进程或多个程序之间互斥问题。
  • 一个获取锁的进程或程序在是否锁之前挂掉,锁不会被释放,可以通过breakLock方式解锁。
  • iOS很少用到,暂不详细研究。

读写锁

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

1. dispatch_barrier_async / dispatch_barrier_sync

先来一个需求:假设我们原先有6个任务要执行,我们现在要插入一个任务0,这个任务0要在1、2、4都并发执行完之后才能执行,而4、5、6号任务要在这几个任务0结束后才允许并发。

objectivec 复制代码
- (void)rwLockOfBarrier {
	dispatch_queue_t queue = dispatch_queue_create("thread", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{
        NSLog(@"test1");
    });
    dispatch_async(queue, ^{
        NSLog(@"test2");
    });
    dispatch_async(queue, ^{
        NSLog(@"test3");
    });
    dispatch_barrier_sync(queue, ^{
        for (int i = 0; i <= 500000000; i++) {
            if (5000 == i) {
                NSLog(@"point1");
            }else if (6000 == i) {
                NSLog(@"point2");
            }else if (7000 == i) {
                NSLog(@"point3");
            }
        }
        NSLog(@"barrier");
    });
    NSLog(@"aaa");
    dispatch_async(queue, ^{
        NSLog(@"test4");
    });
    dispatch_async(queue, ^{
        NSLog(@"test5");
    });
    dispatch_async(queue, ^{
        NSLog(@"test6");
    });
}
  • 共同点:
  1. 等待在它前面插入队列的任务先执行完;
  2. 等待他们自己的任务执行完再执行后面的任务。
  • 不同点:
  1. dispatch_barrier_sync将自己的任务插入到队列的时候,需要等待自己的任务结束之后才会继续插入被写在它后面的任务,然后执行它们;
  2. dispatch_barrier_async将自己的任务插入到队列之后,不会等待自己的任务结束,它会继续把后面的任务插入队列,然后等待自己的任务结束后才执行后面的任务。

2. pthread

与上述初始化方式类似,静态THREAD_RWLOCK_INITIALIZER、动态pthread_rwlock_init()pthread_rwlock_destroy用来销毁该锁

objectivec 复制代码
#import <pthread.h>

    __block pthread_rwlock_t rwlock;
    pthread_rwlock_init(&rwlock,NULL);

    //读
    MM_GLOBAL_QUEUE(^{
        //NSLog(@"线程0:随眠 1 秒");//还是不打印能直观些
        sleep(1);
        NSLog(@"线程0:加锁");
        pthread_rwlock_rdlock(&rwlock);
        NSLog(@"线程0:读");
        pthread_rwlock_unlock(&rwlock);
        NSLog(@"线程0:解锁");
    });
    //写
    MM_GLOBAL_QUEUE(^{
        //NSLog(@"线程1:随眠 3 秒");
        sleep(3);
        NSLog(@"线程1:加锁");
        pthread_rwlock_wrlock(&rwlock);
        NSLog(@"线程1:写");
        pthread_rwlock_unlock(&rwlock);
        NSLog(@"线程1:解锁");
    });

自旋锁

何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

1. OSSpinLock

  • 使用方式
objectivec 复制代码
// 初始化
spinLock = OS_SPINKLOCK_INIT;
// 加锁
OSSpinLockLock(&spinLock);
// 解锁
OSSpinLockUnlock(&spinLock);

不过,自旋锁存在优先级反转的问题。

2. os_unfair_lock

自旋锁已经不再安全,然后苹果推出了 os_unfair_lock_t ,这个锁解决了优先级反转的问题。

objectivec 复制代码
	os_unfair_lock_t unfairLock;
    unfairLock = &(OS_UNFAIR_LOCK_INIT);
    os_unfair_lock_lock(unfairLock);
    os_unfair_lock_unlock(unfairLock);

atomic(property) set / get

利用setter / getter 接口的属性实现原子操作,进而确保"被共享"的变量在多线程中读写安全,这已经是不能满足部分多线程同步要求。

  • 在定义 property 的时候, 有atomic 和 nonatomic的属性修饰关键字。
  • 对于atomic的属性,系统生成的 getter/setter 会保证 get、set 操作的完整性,不受其他线程影响。比如,线程 A 的 getter 方法运行到一半,线程 B 调用了 setter:那么线程 A 的 getter 还是能得到一个完好无损的对象。
  • 而nonatomic就没有这个保证了。所以,nonatomic的速度要比atomic快。

atomic

  • 是默认的
  • 会保证 CPU 能在别的线程来访问这个属性之前,先执行完当前流程
  • 速度不快,因为要保证操作整体完成

nonatomic

  • 不是默认的
  • 更快
  • 线程不安全
  • 如有两个线程访问同一个属性,会出现无法预料的结果

注意,atomic一定线程安全吗?答案是否定的

因为atomic只是对属性的setter/getter方法加锁,所以说只能保证在调用setter/getter方法时线程安全。

假设有一个 atomic 的属性 "name",如果线程 A 调[self setName:@"A"],线程 B 调[self setName:@"B"],线程 C 调[self name],那么所有这些不同线程上的操作都将依次顺序执行------也就是说,如果一个线程正在执行 getter/setter,其他线程就得等待。因此,属性 name 是读/写安全的。

但是,如果有另一个线程 D 同时在调[name release],那可能就会crash,因为 release 不受 getter/setter 操作的限制。也就是说,这个属性只能说是读/写安全的,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作。线程安全需要开发者自己来保证。

ONCE

GCD

多用于创建单例。

objectivec 复制代码
+ (instancetype) sharedInstance {
	static id __instance = nil;
	static dispatch_once_t onceToken;
	dispatch_once(&onceToken, ^{
		__instance = [[self alloc] init];
	});
	return __instance;
}

pthread

objectivec 复制代码
// 定义方法
void fun() {
	NSLog(@"%@", [NSThread currentThread]);
}

- (void)onceOfPthread {
    __block pthread_once_t once = PTHREAD_ONCE_INIT;
    
    int i= 0;
    while (i > 5) {
        pthread_once(&once, fun);
        i++;
    }
}

死锁问题

死锁是一个典型的并发问题,它在iOS中可能会发生。在iOS中,如果一个线程在等待一个任务完成,而这个任务又在等待该线程释放某种资源,那么就会产生一个死锁。尤其是在使用GCD(Grand Central Dispatch)NSOperation时,死锁问题可能会更频繁地出现。

Objective-C中的一个典型死锁示例是在主线程上使用同步调度块:

objectivec 复制代码
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
    // Do some work...
});

在上面的例子中,我们要求主队列同步执行一个任务。但是,因为这是在主线程上发生的,并且主线程正在等待该任务完成,所以就产生了一个死锁。由于主线程已经被阻塞,所以它不能执行队列中的任务,而我们又在等待这个任务完成。

为了防止死锁,我们应该尽量避免在已经在执行任务的线程上同步调度任务。相反,我们应该用异步调度替代,或者使用并行队列来进行同步调度。

以下是一个可以避免死锁的改进示例:

objectivec 复制代码
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_sync(queue, ^{
    // Do some work...
});

在这个例子中,我们使用全局并行队列而不是主队列来同步执行任务。因此,主线程可以继续执行其他任务,而不会被阻塞。

或者:

objectivec 复制代码
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
    // Do some work...
});

我们使用dispatch_async将一个任务放入了主队列中,并且没有设置任何等待这个任务完成的操作。这意味着任务会在主队列中异步执行,而主队列会一直处理任务直到任务完成,而不会等待任务执行完毕。

由于任务是在主队列中执行的,而且没有任何等待操作,主线程会继续往下执行代码,而不会被这个任务阻塞。


  1. 同步(sync)方式调用主队列(Main Dispatch Queue) ,因为主线程始终等待主队列来进行任务调度。但由于我们使用了 sync,这就意味着当前的主线程会等待我们的任务
    完成才能进行下一步,这样就形成了循环等待,造成了死锁。例如:
objectivec 复制代码
dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"This is a deadlock");
});

这种情况与第四种死锁是一致的,满足sync中向当前未完成的串行队列发送任务这一条件,不过这是在主线程中的主队列。

当然,凡事都有例外

objectivec 复制代码
// 使用 NSThread 的 detachNewThreadSelector 方法会创建线程,并自动启动线程执行 selector 任务
[NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];

/**
 * 同步执行 + 主队列
 * 特点(主线程调用):互等卡主不执行。
 * 特点(其他线程调用):不会开启新线程,执行完一个任务,再执行下一个任务。
 */
- (void)syncMain {
    
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"syncMain---begin");
    
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    dispatch_sync(queue, ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_sync(queue, ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_sync(queue, ^{
        // 追加任务 3
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    NSLog(@"syncMain---end");
}

当我们使用NSThread创建一个新的线程,而不是在主线程中"同步+主队列",也不会发生卡死。

所以应该在这里默认一个前提:就是在主线程中执行。

  1. 递归锁中的死锁,比如使用 NSRecursiveLock。在同一个线程中,多次调用 lock,而比较少的或者忘记 unLock,由于一直在等待 unLock 的释放,这样同样会造成死锁情况:
objectivec 复制代码
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];

[lock lock];

// some code...

[lock lock]; // this will cause deadlock!

// some code...

[lock unlock];
[lock unlock];
  1. 多线程操作同一资源导致的死锁。这是在多个线程之间出现的,比如线程 A 锁住了资源 X,想使用资源 Y,但资源 Y 正好被线程 B 锁住,线程 B 正好等待线程 A 的资源 X,这样也会形成死锁。
objectivec 复制代码
- (void)methodA {
    // locking resource X
}

- (void)methodB {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // locking resource Y
        [self methodA]; // waiting for resource X to be released
    });
    [self methodB]; // waiting for resource Y to be released, hence causing deadlock
}
  1. 同步操作中向当前串行队列发送任务

由于我们自己创建并使用queue这个串行队列,但是queue中已经有了任务块,在块内添加任务3,会导致任务块不能完成导致死锁。所以最终结果是打印1,5,2,然后崩溃。这种情况是第一种死锁的父集。

objectivec 复制代码
dispatch_queue_t queue = dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"1"); // 任务1
dispatch_async(queue, ^{
    NSLog(@"2"); // 任务2
    dispatch_sync(queue, ^{  
        NSLog(@"3"); // 任务3
    });
    NSLog(@"4"); // 任务4
});
NSLog(@"5"); // 任务5

这些例子都揭示了产生死锁的可能性,避免死锁的方式最主要的就是避免同步调用,合理的使用锁以保证线程安全,对于资源的使用,注意资源的请求顺序,尽量减少资源的请求数等等。

相关推荐
奶油话梅糖2 小时前
深入解析交换机端口安全:Sticky MAC的工作原理与应用实践
网络·安全·macos
我现在不喜欢coding2 小时前
为什么runloop中先处理 blocks source0 再处理timer source1?
ios·面试
骑着猪狂飙3 小时前
iOS技术之通过Charles抓包http、https数据
网络协议·http·ios·https
1024小神5 小时前
macos使用brew报错解决办法
macos
爱转呼啦圈的小兔子6 小时前
Mac中修改Word的Normal.dotm文件
macos·word
wanghao6664557 小时前
Mac测试端口连接的几种方式
macos
Digitally20 小时前
如何将视频从安卓设备传输到Mac?
android·macos
心灵宝贝21 小时前
Mac用户安装JDK 22完整流程(Intel版dmg文件安装指南附安装包下载)
java·开发语言·macos
Dolphin_海豚1 天前
charles proxying iphone
前端·ios
Magnetic_h1 天前
【iOS】内存管理及部分Runtime复习
笔记·学习·macos·ios·objective-c·cocoa·xcode