线程基础与 iOS 中的多线程 (二)

多线程

接着 线程基础与 iOS 中的多线程 (一) ,继续记录多线程相关的知识点。

NSCondition 和 NSConditinLock

iOS 中 NSLock NSCondition NSConditinLock NSRecursiveLock 都遵守了 NSLocking 协议,其中 NSCondition 和 NSConditinLock 是比较令人迷惑的两个锁;

其实 NSCondition 相比 NSLock 就多了两个方法 wait() 和 signal(),这样这个锁就可以用 wait 来等待某个条件变量来阻塞线程,阻塞后 (执行 wait 后),其他线程就能获得该锁,直到发出 signal 信号后且条件满足后,wait 的线程重新获得锁,开始后续执行,摘抄一段 stackOverFlow 的代码来理解一下

// --- MyTestClass.h File --- //
@interface MyTestClass

- (void)startTest;

@end

// --- MyTestClass.m File --- //
@implementation MyTestClass
{
    NSCondition *_myCondition;
    BOOL _someCheckIsTrue;
}

- (id)init
{
    self = [super init];
    if (self) 
    {
        _someCheckIsTrue = NO;
        _myCondition = [[NSCondition alloc] init];
    }
    return self;
}

#pragma mark Public Methods

- (void)startTest
{
    [self performSelectorInBackground:@selector(_method1) withObject:nil];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(5);
        [self performSelectorInBackground:@selector(_method2) withObject:nil];
    });
}

#pragma mark Private Methods

- (void)_method1
{
    NSLog(@"STARTING METHOD 1");

    NSLog(@"WILL LOCK METHOD 1");
    [_myCondition lock];
    NSLog(@"DID LOCK METHOD 1");

    while (!_someCheckIsTrue)
    {
        NSLog(@"WILL WAIT METHOD 1");
        [_myCondition wait];
        NSLog(@"DID WAIT METHOD 1");
    }

    NSLog(@"WILL UNLOCK METHOD 1");
    [_myCondition unlock];
    NSLog(@"DID UNLOCK METHOD 1");

    NSLog(@"ENDING METHOD 1");
}

- (void)_method2
{
    NSLog(@"STARTING METHOD 2");

    NSLog(@"WILL LOCK METHOD 2");
    [_myCondition lock];
    NSLog(@"DID LOCK METHOD 2");

    _someCheckIsTrue = YES;

    NSLog(@"WILL SIGNAL METHOD 2");
    [_myCondition signal];
    NSLog(@"DID SIGNAL METHOD 2");

    NSLog(@"WILL UNLOCK METHOD 2");
    [_myCondition unlock];
    NSLog(@"DID UNLOCK METHOD 2");
}

@end

// --- Output --- //
/*
2012-11-14 11:01:21.416 MyApp[8375:3907] STARTING METHOD 1
2012-11-14 11:01:21.418 MyApp[8375:3907] WILL LOCK METHOD 1
2012-11-14 11:01:21.419 MyApp[8375:3907] DID LOCK METHOD 1
2012-11-14 11:01:21.421 MyApp[8375:3907] WILL WAIT METHOD 1
2012-11-14 11:01:26.418 MyApp[8375:4807] STARTING METHOD 2
2012-11-14 11:01:26.419 MyApp[8375:4807] WILL LOCK METHOD 2
2012-11-14 11:01:26.419 MyApp[8375:4807] DID LOCK METHOD 2
2012-11-14 11:01:26.420 MyApp[8375:4807] WILL SIGNAL METHOD 2
2012-11-14 11:01:26.420 MyApp[8375:4807] DID SIGNAL METHOD 2
2012-11-14 11:01:26.421 MyApp[8375:4807] WILL UNLOCK METHOD 2
2012-11-14 11:01:26.421 MyApp[8375:3907] DID WAIT METHOD 1
2012-11-14 11:01:26.421 MyApp[8375:4807] DID UNLOCK METHOD 2
2012-11-14 11:01:26.422 MyApp[8375:3907] WILL UNLOCK METHOD 1
2012-11-14 11:01:26.423 MyApp[8375:3907] DID UNLOCK METHOD 1
2012-11-14 11:01:26.423 MyApp[8375:3907] ENDING METHOD 1
*/

NSConditionLock 可以定义一个指定条件的互斥锁,用于线程之间的互斥与同步。这里的条件并不是 bool 表达式中的条件,而是一个特定的 int 值。

// lockWhenCondition :用于condition等于特定值的时候加锁,会阻塞当前线程。
// unlockWithCondition:指定条件时解锁,每次解锁会导致内部的condition值改变为指定的值,同时唤醒其它阻塞的线程检测这里的condition是否满足条件,因此NSConditionLock相对于NSCondition效率更低。

// 示例代码

[NSThread detachNewThreadSelector:@selector(createConsumer)
                                   toTarget:sample
                                 withObject:nil];

[NSThread detachNewThreadSelector:@selector(createProducter)
                                  toTarget:sample
                                withObject:nil];

- (void)createConsumer
{
    NSLog(@"createConsumer");
    while (YES) {
        NSLog(@"createConsumer before lock");
        [self.conditionLock lockWhenCondition:3];
        NSLog(@"createConsumer after lock");
        if([self.products count] > 0)
            [self.products removeObjectAtIndex:0];
        NSLog(@"consume a product,left %@ products", @([self.products count]));
        [self.conditionLock unlockWithCondition:[self.products count]==0?0:10];
        NSLog(@"createConsumer after unlock");
    }
}

- (void)createProducter
{
    NSLog(@"createProducter");
    while (YES) {
        NSLog(@"createProducter before lock");
        [self.conditionLock lock];
        NSLog(@"createProducter after lock");
        [self.products addObject:[[NSObject alloc] init]];
        NSLog(@"produce a product,left %@ products", @([self.products count]));
        [self.conditionLock unlockWithCondition:[self.products count]];
        NSLog(@"createProducter after unlock");
    }
}

// 控制台输出打印如下:
[22232:1943872] createConsumer
[22232:1943874] createProducter
[22232:1943872] createConsumer before lock
[22232:1943874] createProducter before lock
[22232:1943874] createProducter after lock
[22232:1943874] produce a product,left 1 products
[22232:1943874] createProducter after unlock
[22232:1943874] createProducter before lock
[22232:1943874] createProducter after lock
[22232:1943874] produce a product,left 2 products
[22232:1943874] createProducter after unlock
[22232:1943874] createProducter before lock
[22232:1943874] createProducter after lock
[22232:1943874] produce a product,left 3 products
[22232:1943874] createProducter after unlock
[22232:1943872] createConsumer after lock
[22232:1943874] createProducter before lock
[22232:1943872] consume a product,left 2 products
[22232:1943872] createConsumer after unlock
[22232:1943874] createProducter after lock
[22232:1943872] createConsumer before lock
[22232:1943874] produce a product,left 3 products
[22232:1943874] createProducter after unlock
[22232:1943872] createConsumer after lock

当生产者生产个数达 3 个时释放锁:produce a product,left 3 products,此后锁被消费者获取,开始消费。

读写锁


读写锁是一种特定场合的锁,多个线程可以同时读取某个资源,但只要有线程开始写入的时候,就必须用同步的手段保护资源,此时其他线程不能读,也不能写。所以读写锁可以避免这种问题,读写锁有 3 种状态:

  • 自由状态:此时线程可以以共享的方式获取,也可以以独占的方式获取
  • 共享状态:此时说明资源处于共享状态(可能是多线程读取资源中),所以其他线程可以以共享的状态获取锁,但如果想以独占的方式获取锁,则需要等待。等待共享状态变为自由态,才可以以独占方式获取
  • 独占状态:此时很有可能资源被某个线程写入,所以其他线程不管是以共享还是独占方式获取锁都需要等待。

iOS 开发中,可以使用 <pthread.h> 中的 pthread_rwlock_t 来实现读写锁

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; // 初始化
pthread_rwlock_wrlock(&lock); // 读模式
pthread_rwlock_rdlock(&lock); // 写模式
pthread_rwlock_unlock(&lock); // 读模式或者写模式的解锁

条件变量


条件变量的作用类似于栅栏,线程对条件变量的操作有两种,等待和唤醒;一个条件变量可以被多个线程等待,也就是条件变量能够让多个线程一起等待某事件;当某个线程唤醒条件变量后,所有等待的线程可以一起恢复执行。

可重入与线程安全


可重入是线程安全的强力保障,一个可重入函数在多线程环境下可以放心调用。

一个函数被重入,表示函数没有执行完成,由于外部因素或者内部调用,又一次进入函数执行。一个函数被重入,有两种情况:

  • 多个线程同时执行这个函数
  • 函数自身调用自身

例如下面的这个函数就是一个可重入函数:

int add (int a, int b)
{
        return a + b;
}

一个函数可重入,则具有以下几个特点:

  • 不使用任何静态或全局的非 const 变量
  • 不返回任何静态或全局的非 const 变量的指针
  • 仅依赖于调用方提供的参数
  • 不调用任何不可重入的函数
  • 不依赖任何单个资源的锁

编译器过度优化


尽管我们会用 lock 和 unlock 来保护资源,但是编译器往往会对代码做出一些优化,调整寄存器读入和回写的代码顺序,从而导致了 CPU 的乱序执行,破坏锁的保护机制。目前还没遇到具体问题,再次先做个记录。

(完)

点赞