iOS——锁与死锁问题

iOS中的锁

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

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

什么是锁

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

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

	if (lock == 0) {lock = myPID;}

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

	class Account {private(set) var val: Int = 0public 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要一个参数,这个参数相当于信号量
// 用在防止多线程访问属性上比较多
- (void)setTestInt:(NSInteger)testInt {@synchronized (self) {_testInt = testInt;}
}

2. NSLock

  • block及宏定义
// 定义block类型
typedef void(^MMBlock)(void);// 定义获取全局队列方法
#define MM_GLOBAL_QUEUE(block) \
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ \while (1) { \block();\}\
})
  • 测试代码
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:

#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
}
  • 测试代码
__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);
  • 运行结果
 线程 2:加锁线程 0:加锁线程 1:加锁线程 2:睡眠 3 秒线程 2:解锁线程 0:睡眠 1 秒线程 2:加锁线程 0:解锁线程 1:睡眠 2 秒线程 0:加锁

递归锁

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

举例:

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

  • 实现代码
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);});
  • 输出结果(从输出结果可以看出并未发生死锁):
加锁层数 3
加锁层数 2
加锁层数 1
加锁层数 3
加锁层数 2
加锁层数 1
加锁层数 3
加锁层数 2

2. pthread

  • 代码实现
__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);});
  • 输出结果(同样,结果显示并未发生死锁):
加锁层数 3
加锁层数 2
加锁层数 1
加锁层数 3
加锁层数 2
加锁层数 1
加锁层数 3
加锁层数 2

信号量Semaphore

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

1. dispatch_semaphore_t

同步实现

// 参数可以理解为信号的总量,传入的值必须大于或等于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

__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来使用解决线程同步问题,用法完全一样。
- (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是没有问题的。
- (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一样做多线程之间的任务等待调用,而且是线程安全的。
- (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的时候,并且发送信号的时候,该线程才会被唤醒。
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结束后才允许并发。

- (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用来销毁该锁

#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

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

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

2. os_unfair_lock

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

	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

多用于创建单例。

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

pthread

// 定义方法
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中的一个典型死锁示例是在主线程上使用同步调度块:

dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{// Do some work...
});

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

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

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

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_sync(queue, ^{// Do some work...
});

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

或者:

dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{// Do some work...
});

使用异步执行主队列,async允许等待当前队列的任务先执行,也不会阻塞。


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

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

  1. 递归锁中的死锁,比如使用 NSRecursiveLock。在同一个线程中,多次调用 lock,而比较少的或者忘记 unLock,由于一直在等待 unLock 的释放,这样同样会造成死锁情况:
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,这样也会形成死锁。
- (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,然后崩溃。这种情况是第一种死锁的父集。

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/18062.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

超全整理,Jmeter性能测试-常用Jmeter第三方插件详解(超细)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 Jmeter作为一个开…

React(4)

1.属性&#xff08;props&#xff09;初始 状态state都是组件内部写的&#xff0c;也就是A组件内的state就只能A组件里面用&#xff0c;其他组件复用不了。因此属性props就可以。 比如一个导航栏&#xff0c;首页有&#xff0c;购物车有&#xff0c;我的有&#xff0c;他们三个…

《吐血整理》进阶系列教程-拿捏Fiddler抓包教程(20)-Fiddler精选插件扩展安装让你的Fiddler开挂到你怀疑人生

1.简介 Fiddler本身的功能其实也已经很强大了&#xff0c;但是Fiddler官方还有很多其他扩展插件功能&#xff0c;可以更好地辅助Fiddler去帮助用户去开发、测试和管理项目上的任务。Fiddler已有的功能已经够我们日常工作中使用了&#xff0c;为了更好的扩展Fiddler&#xff0c…

P4780 Phi的反函数

题目 思路 φ(x)n 当指数均为1时n最小 证明&#xff1a;容斥原理 代码 #include<bits/stdc.h> using namespace std; #define int long long const int maxn1e9; int ansINT_MAX,n; bool f; map<int,bool> mp; bool is_prime(int n){if(n<1) return false;fo…

Spring事务创建与使用

目录 前言Spring中事务的实现声明式事务Transactional 作⽤范围Transactional 参数说明对于事务不回滚的解决方案 前言 在数据库中我们提到了 事务, 事务的定义为, 将一系列操作封装成一个整体去调用 , 要么一起成功, 要么一起失败 Spring中事务的实现 在Spring中事务的操作…

发npm包

重点文件 .github -> workflow -> .yml文件 发自己的包 新建dev分支&#xff0c;合并到master后自动执行 fork别人的包 fork -> base dev新建本地rebase-dev分支 -> 提交push后合并至dev -> dev合并至master后自动执行 值得注意的是&#xff0c;fork别人的…

flask 点赞系统

dianzan.html页面 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>点赞系统</title> </head> <body><h2>这是一个点赞系统</h2><table border"1"><…

【vue】vue-image-lazy图片懒加载使用与介绍【超详细+npm包源代码】

简介 当前插件是基于vue3&#xff0c;写的一个图片懒加载&#xff0c;文章最下方是npm包的源码&#xff0c;你可以自己拿去研究和修改&#xff0c;如有更好的想法可以留言&#xff0c;如果对你有帮助&#xff0c;可以点赞收藏和关注&#xff0c;谢谢。 后续会添加图片放大和切…

蓝桥云课ROS机器人旧版实验报告-07外设

项目名称 实验七 ROS[Kinetic/Melodic/Noetic]外设 成绩 内容&#xff1a;使用游戏手柄、使用RGBD传感器&#xff0c;ROS[Kinetic/Melodic/Noetic]摄像头驱动、ROS[Kinetic/Melodic/Noetic]与OpenCV库、标定摄像头、视觉里程计&#xff0c;点云库、可视化点云、滤波和缩…

Ansible自动化运维工具 —— Playbook 剧本

playbooks 本身由以下各部分组成 &#xff08;1&#xff09;Tasks&#xff1a;任务&#xff0c;即通过 task 调用 ansible 的模板将多个操作组织在一个 playbook 中运行 &#xff08;2&#xff09;Variables&#xff1a;变量 &#xff08;3&#xff09;Templates&#xff1a;模…

【JVM】(一)深入理解JVM运行时数据区

文章目录 一、JVM 运行流程二、虚拟机栈&#xff08;线程私有&#xff09;三、本地方法栈 &#xff08;线程私有&#xff09;四、方法区&#xff08;元数据区&#xff09;五、堆&#xff08;线程共享&#xff09;六、程序计数器&#xff08;线程私有&#xff09; 一、JVM 运行流…

华为华三思科 交换机基础配置一览

console密码修改 华为 user-interface console 0 authentication-mode password set authentication password cipher XXXXXXXXX华三 line aux 0 authentication-mode password set auth pass simple XXX思科 en configure terminal line console 0 password 123 login忘记…

TypeScript基础学习

目录 一、安装 1、下载国内镜像 2、安装 3、查看安装情况 4、使用例子 二、变量声明 1、规则 2、声明的四种方式 3、注意 4、类型断言 5、类型推断 6、变量作用域 三、基础类型&#xff08;共11种&#xff09; 1、Any 类型 2、Null 和 Undefined 3、never 类型…

【备战csp-j】 csp常考题型详解(2)

二.计算机网络。 1. TCP/IP 协议共有( )层协议 。 A.3 B.4 C.5 D.6 答案&#xff1a;B 解析&#xff1a; 2.Ipv4 地址是由( ) 位二进制数码表示的。 A.16 B.32 C.24 D.8 答案&#xff1a;B 解析&#xff1a;IP地址是IP协议提供的一种统一的地址格式。在目前使用的IPv…

Linux - 进程控制(进程替换)

0.引入 创建子进程的目的是什么&#xff1f; 就是为了让子进程帮我执行特定的任务 让子进程执行父进程的一部分代码 如果子进程想执行一个全新的程序代码呢&#xff1f; 那么就要使用 进程的程序替换 为什么要有程序替换&#xff1f; 也就是说子进程想执行一个全新的程序代码&a…

HCIP OSPF+BGP综合实验

题目 1、该拓扑为公司网络&#xff0c;其中包括公司总部、公司分部以及公司骨干网&#xff0c;不包含运营商公网部分。 2、设备名称均使用拓扑上名称改名&#xff0c;并且区分大小写。 3、整张拓扑均使用私网地址进行配置。 4、整张网络中&#xff0c;运行OSPF协议或者BGP协议…

python森林生物量(蓄积量)数据处理到随机森林估算全流程

python森林生物量&#xff08;蓄积量&#xff09;估算全流程 一.哨兵2号获取/处理/提取数据1.1 影像处理与下载采用云概率影像去云采用6S模型对1C级产品进行大气校正geemap下载数据到本地NDVI 1.2 各种参数计算&#xff08;生物物理变量、植被指数等&#xff09;LAI&#xff1a…

抖音引流推广的几个方法,抖音全自动引流脚本软件详细使用教学

大家好我是你们的小编一辞脚本&#xff0c;今天给大家分享新的知识&#xff0c;很开心可以在CSDN平台分享知识给大家,很多伙伴看不到代码我先录制一下视频 在给大家做代码&#xff0c;给大家分享一下抖音引流脚本的知识和视频演示 不懂的小伙伴可以认真看一下&#xff0c;我们…

【C++】总结9

文章目录 C从源代码到可执行程序经过什么步骤静态链接和动态链接类的对象存储空间C的内存分区内存池在成员函数中调用delete this会出现什么问题&#xff1f;如果在类的析构函数中调用delete this&#xff0c;会发生什么&#xff1f; C从源代码到可执行程序经过什么步骤 预处理…

java学习路程之篇六、进阶知识、常用API、Arrays工具类、冒泡排序、选择排序、二分查找、正则表达式

文章目录 1、Arrays工具类2、冒泡排序3、选择排序4、二分查找5、正则表达式 1、Arrays工具类 2、冒泡排序 3、选择排序 4、二分查找 5、正则表达式