8. iOS中的多线程——锁

题记:虽然有些事情的发生可能是你预料之中的,但是当它真正的发生了的时候,还是很难以接受的,还是需要一点时间,去缓和这种消极的情绪,尽快站起来吧!加油!花了一天半的时间,各种查阅资料,总结了iOS中关于线程锁的知识,希望我能从中学到一些,也希望可以帮到同样有需要的你!(文中如有错误,还请提出,一起交流)


本文主要介绍:

  • 互斥锁
  • 递归锁
  • 读写锁
  • 自旋锁
  • 分布锁
  • 条件变量
  • 信号量
  • 栅栏
  • 一些常用锁的性能。
1. 互斥锁(Mutex)

常用,当一个线程试图获取被另一个线程占用的锁时,它就会被挂起,让出CPU,直到该锁被释放。

  • 互斥锁的实现方式:
    • @synchronized:实现单例模式
    • NSLock:不能迭代加锁,如果发生两次lock,而未unlock过,则会产生死锁问题。

1.@synchronized 同步锁

  • 例程:
/**
 *设置属性值
 */
-(void)setMyTestString:(NSString *)myTestString{
    @synchronized(self) {
        // todo something
        _myTestString = myTestString;
    }
}
  • 常用于单例模式的设计:

    例程:

+(instancetype)shareInstance{
    // 1.定义一个静态实例,初值nil
    static TestSynchronized *myClass = nil;
    // 2.添加同步锁,创建实例
    @synchronized(self) {
        // 3.判断实例是否创建过,创建过则退出同步锁,直接返回该实例
        if (!myClass) {
            // 4.未创建过,则新建一个实例并返回
            myClass = [[self alloc] init];
        }
    }
    return myClass;
}
此时为了保证单例模式的更加严谨,需要重写`allocWithZone`方法,保证其他开发者使用`alloc`和`init`方法时,不再创建新的对象。必要的时候还需要重写`copyWithZone`方法防止`copy`属性对单例模式的影响。

iOS中还有一种更加轻便的方法实现单例模式,即使用GCD中的dispatch_once函数实现。

例程:

+(instancetype)shareInstance{
    static TestSynchronized *myClass = nil;
    static dispatch_once_t once_token;
    dispatch_once(&once_token, ^{
        myClass = [[self alloc] init];
    });
    return myClass;
}

2.NSLock

  • 例程:
static NSLock *mylock;
-(void)viewDidLoad {
    [super viewDidLoad];
    mylock = [[NSLock alloc] init];
}
-(void)myLockTest1{
    if ([mylock tryLock]) {
        // to do something
        [mylock unlock];
    }
}
-(void)myLockTest2{
    [mylock lock];
    // to do something
    [mylock unlock];
}
2. 递归锁(Recursive Lock)
  • 递归锁可以被同一线程多次请求,而不会引起死锁,即在多次被同一个线程进行加锁时,不会造成死锁。这主要是用在循环或递归操作中。

  • 可以允许同一线程多次加锁,而不会造成死锁。

  • 递归锁会跟踪它被lock的次数。每次成功的lock都必须平衡调用unlock操作。只有所有达到这种平衡,锁最后才能被释放,以供其它线程使用。

  • 例程:

NSRecursiveLock *myRecursiveLock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        static void (^MyRecursiveLockBlk)(int value);
        MyRecursiveLockBlk = ^(int value){
            [myRecursiveLock lock];
            if (value > 0) {
                // to do something
                NSLog(@"MyRecursiveLockBlk value = %d", value);
                MyRecursiveLockBlk(value - 1);
            }
            [myRecursiveLock unlock];
        };
        MyRecursiveLockBlk(6);
    });

此时如果将例程中的递归锁换成互斥锁:
NSRecursiveLock *myRecursiveLock = [[NSRecursiveLock alloc] init];换成
NSLock *myLock = [[NSLock alloc] init];,则会发生死锁问题。

3. 读写锁(Read-write Lock)
  • 读写锁将访问者分为读出写入两种,当读写锁在读加锁模式下,所有以读加锁方式访问该资源时,都会获得访问权限,而所有试图以写加锁方式对其加锁的线程都将阻塞,直到所有的读锁释放。

  • 当在写加锁模式下,所有试图对其加锁的线程都将阻塞。

  • 例程:

#import "ViewController.h"
#import <pthread.h>
@interface ViewController ()
@property(nonatomic, copy) NSString *rwStr;
@end
@implementation ViewController
pthread_rwlock_t rwlock;
-(void)viewDidLoad {
    [super viewDidLoad];
    // 初始化读写锁
    pthread_rwlock_init(&rwlock,NULL);
    __block int i;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        i = 5;
        while (i>=0) {
            NSString *temp = [NSString stringWithFormat:@"writing == %d", i];
            [self writingLock:temp];
            i--;
        }  
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        i = 5;
        while (i>=0) {
            [self readingLock];
            i--;
        }
    });
}
// 写加锁
-(void)writingLock:(NSString *)temp{
    pthread_rwlock_wrlock(&rwlock);
    // writing
    self.rwStr = temp;
    NSLog(@"%@", temp);
    pthread_rwlock_unlock(&rwlock);
}
// 读加锁
-(NSString *)readingLock{
    pthread_rwlock_rdlock(&rwlock);
    // reading
    NSString *str = self.rwStr;
    NSLog(@"reading == %@",self.rwStr);
    pthread_rwlock_unlock(&rwlock);
    return str;
}
@end
4. 自旋锁(Spin Lock)
  • 自旋锁与互斥锁类似

  • 但不同的是:自旋锁是非阻塞的,当一个线程无法获取自旋锁时,会自旋,直到该锁被释放,等待的过程中线程并不会挂起。(实质上就是,如果自旋锁已经被别的执行单元保持,调用者就一直循环在等待该自旋锁的保持着已经释放了锁)。

  • 自旋锁的使用者一般保持锁的时间很短,此时其效率远高于互斥锁。

  • 自旋锁保持期间是抢占失效的

    优点:效率高,不用进行线程的切换

    缺点:如果一个线程霸占锁的时间过长,自旋会消耗CPU资源

  • 例程:

// 头文件
#import <libkern/OSAtomic.h>
// 初始化自旋锁
static OSSpinLock myLock = OS_SPINLOCK_INIT;
// 自旋锁的使用
-(void)SpinLockTest{
    OSSpinLockLock(&myLock);
    // to do something
    OSSpinLockUnlock(&myLock);
}
5. 分布锁(Didtributed Lock)
  • 跨进程的分布式锁,是进程间同步的工具,底层是用文件系统实现的互斥锁,并不强制进程休眠,而是起到告知的作用。
  • NSDistributedLock没有实现NSLocking协议,所以没有会阻塞线程的lock方法,取而代之的是非阻塞的tryLock方法来获取锁,用unlock方法释放锁。
  • 如果一个获取锁的进程在释放锁之前就退出了,那么锁就一直不能释放,此时可以通过breakLock强行获取锁。
6. 条件变量(Condition Variable)
  • 使用情况:如果一个线程需要等待某一条件出现才能继续执行,而这个条件是由别的线程产生的,这个时候就用到条件变量。常见的情况是:生产者-消费者问题。

  • 条件变量可以让一个线程等待某一条件,当条件满足时,会收到通知。在获取条件变量并等待条件发生的过程中,也会产生多线程的竞争,所以条件变量通常和互斥锁一起工作。

    • NSCondition:是互斥锁和条件锁的结合,即一个线程在等待signal而阻塞时,可以被另一个线程唤醒,由于操作系统实现的差异,即使没有发送signal消息,线程也有可能被唤醒,所以需要增加谓词变量来保证程序的正确性。
    • NSConditionLock:与NSCondition的实现机制不一样,当定义的条件成立的时候会获取锁,反之,释放锁。

    NSCondition的例程:

    // 创建锁
     NSCondition *condition = [[NSCondition alloc] init];
    static int count = 0;
    // 生产者
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while(count<20)
        {
            [condition lock];
            // 生产
            count ++;
            NSLog(@"生产 = %d",count);
            [condition signal];
            [condition unlock];
        }
    });
    // 消费者
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while (count>0)
        {
            [condition lock];
            // 消耗
            count --;
            NSLog(@"消耗剩余 = %d",count);
            [condition unlock];
        }
    });

NSConditionLock的例程:

    // 创建锁
    NSConditionLock *condLock = [[NSConditionLock alloc] initWithCondition:ConditionHASNOT];
    static int count = 0;
    // 生产者
    while(true)
    {
        [condLock lock];
        // 生产
        count ++;
        [condLock unlockWithCondition:ConditionHAS];
    }
    // 消费者
    while (true)
    {
        [condLock lockWhenCondition:ConditionHAS];
        // 消耗
        count --;
        [condLock unlockWithCondition:(count<=0 ? ConditionHASNOT : ConditionHAS)];
    }
7. 信号量(Semaphore)
  • 信号量:可以是一种特殊的互斥锁,可以是资源的计数器
  • 可以使用GCD中的Dispatch Semaphore实现,Dispatch Semaphore是持有计数的信号,该计数是多线程编程中的计数类型信号。计数为0时等待,计数大于等于1时,减1为不等待。
8. 栅栏/屏障(Barrier)
  • 栅栏必须单独执行,不能与其他任务并发执行,栅栏只对并发队列有意义。
  • 栅栏只有等待当前队列所有并发任务都执行完毕后,才会单独执行,带起执行完毕,再按照正常的方式继续向下执行。

iOS中线程锁的性能对比:

点击这里,参考网址

  • No1.自旋锁OSSpinLock耗时最少
  • No2.pthread_mutex
  • No3.NSLock/NSCondition/NSRecursiveLock 耗时接近
  • No4.@synchronized
  • No5.NSConditionLock
  • 栅栏的性能并没有很好,在实际开发中也很少用到(笔者在最近一次面试中就遇到,问栅栏的性能怎么样?当时并不知道栅栏在实际应用中的性能并不是很理想,又被问到苹果官方常使用的锁是什么?应该是自旋锁,--然而笔者当时还是不知道。。。)

> 划重点:自旋锁是线程不安全的在 ibireme 的 不再安全的 OSSpinLock有解释,进一步的ibireme在文中也有提到苹果在新系统中已经优化了 pthread_mutex 的性能,所以它看上去和 OSSpinLock 差距并没有那么大,所以笔者觉得不妨多了解了解pthread_mutex
  • pthread_mutex
    __block pthread_mutex_t theLock;
    pthread_mutex_init(&theLock, NULL);
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            pthread_mutex_lock(&theLock);
            NSLog(@"需要线程同步的操作1 开始");
            sleep(3);
            NSLog(@"需要线程同步的操作1 结束");
            pthread_mutex_unlock(&theLock);
        
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            sleep(1);
            pthread_mutex_lock(&theLock);
            NSLog(@"需要线程同步的操作2");
            pthread_mutex_unlock(&theLock);
        
    });

c语言定义下多线程加锁方式。

  1. pthread_mutex_init(pthread_mutex_t * mutex,const pthread_mutexattr_t attr);
    初始化锁变量mutex。attr为锁属性,NULL值为默认属性。
  2. pthread_mutex_lock(pthread_mutex_t* mutex);加锁
  3. pthread_mutex_tylock(pthread_mutex_t* mutex);加锁,但是与2不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待。
  4. pthread_mutex_unlock(pthread_mutex_t* mutex);释放锁
  5. pthread_mutex_destroy(pthread_mutex_t* *mutex);使用完后释放

代码执行操作结果如下:

2016-06-30 21:13:32.440 SafeMultiThread[31429:548869] 需要线程同步的操作1 开始
2016-06-30 21:13:35.445 SafeMultiThread[31429:548869] 需要线程同步的操作1 结束
2016-06-30 21:13:35.446 SafeMultiThread[31429:548866] 需要线程同步的操作2

  • pthread_mutex(recursive)
    __block pthread_mutex_t theLock;
    //pthread_mutex_init(&theLock, NULL);
    
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&lock, &attr);
    pthread_mutexattr_destroy(&attr);
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        static void (^RecursiveMethod)(int);
        
        RecursiveMethod = ^(int value) {
            
            pthread_mutex_lock(&theLock);
            if (value > 0) {
                
                NSLog(@"value = %d", value);
                sleep(1);
                RecursiveMethod(value - 1);
            }
            pthread_mutex_unlock(&theLock);
        };
        
        RecursiveMethod(5);
    });

这是pthread_mutex为了防止在递归的情况下出现死锁而出现的递归锁。作用和NSRecursiveLock递归锁类似。
如果使用pthread_mutex_init(&theLock, NULL);初始化锁的话,上面的代码会出现死锁现象,但是改成使用递归锁的形式,则没有问题。

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

推荐阅读更多精彩内容