OC 多线程基础知识:锁

想要深入理解多线程,锁是预备知识,这里总结一下OC中锁相关的知识,打好基础。

为什么要有锁?

锁概念的提出,是为了解决多线程资源共享的问题,在多线程环境下,有的资源可能会同时被多个线程访问,可能会出现资源抢夺的问题。这里引入一个概念叫临界区(Critical Section),就是一段代码,同一时间只能由一个线程访问,以保障临界区内的线程是安全的(资源不被抢夺,改变)。锁就是用来解决临界区内线程安全的问题。

下面介绍三种常见的锁:

自旋锁(spin lock)

自旋锁长这样:

while (抢锁(lock) == 没抢到) {
}

就是利用一个while循环,不断的尝试去抢锁(这里的锁lock是一个抽象的概念,可以是一个整数,一开始是1,表示没有锁,抢到后变为0,表示有锁),抢到了锁就跳出循环,抢不到锁就不断重试。

自旋锁的缺点很明显,不断的抢锁会占用CPU资源。优点是线程不用休眠,不用花时间在上下文切换(context switch)从用户态转化为内核态,用在轻量级的临界区上效率高。

互斥锁(mutex

互斥锁长这样:

while (抢锁(lock) == 没抢到) {
    线程休眠,请在这把锁的状态发生改变时再唤醒(lock);
}

和自旋锁很相似,不同就是抢不到锁的时候,让线程去休眠,当锁的状态改变的时候再唤醒该线程。

互斥锁的缺点是,线程休眠会让线程从用户态转化为内核态,唤醒的时候从内核态转化为用户态,需要两次上下文切换,花费大量时间。优点是不用忙等,临界区很长时效率高。

读写锁(readers-writer lock

读写锁就是分了两种情况,一种是读时的锁,一种是写时的锁,同时规定:

  • 同时可以存在多个读锁,也就是读-读不互斥
  • 只能存在一个写锁,也就是读-写互斥,写-写互斥

读写锁的实现时用了两个互斥锁(或者两个自旋锁):

//读者加锁
- (void)readerLock {
    加锁(rlock);
    condition++;
    if (condition == 1) {
        加锁(wlock);
    }
    解锁(rlock);
}
//读者解锁
- (void)readerUnlock {
    加锁(rlock);
    condition--;
    if (condition == 0) {
        加锁(wlock);
    }
    解锁(rlock);
}
//写者加锁
- (void)writerLock {
    加锁(wlock);
}
//写者解锁
- (void)writerUnlock {
    解锁(wlock);
}
@end

这里我们用了两把互斥锁(rlockwlock)来实现读写锁,利用了:

  • 计数器condition跟踪被阻塞的读线程。如果先有写锁,读锁中condition==1,读会被wlock阻塞。如果先有读锁,写锁会被wlock堵塞;读锁再次获取时,可以使condition>1,从而读锁不被堵塞。
  • 互斥锁rlock保护condition,供读者使用
  • 互斥锁wlock 确保写操作互斥

下面介绍一个更高级的实现读写锁的方法:条件变量+互斥锁

首先介绍一下条件变量

条件变量可以简单理解为,一个条件,如果达成了就发通知。这样说有点抽象,把条件变量用到读写锁里就清楚了:

//读者加锁
- (void)readerLock {
    加锁(rwlock);
    while (self.isWriting) {
        解锁,等待条件变量达成时的通知唤醒,再加锁(cond, rwlock);
    }
    self.readCount++;
    解锁(rwlock);
}
//读者解锁
- (void)readerUnlock {
   加锁(rwlock);
   self.readCount--;
   if (self.readCount == 0) {
       //唤起一条写的线程
       条件变量达成时,触发通知(cond);
   }
   解锁(rwlock);
}
//写者加锁
- (void)writerLock {
    加锁(rwlock);
    while (self.isWriting || self.readCount > 0) {
         解锁,等待条件变量达成时的通知唤醒,再加锁(cond, rwlock);
    }
    self.isWriting = YES;
    解锁(rwlock);
}
//写者解锁
- (void)writerUnlock {
    加锁(rwlock);
    self.isWriting = NO;
    //唤起多个读的线程
    条件变量达成时的,触发通知(cond);
    解锁(rwlock);
}
@end

这里使用了使用[条件变量cond与普通的互斥锁rwlock、整型计数器readCount(表示正在读的个数)与布尔标志isWrite(表示正在写)来实现读写锁。

  • 当有读锁时,readCount>0,写锁进入while循环,只有条件变量达成时,才会收到通知唤醒跳出循环,条件变量达成的条件就是读锁全部释放(readCount==0)。
  • 当有写锁时,isWriting == YES,读锁进入while循环,只有条件变量达成时,才会收到通知唤醒出循环,条件变量达成的条件就是写锁释放(isWriting == NO)。

下面总结一个OC钟常用锁的用法,一下所有例子,都用卖股票的例子:

@synchronized 关键字

@synchronized(这里添加一个OC对象,一般使用self) {
    要加锁的代码
}

这是一个互斥锁,简单易用,但性能最差,建议加锁的代码尽量少,例子如下:

- (void)viewDidLoad {
    [super viewDidLoad];

    //设置票的数量为5
    self.tickets = 5;

    self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);

    //线程1
    dispatch_async(self.q1, ^{
        [self saleTickets];
    });

    //线程2
    dispatch_async(self.q1, ^{
        [self saleTickets];
    });
}

- (void)saleTickets {
    while (1) {
        [NSThread sleepForTimeInterval:1];
        @synchronized(self) {
            if (self.tickets > 0) {
                self.tickets -= 1;
                NSLog(@"剩余票数=%d, Thread:%@", self.tickets, [NSThread currentThread]);
            } else {
                NSLog(@"票卖完了  Thread:%@", [NSThread currentThread]);
                break;
            }
        }
    }
}

// 剩余票数=4, Thread:<NSThread: 0x60000249cd80>{number = 5, name = (null)}
// 剩余票数=3, Thread:<NSThread: 0x6000024c0040>{number = 6, name = (null)}
// 剩余票数=2, Thread:<NSThread: 0x6000024c0040>{number = 6, name = (null)}
// 剩余票数=1, Thread:<NSThread: 0x60000249cd80>{number = 5, name = (null)}
// 剩余票数=0, Thread:<NSThread: 0x6000024c0040>{number = 6, name = (null)}
// 票卖完了  Thread:<NSThread: 0x60000249cd80>{number = 5, name = (null)}
// 票卖完了  Thread:<NSThread: 0x6000024c0040>{number = 6, name = (null)}

NSLock

_mutexLock = [[NSLock alloc] init];
[_mutexLock lock];
[_mutexLock unlock];

互斥锁,有lockunlock方法:

- (void)viewDidLoad {
    [super viewDidLoad];

    //设置票的数量为5
    self.tickets = 5;

    self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);

    _mutexLock = [[NSLock alloc] init];

    //线程1
    dispatch_async(self.q1, ^{
        [self saleTickets];
    });

    //线程2
    dispatch_async(self.q1, ^{
        [self saleTickets];
    });
}

- (void)saleTickets {
    while (1) {
        [NSThread sleepForTimeInterval:1];
        [_mutexLock lock];
        if (self.tickets > 0) {
            self.tickets -= 1;
            NSLog(@"剩余票数=%d, Thread:%@", self.tickets, [NSThread currentThread]);
        } else {
            NSLog(@"票卖完了  Thread:%@", [NSThread currentThread]);
            break;
        }
        [_mutexLock unlock];
    }
}

// 剩余票数=4, Thread:<NSThread: 0x600003758900>{number = 6, name = (null)}
// 剩余票数=3, Thread:<NSThread: 0x6000037423c0>{number = 3, name = (null)}
// 剩余票数=2, Thread:<NSThread: 0x600003758900>{number = 6, name = (null)}
// 剩余票数=1, Thread:<NSThread: 0x6000037423c0>{number = 3, name = (null)}
// 剩余票数=0, Thread:<NSThread: 0x600003758900>{number = 6, name = (null)}
// 票卖完了  Thread:<NSThread: 0x6000037423c0>{number = 3, name = (null)}

pthread_mutex

pthread_mutex_init(&_mutex, NULL);
pthread_mutex_lock(&_mutex);
pthread_mutex_unlock(&_mutex);

互斥锁

- (void)viewDidLoad {
    [super viewDidLoad];

    //设置票的数量为5
    self.tickets = 5;

    self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
    
    // @property pthread_mutex_t mutex;
    pthread_mutex_init(&_mutex, NULL);

    //线程1
    dispatch_async(self.q1, ^{
        [self saleTickets];
    });

    //线程2
    dispatch_async(self.q1, ^{
        [self saleTickets];
    });
}

- (void)saleTickets {
    while (1) {
        [NSThread sleepForTimeInterval:1];
        pthread_mutex_lock(&_mutex);
        if (self.tickets > 0) {
            self.tickets -= 1;
            NSLog(@"剩余票数=%d, Thread:%@", self.tickets, [NSThread currentThread]);
        } else {
            NSLog(@"票卖完了  Thread:%@", [NSThread currentThread]);
            break;
        }
        pthread_mutex_unlock(&_mutex);
    }
}

@end
  
// 剩余票数=4, Thread:<NSThread: 0x600003e49840>{number = 7, name = (null)}
// 剩余票数=3, Thread:<NSThread: 0x600003e36a00>{number = 6, name = (null)}
// 剩余票数=2, Thread:<NSThread: 0x600003e49840>{number = 7, name = (null)}
// 剩余票数=1, Thread:<NSThread: 0x600003e36a00>{number = 6, name = (null)}
// 剩余票数=0, Thread:<NSThread: 0x600003e36a00>{number = 6, name = (null)}
// 票卖完了  Thread:<NSThread: 0x600003e49840>{number = 7, name = (null)}

dispatch_semaphore

_semaphore = dispatch_semaphore_create(1);
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
dispatch_semaphore_signal(_semaphore);

信号量实现加锁,线程获取一个信号量时,信号量数量减一,线程释放信号量时,信号量数量加一,信号量数量大于等于零时,加锁的代码可以执行。互斥锁可以看做是一种特殊的信号量(初始信号量等于一)。

- (void)viewDidLoad {
    [super viewDidLoad];

    //设置票的数量为5
    self.tickets = 5;

    self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
    
    _semaphore = dispatch_semaphore_create(1);

    //线程1
    dispatch_async(self.q1, ^{
        [self saleTickets];
    });

    //线程2
    dispatch_async(self.q1, ^{
        [self saleTickets];
    });
}

- (void)saleTickets {
    while (1) {
        [NSThread sleepForTimeInterval:1];
        dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
        if (self.tickets > 0) {
            self.tickets -= 1;
            NSLog(@"剩余票数=%d, Thread:%@", self.tickets, [NSThread currentThread]);
        } else {
            NSLog(@"票卖完了  Thread:%@", [NSThread currentThread]);
            break;
        }
        dispatch_semaphore_signal(_semaphore);
    }
}

@end
  
// 剩余票数=4, Thread:<NSThread: 0x600001815e40>{number = 7, name = (null)}
// 剩余票数=3, Thread:<NSThread: 0x60000184f600>{number = 4, name = (null)}
// 剩余票数=2, Thread:<NSThread: 0x60000184f600>{number = 4, name = (null)}
// 剩余票数=1, Thread:<NSThread: 0x600001815e40>{number = 7, name = (null)}
// 剩余票数=0, Thread:<NSThread: 0x60000184f600>{number = 4, name = (null)}
// 票卖完了  Thread:<NSThread: 0x600001815e40>{number = 7, name = (null)}

OSSpinLock

_pinLock = OS_SPINLOCK_INIT;
OSSpinLockLock(&_pinLock);
OSSpinLockUnlock(&_pinLock);

自旋锁,效率最高,但有隐患:

可能会出现优先级翻转的情况。比如线程1优先级比较高,线程2优先级比较低,然后在某一时刻是线程2先获取到锁,所以先是线程2加锁,这时候,线程1就在while(目标锁还未释放),这个状态,但因为线程1优先级比较高,所以系统分配的时间比较多,有可能会没有分配时间给线程2执行后续的操作(需要做的任务和解锁)了,这时候就会造成死锁。

所以iOS10之后自旋锁OSSpinLock就被os_unfair_lock(底层是互斥锁)替换了。

- (void)viewDidLoad {
    [super viewDidLoad];

    //设置票的数量为5
    self.tickets = 5;

    self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
    
    // #import <libkern/OSAtomic.h>
    // @property OSSpinLock pinLock;
    // 'OSSpinLock' is deprecated: first deprecated in iOS 10.0 - Use os_unfair_lock() from <os/lock.h> instead
    _pinLock = OS_SPINLOCK_INIT;

    //线程1
    dispatch_async(self.q1, ^{
        [self saleTickets];
    });

    //线程2
    dispatch_async(self.q1, ^{
        [self saleTickets];
    });
}

- (void)saleTickets {
    while (1) {
        [NSThread sleepForTimeInterval:1];
        OSSpinLockLock(&_pinLock);
        if (self.tickets > 0) {
            self.tickets -= 1;
            NSLog(@"剩余票数=%d, Thread:%@", self.tickets, [NSThread currentThread]);
        } else {
            NSLog(@"票卖完了  Thread:%@", [NSThread currentThread]);
            break;
        }
        OSSpinLockUnlock(&_pinLock);
    }
}

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

推荐阅读更多精彩内容