iOS线程和锁实现原理分析

作者: 魁拔2015 | 来源:发表于2018-09-11 09:39 被阅读482次

    一、线程分享梗概

    avatar

    二、线程的概念和实现

    1. 线程:是程序执行流的最小单元。一个标准的线程由线程ID,当前指令集合,寄存器集合和栈结构组成。线程是进程中的一个实体,为了解决进程调度的性能损耗问题,被系统独立调度和分派的基本单位。 线程占用的资源:
      Stack pointer
    
    Registers
    
    Scheduling properties (such as policy or priority)
    
    Set of pending and blocked signals
    
    Thread specific data.
    

    相较于进程,线程有更好的性能,充分利用进程资源(开辟50000个进程和线程的时间对比,单位为秒);


    avatar

    cpu带宽(带宽越高越好):


    avatar
    1. 线程实现方式: 如图所示:
      avatar
      对比如下:
      avatar

    三、iOS线程同步方式

    Atomic OperationsAtomic Operations是一类针对简单的数据类型的同步工具,Atomic操作不会阻塞竞争线程。比如自加操作,将会比互斥锁有更好的性能。 常用的原子操作如下:

    • 原子布尔操作 如OSAtomicOr32,OSAtomicAnd32,OSAtomicXor32等,分别或,与,异或。
    • 原子数学操作 如OSAtomicAdd32,原子相加。
    • 原子比较与交换 OSAtomicCompareAndSwapPtrBarrier,原子性比较并赋值。
    • 原子队列 原子性地加入和弹出栈。
    (一)、Atomic Operations使用举例
    1. OSAtomicAdd32
    int32_t b = 0;
    
    OSAtomicAdd32(a,&b);
    

    b将实现自加操作;因为ios10下不建议使用OSAtomic,建议的api使用举例如下:

    static atomic_int counter = 1;
    
    int oldValue = atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed);
    

    同时Atomic还支持or、xor异或、对NSObject变量赋值等操作;

    1. 利用OSAtomicCompareAndSwapPtrBarrier的原子性,来做静态变量赋值,从而保证单例方法的线程安全。 OSAtomicCompareAndSwapPtrBarrier( void *__oldValue, void *__newValue, void * volatile *__theValue ) 将比较__theValue与__oldValue的值,如果匹配,将执行
      *__theValue=__newValue;
    

    示例代码如下:

    static ViewController * sharedInstance;
    
    + (ViewController *) sharedInstance {    
      if (!sharedInstance) {       
         id temp = [super allocWithZone:NSDefaultMallocZone()];        //- checks whether       sharedInstance is NULL and only actually sets it to temp to it if it is.        //- This uses hardware support to really, literally only perform the swap once and tell whether it happened.       
         if(OSAtomicCompareAndSwapPtrBarrier(0x0, (__bridge void *)temp, (void *)(&sharedInstance))) {
           ViewController *singleton = (ViewController *) sharedInstance;                  
           [singleton testHash:singleton];        
           return sharedInstance;       
      }       
      else {         
        temp = nil;       
      }    
    }  
    return nil;}
    
    1. 原子性入栈、出栈操作; 只适用于结构体类型链式结构,不适用于oc对象出栈入栈。
    typedef struct elem {        int    data2;        int    data1;        struct elem *link;    } elem_t;
    
    elem_t fred, mary, *p;fred.data1 = 11;mary.data1 = 12;
    
    OSQueueHead q = OS_ATOMIC_QUEUE_INIT;
    
    OSAtomicEnqueue( &q, &fred, offsetof(elem_t,link));OSAtomicEnqueue( &q, &mary, offsetof(elem_t,link));
    
    p = (elem_t *)OSAtomicDequeue( &q, offsetof(elem_t,link));//输出第一个元素,data1的值为11.
    
    (二)、总结: 针对简单的赋值操作,如自加、减、异或、取交集并集、给变量赋值等条件下,atomic operations没有线程上下文切换的开销,将会有更好的性能。

    Memory Barriers and Volatile Variables

    (一)、内存屏障--Memory Barriers 1. 有这么一种场景:
    1 // thread 1
    
    2 while (!ok);
    
    3 do(x);
    
    4 // thread 2
    
    5 x = 42;
    
    6 ok = 1;
    

    如果第6行的ok=1先于第5行执行,那么do(x),将不会执行,这样会出现与开发者预先设想的内容不一样的结果。编译器为了获得更好的性能,编译器会对指令进行重新排序,这将导致cpu访问内存的顺序和我们的想像的不一样。

    1. Memory Barriers是一种为了让数据以正确的顺序访问,系统实现的一种非阻塞的同步方式,强制进程强制进程以barrier的顺序读写内存。

    2. 使用范例:

    // thread 1
    
    while (!ok);
    
    do(x);
    
    //    OSMemoryBarrier();
    
    atomic_thread_fence(memory_order_relaxed);
    
    x = 42;
    
    ok = 1;
    

    在需要强制顺序执行的代码块之间用OSMemoryBarrier或atomic_thread_fence方法分隔开,这将保证指令执行的顺序。

    (二)、Volatile变量 Volatile变量是提供内存管理的另一种方式。编译器经常通过从寄存器中读取变量来优化编译性能,但在多线程环境下,从寄存器中读取变量将导致数据不一致问题。Volatile声明的变量能保证cpu每次从内存中读取变量。所以系统提供这种方式来解决数据读取来源的问题。
    (三)、总结在Atomic Operations和lock当中,内部会引用Memory Barriers和Volatile来保证数据的访问一致性。

    Locks

    (一). Mutex

    a. 互斥锁是使用最广泛的一种锁。iOS的NSOperationQueue、GCD、NSThread都是基于pthread_mutex实现的。mutex是一种特殊的信号量,任务总数为1,同一时间内只有一个任务可以进入互斥区。
    b. 使用举例

    pthread_mutex_init(&mutt,0);
    
    pthread_mutex_lock(&mutt);
    
    b++;
    
    pthread_mutex_unlock(&mutt);
    

    c.实现原理:mutex底层有实现一个阻塞队列,如果当前有其他任务正在执行,则加入到队列中,放弃当前cpu时间片。一旦其他任务执行完,则从队列中取出等待执行的线程对象,恢复上下文重新执行。

    (二). Recursive lock

    a. 适合可能会出现递归调用的情形,如果用NSLock将导致阻塞。
    b. 用法和NSLock一致,只是实例化NSRecursiveLock类而已。
    c. 底层采用mutex实现,只设置了pthread的递归属性。

    pthread_mutex_t  Mutex;
    
    pthread_mutexattr_t attr;
    
    pthread_mutexattr_init(&attr);
    
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    
    pthread_mutex_init(&Mutex, &attr);
    

    从底层源代码来看,会检查当前线程是否已经获取了锁,如果是只是增加了引用次数。


    avatar
    (三). @synchronized 底层采用递归锁实现。 举例:
      static NSString *test22 = @"test";
    
        test22 = nullptr;
    
        static NSMutableArray * ar = [NSMutableArray array];
    
        NSLog(@"test22 = %@",test22);
    
        @synchronized(test22) {
    
            NSLog(@"exe = %lu",(unsigned long)[[NSThread currentThread] hash]);
    
            if (test22) {
    
                test22 = nil;
    
            }
    
            for (int i  = 0 ; i < 10000; i++) {
    
                [ar addObject:@(i)];
    
            }
    
        }
    

    注意如果@synchronized传的是一个null对象,将不会有锁作用。在使用synchronized时,应保证传入变量的值不会被修改,不会置null,和不会被改成其他值。

    avatar
    如果传入的value值变了,这里的SyncData对象是另一个SyncData对象,相应的mutex锁也发生了变化。不是同一把锁,也就不能起到线程安全的作用。
    typedef struct SyncData {
    
        struct SyncData* nextData;
    
        id              object;
    
        int              threadCount;  // number of THREADS using this block
    
        recursive_mutex_t        mutex;
    
    } SyncData;
    
    (四). Read-write lock * 针对多读少写的情况,有较大的性能优势。
    • 可以并发读取资源,但是有写的请求进来,则需要等读操作执行完之后才能执行写。
    • 如果当前正在写,有读操作进来,则需要等所有读操作完成才可以获得锁。写是串行,读是并行。举例:
    void* readerN(void* arg)
    
    {
    
        while(1)
    
        {
    
            pthread_rwlock_rdlock(&rwlock);
    
            printf(" N读者读出: %d \n",data);
    
            pthread_rwlock_unlock(&rwlock);
    
            sleep(1.2);
    
        }
    
        return NULL;
    
    }
    
    void* writerA(void* arg)
    
    {
    
        while(1)
    
        {
    
            pthread_rwlock_wrlock(&rwlock);      //写者加写锁
    
            data++;                              //对共享资源写数据
    
            printf("    A写者写入: %d\n",data);
    
            pthread_rwlock_unlock(&rwlock);      //释放写锁
    
            sleep(2);
    
        }
    
        return NULL;
    
    }
    
    void init(){
    
    pthread_rwlock_init(&rwlock, NULL);  //初始化读写锁   
    
    pthread_create(&t1,NULL,readerN,NULL);
    
    pthread_create(&t2,NULL,writerA,NULL);
    
        pthread_rwlock_destroy(&rwlock);
    
    }
    
    (五). Distributed lock 在多线程访问共享文件资源时使用,在iOS时用的较少。
    (六). Spin lock 自旋锁通过不断poll,轮循是否可以访问共享资源的方式实现线程同步。自旋锁有造成优先级反转的风险。 如果低优先级的线程占有了锁,而此时有很多高优先级的线程在等待,而此时低优先级线程竞争不过高优先级线程,将导致低优先级线程长期占用锁资源。故苹果不建议使用。建议采用os_unfair_lock锁,让内核接入线程调度的方式避免活锁现象的发生。举例:

    os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;

    os_unfair_lock_lock(&lock);

    //do something

    os_unfair_lock_unlock(&lock);

    (六). Double-checked lock 可能不安全,系统不建议使用。#####(七). Conditions 是一种允许线程发信号通知,在该条件上等待的线程,得到唤醒。很适合生产者消费者模式。举例:
    NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0];
    
    //线程1
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    
        [lock lockWhenCondition:1];
    
        NSLog(@"线程1");
    
        sleep(2);
    
        [lock unlock];
    
    });
    
    //线程2
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    
        sleep(1);//以保证让线程2的代码后执行
    
        if ([lock tryLockWhenCondition:0]) {
    
            NSLog(@"线程2");
    
            [lock unlockWithCondition:2];
    
            NSLog(@"线程2解锁成功");
    
        }
    
        else {
    
            NSLog(@"线程2尝试加锁失败");
    
        }
    
    });
    
    //线程4
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    
        [lock lockWhenCondition:2];
    
        NSLog(@"线程4");
    
        [lock unlockWithCondition:1];
    
        NSLog(@"线程4解锁成功");
    
    });
    
    (八). atomicatomic只能实现读写的线程安全,并不能保证该属性本身的线程安全。

    Perform Selector Routines 采用performSelector: onThread: withObject: waitUntilDone:的方式实现线程同步,但是任务是串行执行的,通过给thread所在的runloop发信号,实现线程通信的。

    四、多线程同步造成的问题总结

    1. 一边枚举边写会crash;
    2. 同时去写会crash;
    3. 锁应注意在每个要使用该变量的地方加锁;
    4. 宜用读写锁解决多读少写问题;或者dispatch_barrier;
    5. atomic只能保证set和get的安全,不能保证『改变集合类型变量』的线程安全;
    6. 注意锁的重入问题,对可能造成递归调用的情况用递归锁;
    7. 慎用并发队列dispatch_get_global_queue;

    五、锁实现原理

    1. 线程的划分: 从锁的实现原理上,分为"互斥锁、自旋锁、乐观锁"。
      a. 互斥锁基于futex(快速用户空间互斥体),原子性的访问各进程共享的外部空间,通过维护一个阻塞队列,如果当前没有线程使用该mutex,则直接取用。如果有,则加入队列,等待唤醒执行。
      b. 自旋锁,没有线程上下文的切换,在该线程中不停poll轮循,试探可不可以获得锁,这在耗时比较短的线程同步操作的场景下比较适合。
      c. 乐观锁。

    乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

    如图所示: avatar
    1. 信号量的实现原理:
    dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
    相当于信号量的P操作:
    void P(semaphore s){  
    s.count--;//信号量s的计数器减1 
     if(s.count < 0){    
    //调用p的进程到队列s.queue上排队;该进程为阻塞状态;  
    }}
    
    dispatch_semaphore_signal(_semaphore);
    相当于信号量的V操作:
    void V(semaphore s){
      s.count++; 
     if(s.count <= 0){
         //从队列s.queue上摘下一个进程,摘下进程状态为就绪,参与cpu调度 
    }}
    

    六、扩展

    1. 悲观锁、乐观锁、公平锁、非公平锁。
    2. 最多可以开辟64个线程

    七、各种锁的性能对比

    avatar

    执行30万次i++,并对比加锁与不加锁完成所有操作的时间。

    八、参考文档

    《操作系统》人民邮电出版社
    ios多线程编程指南官方文档
    POSIX Threads Programming——POSIX多线程编程指南(英文版)
    @synchronized底层实现,源码及博客
    实现互斥的linux底层Futex接口
    apple官方工程师Chris Lattner对自旋锁不安全的表述:
    Memory Barriers
    java中的锁分类
    atomic底层实现:
    OSAtomicCompareAndSwapPtrBarrier单例模式实现

    相关文章

      网友评论

        本文标题:iOS线程和锁实现原理分析

        本文链接:https://www.haomeiwen.com/subject/zweygftx.html