线程同步

当我们在应用程序中开辟多个线程的时候,就有可能会涉及这几个线程会同时访问同一资源的情况,比如读写同一个文件,如果我们不加以控制,将会 造成数据错乱或者其它一些未知的异常。

为了让各线程有序的访问同一资源,我们需要用到一种叫做线程同步的技术。

线程同步 - 即当有一个线程在对内存进行操作时,其它线程都不可以对这个内存地址进行操作,直到该线程完成操作,其它的线程才能对该内存地址进行操作。

线程同步的方式和机制

临界区

通过对多线程的串行化来访问公共资源,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其它试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其它线程才可以抢占。

互斥量

采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享。

信号量

允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。

事件

通过通过操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作。

在 Cocoa 中,为了实现线程同步,我们将通过给资源加锁来控制多个线程对其的操作。锁的机制即上文所讲的互斥量机制,所以互斥量通过也叫做互斥锁。

NSLock

NSLock 是 Cocoa 提供给开发者最基本的锁对象,其使用也是相当简单。

@interface AppDelegate ()
@property (nonatomic, strong) NSLock *lock;
@end

@implementation AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.lock = [[NSLock alloc] init];
    
    __weak AppDelegate *weakSelf = self;
    
    // 代码块 2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1); // 只是为了控制一下加锁的顺序
        [weakSelf.lock lock];
        [weakSelf print:@"2"];
        [weakSelf.lock unlock];
    });
    
    // 代码块 3
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(2); // 只是为了控制一下加锁的顺序
        [weakSelf.lock lock];
        [weakSelf print:@"3"];
        [weakSelf.lock unlock];
    });
    
    // 代码块 1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [weakSelf.lock lock];
        [weakSelf print:@"1"];
        [weakSelf.lock unlock];
    });

    return YES;
}

- (void)print:(NSString *)msg {
    NSLog(@"%@", msg);
}
@end

这就是一个最简单的加锁的方法:创建一个 NSLock 的实例,在需要用到线程同步的线程内部调用其 lock 方法,如果之前没有调用过 lock 方法,那么我们就成功添加了一把锁,这时候可以放心的去做相应的操作,等到操作完成之后,调用 unlock 方法释放锁,以通知其它线程中同样调用了该锁实例 lock 方法的地方可以继续做相应的操作。

NSConditionLock

NSLock 为我们提供了最简单的加锁和解锁的方式,大多数情况下,其能够满足我们的需求。但是,在一些特殊条件下,单凭加锁和解锁未必能够满足我们的需求。

比如,我们可以根据当前线程执行任务的状态来决定其下一步该执行什么样的操作。也就是说,我们可以给锁加一个条件,当满足该条件的时候才会执行相应的操作。为此,Cocoa 为我们提供了 NSConditionLock,下面通过一段示例代码来介绍其使用方法:

@interface AppDelegate ()
@property (nonatomic, strong) NSConditionLock *lock;
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.lock = [[NSConditionLock alloc] init];
    
    __weak AppDelegate *weakSelf = self;
    
    // 代码块 2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [weakSelf.lock lockWhenCondition:2];
        [weakSelf print:@"2"];
        [weakSelf.lock unlockWithCondition:3];
    });
    
    // 代码块 3
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [weakSelf.lock lockWhenCondition:3];
        [weakSelf print:@"3"];
        [weakSelf.lock unlock];
    });
    
    // 代码块 1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1); // sleep 1 秒,让其优先级低于其它两个线程
        [weakSelf.lock lock];
        [weakSelf print:@"1"];
        [weakSelf.lock unlockWithCondition:2];
    });

    return YES;
}

- (void)print:(NSString *)msg {
    NSLog(@"%@", msg);
}
@end

上述代码,如果我们去掉所有锁相关的代码,那么输出的顺序是 2、3、1 或者 3、2、1,但绝对不可能是 1、2、3。加上锁之后,输出的顺序就可以保证为 1、2、3。

其实不难理解,代码块2和代码块3虽然都优先比代码块1执行了锁操作,但是其锁是有条件的,一个为2,一个为3。但是在此时,这两个条件都不满足。因为我们是调用 NSConditionLock 的 init 方法进行初始化的,其 condition 默认为0,所以代码块2和代码块3都被阻塞了。这时候,虽然代码块1里面的 lock 方法被延迟1秒调用,但是确是第一把锁,所以能够顺利的执行相关的操作。

代码块1的最后一行代码是解锁操作,我们调用的是 NSConditionLock 实例的 unlockWithCondition 方法,它需要一个 condition 作为参数,意思就是解锁满足该条件的锁。到这里,大家都应该明白了。其实也有点黑科技的感觉,可以通过 NSConditionLock 来指定不同线程的执行顺序。是不是和 goto 很象?

NSRecursiveLock

在实际运用中,如果我们稍微不小心,就可能造成死锁的问题,这样我们的程序就没有办法正常工作了。最容易出现死锁的情形就是在递归或者循环中。

要避免在递归或者循环中造成死锁,可以使用 Cocoa 提供的 NSRecursiveLock。NSRecursiveLock 的使用和 NSLock 一样简单,只是它可以在同一线程中多次 lock,而不会造成死锁,NSRecursiveLock 对象会跟踪其被 lock 了多少次,每次成功的 lock 都需要 unlock,所以只要 unlock 的次数和 lock 的次数相同就不会造成死锁。

synchronized

Apple 为了方便广大开发者,提供了一个更为简便的加锁方法 - @synchronized(…)。

synchronized 指令需要指定一个 Objective-c 对象作为该锁的唯一标识,只有当标识相同时,才会满足互斥。

pthread_mutex

POSIX 标准的 unix 多线程库(pthread)中使用的互斥量,支持递归。

__block pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);

 __weak AppDelegate *weakSelf = self;

// 代码块1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    pthread_mutex_lock(&mutex);
    [weakSelf print:@"1"];
    pthread_mutex_unlock(&mutex);
});

// 代码块2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(1);
    pthread_mutex_lock(&mutex);
    [weakSelf print:@"2"];
    pthread_mutex_unlock(&mutex);
});

其它

dispatch_semaphore、OSSpinLock


参考文献: