今天看啥  ›  专栏  ›  Fade_VV

OC-ios多线程与线程同步方案

Fade_VV  · 掘金  ·  · 2021-04-02 16:45
阅读 15

OC-ios多线程与线程同步方案

多线程的安全隐患

  • 资源共享

    • 1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源
    • 比如多个线程访问同一个对象、同一个变量、同一个文件
  • 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题

多线程安全隐患示例01 – 存钱取钱

image.png

  • 案例分析
    • 1.时间顺序 存钱线程读取金额1000,
    • 2.存钱线程还没有存入的时候,取钱线程也读取金额1000
    • 3.存钱线程存入1000 存钱线程显示为2000
    • 4.取钱金额取出500 显示金额为500
    • 5.最后显示为500

多线程安全隐患示例02 – 卖票

image.png

  • 案例代码
#import "ViewController.h"

@interface ViewController ()
@property (assign, nonatomic) int ticketsCount;
@property (assign, nonatomic) int money;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    [self moneyTest];
}

/**
 存钱、取钱演示
 */
- (void)moneyTest
{
    self.money = 100;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 10; i++) {
            [self saveMoney];
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 10; i++) {
            [self drawMoney];
        }
    });
}

/**
 存钱
 */
- (void)saveMoney
{
    int oldMoney = self.money;
    sleep(.2);
    oldMoney += 50;
    self.money = oldMoney;
    
    NSLog(@"存50,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
}

/**
 取钱
 */
- (void)drawMoney
{
    int oldMoney = self.money;
    sleep(.2);
    oldMoney -= 20;
    self.money = oldMoney;
    
    NSLog(@"取20,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
}

/**
 卖1张票
 */
- (void)saleTicket
{
    int oldTicketsCount = self.ticketsCount;
    sleep(.2);
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    
    NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
}

/**
 卖票演示
 */
- (void)ticketTest
{
    self.ticketsCount = 15;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
             [self saleTicket];
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
}


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
}

复制代码

多线程安全隐患分析

image.png

  • 解决方案:使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)
  • 常见的线程同步技术是:加锁

image.png

  • 线程A访问Integer的时候先加锁,访问完成后解锁
  • 在线程B访问加锁,完成后解锁。

iOS中的线程同步方案

  • OSSpinLock
  • os_unfair_lock
  • pthread_mutex
  • dispatch_semaphore
  • dispatch_queue(DISPATCH_QUEUE_SERIAL) //串行队列
  • NSLock
  • NSRecursiveLock
  • NSCondition
  • NSConditionLock

@synchronized

OSSpinLock

  • OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
  • 目前已经不再安全,可能会出现优先级反转问题
  • 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
  • 需要导入头文件#import #import <libkern/OSAtomic.h>

image.png

  • 总结
    • 自旋锁:当线程加锁时,如果这个锁已经被其他线程锁上,会一直在等待其开锁。内部做一个while循环占用cpu资源。
    • 简单的初始化加锁和解锁,不同的线程必须用的同一把锁才可以。
  • 可能会出现优先级反转问题。
    • 假设线程1的优先级高于线程2。
    • 但是开启任务是先进入线程2,线程2上锁,
    • 线程1开启任务了,等待线程2开锁。
    • 这个时候由于线程1的优先级高于线程2,要先完成线程1才去做线程2,但是线程1在等待线程2开锁。
    • 最终程序死锁。

os_unfair_lock

  • os_unfair_lock用于取代不安全的OSSpinLock ,从iOS10开始才支持
  • 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等
  • 需要导入头文件#import <os/lock.h>

image.png

  • 总结
    • 互斥锁代替自旋锁。 用法同上,简单的加锁解锁。

pthread_mutex

  • mutex叫做”互斥锁”,等待锁的线程会处于休眠状态,互斥锁也会引起死锁。
  • 需要导入头文件#import <pthread.h>

image.png image.png

  • 总结
    • pthread_mutex_init(mutex,NULL) //创建锁
    • 可以添加属性,但是在添加属性之前需要初始化属性,属性初始化完要销毁属性
    • 最后销毁锁

pthread_mutex – 递归锁

  • // 递归锁:允许同一个线程对一把锁进行重复加锁,必须是同一线程。

image.png

- (void)otherTest
{
    pthread_mutex_lock(&_mutex);
    
    NSLog(@"%s", __func__);
    
    static int count = 0;
    if (count < 10) {
        count++;
        [self otherTest];
    }
    
    pthread_mutex_unlock(&_mutex);
}

复制代码
  • 总结
    • 当调用的函数为递归函数时,还没有解锁就又一次进入方法开始加锁,引起死锁。
    • 在创建锁的时候传入 PTHREAD_MUTEX_RECURSIVE
    • 递归锁:允许同一个线程对一把锁进行重复加锁,必须是同一线程。
     线程1:otherTest(+-)
            otherTest(+-)
             otherTest(+-)
     线程2:otherTest(等待)        
    复制代码
    • 知道线程1的递归方法全部调用完毕之后,线程2才开始任务。

pthread_mutex – 条件

image.png

  • // 等待
    • pthread_cond_wait(&_cond, &_mutex);
  • // 信号
    • pthread_cond_signal(&_cond);
  • 总结
    • 先初始化条件 pthread_cond_init(&_cond, NULL);
    • 加入线程1先开启任务,1上锁,线程2等待线程1开锁后再上锁执行。
    • 线程1中有判断条件成立后,执行这个pthread_cond_wait(&_cond, &_mutex);
    • pthread_cond_wait(&_cond, &_mutex);线程1的锁先解锁,进入休眠状态。
    • 线程2发现线程1开锁了就开始执行了,执行完毕后发送信号 pthread_cond_signal(&_cond);
    • 线程1接收到信号,等待线程2的开锁,当线程2完成开锁后,线程1上锁,接着执行下面的任务。

NSLock、NSRecursiveLock

  • NSLock是对mutex普通锁的封装

image.png image.png image.png

  • NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致
  • 总结
    • 是上面方法的封装用法方便,不需要很麻烦的创建。
    self.ticketLock = [[NSLock alloc] init]; //创建锁
    [self.ticketLock lock]; //上锁
    [self.ticketLock unlock]; //解锁
    复制代码
    • NSRecursiveLock 对递归锁的封装。

NSCondition

  • NSCondition是对mutex和cond的封装

image.png

  • 总结
    • 对条件锁的封装 不需要初始化条件
    @property (strong, nonatomic) NSCondition *condition;
    self.condition = [[NSCondition alloc] init];
    [self.condition lock];
    [self.condition wait];
    [self.condition signal];
    [self.condition unlock];
    复制代码

NSConditionLock

  • NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值

image.png

self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];

- (void)__one
{
    [self.conditionLock lock];
    
    NSLog(@"__one");
    sleep(1);
    
    [self.conditionLock unlockWithCondition:2];
}

- (void)__two
{
    [self.conditionLock lockWhenCondition:2];
    
    NSLog(@"__two");
    sleep(1);
    
    [self.conditionLock unlockWithCondition:3];
}

- (void)__three
{
    [self.conditionLock lockWhenCondition:3];
    
    NSLog(@"__three");
    
    [self.conditionLock unlock];
}
复制代码
  • 总结
    • 初始化条件1,符合条件的加锁
    • 2等待条件为2,3等待条件为3.
    • 1执行后解锁,设置条件为2
    • 条件2的上锁,解锁后设置条件为3
    • ...
    • 保证了线程的一步一步执行,执行1->2->3
  • condition和conditionLock的差别
    • condition是1执行到中途解锁休眠等待,2执行后发送通知给1,1被唤醒继续执行
    • conditionLock,设置了条件只有条件符合了才能被唤醒执行。

dispatch_semaphore

  • semaphore叫做”信号量”
  • 信号量的初始值,可以用来控制线程并发访问的最大数量
  • 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步

image.png

- (instancetype)init
{
    if (self = [super init]) {
        self.semaphore = dispatch_semaphore_create(5);
        self.ticketSemaphore = dispatch_semaphore_create(1);
        self.moneySemaphore = dispatch_semaphore_create(1);
    }
    return self;
}

- (void)__drawMoney
{
    dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
    
    [super __drawMoney];
    
    dispatch_semaphore_signal(self.moneySemaphore);
}

- (void)__saveMoney
{
    dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
    
    [super __saveMoney];
    
    dispatch_semaphore_signal(self.moneySemaphore);
}

- (void)__saleTicket
{
    dispatch_semaphore_wait(self.ticketSemaphore, DISPATCH_TIME_FOREVER);
    
    [super __saleTicket];
    
    dispatch_semaphore_signal(self.ticketSemaphore);
}

- (void)otherTest
{
    for (int i = 0; i < 20; i++) {
        [[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
    }
}

// 线程10、7、6、9、8
- (void)test
{
    // 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
    // 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    
    sleep(2);
    NSLog(@"test - %@", [NSThread currentThread]);
    
    // 让信号量的值+1
    dispatch_semaphore_signal(self.semaphore);
}

复制代码
  • 总结
    • 类似下面的串行队列,并发数设置为1只有一条线程访问资源
    • self.semaphore = dispatch_semaphore_create(5); 这个一开始给他5条并发量 线程1、2、3、4、5.
    • DISPATCH_TIME_FOREVER 永远
    • dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);这个内部会执行命令的同时信号量-1,当信号量<=0值进入等待,等待时间有参数控制。所以这里会开启5条线程做任务,当着5条线程中的任务都还没有完成时,在执行到这里是,就会进入睡眠等待。所以不会开启线程6.
    • dispatch_semaphore_signal(self.semaphore); +1,后唤醒了dispatch_semaphore_wait,这个时候线程1完成了任务并被销毁。开启线程6.这是在工作的线程是 线程2、3、4、5、6.依旧是5个线程工作。

dispatch_queue

  • 直接使用GCD的串行队列,也是可以实现线程同步的

image.png

  • 串行队列无法开启新线程,相当于任务只有一个人来做,保证任务的顺序执行

@synchronized

  • @synchronized是对mutex递归锁的封装
  • 源码查看:objc4中的objc-sync.mm文件
  • @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作

image.png

image.png

  • 传入一个对象并以这个对象创建一个互斥锁。
  • 加入两个方法控制了同一对象就用同一个对象

image.png

  • 可以创建一个静态变量做个锁保证同一个对象。

image.png

  • 性能最差并不推荐。

iOS线程同步方案性能比较 仅供参考

  • 性能从高到低排序
  • os_unfair_lock //底层源码中有看到这个 ios10 开始支持
  • OSSpinLock
  • dispatch_semaphore
  • pthread_mutex
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSCondition
  • pthread_mutex(recursive) //递归锁保证递归方法的调用。
  • NSRecursiveLock
  • NSConditionLock
  • @synchronized

自旋锁、互斥锁比较

  • 什么情况使用自旋锁比较划算?

    • 预计线程等待锁的时间很短
    • 加锁的代码(临界区)经常被调用,但竞争情况很少发生
    • CPU资源不紧张
    • 多核处理器
  • 什么情况使用互斥锁比较划算?

    • 预计线程等待锁的时间较长
    • 单核处理器
    • 临界区有IO操作
    • 临界区代码复杂或者循环量大
    • 临界区竞争非常激烈



原文地址:访问原文地址
快照地址: 访问文章快照