为什么需要在应用层管理多线程读写?

应用层可以根据实际的业务逻辑和需求,有选择性地选择lock和unlock的时机,从而更有效地管理并发,这个管理的过程和业务强绑定。抽象出来描述类似,你正在写的代码,写到一半停下来去打水,不希望别人动你的代码,你打水的时候决定把电脑锁屏。这是一个普遍的处理多个任务需要考虑的问题。

现代计算机已经从各个层面支持锁的实现,包括CPU的总线锁,缓存锁,汇编的Lock前缀等。总的来说,有多线程多进程的场景就需要用到锁来管理资源访问。

最简单的自旋锁的实现?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <stdatomic.h>

// 定义自旋锁类型
typedef struct spinlock {
atomic_flag locked;
} spinlock;

// 初始化锁
void spinlock_init(spinlock *lock) {
atomic_flag_clear(&lock->locked);
}

// 加锁
void spinlock_lock(spinlock *lock) {
while (atomic_flag_test_and_set(&lock->locked)) {
// 自旋等待,直到锁变为未锁定状态
}
}

// 解锁
void spinlock_unlock(spinlock *lock) {
atomic_flag_clear(&lock->locked);
}

int main() {
// 创建并初始化锁
spinlock lock;
spinlock_init(&lock);

// 加锁
spinlock_lock(&lock);
printf("Critical section\n");
// 解锁
spinlock_unlock(&lock);

return 0;
}

自旋锁在锁被持有时间很短的情况下效率较高,但如果持有时间较长,它可能会在等待锁时浪费大量的CPU资源,简单的说就是忙等

在实际应用中,更复杂的锁机制(如互斥锁、读写锁等)会更常见,它们可能会结合使用原子操作、内核同步原语(如信号量、条件变量等)来实现。这些更高级的锁在锁不可用时会使线程休眠,直到锁可用时再唤醒线程,从而避免不必要的CPU资源浪费。

iOS中都有哪些锁可用?

在 iOS 开发中,有多种锁可用于同步访问共享资源,保证线程安全。不同类型的锁在性能和特性上各不相同。以下是一些常用的锁及其简要说明:

  1. NSLock:
    • NSLock 是一个基本的互斥锁,提供了简单的加锁和解锁功能。使用起来非常直接,但没有提供高级功能。
  2. NSRecursiveLock:
    • NSRecursiveLock 允许同一个线程多次获得相同的锁,而不会导致死锁。这对于递归函数或可以从多个途径调用的方法很有用。
  3. NSCondition:
    • NSCondition 是一个条件锁,允许线程在满足特定条件之前等待,或在条件发生变化时通知其他线程。
  4. NSConditionLock:
    • NSConditionLockNSCondition 的一个变体,它添加了与特定条件关联的锁定状态。
  5. @synchronized:
    • 这是一个 Objective-C 语言特性,它可以锁定一个对象,以保护一段代码的执行。使用起来简单,但性能不是最优。
  6. pthread_mutex:
    • 这是一个基于 POSIX 线程(pthreads)的低级互斥锁。它比 NSLock 提供更多的灵活性和配置选项。
  7. pthread_rwlock:
    • 这是一个基于 pthreads 的读写锁,允许多个线程同时读取但只允许一个线程写入。
  8. dispatch_semaphore:
    • dispatch_semaphore 是 GCD(Grand Central Dispatch)提供的一个计数信号量,可以用来控制访问共享资源的线程数量。
  9. OSSpinLock (已被弃用):
    • OSSpinLock 曾经是 iOS 中最快的锁类型之一,但由于潜在的安全问题,它在 iOS 10 中被标记为弃用。
  10. os_unfair_lock:
    • os_unfair_lock 是替代 OSSpinLock 的锁,它为等待锁的线程提供了一种更为公平的调度策略。
  11. dispatch_queue:
    • 虽然不是传统意义上的锁,但串行 dispatch_queue 可以用来同步访问,保证代码块按顺序执行。

每种锁都有其适用场景和性能特点。例如,在不需要高性能的简单场景中,NSLock@synchronized 可能已足够。在需要支持递归锁定的场景中,可以使用 NSRecursiveLock。而在性能至关重要的环境中,os_unfair_lockdispatch_semaphore 可能是更好的选择。

在选择锁时,开发者需要考虑锁的性能特性以及线程竞争的程度。一般而言,应该尽量避免使用锁来提高并发性能,例如通过设计无锁的数据结构或使用其他同步机制(如原子操作)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Darwin
// 定义一个互斥锁
var mutex = pthread_mutex_t()
// 初始化互斥锁
pthread_mutex_init(&mutex, nil)
// 使用互斥锁保护一个共享资源
func criticalSection() {
pthread_mutex_lock(&mutex)
// 执行需要同步的代码
// ...
pthread_mutex_unlock(&mutex)
}
// 在代码中的某个地方调用
criticalSection()
// 最后销毁互斥锁
pthread_mutex_destroy(&mutex)

死锁问题

  • 进程 A 持有锁 L1 并等待锁 L2。
  • 进程 B 持有锁 L2 并等待锁 L1。

饥饿问题

  • 饥饿是指一个或多个线程无法获得所需的资源,从而无法继续执行的情况。在锁的上下文中,饥饿可能发生在某些线程尝试获取锁,但总是有其他线程抢先一步获取锁,导致这些线程无限期地等待。
  • 解决饥饿的方法包括使用公平锁(fair locks),确保线程按照请求锁的顺序获得锁,或者调整线程的优先级和调度策略。

优先级反转

  • 高优先级任务A尝试获取一个当前被低优先级任务B持有的锁。

  • 任务A因为锁被占用而被迫等待(阻塞状态)。

  • 此时,中优先级任务C就绪并开始执行。因为任务C的优先级高于任务B,所以它得到了CPU时间。

  • 由于任务C在运行,低优先级任务B无法获得CPU执行时间来完成其工作并释放锁。

  • 因此,尽管任务A有最高的优先级,它仍然被阻塞,因为它等待的锁被低优先级任务B持有,而任务B又无法运行来释放锁。

在这个场景中,中优先级任务C并没有直接参与锁的竞争,但它的运行阻碍了低优先级任务B的进度,从而间接导致了

有关锁的一篇笔记(互斥、公平性、性能)

YY大神对iOS中锁的一篇文章(OSSpinLock)