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...
});

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

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


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

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

当然,凡事都有例外

// 使用 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 的释放,这样同样会造成死锁情况:
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.rhkb.cn/news/75503.html

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

相关文章

css实现卡片的左上角有一个三角形的遮盖效果

需求: 卡片的左上角有一个绿色的三角形标签,用来区分状态 实现: .vCard{position: relative;overflow: hidden; } .vCard::before {content: "";position: absolute;top: 0;left: 0;width: 0;height: 0;border-bottom: 20px solid transparent;border-left: 20px …

Ariadne’s Thread-使用文本提示改进对感染区域的分割胸部x线图像

论文&#xff1a;https://arxiv.org/abs/2307.03942&#xff0c; Miccai 2023 代码&#xff1a;GitHub - Junelin2333/LanGuideMedSeg-MICCAI2023: Pytorch code of MICCAI 2023 Paper-Ariadne’s Thread : Using Text Prompts to Improve Segmentation of Infected Areas fro…

2、Tomcat介绍(下)

组件分类 在Apache Tomcat中&#xff0c;有几个顶级组件&#xff0c;它们是Tomcat的核心组件&#xff0c;负责整个服务器的运行和管理。这些顶级组件包括&#xff1a; Server(服务器)&#xff1a;Tomcat的server.xml配置文件中的<Server>元素代表整个Tomcat服务器实例。每…

vmware网络配置

效果&#xff1a; 虚拟机和物理机网络互通&#xff1b; 虚拟机可以上外网 环境&#xff1a; vmware version 17.0.0 Centos 7.9 配置 1&#xff0c;vmware 菜单 - 编辑 - Virtual Network Edit 2&#xff0c; 选择VMnet8 VMnet information:NAT&#xff1b; 勾选2个…

运输层---概述

目录 运输层主要内容一.概述和传输层服务1.1 概述1.2 传输服务和协议1.3 传输层 vs. 网络层1.4 Internet传输层协议 二. 多路复用与多路分解&#xff08;解复用&#xff09;2.1 概述2.2 无连接与面向连接的多路分解&#xff08;解复用&#xff09;2.3面向连接的多路复用*2.4 We…

Html5播放器按钮在移动端变小的问题解决方法

Html5播放器按钮在移动端变小的问题解决方法 用手机浏览器打开酷播云视频&#xff0c;有时会出现播放器按钮太小的情况&#xff0c;此时只需在<head>中加入下面这段代码即可解决&#xff1a; <meta name"viewport" content"widthdevice-width, initia…

c语言指针的运算

1、通过指针计算数组的元素&#xff08;指针相减&#xff0c;类型需要一致&#xff09;&#xff0c;比如数组元素指针相减得到的是中间相差的元素个数&#xff0c;可以用于计算数组元素的个数等 #include "stdio.h" #include <stdlib.h>int main() {int a[10]…

SuperNova论文赏析

1. 引言 前序博客有&#xff1a; Nova: Recursive Zero-Knowledge Arguments from Folding Schemes学习笔记 卡内基梅隆大学 Abhiram Kothapalli 和 微软研究中心 Srinath Setty 2022年论文《SuperNova: Proving universal machine executions without universal circuits》…

责任链模式

责任链模式 1、定义&#xff1a; 将能够处理同一类请求的对象连成一条链&#xff0c;所提交的请求沿着链传递&#xff0c;链上的对象逐个判断是否有能力处理该请求 &#xff0c;如果能则处理&#xff1b; 如果不能则传递给链上的下一个对象 2、场景&#xff1a; 大学中奖学…

如何用python做自然语言处理

如何用python做自然语言处理 使用Python进行自然语言处理&#xff08;NLP&#xff09;是非常常见和强大的。以下是一些基本步骤&#xff1a; 安装所需的库&#xff1a; 首先&#xff0c;您需要安装一些用于自然语言处理的Python库&#xff0c;如NLTK&#xff08;自然语言工具包…

二十三种设计模式第十九篇--命令模式

命令模式是一种行为设计模式&#xff0c;它将请求封装成一个独立的对象&#xff0c;从而允许您以参数化的方式将客户端代码与具体实现解耦。在命令模式中&#xff0c;命令对象充当调用者和接收者之间的中介。这使您能够根据需要将请求排队、记录请求日志、撤销操作等。 命令模…

(具体解决方案)训练GAN深度学习的时候出现生成器loss一直上升但判别器loss趋于0

今天小陶在训练CGAN的时候出现了绷不住的情况&#xff0c;那就是G_loss&#xff08;生成器的loss值&#xff09;一路狂飙&#xff0c;一直上升到了6才逐渐平稳。而D_loss&#xff08;判别器的loss值&#xff09;却越来越小&#xff0c;具体的情况就看下面的图片吧。其实这在GAN…

arcgis字段计算器

1、两字段叠加。要求待叠加的字段类型为文本或字符串类型。如下&#xff1a; 2、字符串部分提取。

为Win12做准备?微软Win11 23H2将集成AI助手:GPT4免费用

微软日前确认今年4季度推出Win11 23H2&#xff0c;这是Win11第二个年度更新。 Win11 23H2具体有哪些功能升级&#xff0c;现在还不好说&#xff0c;但它会集成微软的Copilot&#xff0c;它很容易让人想到多年前的“曲别针”助手&#xff0c;但这次是AI技术加持的&#xff0c;Co…

编写SPI_Master驱动程序_新方法

编写SPI_Master驱动程序_新方法 文章目录 编写SPI_Master驱动程序_新方法一. SPI驱动框架1.1 总体框架1.2 怎么编写SPI_Master驱动1.2.1 编写设备树1.2.2 编写驱动程序 二、 编写程序2.1 数据传输流程2.2 写代码 致谢 参考资料&#xff1a; 内核头文件&#xff1a;include\lin…

Rookit系列一 【隐藏网络端口】【支持Win7 x32/x64 ~ Win10 x32/x64】

文章目录 Rookit系列一 【隐藏网络端口】【支持Win7 x32/x64 ~ Win10 x32/x64】前言探究隐藏网络端口netstat分析隐藏网络端口的原理关键数据结构隐藏网络端口源码 效果演示 Rookit系列一 【隐藏网络端口】【支持Win7 x32/x64 ~ Win10 x32/x64】 前言 Rookit是个老生常谈的话…

python中*与**的使用

文章目录 前言一、*与**在函数定义时二、*与**在函数调用时 前言 在python中*与**的使用要区分是在函数定义时还是在函数调用时。 一、*与**在函数定义时 def deng(*args,**kwargs):print(args)print(kwargs)deng(1,2,3,a 4,b 5)在函数定义时参数前面使用*&#xff0c;代表…

vue echart3个饼图

概览&#xff1a;根据UI设计需要做3个饼图且之间有关联&#xff0c;并且处理后端返回的数据。 参考链接&#xff1a; echart 官网的一个案例&#xff0c;3个饼图 实现思路&#xff1a; 根据案例&#xff0c;把数据处理成对应的。 参考代码&#xff1a; 1.处理后端数据&am…

Android Tencent Shadow 插件接入指南

Android Tencent Shadow 插件接入指南 插件化简述一、clone 仓库二、编译运行官方demo三、发布Shadow到我们本地仓库3.1、安装Nexus 3.x版本3.2、修改发布配置3.3、发布仓库3.4、引用仓库包 四、编写我们自己的代码4.1、新建项目导入maven等共同配置4.1.1、导入buildScript4.1.…

DoIP学习笔记系列:(四)用CAPL脚本读取DID的关键点

文章目录 1. 如何在CAPL中读取DID?1.1 避坑如何新建CAPL工程,在此不再赘述,本章主要分享一下如何在CAPL中调用DoIP接口、diag接口进行DoIP和诊断的测试。 1. 如何在CAPL中读取DID? 通常在实际项目中,会有很多DID,各种版本号、各种观测量,如果手动点,显然很麻烦,如果要…