美文网首页一个苹果
细数iOS中的线程同步方案(一)

细数iOS中的线程同步方案(一)

作者: _小沫 | 来源:发表于2020-01-08 21:40 被阅读0次

    细数iOS中的线程同步方案(一)
    细数iOS中的线程同步方案(二)

    多线程安全问题

    多个线程可能访问同一块资源,比如同一个文件,同一个对象,同一个变量等;当多个线程访问同一资源时,容易引发数据错乱和数据安全问题

    如下面这个经典图所示,线程A、B均访问了Integer变量,但最终的结果(18)可能并不是我们想要的(19);

    如果要保证共享的数据是正确的安全的,就需要使用线程同步技术:让多个线程间按顺序执行而不是并发执行;常见的线程同步技术就是加锁,同上面例子一样,加锁后能保证最终结果是正常的;

    iOS中的线程同步方案常见的有以下几种:

    • pthread相关方案
    • OSSpinLock
    • os_unfair_lock
    • GCD相关方案
    • NSOperationQueue相关方案
    • NSLock
    • NSRecursiveLock
    • NSCondition
    • NSConditionLock
    • @synchronized

    pthread相关方案

    pthread是跨平台的,而且更加底层;我们先来了解pthread相关的锁;

    PTHREAD_MUTEX_NORMAL 普通互斥锁

    互斥锁的机制:被这个锁保护的临界区就只允许一个线程进入,其它线程如果没有获得锁权限,那就只能在外面等着;等待锁的线程会处于休眠状态,处于休眠状态不会占用CPU资源;

    pthread_mutex的使用:

        // 两种初始化方式
        // 1.静态初始化
        static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
        
        // 2.动态创建
        pthread_mutex_t lock1;
        pthread_mutex_init(&lock1, NULL); // 可以根据需要配置pthread_mutexattr NULL默认为互斥锁
        
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        [queue addOperationWithBlock:^{
            pthread_mutex_lock(&lock); // 加锁
            NSLog(@"%@===write===start",[NSThread currentThread]);
            sleep(3);
            NSLog(@"%@===write===end",[NSThread currentThread]);
            pthread_mutex_unlock(&lock); // 解锁
        }];
        
        [queue addOperationWithBlock:^{
            pthread_mutex_lock(&lock); 
            NSLog(@"%@===read===start",[NSThread currentThread]);
            sleep(2);
            NSLog(@"%@===read===end",[NSThread currentThread]);
            pthread_mutex_unlock(&lock);
        }];
    
    

    两个线程任务同步执行:

         <NSThread: 0x2834863c0>{number = 5, name = (null)}===write===start
         <NSThread: 0x2834863c0>{number = 5, name = (null)}===write===end
         <NSThread: 0x2834848c0>{number = 6, name = (null)}===read===start
         <NSThread: 0x2834848c0>{number = 6, name = (null)}===read===end
         // 或
         <NSThread: 0x283486200>{number = 8, name = (null)}===read===start
         <NSThread: 0x283486200>{number = 8, name = (null)}===read===end
         <NSThread: 0x283486480>{number = 7, name = (null)}===write===start
         <NSThread: 0x283486480>{number = 7, name = (null)}===write===end
    

    PTHREAD_MUTEX_RECURSIVE 递归锁

    顾名思义,递归锁用于递归调用加锁的情况;对于递归调用的加锁,如果使用上面normal锁,则会出现死锁;递归锁就是保证了对同一把锁能多次加锁,而不用等待解锁,从而避免了递归造成的死锁问题;

    - (void)synchronizedTest {
        pthread_mutexattr_t att;
        pthread_mutexattr_init(&att);
        pthread_mutexattr_settype(&att, PTHREAD_MUTEX_RECURSIVE); // PTHREAD_MUTEX_NORMAL普通互斥锁 PTHREAD_MUTEX_RECURSIVE递归锁
        pthread_mutex_init(&_lock, &att);
        pthread_mutexattr_destroy(&att);
        
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        [queue addOperationWithBlock:^{
            [self recursiveTest:3]; // 递归调用
        }];
    }
    
    // 递归方法
    - (void)recursiveTest:(NSInteger)value {
        pthread_mutex_lock(&_lock);
        
        if (value > 0) {
            NSLog(@"%@===start",[NSThread currentThread]);
            sleep(1);
            NSLog(@"%@===end",[NSThread currentThread]);
            [self recursiveTest:value-1];
        }
        
        pthread_mutex_unlock(&_lock);
    }
    

    输出正确的结果:

         <NSThread: 0x280d642c0>{number = 3, name = (null)}===start
         <NSThread: 0x280d642c0>{number = 3, name = (null)}===end
         <NSThread: 0x280d642c0>{number = 3, name = (null)}===start
         <NSThread: 0x280d642c0>{number = 3, name = (null)}===end
         <NSThread: 0x280d642c0>{number = 3, name = (null)}===start
         <NSThread: 0x280d642c0>{number = 3, name = (null)}===end
    

    pthread_rwlock 读写锁

    以上锁能很好的解决线程安全问题,但是这样的话同一时间,只会有一个线程能执行;有时我们的需求并不希望这样,比如读写操作:我们希望读是不受同步机制限制,即允许多个线程同时读;对于写,我们希望同一时间只允许一个线程操作;同时,在写操作进行时不允许同时读;而读写锁就是为这种场景而生的:
    pthread_rwlock 读写锁与基本的互斥锁的创建使用方式大同小异:

        // 两种初始化方式
        // 1.静态初始化
        static pthread_rwlock_t lock = PTHREAD_RWLOCK_INITIALIZER;
        
        // 2.动态创建
        static pthread_rwlock_t lock1;
        pthread_rwlock_init(&lock1, NULL);
        
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        
        for (int i = 0; i < 3; i ++) {
            [queue addOperationWithBlock:^{
                pthread_rwlock_wrlock(&lock);
                NSLog(@"%@===write===start",[NSThread currentThread]);
                sleep(3);
                NSLog(@"%@===write===end",[NSThread currentThread]);
                pthread_rwlock_unlock(&lock);
            }];
        }
        
        for (int i = 0; i < 3; i ++) {
            [queue addOperationWithBlock:^{
                pthread_rwlock_rdlock(&lock);
                NSLog(@"%@===read===start",[NSThread currentThread]);
                sleep(2);
                NSLog(@"%@===read===end",[NSThread currentThread]);
                pthread_rwlock_unlock(&lock);
            }];
        }
    

    结果中多个read是可以并发的,write是同步执行的;

         <NSThread: 0x281b83440>{number = 5, name = (null)}===write===start
         <NSThread: 0x281b83440>{number = 5, name = (null)}===write===end
         <NSThread: 0x281b83400>{number = 6, name = (null)}===write===start
         <NSThread: 0x281b83400>{number = 6, name = (null)}===write===end
         <NSThread: 0x281b94940>{number = 4, name = (null)}===write===start
         <NSThread: 0x281b94940>{number = 4, name = (null)}===write===end
         <NSThread: 0x281b9aa00>{number = 3, name = (null)}===read===start
         <NSThread: 0x281b864c0>{number = 7, name = (null)}===read===start
         <NSThread: 0x281b87780>{number = 8, name = (null)}===read===start
         <NSThread: 0x281b87780>{number = 8, name = (null)}===read===end
         <NSThread: 0x281b9aa00>{number = 3, name = (null)}===read===end
         <NSThread: 0x281b864c0>{number = 7, name = (null)}===read===end
    

    pthread_join

    使用场景:有A,B两个线程,B线程在做某些事情之前,必须要等待A线程把事情做完,然后才能接着做下去。这时候就可以用join。

    static pthread_t thread1;
    static pthread_t thread2;
    
    void * writeFunc(void *args) {
        NSLog(@"%u===write===start",(unsigned int)pthread_self());
        sleep(3);
        NSLog(@"%u===write===end",(unsigned int)pthread_self());
        pthread_exit(NULL);
        return NULL;
    }
    
    void* readFunc(void *args) {
        pthread_join(thread1, NULL);
        NSLog(@"%u===read===start",(unsigned int)pthread_self());
        sleep(2);
        NSLog(@"%u===read===end",(unsigned int)pthread_self());
        return NULL;
    }
    
    - (void)synchronizedTest {
        pthread_create(&thread1, NULL, writeFunc, NULL);
        pthread_create(&thread2, NULL, readFunc, NULL);
    }
    

    这样就保证了read一定是在write后

         871015936===write===start
         871015936===write===end
         871589376===read===start
         871589376===read===end
    

    pthread_cond 条件锁

    条件锁能在合适的时候唤醒正在等待的线程。具体什么时候合适由程序员自己控制条件变量决定;
    具体的场景就是:
    B线程和A线程之间有合作关系,当A线程完操作前,B线程会等待。当A线程完成后,需要让B线程知道,然后B线程从等待状态中被唤醒,然后处理自己的任务。

        // 1.静态初始化
        static pthread_cond_t cond_lock = PTHREAD_COND_INITIALIZER;
        static pthread_mutex_t mutex_lock = PTHREAD_MUTEX_INITIALIZER; // 需要配合mutex互斥锁使用
        
        // 2.动态创建
        static pthread_cond_t cond_lock1;
        pthread_cond_init(&cond_lock1, NULL);
        
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        [queue addOperationWithBlock:^{
            pthread_mutex_lock(&mutex_lock);
            while (self.condition_value <= 0) { // 条件成立则暂时解锁并等待
                pthread_cond_wait(&cond_lock, &mutex_lock);
            }
            
            NSLog(@"%@===read===start",[NSThread currentThread]);
            sleep(2);
            NSLog(@"%@===read===end",[NSThread currentThread]);
            pthread_mutex_unlock(&mutex_lock);
        }];
        
        [queue addOperationWithBlock:^{
            pthread_mutex_lock(&mutex_lock);
            NSLog(@"%@===write===start",[NSThread currentThread]);
            sleep(3);
            self.condition_value = 1; // 一定要更改条件 否则上面read线程条件成立又会wait
            NSLog(@"%@===write===end",[NSThread currentThread]);
            
            pthread_cond_signal(&cond_lock); // 传递信号给等待的线程 而且是在解锁前
    //        pthread_cond_broadcast(pthread_cond_t * _Nonnull) // 通知所有线程
            
            pthread_mutex_unlock(&mutex_lock);
        }];
    
         <NSThread: 0x283783e40>{number = 3, name = (null)}===write===start
         <NSThread: 0x283783e40>{number = 3, name = (null)}===write===end
         <NSThread: 0x28379aa40>{number = 4, name = (null)}===read===start
         <NSThread: 0x28379aa40>{number = 4, name = (null)}===read===end
    

    这里有几个需要注意的地方:

    • 一定要配合互斥锁使用;
    • 一定要判断条件并更改条件;
    • 最好使用while做条件判断(而不是if)
    • 发送信号时,最好在临近区内发送(即互斥锁范围内);

    以上几点的原因,可以参考下面大神的文章;

    semaphore 信号量

    信号量维护了一个unsigned int类型的value,通过这个值控制线程同步;具体有以下使用场景:

    • 信号量的初始值设为1,代表同时只允许1条线程访问资源,保证线程同步
        // 创建 原型sem_t *sem_open(const char *name,int oflag,mode_t mode,unsigned int value);
        // name 信号的外部名字
        // oflag 选择创建或打开一个现有的信号灯
        // mode 权限位
        // value 信号初始值
        sem_t * sem = sem_open("semname", O_CREAT, 0644, 1);
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        [queue addOperationWithBlock:^{
            sem_wait(sem); // 首先判断信号量value 如果=0则等待,否则value-1并正常往下走
            NSLog(@"%@===write===start",[NSThread currentThread]);
            sleep(3);
            NSLog(@"%@===write===end",[NSThread currentThread]);
            sem_post(sem); // 执行完发送信号,value+1
        }];
    
        [queue addOperationWithBlock:^{
            sem_wait(sem);
            NSLog(@"%@===read===start",[NSThread currentThread]);
            sleep(2);
            NSLog(@"%@===read===end",[NSThread currentThread]);
            sem_post(sem);
        }];
    
    • 信号量的初始值value,可以用来控制线程并发访问的最大数量
    sem_t *sem = sem_open("semname_count", O_CREAT, 0644, 3);
    
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        for (int i = 0; i < 21; i ++) {
            [queue addOperationWithBlock:^{
                sem_wait(sem);
                NSLog(@"%@===write===start",[NSThread currentThread]);
                sleep(2);
                NSLog(@"%@===write===end",[NSThread currentThread]);
                sem_post(sem);
            }];
        }
    

    输出结果可以看出最多最会有3个线程:

         <NSThread: 0x280431380>{number = 6, name = (null)}===write===start
         <NSThread: 0x28040cb80>{number = 5, name = (null)}===write===start
         <NSThread: 0x280431500>{number = 7, name = (null)}===write===start
         <NSThread: 0x28040cb80>{number = 5, name = (null)}===write===end
         <NSThread: 0x28040cb80>{number = 5, name = (null)}===write===start
         <NSThread: 0x280431380>{number = 6, name = (null)}===write===end
         <NSThread: 0x280431380>{number = 6, name = (null)}===write===start
    

    以上代码,其实就类似设置NSOperationQueue的maxConcurrentOperationCount效果;

        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        queue.maxConcurrentOperationCount = 3;
    

    OSSpinLock自旋锁

    自旋锁的作用同互斥锁一样,不同于互斥锁的线程休眠机制,自旋锁等待的线程会忙等,也就是等待的过程其实是在跑一个while循环;这样等待的过程同样消耗CPU资源,但这种方式不会涉及线程唤醒、休眠的切换,性能会高点;

    __block OSSpinLock lock = OS_SPINLOCK_INIT;
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperationWithBlock:^{
        OSSpinLockLock(&lock);
        NSLog(@"%@===write===start",[NSThread currentThread]);
        sleep(3);
        NSLog(@"%@===write===end",[NSThread currentThread]);
        OSSpinLockUnlock(&lock);
    }];
    [queue addOperationWithBlock:^{
        OSSpinLockLock(&lock);
        NSLog(@"%@===read===start",[NSThread currentThread]);
        sleep(2);
        NSLog(@"%@===read===end",[NSThread currentThread]);
        OSSpinLockUnlock(&lock);
    }];
    

    同样能同步执行,但代码会有警告:

    'OSSpinLockUnlock' is deprecated: first deprecated in iOS 10.0 - Use os_unfair_lock_unlock() from <os/lock.h> instead

    这是因为OSSpinLock已经不再安全了,会有优先级反转问题;
    多线程并发处理,原理上说是CPU时间片轮转机制,即将时间划分为极小单位,每个线程依次执行这极段的时间;这样多个线程看起来是同时执行的;另外,不同的线程有可能是不同的优先级;高优先级的线程要占用较长的时间、CPU资源;高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。
    如果使用自旋锁,且一个低优先级的线程先于高优先级的线程获得锁并访问共享资源;同时高优先级的线程也会尝试获取锁,获取锁失败就一直忙等,忙等状态占用大量CPU资源;而低优先级的线程也需要CPU资源,但是竞争不过从而导致任务迟迟完不成,无法解锁;

    苹果给的建议是使用os_unfair_lock替代,但这个最低只支持iOS10;

        __block os_unfair_lock lock = OS_UNFAIR_LOCK_INIT; // 初始化
        os_unfair_lock_lock(&lock); // 加锁
        os_unfair_lock_unlock(&lock); // 解锁
    

    参考:
    pthread的各种同步机制
    不再安全的 OSSpinLock

    相关文章

      网友评论

        本文标题:细数iOS中的线程同步方案(一)

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