iOS 开发 各种锁 总结(1)

锁是常用的同步工具。一段代码段在同一个时间只能允许被有限个线程访问,比如一个线程 A 进入需要保护的代码之前添加简单的互斥锁,另一个线程 B 就无法访问这段保护代码了,只有等待前一个线程 A 执行完被保护的代码后解锁,B 线程才能访问被保护的代码段。本篇就来总结这些 iOS 开发中使用到的锁,包括 spinlock_t、os_unfair_lock、pthread_mutex_t、NSLock、NSRecursiveLock、NSCondition、NSConditionLock、@synchronized、dispatch_semaphore、pthread_rwlock_t。

spinlock_t

自旋锁,也只有加锁、解锁和尝试加锁三个方法。和 NSLock 不同的是 NSLock 请求加锁失败的话,会先轮询,但一秒后便会使线程进入 waiting 状态,等待唤醒。而 OSSpinLock 会一直轮询,等待时会消耗大量 CPU 资源,不适用于较长时间的任务。
 使用 OSSpinLock 需要先引入 #import <libkern/OSAtomic.h>。看到 usr/include/libkern/OSSpinLockDeprecated.h 名字后面的 Deprecated 强烈的提示着我们 OSSpinLock 已经不赞成使用了。
 查看 OSSpinLockDeprecated.h 文件内容 OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock) 提示我们使用 os_unfair_lock 代替 OSSpinLock。
 OSSpinLock 存在线程安全问题,它可能导致优先级反转问题,目前我们在任何情况下都不应该再使用它,我们可以使用 apple 在 iOS 10.0 后推出的 os_unfair_lock (作为 OSSpinLock 的替代) 。关于 os_unfair_lock 我们下一节展开学习。

OSSpinLock API 简单使用

OSSpinLock API很简单,首先看下使用示例。

#import "ViewController.h"
#import <libkern/OSAtomic.h> // 引入 OSSpinLock

@interface ViewController ()

@property (nonatomic, assign) NSInteger sum;
@property (nonatomic, assign) OSSpinLock lock;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    self.lock = OS_SPINLOCK_INIT; // 初始化锁
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 取得一个全局并发队列
    self.sum = 0; // sum 从 0 开始

    dispatch_async(globalQueue, ^{ // 异步任务 1
        OSSpinLockLock(&_lock); // 获得锁
        for (unsigned int i = 0; i < 10000; ++i) {
            self.sum++;
        }
        NSLog(@"⏰⏰⏰ %ld", self.sum);
        OSSpinLockUnlock(&_lock); // 解锁
    });
    
    dispatch_async(globalQueue, ^{ // 异步任务 2
        OSSpinLockLock(&_lock); // 获得锁
        for (unsigned int i = 0; i < 10000; ++i) {
            self.sum++;
        }
        NSLog(@"⚽️⚽️⚽️ %ld", self.sum);
        OSSpinLockUnlock(&_lock); // 解锁
    });
}
@end

// 打印 🖨️:
⏰⏰⏰ 10000
⚽️⚽️⚽️ 20000

// 把 lock 注释后,运行多次可总结出如下三种不同情况,
// 其中只有一个任务达到了 10000 以上另一个是 10000 以下,另一种是两者都达到了 10000 以上

// 情况 1:
⏰⏰⏰ 9064
⚽️⚽️⚽️ 13708
// 情况 2:
⏰⏰⏰ 11326
⚽️⚽️⚽️ 9933
// 情况 3:
⏰⏰⏰ 10906
⚽️⚽️⚽️ 11903
...

sum 属性使用 atomic 或 nonatomic 时结果相同,atomic 虽是原子操作,但它不是线程安全的,它的原子性只是限于对它所修饰的变量在 setter 和 getter 时加锁而已,当遇到 self.sum++ 或者 self.sum = self.sum + 1 等等这种复合操作时,atomic 是完全保证不了线程安全的。

在不加锁情况下打印的数字有一些有趣的点,这里分析一下:(假设在全局并发队列下的两个 dispatch_async任务都开启了新线程,并把两条线分别命名为 “⏰线程” 和 “⚽️线程”)

1.可以确定的是 ⏰线程 和 ⚽️线程 不会有任何一个可以打印 20000

2.⏰线程 和 ⚽️线程 两者的打印都到了10000以上。

3.⏰线程 或 ⚽️线程 其中一个打印在 10000以上一个在 10000 以下。

情况 1 我们都能想到,因为 ⏰线程 和 ⚽️线程 是并发进行的,不会存在一个线程先把 sum 自增到 10000然后另一个线程再把 sum自增到 20000,只有加锁或者self.sum自增的任务在串行队列中执行才行。

情况 2 我们可能也好理解,两者都打印到 10000以上,可以分析为某个时间点 ⏰线程 持续自增,然后 ⚽️线程 在这个时间点后执行循环时 sum 已经大于它上一次循环时的值了,然后 ⏰线程 和 ⚽️线程 下sum 的值都是以大于其上一次循环的值往下继续循环,最后两条线程的打印sum值都是大于10000 的。

情况 3 则理解比较麻烦,为什么其中一个可以小于10000,可能是其中一个线程执行忽快忽慢造成的吗? 还有如果被缩小一次,那不是会导致两条线程最终打印sum都会小于10000吗?可能是 self.sum 读取时是从寄存器或内存中读取造成的吗?想到了volatile 关键字。(暂时先分析到这里,分析不下去了)

OSSpinLockDeprecated.h 文件内容

下面直接查看 OSSpinLockDeprecated.h 中的代码内容

上面示例代码中每一行与 OSSpinLock相关的代码都会有这样一行警告 ⚠️⚠️'OSSpinLock' is deprecated: first deprecated in iOS 10.0 - Use os_unfair_lock() from <os/lock.h> instead。正是由下面的 OSSPINLOCK_DEPRECATED所提示,在 4 大系统中都提示我们都不要再用 OSSpinLock 了。

#ifndef OSSPINLOCK_DEPRECATED

#define OSSPINLOCK_DEPRECATED 1
#define OSSPINLOCK_DEPRECATED_MSG(_r) "Use " #_r "() from <os/lock.h> instead"
#define OSSPINLOCK_DEPRECATED_REPLACE_WITH(_r) \
    __OS_AVAILABILITY_MSG(macosx, deprecated=10.12, OSSPINLOCK_DEPRECATED_MSG(_r)) \
    __OS_AVAILABILITY_MSG(ios, deprecated=10.0, OSSPINLOCK_DEPRECATED_MSG(_r)) \
    __OS_AVAILABILITY_MSG(tvos, deprecated=10.0, OSSPINLOCK_DEPRECATED_MSG(_r)) \
    __OS_AVAILABILITY_MSG(watchos, deprecated=3.0, OSSPINLOCK_DEPRECATED_MSG(_r))
    
#else

#undef OSSPINLOCK_DEPRECATED
#define OSSPINLOCK_DEPRECATED 0
#define OSSPINLOCK_DEPRECATED_REPLACE_WITH(_r)

#endif

下面是不同情况下的 OSSpinLock API 实现:
1.#if !(defined(OSSPINLOCK_USE_INLINED) && OSSPINLOCK_USE_INLINED)为真不使用内联时的原始 API:

  • #define OS_SPINLOCK_INIT 0 初始化。
/*! @abstract The default value for an <code>OSSpinLock</code>. OSSpinLock 的默认值是 0(unlocked 状态)
    @discussion
    The convention is that unlocked is zero, locked is nonzero. 惯例是: unlocked 时是零,locked 时时非零
 */
#define    OS_SPINLOCK_INIT    0

  • OSSpinLock数据类型。
/*! @abstract Data type for a spinlock. 自旋锁的数据类型是 int32_t
    @discussion
    You should always initialize a spinlock to {@link OS_SPINLOCK_INIT} before using it. 
    在使用一个自旋锁之前,我们应该总是先把它初始化为 OS_SPINLOCK_INIT。(其实是对它赋值为数字 0)
 */
typedef int32_t OSSpinLock OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock);

  • OSSpinLockTry尝试加锁,bool类型的返回值表示是否加锁成功,即使加锁失败也不会阻塞线程。
/*! @abstract Locks a spinlock if it would not block. 如果一个 spinlock 未锁定,则锁定它。
    @result
    Returns <code>false</code> if the lock was already held by another thread,
    <code>true</code> if it took the lock successfully. 
    如果锁已经被另一个线程所持有则返回 false,否则返回 true 表示加锁成功。
 */
OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock_trylock)
__OSX_AVAILABLE_STARTING(__MAC_10_4, __IPHONE_2_0)
bool    OSSpinLockTry( volatile OSSpinLock *__lock );

  • OSSpinLockLock 加锁。
/*! @abstract Locks a spinlock. 锁定一个 spinlock
    @discussion
    Although the lock operation spins, it employs various strategies to back
    off if the lock is held.
    尽管锁定操作旋转,(当加锁失败时会一直处于等待状态,一直到获取到锁为止,获取到锁之前会一直处于阻塞状态)
    它采用各种策略来支持如果加锁成功,则关闭旋转。
 */
OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock_lock)
__OSX_AVAILABLE_STARTING(__MAC_10_4, __IPHONE_2_0)
void    OSSpinLockLock( volatile OSSpinLock *__lock );

  • OSSpinLockUnlock 解锁。
/*! @abstract Unlocks a spinlock */
OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock_unlock)
__OSX_AVAILABLE_STARTING(__MAC_10_4, __IPHONE_2_0)
void    OSSpinLockUnlock( volatile OSSpinLock *__lock );

2.OSSPINLOCK_USE_INLINED 为真使用内联,内联实现是使用 os_unfair_lock_t代替 OSSpinLock

Inline implementations of the legacy OSSpinLock interfaces in terms of the of the <os/lock.h> primitives. Direct use of those primitives is preferred.
 NOTE: the locked value of os_unfair_lock is implementation defined and subject to change, code that relies on the specific locked value used by the legacy OSSpinLock interface WILL break when using these inline implementations in terms of os_unfair_lock.

就 <os/lock.h> 中的原始接口而言,此处是原始 OSSpinLock 接口的内联实现。最好直接使用这些 primitives。
 NOTE: os_unfair_lock 的锁定值是实现定义的,可能会更改。当使用这些内联实现时,依赖于旧版 OSSpinLock 接口使用的特定锁定值的代码会中断 os_unfair_lock。

在函数前加 OSSPINLOCK_INLINE 告诉编译器尽最大努力保证被修饰的函数内联实现。

  #if __has_attribute(always_inline) // 尽最大努力保证函数内联实现
  #define OSSPINLOCK_INLINE static __inline
  #else
  #define OSSPINLOCK_INLINE static __inline __attribute__((__always_inline__))
  #endif

  #define OS_SPINLOCK_INIT 0 // 初始化为 0
  typedef int32_t OSSpinLock; // 类型依然是 int32_t

  #if  __has_extension(c_static_assert)
  // 如果 OSSpinLock 和 os_unfair_lock 内存长度不同,即类型不兼容,不能保证双方能正确的转换,直接断言。
  _Static_assert(sizeof(OSSpinLock) == sizeof(os_unfair_lock), "Incompatible os_unfair_lock type"); 
  #endif

  • os_unfair_lock 加锁。
  OSSPINLOCK_INLINE
  void
  OSSpinLockLock(volatile OSSpinLock *__lock)
  {
      // 转换为 os_unfair_lock_t。
      os_unfair_lock_t lock = (os_unfair_lock_t)__lock;
      return os_unfair_lock_lock(lock);
  }

  • os_unfair_lock 尝试加锁。
OSSPINLOCK_INLINE
bool
OSSpinLockTry(volatile OSSpinLock *__lock)
{
    // 转换为 os_unfair_lock_t。
    os_unfair_lock_t lock = (os_unfair_lock_t)__lock;
    return os_unfair_lock_trylock(lock);
}

  • os_unfair_lock 解锁。
OSSPINLOCK_INLINE
void
OSSpinLockUnlock(volatile OSSpinLock *__lock)
{
    // 转换为 os_unfair_lock_t。
    os_unfair_lock_t lock = (os_unfair_lock_t)__lock;
    return os_unfair_lock_unlock(lock);
}

#undef OSSPINLOCK_INLINE 解除上面的宏定义。
3.最后一种情况。

#define OS_SPINLOCK_INIT 0 // 初始化
typedef int32_t OSSpinLock OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock); // 类型 int32_t
typedef volatile OSSpinLock *_os_nospin_lock_t
        OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock_t); // 命名 _os_nospin_lock_t

OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock_lock)
OS_NOSPIN_LOCK_AVAILABILITY
void _os_nospin_lock_lock(_os_nospin_lock_t lock); // 加锁
#undef OSSpinLockLock // 解除上面的原始 API 的加锁的宏定义
#define OSSpinLockLock(lock) _os_nospin_lock_lock(lock)

OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock_trylock)
OS_NOSPIN_LOCK_AVAILABILITY
bool _os_nospin_lock_trylock(_os_nospin_lock_t lock); // 尝试加锁
#undef OSSpinLockTry // 解除上面的原始 API 的判断能否加锁的宏定义
#define OSSpinLockTry(lock) _os_nospin_lock_trylock(lock)

OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock_unlock)
OS_NOSPIN_LOCK_AVAILABILITY
void _os_nospin_lock_unlock(_os_nospin_lock_t lock); // 解锁
#undef OSSpinLockUnlock // 解除上面的原始 API 的解锁的宏定义
#define OSSpinLockUnlock(lock) _os_nospin_lock_unlock(lock)

至此 OSSpinLockDeprecated.h 文件代码结束,整体而言只有 4 条。

1.OS_SPINLOCK_INIT初始化。

2.OSSpinLockTry()尝试加锁,如果锁已经被另一个线程所持有则返回 false,否则返回 true,即使加锁失败也不会阻塞当前线程。

3.OSSpinLockLock() 加锁,加锁失败会一直等待,会阻塞当前线程。

4.OSSpinLockUnlock 解锁。

OSSpinLock 的安全问题

自旋锁 OSSpinLock不是一个线程安全的锁,等待锁的线程会处于忙等(busy-wait)状态,一直占用着 CPU资源。(类似一个 while(1) 循环一样,不停的查询锁的状态,注意区分 runloop的机制,同样是阻塞,但是 runloop 是类似休眠的阻塞,不会耗费CPU资源,自旋锁的这种忙等机制使它相比其它锁效率更高,毕竟没有唤醒-休眠这些类似操作,从而能更快的处理事情。)自旋锁目前已经被废弃了,它可能会导致优先级反转。

例如A/B两个线程,A 的优先级大于 B的,我们的本意是A的任务优先执行,但是使用 OSSpinLock后,如果是 B优先访问了共享资源获得了锁并加锁,而A线程再去访问共享资源的时候锁就会处于忙等状态,由于 A 的优先级高它会一直占用 CPU资源不会让出时间片,这样 B一直不能获得CPU 资源去执行任务,导致无法完成。

《不再安全的 OSSpinLock》原文: 新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。

具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。这并不只是理论上的问题,libobjc 已经遇到了很多次这个问题了,于是苹果的工程师停用了 OSSpinLock。 苹果工程师 Greg Parker 提到,对于这个问题,一种解决方案是用 truly unbounded backoff 算法,这能避免 livelock 问题,但如果系统负载高时,它仍有可能将高优先级的线程阻塞数十秒之久;另一种方案是使用 handoff lock 算法,这也是 libobjc 目前正在使用的。锁的持有者会把线程 ID 保存到锁内部,锁的等待者会临时贡献出它的优先级来避免优先级反转的问题。理论上这种模式会在比较复杂的多锁条件下产生问题,但实践上目前还一切都好。 libobjc 里用的是 Mach 内核的 thread_switch() 然后传递了一个 mach thread port 来避免优先级反转,另外它还用了一个私有的参数选项,所以开发者无法自己实现这个锁。另一方面,由于二进制兼容问题,OSSpinLock 也不能有改动。 最终的结论就是,除非开发者能保证访问锁的线程全部都处于同一优先级,否则 iOS 系统中所有类型的自旋锁都不能再使用了。-《不再安全的 OSSpinLock》

os_unfair_lock

os_unfair_lock 设计宗旨是用于替换 OSSpinLock,从 iOS 10 之后开始支持,跟 OSSpinLock 不同,等待 os_unfair_lock 的线程会处于休眠状态(类似Runloop 那样),不是忙等(busy-wait)。

os_unfair_lock 引子

看到 struct SideTable 定义中第一个成员变量是 spinlock_t slock;, 这里展开对 spinlock_t的学习。

struct SideTable {
    spinlock_t slock;
    ...
};

spinlock_t其实是使用 using 声明的一个模版类。

#if DEBUG
#   define LOCKDEBUG 1
#else
#   define LOCKDEBUG 0
#endif

template <bool Debug> class mutex_tt;
using spinlock_t = mutex_tt<LOCKDEBUG>;

所以 spinlock_t其实是一个互斥锁,与它的名字自旋锁是不符的,其实以前它是OSSpinLock,因为其优先级反转导致的安全问题而被遗弃了。

template <bool Debug>
class mutex_tt : nocopy_t { // 继承自 nocopy_t
    os_unfair_lock mLock;
 public:
    constexpr mutex_tt() : mLock(OS_UNFAIR_LOCK_INIT) {
        lockdebug_remember_mutex(this);
    }
    ...
};

nocopy_t正如其名,删除编译器默认生成的复制构造函数和赋值操作符,而构造函数和析构函数则依然使用编译器默认生成的。

// Mix-in for classes that must not be copied.
// 构造函数 和 析构函数 使用编译器默认生成的,删除 复制构造函数 和 赋值操作符。
class nocopy_t {
  private:
    nocopy_t(const nocopy_t&) = delete;
    const nocopy_t& operator=(const nocopy_t&) = delete;
  protected:
    constexpr nocopy_t() = default;
    ~nocopy_t() = default;
};

mute_tt类的第一个成员变量是: os_unfair_lock mLock

os_unfair_lock 正片

usr/include/os/lock.h 中看到 os_unfair_lock的定义,使用 os_unfair_lock首先需要引入 #import <os/lock.h>

os_unfair_lock API 简单使用

os_unfair_lock API很简单,首先看下使用示例。

#import "ViewController.h"
#import <os/lock.h> // os_unfair_lock

@interface ViewController ()
@property (nonatomic, assign) NSInteger sum;
@property (nonatomic, assign) os_unfair_lock unfairL;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    _sum = 100; // 只是给自动生成的 _sum 成员变量赋值,不会调用 sum 的 setter 函数
    self->_sum = 1000; // 只是给自动生成的 _sum 成员变量赋值,不会调用 sum 的 setter 函数
    
    // 一定要区分 self. 中的 .,它和 C/C++ 中的 . 是不一样的,OC 中的 . 是调用 getter/setter 函数。
    // 开始一直疑惑 self.xxx,self 是一个指针,不是应该使用 self->xxx 吗?
    // 在 OC 中,应该是 self->_xxx,_xxx 是 xxx 属性自动生成的对应的成员变量 _xxx
    // self 是一个结构体指针,所以访问指针的成员变量,只能是 self->_xxx,不能是 self->xxx
    
    // 等号左边的 "self.unfairL = xxx" 相当于调用 unfairL 的 setter 函数给它赋值
    // 即 [self setUnfairL:OS_UNFAIR_LOCK_INIT];
    
    // 等号右边的 "xxx = self.unfaiL" 或者 "self.unfairL" 的使用,
    // 相当于调用 unfairL 的 getter 函数,读取它的值
    // 相当于调用 getter 函数:[self unfairL]
    
    /*
     // os_unfair_lock 是一个结构体
     typedef struct os_unfair_lock_s {
     uint32_t _os_unfair_lock_opaque;
     } os_unfair_lock, *os_unfair_lock_t;
     */
     
    self.unfairL = OS_UNFAIR_LOCK_INIT; // 初始化
    dispatch_queue_t globalQueue_DEFAULT = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    self.sum = 0;
    
    __weak typeof(self) _self = self;
    dispatch_async(globalQueue_DEFAULT, ^{
        __strong typeof(_self) self = _self;

        // 不是使用 &self.unfairL,
        // 这样使用相当于 &[self unfairL]
        // 不能这样取地址
        // &[self unfairL],
        // 报错: Cannot take the address of an rvalue of type 'os_unfair_lock'
        // 报错: 不能获取类型为 "os_unfair_lock" 的右值的地址
        // &self.unfairL;
        // 报错: Address of property expression requested
        // 只能使用 &self->_unfairL
        // 先拿到成员变量 _unfairL,然后再取地址
        
        os_unfair_lock_lock(&self->_unfairL); // 加锁
        // os_unfair_lock_lock(&self->_unfairL); // 重复加锁会直接 crash
        
        for (unsigned int i = 0; i < 10000; ++i) {
            self.sum++;
        }
        os_unfair_lock_unlock(&self->_unfairL); // 解锁
        NSLog(@"⏰⏰⏰ %ld", self.sum);
    });

    dispatch_async(globalQueue_DEFAULT, ^{
        __strong typeof(_self) self = _self;
        os_unfair_lock_lock(&self->_unfairL); // 加锁
        for (unsigned int i = 0; i < 10000; ++i) {
            self.sum++;
        }
        os_unfair_lock_unlock(&self->_unfairL); // 解锁
        NSLog(@"⚽️⚽️⚽️ %ld", self.sum);
    });
}
@end

// 打印:
⚽️⚽️⚽️ 10000
⏰⏰⏰ 20000

lock.h 文件内容

首先是一个宏定义告诉我们 os_unfair_lock 出现的时机。看到os_unfair_lock是在iOS 10.0 以后首次出现的。

#define OS_LOCK_API_VERSION 20160309
#define OS_UNFAIR_LOCK_AVAILABILITY \
__API_AVAILABLE(macos(10.12), ios(10.0), tvos(10.0), watchos(3.0))

/*!

@typedef os_unfair_lock

@abstract
Low-level lock that allows waiters to block efficiently on contention.

In general, higher level synchronization primitives such as those provided by
the pthread or dispatch subsystems should be preferred.

The values stored in the lock should be considered opaque and implementation
defined, they contain thread ownership information that the system may use
to attempt to resolve priority inversions.

This lock must be unlocked from the same thread that locked it, attempts to
unlock from a different thread will cause an assertion aborting the process.

This lock must not be accessed from multiple processes or threads via shared
or multiply-mapped memory, the lock implementation relies on the address of
the lock value and owning process.

Must be initialized with OS_UNFAIR_LOCK_INIT

@discussion
Replacement for the deprecated OSSpinLock. Does not spin on contention but
waits in the kernel to be woken up by an unlock.

As with OSSpinLock there is no attempt at fairness or lock ordering, e.g. an
unlocker can potentially immediately reacquire the lock before a woken up
waiter gets an opportunity to attempt to acquire the lock. This may be
advantageous for performance reasons, but also makes starvation of waiters a
possibility.

*/

对以上摘要内容进行总结,大概包括以下 几 点:

1.os_unfair_lock是一个低等级锁。一些高等级的锁才应该是我们日常开发中的首选。

2.必须使用加锁时的同一个线程来进行解锁,尝试使用不同的线程来解锁将导致断言中止进程。

3.锁里面包含线程所有权信息来解决优先级反转问题。

4.不能通过共享或多重映射内存从多个进程或线程访问此锁,锁的实现依赖于锁值的地址和所属进程。

5.必须使用 OS_UNFAIR_LOCK_INIT 进行初始化。

os_unfair_lock_s结构,typedef 定义别名,os_unfair_lock 是一个os_unfair_lock_s结构体,os_unfair_lock_t是一个 os_unfair_lock_s指针,该结构体内部就一个uint32_t _os_unfair_lock_opaque成员变量。

OS_UNFAIR_LOCK_AVAILABILITY
typedef struct os_unfair_lock_s {
    uint32_t _os_unfair_lock_opaque;
} os_unfair_lock, *os_unfair_lock_t;

针对不同的平台或者 C++版本以不同的方式来进行初始化 (os_unfair_lock){0}

1.(os_unfair_lock){0}
2.os_unfair_lock{}
3.os_unfair_lock()
4.{0}

#ifndef OS_UNFAIR_LOCK_INIT
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
#define OS_UNFAIR_LOCK_INIT ((os_unfair_lock){0}) // ⬅️
#elif defined(__cplusplus) && __cplusplus >= 201103L
#define OS_UNFAIR_LOCK_INIT (os_unfair_lock{}) // ⬅️
#elif defined(__cplusplus)
#define OS_UNFAIR_LOCK_INIT (os_unfair_lock()) // ⬅️
#else
#define OS_UNFAIR_LOCK_INIT {0} // ⬅️
#endif
#endif // OS_UNFAIR_LOCK_INIT

  • os_unfair_lock_lock 加锁。
/*!
 * @function os_unfair_lock_lock
 *
 * @abstract
 * Locks an os_unfair_lock. // 锁定一个 os_unfair_lock
 *
 * @param lock
 * Pointer to an os_unfair_lock. // 参数是一个 os_unfair_lock 指针
 */
OS_UNFAIR_LOCK_AVAILABILITY
OS_EXPORT OS_NOTHROW OS_NONNULL_ALL
void os_unfair_lock_lock(os_unfair_lock_t lock);

  • os_unfair_lock_trylock 尝试加锁。
/*!
 * @function os_unfair_lock_trylock
 *
 * @abstract
 * Locks an os_unfair_lock if it is not already locked.
 * 锁定一个 os_unfair_lock,如果它是之前尚未锁定的。
 *
 * @discussion
 * It is invalid to surround this function with a retry loop, if this function
 * returns false, the program must be able to proceed without having acquired
 * the lock, or it must call os_unfair_lock_lock() directly (a retry loop around
 * os_unfair_lock_trylock() amounts to an inefficient implementation of
 * os_unfair_lock_lock() that hides the lock waiter from the system and prevents
 * resolution of priority inversions).
 * 如果此函数返回 false,则用重试循环包围此函数是无效的,程序必须能够有能力处理这种没有获得锁的情况保证程序正常运行,
 * 或者必须直接调用 os_unfair_lock_lock()(os_unfair_lock_lock 会使线程阻塞一直到获得锁为止)。
 * (围绕 os_unfair_lock_trylock() 的重试循环等于 os_unfair_lock_lock() 的低效实现,
 * 该实现将 lock waiter 从系统中隐藏并解决了优先级反转问题)
 * 
 * @param lock
 * Pointer to an os_unfair_lock.
 * 参数是一个指向 os_unfair_lock 的指针。
 *
 * @result
 * Returns true if the lock was succesfully locked and false if the lock was already locked.
 * 锁定成功返回 true,如果之前已经被锁定则返回 false。
 * 
 */
OS_UNFAIR_LOCK_AVAILABILITY
OS_EXPORT OS_NOTHROW OS_WARN_RESULT OS_NONNULL_ALL
bool os_unfair_lock_trylock(os_unfair_lock_t lock);

  • os_unfair_lock_unlock解锁。
/*!
 * @function os_unfair_lock_unlock
 *
 * @abstract
 * Unlocks an os_unfair_lock. // 解锁
 *
 * @param lock
 * Pointer to an os_unfair_lock.
 */
OS_UNFAIR_LOCK_AVAILABILITY
OS_EXPORT OS_NOTHROW OS_NONNULL_ALL
void os_unfair_lock_unlock(os_unfair_lock_t lock);

  • os_unfair_lock_assert_owner 判断当前线程是否是 os_unfair_lock 的所有者,否则触发断言。
/*!
 * @function os_unfair_lock_assert_owner
 *
 * @abstract
 * Asserts that the calling thread is the current owner of the specified unfair lock.
 *
 * @discussion
 * If the lock is currently owned by the calling thread, this function returns. 
 * 如果锁当前由调用线程所拥有,则此函数正常执行返回。
 *
 * If the lock is unlocked or owned by a different thread, this function asserts and terminates the process.
 * 如果锁是未锁定或者由另一个线程所拥有,则执行断言。
 *
 * @param lock
 * Pointer to an os_unfair_lock.
 */
OS_UNFAIR_LOCK_AVAILABILITY
OS_EXPORT OS_NOTHROW OS_NONNULL_ALL
void os_unfair_lock_assert_owner(os_unfair_lock_t lock);

  • os_unfair_lock_assert_not_owner 与上相反,如果当前线程是指定 os_unfair_lock 的所有者则触发断言。
/*!
 * @function os_unfair_lock_assert_not_owner
 *
 * @abstract
 * Asserts that the calling thread is not the current owner of the specified unfair lock.
 *
 * @discussion
 * If the lock is unlocked or owned by a different thread, this function returns.
 *
 * If the lock is currently owned by the current thread, this function assertsand terminates the process.
 *
 * @param lock
 * Pointer to an os_unfair_lock.
 */
OS_UNFAIR_LOCK_AVAILABILITY
OS_EXPORT OS_NOTHROW OS_NONNULL_ALL
void os_unfair_lock_assert_not_owner(os_unfair_lock_t lock);

  • 测试 os_unfair_lock_assert_owneros_unfair_lock_assert_not_owner
dispatch_async(globalQueue_DEFAULT, ^{
    os_unfair_lock_assert_owner(&self->_unfairL);
});
os_unfair_lock_assert_not_owner(&self->_unfairL);

pthread_mutex_t

pthread_mutex_t 是 C 语言下多线程互斥锁的方式,是跨平台使用的锁,等待锁的线程会处于休眠状态,可根据不同的属性配置把pthread_mutex_t初始化为不同类型的锁,例如:互斥锁、递归锁、条件锁。当使用递归锁时,允许同一个线程重复进行加锁,另一个线程访问时就会等待,这样可以保证多线程时访问共用资源的安全性。pthread_mutex_t 使用时首先要引入头文件 #import <pthread.h>

PTHREAD_MUTEX_NORMAL // 缺省类型,也就是普通类型,当一个线程加锁后,其余请求锁的线程将形成一个队列,并在解锁后先进先出原则获得锁。
PTHREAD_MUTEX_ERRORCHECK // 检错锁,如果同一个线程请求同一个锁,则返回 EDEADLK,否则与普通锁类型动作相同。这样就保证当不允许多次加锁时不会出现嵌套情况下的死锁
PTHREAD_MUTEX_RECURSIVE //递归锁,允许同一个线程对同一锁成功获得多次,并通过多次 unlock 解锁。
PTHREAD_MUTEX_DEFAULT // 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争,没有等待队列。

pthread_mutex_trylocktrylock 不同,trylock 返回的是 YESNOpthread_mutex_trylock 加锁成功返回的是0,失败返回的是错误提示码。

pthread_mutex_t 简单使用

pthread_mutex_t初始化时使用不同的 pthread_mutexattr_t可获得不同类型的锁。

互斥锁( PTHREAD_MUTEX_DEFAULT 或 PTHREAD_MUTEX_NORMAL )

#import "ViewController.h"
#import <pthread.h> // pthread_mutex_t

@interface ViewController ()

@property (nonatomic, assign) NSInteger sum;
@property (nonatomic, assign) pthread_mutex_t lock;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.sum = 0;
    dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    // 1. 互斥锁,默认状态为互斥锁
    // 初始化属性
    pthread_mutexattr_t att;
    pthread_mutexattr_init(&att);
    
    // 设置属性,描述锁是什么类型
    pthread_mutexattr_settype(&att, PTHREAD_MUTEX_DEFAULT);
    
    // 初始化锁
    pthread_mutex_init(&self->_lock, &att);
    // 销毁属性
    pthread_mutexattr_destroy(&att);

    __weak typeof(self) _self = self;
    dispatch_async(global_queue, ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
    
        pthread_mutex_lock(&self->_lock);
        for (unsigned int i = 0; i < 10000; ++i) {
            self.sum++;
        }
        pthread_mutex_unlock(&self->_lock);
        
        NSLog(@"😵😵😵 %ld", (long)self.sum);
    });
    
    dispatch_async(global_queue, ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
        
        pthread_mutex_lock(&self->_lock);
        for (unsigned int i = 0; i < 10000; ++i) {
            self.sum++;
        }
        pthread_mutex_unlock(&self->_lock);

        NSLog(@"👿👿👿 %ld", (long)self.sum);
    });
}

#pragma mark - dealloc

- (void)dealloc {
    NSLog(@"🧑‍🎤🧑‍🎤🧑‍🎤 dealloc 同时释放🔒...");
    // 销毁锁
    pthread_mutex_destroy(&self->_lock);
}

@end

// 打印 🖨️:
😵😵😵 10000
👿👿👿 20000
🧑‍🎤🧑‍🎤🧑‍🎤 dealloc 同时释放🔒...

递归锁( PTHREAD_MUTEX_RECURSIVE )

#import "ViewController.h"
#import <pthread.h> // pthread_mutex_t

static int count = 3;
@interface ViewController ()

@property (nonatomic, assign) NSInteger sum;
@property (nonatomic, assign) pthread_mutex_t recursivelock;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.sum = 0;
    dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    // 2. 递归锁(PTHREAD_MUTEX_RECURSIVE)
    pthread_mutexattr_t recursiveAtt;
    pthread_mutexattr_init(&recursiveAtt);
    
    // 设置属性,描述锁是什么类型
    pthread_mutexattr_settype(&recursiveAtt, PTHREAD_MUTEX_RECURSIVE);
    
    // 初始化锁
    pthread_mutex_init(&self->_recursivelock, &recursiveAtt);
    // 销毁属性
    pthread_mutexattr_destroy(&recursiveAtt);
    
    __weak typeof(self) _self = self;
    dispatch_async(global_queue, ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
        
        pthread_mutex_lock(&self->_recursivelock);
        for (unsigned int i = 0; i < 10000; ++i) {
            self.sum++;
        }
        pthread_mutex_unlock(&self->_recursivelock);

        NSLog(@"😵😵😵 %ld", (long)self.sum);
    });
    
    dispatch_async(global_queue, ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
        
        // 递归锁验证
        [self recursiveAction];
    });
    
    dispatch_async(global_queue, ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
        
        pthread_mutex_lock(&self->_recursivelock);
        for (unsigned int i = 0; i < 10000; ++i) {
            self.sum++;
        }
        pthread_mutex_lock(&self->_recursivelock);
        
        NSLog(@"👿👿👿 %ld", (long)self.sum);
    });
    
    dispatch_async(global_queue, ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
        
        // 递归锁验证
        [self recursiveAction];
    });
}

#pragma mark - Private Methods
- (void)recursiveAction {
    pthread_mutex_lock(&self->_recursivelock);
    
    NSLog(@"😓😓😓 count = %d", count);
    if (count > 0) {
        count--;
        [self recursiveAction];
    }

    // else { // 如果是单线程的话,这里加一个递归出口没有任何问题
    // return;
    // }
    
    pthread_mutex_unlock(&self->_recursivelock);
    count = 3;
}

#pragma mark - dealloc
- (void)dealloc {
    NSLog(@"🧑‍🎤🧑‍🎤🧑‍🎤 dealloc 同时释放🔒...");
    pthread_mutex_destroy(&self->_recursivelock);
}

@end

// 打印 🖨️:
😵😵😵 10000
😓😓😓 count = 3
😓😓😓 count = 2
😓😓😓 count = 1
😓😓😓 count = 0

👿👿👿 20000
😓😓😓 count = 3
😓😓😓 count = 2
😓😓😓 count = 1
😓😓😓 count = 0

🧑‍🎤🧑‍🎤🧑‍🎤 dealloc 同时释放🔒...

条件锁

首先设定以下场景,两条线程ABA 线程中执行删除数组元素,B 线程中执行添加数组元素,由于不知道哪个线程会先执行,所以需要加锁实现,只有在添加之后才能执行删除操作,为互斥锁添加条件可以实现。通过此方法可以实现线程依赖。

#import "ViewController.h"

#import <pthread.h> // pthread_mutex_t

@interface ViewController ()

@property (nonatomic, strong) NSMutableArray *dataArr;
@property (nonatomic, assign) pthread_mutex_t lock;
@property (nonatomic, assign) pthread_cond_t condition;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 初始化数组
    self.dataArr = [NSMutableArray array];
    
    // 初始化锁
    pthread_mutexattr_t att;
    pthread_mutexattr_init(&att);
    pthread_mutexattr_settype(&att, PTHREAD_MUTEX_DEFAULT);
    pthread_mutex_init(&self->_lock, &att);
    pthread_mutexattr_destroy(&att);

    // 初始化条件
    pthread_cond_init(&self->_condition, NULL);
    
    dispatch_queue_t global_DEFAULT = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_queue_t global_HIGH = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

    __weak typeof(self) _self = self;
    
    dispatch_async(global_HIGH, ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
        
        pthread_mutex_lock(&self->_lock);
        NSLog(@"🧑‍💻🧑‍💻🧑‍💻 delete begin");
        
        if (self.dataArr.count < 1) {
            pthread_cond_wait(&self->_condition, &self->_lock);
        }
        
        [self.dataArr removeLastObject];
        NSLog(@"数组执行删除元素操作");
        pthread_mutex_unlock(&self->_lock);
    });
    
    dispatch_async(global_DEFAULT, ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
        
        pthread_mutex_lock(&self->_lock);
        NSLog(@"🧑‍💻🧑‍💻🧑‍💻 add begin");
        
        [self.dataArr addObject:@"CHM"];
        pthread_cond_signal(&self->_condition);
        
        NSLog(@"数组执行添加元素操作");
        pthread_mutex_unlock(&self->_lock);
    });

    NSThread *deThread = [[NSThread alloc] initWithTarget:self selector:@selector(deleteObj) object:nil];
    [deThread start];

    // sleep 1 秒,确保删除元素的线程先获得锁
    sleep(1);

    NSThread *addThread = [[NSThread alloc] initWithTarget:self selector:@selector(addObj) object:nil];
    [addThread start];
}

#pragma mark - Private Methods

- (void)deleteObj {
    pthread_mutex_lock(&self->_lock);

    NSLog(@"🧑‍💻🧑‍💻🧑‍💻 delete begin");
    // 添加判断,如果没有数据则添加条件
    
    if (self.dataArr.count < 1) {
        // 添加条件,如果数组为空,则添加等待线程休眠,将锁让出,这里会将锁让出去,所以下面的 addObj 线程才能获得锁
        // 接收到信号时会再次加锁,然后继续向下执行
        pthread_cond_wait(&self->_condition, &self->_lock);
    }
    
    [self.dataArr removeLastObject];
    NSLog(@"数组执行删除元素操作");

    pthread_mutex_unlock(&self->_lock);
}

- (void)addObj {
    pthread_mutex_lock(&self->_lock);

    NSLog(@"🧑‍💻🧑‍💻🧑‍💻 add begin");
    [self.dataArr addObject:@"HTI"];
    
    // 发送信号,说明已经添加元素了
    pthread_cond_signal(&self->_condition);
    
    NSLog(@"数组执行添加元素操作");
    pthread_mutex_unlock(&self->_lock);
}

#pragma mark - dealloc

- (void)dealloc {
    NSLog(@"🧑‍🎤🧑‍🎤🧑‍🎤 dealloc 同时释放🔒...");
    
    pthread_mutex_destroy(&self->_lock);
    pthread_cond_destroy(&self->_condition);
}

@end

// 打印 🖨️:
🧑‍💻🧑‍💻🧑‍💻 delete begin
🧑‍💻🧑‍💻🧑‍💻 add begin
数组执行添加元素操作
数组执行删除元素操作
🧑‍🎤🧑‍🎤🧑‍🎤 dealloc 同时释放🔒...

NSLock

继承自 NSObject 并遵循 NSLocking 协议,lock 方法加锁,unlock 方法解锁,tryLock 尝试并加锁,如果返回 true 表示加锁成功,返回 false 表示加锁失败,谨记返回的 BOOL 表示加锁动作的成功或失败,并不是能不能加锁,即使加锁失败也会不会阻塞当前线程。lockBeforeDate: 是在指定的 Date 之前尝试加锁,如果在指定的时间之前都不能加锁,则返回 NO,且会阻塞当前线程。大概可以使用在:先预估上一个临界区的代码执行完毕需要多少时间,然后在这个时间之后为另一个代码段来加锁。

1.基于 mutex 基本锁的封装,更加面向对象,等待锁的线程会处于休眠状态。

2.遵守 NSLocking 协议,NSLocking 协议中仅有两个方法 -(void)lock 和 -(void)unlock。

3.可能会用到的方法:

4.初始化跟其他 OC 对象一样,直接进行 alloc 和 init 操作。

5.-(void)lock; 加锁。

6.-(void)unlock; 解锁。

7.-(BOOL)tryLock; 尝试加锁。

8.-(BOOL)lockBeforeDate:(NSDate *)limit; 在某一个时间点之前等待加锁。

9.在主线程连续调用 [self.lock lock] 会导致主线程死锁。

10.在主线程没有获取 Lock 的情况下和在获取 Lock 的情况下,连续两次 [self.lock unlock] 都不会发生异常。(其他的锁可能连续解锁的情况下会导致 crash,还没有来的及测试)

11.在子线程连续 [self.lock lock] 会导致死锁,同时别的子线获取 self.lock 则会一直等待下去。

12.同时子线程死锁会导致 ViewController 不释放。

NSLock 使用

@interface ViewController ()

@property (nonatomic, assign) NSInteger sum;
@property (nonatomic, strong) NSLock *lock;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.sum = 0;
    self.lock = [[NSLock alloc] init];
    dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    __weak typeof(self) _self = self;
    dispatch_async(global_queue, ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
        
        // 如果此处加锁失败,则阻塞当前线程,下面的代码不会执行,
        // 直到等到 lock 被其他线程释放了,它可以加锁了,才会接着执行下面的代码
        [self.lock lock];
        for (unsigned int i = 0; i < 10000; ++i) {
            self.sum++;
        }
        [self.lock unlock];
        
        NSLog(@"👿👿👿 %ld", (long)self.sum);
    });
    
    dispatch_async(global_queue, ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
        
        // 如果此处加锁失败,则阻塞当前线程,下面的代码不会执行,
        // 直到等到 lock 被其他线程释放了,它可以加锁了,才会接着执行下面的代码
        [self.lock lock];
        for (unsigned int i = 0; i < 10000; ++i) {
            self.sum++;
        }
        [self.lock unlock];
        
        NSLog(@"😵😵😵 %ld", (long)self.sum);
    });
}

#pragma mark - dealloc
- (void)dealloc {
    NSLog(@"🧑‍🎤🧑‍🎤🧑‍🎤 dealloc 同时释放🔒...");
}

@end

// 打印结果:
😵😵😵 20000
👿👿👿 10000
🧑‍🎤🧑‍🎤🧑‍🎤 dealloc 同时释放🔒...

__weak typeof(self) _self = self;
// 线程 1
dispatch_async(global_queue, ^{
    __strong typeof(_self) self = _self;
    if (!self) return;

    [self.lock lock];
    for (unsigned int i = 0; i < 10000; ++i) {
        self.sum++;
    }
    sleep(3);
    [self.lock unlock];
    NSLog(@"👿👿👿 %ld", (long)self.sum);
});

// 线程 2
dispatch_async(global_queue, ^{
    __strong typeof(_self) self = _self;
    if (!self) return;
    sleep(1); // 保证让线程 1 先获得锁
    
    // 如果此处用 1,则在这个时间点不能获得锁
    // 如果是用大于 2 的数字,则能获得锁
    // 且这个 if 函数是会阻塞当前线程的
    if ([self.lock lockBeforeDate: [NSDate dateWithTimeIntervalSinceNow:1]]) {
        for (unsigned int i = 0; i < 10000; ++i) {
            self.sum++;
        }
        [self.lock unlock];
    } else {
        NSLog(@"lockBeforeDate 失败,会直接来到这里吗,会不阻塞当前线程吗?");
    }
    
    NSLog(@"😵😵😵 %ld", (long)self.sum);
});

[self.lock lockBeforeDate: [NSDate dateWithTimeIntervalSinceNow:1]],lockBeforeDate: 方法会在指定 Date 之前尝试加锁,且这个过程是会阻塞线程 2 的,如果在指定时间之前都不能加锁,则返回 false,在指定时间之前能加锁,则返回 true。

_priv 和 name,检测各个阶段,_priv 一直是 NULL。name 是用来标识的,用来输出的时候作为 lock 的名称。如果是三个线程,那么一个线程在加锁的时候,其余请求锁的的线程将形成一个等待队列,按先进先出原则,这个结果可以通过修改线程优先级进行测试得出。

NSRecursiveLock

NSRecursiveLock 是递归锁,和 NSLock 的区别在于,它可以在同一个线程中重复加锁也不会导致死锁。NSRecursiveLock 会记录加锁和解锁的次数,当二者次数相等时,此线程才会释放锁,其它线程才可以上锁成功。

1.同 NSLock 一样,也是基于 mutex 的封装,不过是基于 mutex 递归锁的封装,所以这是一个递归锁。

2.遵守 NSLocking 协议,NSLocking 协议中仅有两个方法 -(void)lock 和 -(void)unlock。

3.可能会用到的方法:

4.继承自 NSObject,所以初始化跟其他 OC 对象一样,直接进行 alloc 和 init 操作。

5.-(void)lock; 加锁

6.-(void)unlock; 解锁

7.-(BOOL)tryLock; 尝试加锁

8-(BOOL)lockBeforeDate:(NSDate *)limit; 在某一个时间点之前等待加锁。

9.递归锁是可以在同一线程连续调用 lock 不会直接导致阻塞死锁,但是依然要执行相等次数的 unlock。不然异步线程再获取该递归锁会导致该异步线程阻塞死锁。

10.递归锁允许同一线程多次加锁,不同线程进入加锁入口会处于等待状态,需要等待上一个线程解锁完成才能进入加锁状态。

NSRecursiveLock 使用

其实是实现上面 pthread_mutex_tPTHREAD_MUTEX_RECURSIVE完成的递归锁场景,只是这里使用 NSRecursiveLock API更加精简,使用起来更加简单方便。

#import "ViewController.h"

static int count = 3;

@interface ViewController ()

@property (nonatomic, assign) NSInteger sum;
@property (nonatomic, strong) NSLock *lock;
@property (nonatomic, strong) NSRecursiveLock *recursiveLock;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    self.sum = 0;
    self.recursiveLock = [[NSRecursiveLock alloc] init];
    dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    __weak typeof(self) _self = self;
    dispatch_async(global_queue, ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
    
        [self.recursiveLock lock];
        for (unsigned int i = 0; i < 10000; ++i) {
            self.sum++;
        }
        [self.recursiveLock unlock];
        
        NSLog(@"👿👿👿 %ld", (long)self.sum);
    });
    
    dispatch_async(global_queue, ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
        
        [self recursiveAction];
    });
    
    dispatch_async(global_queue, ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
        
        [self.recursiveLock lock];
        for (unsigned int i = 0; i < 10000; ++i) {
            self.sum++;
        }
        [self.recursiveLock unlock];
        
        NSLog(@"😵😵😵 %ld", (long)self.sum);
    });
    
    dispatch_async(global_queue, ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
        
        [self recursiveAction];
    });
}

#pragma mark - Private Methods

- (void)recursiveAction {
    [self.recursiveLock lock];
    NSLog(@"😓😓😓 count = %d", count);
    if (count > 0) {
        count--;
        [self recursiveAction];
    }

    // else { // 如果是单线程的话,这里加一个递归出口没有任何问题
    // return;
    // }

    [self.recursiveLock unlock];
    count = 3;
}

#pragma mark - dealloc
- (void)dealloc {
    NSLog(@"🧑‍🎤🧑‍🎤🧑‍🎤 dealloc 同时释放🔒...");
}

@end
// 打印结果:
😓😓😓 count = 3
👿👿👿 10000
😓😓😓 count = 2
😓😓😓 count = 1
😓😓😓 count = 0

😵😵😵 20000
😓😓😓 count = 3
😓😓😓 count = 2
😓😓😓 count = 1
😓😓😓 count = 0

NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  static void (^RecursieveBlock)(int);
  
  RecursiveBlock = ^(int value) {
      [lock lock];
      if (value > 0) {
          NSLog(@"value: %d", value);
          
          RecursiveBlock(value - 1);
      }
      [lock unlock];
  };
  
  RecursiveBlock(2);
});

如上示例,如果用 NSLock 的话,lock 先锁上,但未执行解锁的时候,就会进入递归的下一层,并再次请求上锁,阻塞了该线程,线程被阻塞了,自然后面的解锁代码就永远不会执行,而形成了死锁。而 NSRecursiveLock 递归锁就是为了解决这个问题。

NSCondition

NSCondition 的对象实际上作为一个锁和一个线程检查器,锁上之后其它线程也能上锁,而之后可以根据条件决定是否继续运行线程,即线程是否要进入 waiting 状态,经测试,NSCondition 并不会像上文的那些锁一样,先轮询,而是直接进入 waiting 状态,当其它线程中的该锁执行 signal 或者 broadcast 方法时,线程被唤醒,继续运行之后的方法。也就是使用 NSCondition 的模型为: 1. 锁定条件对象。2. 测试是否可以安全的履行接下来的任务。如果布尔值为假,调用条件对象的 wait 或者 waitUntilDate: 方法来阻塞线程。再从这些方法返回,则转到步骤 2 重新测试你的布尔值。(继续等待信号和重新测试,直到可以安全的履行接下来的任务,waitUntilDate: 方法有个等待时间限制,指定的时间到了,则返回 NO,继续运行接下来的任务。而等待信号,既线程执行 [lock signal] 发送的信号。其中 signal 和 broadcast 方法的区别在于,signal 只是一个信号量,只能唤醒一个等待的线程,想唤醒多个就得多次调用,而 broadcast 可以唤醒所有在等待的线程,如果没有等待的线程,这两个方法都没有作用。)

1.基于 mutex 基础锁和 cont条件的封装,所以它是互斥锁且自带条件,等待锁的线程休眠。

2.遵守NSLocking协议,NSLocking 协议中仅有两个方法 -(void)lock-(void)unlock

3.可能会用到的方法

4.初始化跟其它 OC 对象一样,直接进行allocinit操作。

5.-(void)lock;加锁

6.-(void)unlock; 解锁

7.-(BOOL)tryLock; 尝试加锁

8.-(BOOL)lockBeforeDate:(NSDate *)limit; 在某一个时间点之前等待加锁

9.-(void)wait; 等待条件(进入休眠的同时放开锁,被唤醒的同时再次加锁)

10.-(void)signal;发送信号激活等待该条件的线程,切记线程收到后是从 wait 状态开始的

11.- (void)broadcast; 发送广播信号激活等待该条件的所有线程,切记线程收到后是从 wait 状态开始的

NSCondition 使用

#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, strong) NSMutableArray *dataArr;
@property (nonatomic, strong) NSCondition *condition;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    // 初始化数组
    self.dataArr = [NSMutableArray array];
    
    self.condition = [[NSCondition alloc] init];
    dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    __weak typeof(self) _self = self;
    dispatch_async(global_queue, ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
        
        [self deleteObj];
    });
    
    dispatch_async(global_queue, ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
        
        [self deleteObj];
    });
    
    // sleep 0.5 秒,确保删除元素的操作先取得锁
    sleep(0.5);
    
    dispatch_async(global_queue, ^{
        __strong typeof(_self) self = _self;
        if (!self) return;
        
        [self addObj];
    });
}

#pragma mark - Private Methods

- (void)deleteObj {
    [self.condition lock];
    NSLog(@"🧑‍💻🧑‍💻🧑‍💻 delete begin");
    
    // 添加判断,如果没有数据则添加条件
    if (self.dataArr.count < 1) {
        // 添加条件,如果数组为空,则添加等待线程休眠,将锁让出,这里会将锁让出去,所以下面的 addObj 线程才能获得锁
        // 接收到信号时会再次加锁,然后继续向下执行
        
        NSLog(@"下面是进入 wait...");
        [self.condition wait];
        
        // 当 broadcast 过来的时候还是继续往下执行,
        // 切记不是从 deleteObj 函数头部开始的,是从这里开始的
        // 所以当第一个异步删除数组元素后,第二个异步进来时数组已经空了
        NSLog(@"接收到 broadcast 或 signal 后的函数起点");
    }
    
    NSLog(@"%@", self.dataArr);
    [self.dataArr removeLastObject];
    NSLog(@"🧑‍💻🧑‍💻🧑‍💻 数组执行删除元素操作");
    [self.condition unlock];
}

- (void)addObj {
    [self.condition lock];
    NSLog(@"🧑‍💻🧑‍💻🧑‍💻 add begin");
    
    [self.dataArr addObject:@"CHM"];
    
    // 发送信号,说明已经添加元素了
    // [self.condition signal];
    // 通知所有符合条件的线程
    [self.condition broadcast];
    
    NSLog(@"🧑‍💻🧑‍💻🧑‍💻 数组执行添加元素操作");
    [self.condition unlock];
}

#pragma mark - dealloc
- (void)dealloc {
    NSLog(@"🧑‍🎤🧑‍🎤🧑‍🎤 dealloc 同时释放🔒...");
}
@end

// 打印结果:

// 这里两个异步线程执行都 [self.condition lock],都正常进入了,
// 并没有因为 self.condition 先被一条线程获取加锁了而另一条线程处于阻塞等待状态, 

🧑‍💻🧑‍💻🧑‍💻 delete begin
下面是进入 wait...
🧑‍💻🧑‍💻🧑‍💻 delete begin
下面是进入 wait...

🧑‍💻🧑‍💻🧑‍💻 add begin
🧑‍💻🧑‍💻🧑‍💻 数组执行添加元素操作
接收到 broadcast 或 signal 后的函数起点
(
    CHM
)
🧑‍💻🧑‍💻🧑‍💻 数组执行删除元素操作
接收到 broadcast 或 signal 后的函数起点
(
)
🧑‍💻🧑‍💻🧑‍💻 数组执行删除元素操作
🧑‍🎤🧑‍🎤🧑‍🎤 dealloc 同时释放🔒...

NSConditionLock

NSConditionLock 和 NSLock 类似,同样是继承自 NSObject 和遵循 NSLocking 协议,加解锁 try 等方法都类似,只是多了一个 condition 属性,以及每个操作都多了一个关于 condition 属性的方法,例如 tryLock 和 tryLockWhenCondition:,NSConditionLock 可以称为条件锁。只有 condition 参数与初始化的时候相等或者上次解锁后设置的 condition 相等,lock 才能正确的进行加锁操作。unlockWithCondition: 并不是当 condition 符合条件时才解锁,而是解锁之后,修改 condition 的值为入参,当使用 unlock 解锁时, condition 的值保持不变。如果初始化用 init,则 condition 默认值为 0。lockWhenCondition: 和 lock 方法类似,加锁失败会阻塞当前线程,一直等下去,直到能加锁成功。tryLockWhenCondition: 和 tryLock 类似,表示尝试加锁,即使加锁失败也不会阻塞当前线程,但是同时满足 lock 是空闲状态并且 condition 符合条件才能尝试加锁成功。从上面看出,NSConditionLock 还可以实现任务之间的依赖。

1.基于 NSCondition 的进一步封装,可以更加高级的设置条件值。

假设有这样的场景,三个线程 A B C,执行完 A 线程后才能执行 B,执行完 B 线程后执行 C,就是为线程之间的执行添加依赖,NSConditionLock 可以方便的完成这个功能。

2.遵守 NSLocking 协议,NSLocking 协议中仅有两个方法 -(void)lock 和 -(void)unlock。

3.可能用到的方法:

4.初始化跟其他 OC 对象一样,直接 alloc 和 initWithCondition:(NSInteger)condition 操作;(如果使用 init 方法,则 condition 默认为 0)。

5.有一个属性是 @property(readonly) NSInteger condition; 用来设置条件值,如果不设定,则默认为零。

6.-(void)lock; 直接加锁。

7.-(void)lockWhenCondition:(NSInteger)condition; 根据 condition 值加锁,如果入参和当前的 condition 不等则不加。

8.-(void)unlockWithCondition:(NSInteger)condition; 解锁, 并设定 condition 的值为入参。

9.-(BOOL)tryLock; 尝试加锁。

10.-(BOOL)lockBeforeDate:(NSDate *)limit; 在某一个时间点之前等待加锁。

NSConditionLock 使用

#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, strong) NSConditionLock *lock;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.lock = [[NSConditionLock alloc] initWithCondition:0];
    [self createThreads];
}

#pragma mark - Private Methods

- (void)createThreads {
    // 需要执行的顺序为 A-B-C,但是因为在子线程中所以我们不能确定谁先执行,添加 sleep 使问题更突出点,c 线程先启动然后是 b 然后是 a。
    NSThread *c = [[NSThread alloc] initWithTarget:self selector:@selector(threadC) object:nil];
    [c start];
    sleep(0.2);
    
    NSThread *b = [[NSThread alloc] initWithTarget:self selector:@selector(threadB) object:nil];
    [b start];
    sleep(0.2);
    
    NSThread *a = [[NSThread alloc] initWithTarget:self selector:@selector(threadA) object:nil];
    [a start];
}

- (void)threadA {
    NSLog(@"A begin");
    [self.lock lockWhenCondition:0]; // 此时 Condition 值为 0 才能加锁成功,因为 Condition 初始值是 0,所以只有 A 能加锁成功
    NSLog(@"A threadExcute");
    [self.lock unlockWithCondition:1]; // 解锁并把 Condition 设置为 1
    // [self unlock]; // 如果此处使用 unlock,则导致 B C 线程死锁,且导致 ViewController 不释放
}

- (void)threadB {
    NSLog(@"B begin");
    [self.lock lockWhenCondition:1]; // 此时 Condition 值为 1 才能加锁成功
    NSLog(@"B threadExcute");
    [self.lock unlockWithCondition:2]; // 解锁并把 Condition 设置为 2
}

- (void)threadC {
    NSLog(@"C begin");
    [self.lock lockWhenCondition:2]; // 此时 Condition 值为 2 才能加锁成功
    NSLog(@"C threadExcute");
    [self.lock unlock]; // 解锁
}

#pragma mark - dealloc
- (void)dealloc {
    NSLog(@"🧑‍🎤🧑‍🎤🧑‍🎤 dealloc 同时释放🔒...");
}

// 打印结果:
// 虽然启动顺序是 C B A,但是执行顺序是 A B C,正是由 Condition 条件控制的,只有 Condition 匹配才能加锁成功,否则一直阻塞等待
C begin
B begin
A begin

A threadExcute
B threadExcute
C threadExcute
🧑‍🎤🧑‍🎤🧑‍🎤 dealloc 同时释放🔒...

1.[self.lock unlock];执行后 condition保持不变,依然是初始化的值或者是上次执行 lockWhenCondition:时的值。

2.A B C 3 条线程必须都执行加锁和解锁后 ViewController才能正常释放,除了最后一条线程可以直接使用 unlock执行解锁外,前两条线程 unlockWithCondition: 的入参condition 的值必须和 NSConditionLock当前的 condition 的值匹配起来。保证每条线程都lockunlock,无法正常执行时都会导致线程阻塞等待,ViewController不会释放。

3.在同一线程连续[self.lock lockWhenCondition:1];会直接阻塞死锁,不管用的condition是否和当前锁的 condition相等,都会导致阻塞死锁。

NSLocking、NSLock、NSConditionLock、NSRecursiveLock、NSCondition 定义

#import <Foundation/NSObject.h>

@class NSDate;

NS_ASSUME_NONNULL_BEGIN

// NSLocking 协议,上面提到锁的类型只要是 NS 开头的都会遵守此协议
@protocol NSLocking // 看到 NSLocking 协议只有加锁和解锁两个协议方法

- (void)lock;
- (void)unlock;

@end

@interface NSLock : NSObject <NSLocking> { // NSLock 是继承自 NSObject 并遵守 NSLocking 协议
@private
    void *_priv;
}

- (BOOL)tryLock; // 尝试加锁,返回 true 表示加锁成功
- (BOOL)lockBeforeDate:(NSDate *)limit; // 在某个 NSDate 之前加锁

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

@end

// 条件锁
@interface NSConditionLock : NSObject <NSLocking> { // 继承自 NSObject 并遵守 NSLocking 协议
@private
    void *_priv;
}

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

@property (readonly) NSInteger condition; // 只读的 condition 属性
- (void)lockWhenCondition:(NSInteger)condition; // 根据 condition 值加锁, 如果值不满足, 则不加;

- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition; 

- (void)unlockWithCondition:(NSInteger)condition; // 解锁, 并设定 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

// 递归锁
@interface NSRecursiveLock : NSObject <NSLocking> { // 继承自 NSObject 并遵守 NSLocking 协议
@private
    void *_priv;
}

- (BOOL)tryLock; // 尝试加锁,返回 true 表示加锁成功
- (BOOL)lockBeforeDate:(NSDate *)limit; // 在某个 NSDate 之前加锁

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

@end

API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0))
@interface NSCondition : NSObject <NSLocking> { // 继承自 NSObject 并遵守 NSLocking 协议
@private
    void *_priv;
}

- (void)wait; // 添加等待,线程休眠,并将锁让出
- (BOOL)waitUntilDate:(NSDate *)limit; // 某个 NSDate 
- (void)signal; // 发送信号,告知等待的线程,条件满足了
- (void)broadcast; // 通知所有符合条件的线程,(通知所有在等待的线程)

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

@end

NS_ASSUME_NONNULL_END

文章来源于网络
原文地址:https://juejin.cn/post/6892322602602201102

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容

  • 本文节选自成长手册 文章推荐和参考深入理解 iOS 开发中的锁pthread的各种同步机制 多线程编程被普遍认为复...
    百草纪阅读 2,739评论 1 9
  • 在iOS开发中,不可避免的需要使用到多线程。但是使用多线程的过程中,如果使用不当,就会造成数据混乱,那要怎么保证多...
    coolLee阅读 1,194评论 0 3
  • 目录:1.为什么要线程安全2.多线程安全隐患分析3.多线程安全隐患的解决方案4.锁的分类-13种锁4.1.1OSS...
    二斤寂寞阅读 1,135评论 0 3
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,395评论 16 21
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,498评论 0 11