线程同步及线程锁

1 资源竞争与线程同步

竞争态条件下,多个线程对同一竞态资源的抢夺会引发线程安全问题。竞态资源是对多个线程可见的共享资源,主要包括全局(非const)变量、静态(局部)变量、堆变量、资源文件等。

线程之间的竞争,可能带来一些列问题:

  • 线程在操作某个共享资源的过程中被其他线程所打断,时间片耗尽而被迫切换到其他线程
  • 共享资源被其他线程修改后的不到告知,造成线程间数据不一致
  • 由于编译器优化等原因,若干操作指令的执行顺序被打乱,造成结果的不可预期

1.1 原子操作

原子操作,即不可分割开的操作;该操作一定是在同一个cpu时间片中完成,这样即使线程被切换,多个线程也不会看到同一块内存中不完整的数据。

原子表示不可分割的最小单元,具体来说是指在所处尺度空间或者层(layer)中不能观测到更为具体的内部实现与结构。对于计算机程序执行的最小单位是单条指令。我们可以通过参考各种cpu的指令操作手册,用其汇编指令编写原子操作。而这种方式太过于低效。

某些简单的表达式可以算作现代编程语言的最小执行单元 某些简单的表达式,其实编译之后的得到的汇编指令,不止一条,所以他们并不是真正意义原子的。以加法指令操作实现 x += n为例 ,gcc编译出来的汇编形式上如下:

...
movl 0xc(%ebp), %eax
addl $n, %eax
movl %eax, 0xc(%ebp)
...
复制代码

而将它放在所线程环境之中,显然也是不安全的:

dispatch_group_t group = dispatch_group_create();
    __block int  i = 1;
for (int k = 0; k < 300; k++) {
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        ++i;
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        --i;
        dispatch_group_leave(group);
    });
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"----result=%d  i=%d",self.pro1,i);
});
复制代码

上述例子中,全局变量i理论上应该最后得到1,而实际上却几率性得到0,-1,2,-2,1。

为了避免错误,很多操作系统或编译器都提供了一些常用原子化操作的内建函数或API,包括把一些实际是多条指令的常用表达式。上述操作中,将i++/i--,替换为 OSAtomicIncrement32(&i) / OSAtomicDecrement32(&i) ,将得到预期的结果1

下边列举了不同平台上原子操作API的部分例子

windows APImacOS/iOS APIgcc内建函数作用
InterlockExchangeOSAtomicAdd32AO_SWAP原子的交换两个值
InterlockDecrementOSAtomicDecrement32AO_DEC原子的减少一个值
InterlockIncrementOSAtomicIncrement32AO_INC原子的增加一个值
InterlockXorOSAtomicXor32AO_XOR原子的进行异或

在OC中,属性变量的atomoc修饰符,起到的作用跟上述API相似,编译器会通过锁定机制确保所修饰变量的原子性,而且它是默认情况下添加的。而在实际应用场景中,在操作属性值时一般会包含三步(读取、运算、写入),即便写操作是原子,也不能保证线程安全。而ios中同步锁的开销很大(macOS中没有类似问题),所以一般会加上nonatomic修饰。

@property (nonatomic,assign)int pro1;
复制代码

在实际业务中,通常是给核心业务代码加同步锁,使其整体变为原子的,而不是针对具体的属性读写方法。

1.2 可重入与线程安全

函数被重入 一个程序被重入,表示这个函数没有执行完成,由于外部因数或内部调用,又一次进入函数执行。函数被重入分两种情况

  • 多个线程同时执行这个函数
  • 函数自身(可能是经过多层调用之后)调用自身

可重入 一个函数称为可重入的,表明该函数被重入之后没有产生任何不良后果。 可重入函数具备以下特点:

  • 不使用任何局部(静态)非const变量
  • 不使用任何局部(静态)或全局的非const变量的指针
  • 仅依赖调用方法提供的参数
  • 不依赖任何单个资源提供的锁(互斥锁等)
  • 不调用任何不可重入的函数

可重入是并发的强力保障,一个可重入函数可以在多线程环境下放心使用。也就是说在处理多线程问题时,我们可以讲程序拆分为若干可重入的函数,而把注意的焦点放在可重入函数之外的地方。

函数式编程范式中,由于整个系统不需要维护多余数据变量,而是状态流方式。所以可以认为全是由一些可重入的函数组成的。所以函数式编程在高并发编程中有其先天的优势。

1.3 CPU的过度优化

1.3.1 乱序优化与内存屏障

cpu有动态调度机制,在执行过程中可能因为执行效率交换指令的顺序。而一些看似独立的变量实际上是相互影响,这种编译器优化会导致潜在不正确结果。

面对这种情况我们一般采用内存屏障(memory barrier)。其作用就相当于一个栅栏,迫使处理器来完成位于障碍前面的任何加载和存储操作,才允许它执行位于屏障之后的加载和存储操作。确保一个线程的内存操作总是按照预定的顺序完成。为了使用一个内存屏障,你只要在你代码里面需要的地方简单的调用 OSMemoryBarrier() 函数。

class A {
    let lock = NSRecursiveLock()
    var _a : A? = nil
    var a : A? {
        lock.lock()
        if _a == nil {
            let temp = A()
            
            OSMemoryBarrier()
            
            _a = temp
        }
        lock.unlock()
        return _a
    }
}
复制代码

值得注意的是,大部分锁类型都合并了内存屏障,来确保在进入临界区之前它前面的加载和存储指令都已经完成。

1.3.2 寄存器优化与volatile变量

在某些情况下编译器会把某些变量加载进入寄存器,而如果这些变量对多个线程可见,那么这种优化可能会阻止其他线程发现变量的任何变化,从而带来线程同步问题。

在变量之前加上关键字volatile可以强制编译器每次使用变量的时候都从内存里面加载。如果一个变量的值随时可能给编译器无法检测的外部源更改,那么你可以把该变量声明为volatile变量。在许多原子性操作API中,大量使用了volatile 标识符修饰。譬如 在系统库中,所有原子性变量都使用了

<libkern/OSAtomic.h>

int32_t	OSAtomicIncrement32( volatile int32_t *__theValue )
复制代码

##2.线程同步的主要方式--线程锁 线程同步最常用的方法是使用(Lock)。锁是一种非强制机制,每一个线程访问数据或资源之前,首先试图获取(Acquireuytreewq)锁,并在访问结束之后释放(release)。在锁已经被占用时获取锁,线程会等待,直到该锁被释放。

2.1 互斥锁(Mutex)

2.1.1 基本概念

互斥锁 是在很多平台上都比较常用的一种锁。它属于sleep-waiting类型的锁。即当锁处于占用状态时,其他线程会挂起,当锁被释放时,所有等待的线程都将被唤醒,再次对锁进行竞争。在挂起与释放过程中,涉及用户态与内核态之间的context切换,而这种切换是比较消耗性能的。

互斥锁和二元信号量很相似,唯一不同是只能由获取锁的线程释放而不能假手于人。在某些平台中,他是用二元信号量实现的。关于信号量,我们将在2.3中详细介绍。

互斥锁可以是多进程共享的,也可以是进程内线程可见的。它可以分为分为普通锁、检错锁、递归锁。让我们通过pthread中的pthread_mutex,来详细了解互斥锁的一些用法及注意事项。

2.1.2 pthread_mutex

pthread_mutex 是pthread中的互斥锁,具有跨平台性质。pthread是POSIX线程(POSIX threads)的简称,是线程的POSIX标准(可移植操作系统接口 Portable Operation System Interface)。POSIX是unix的api设计标准,兼容各大主流平台。所以pthread_mutex是比较低层的,可以跨平台的互斥锁实现。

我们先来看看最常规的调用方式:

static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex);
block();
pthread_mutex_unlock(&mutex);
复制代码

pthread_mutex可以定义它的作用范围,是多进程共享,还是只是进程内可见。默认是后者

/**
 PTHREAD_PROCESS_SHARE:该进程与其他进程的同步
 PTHREAD_PROCESS_PRIVATE:同一进程内不同的线程之间的同步
**/
pthread_mutexattr_setpshared(&mattr,PTHREAD_PROCESS_PRIVATE);
复制代码

pthread_mutex又可分为普通锁、检错锁、递归锁。可以通过属性,实现相应的功能。

/*
互斥锁的类型:有以下几个取值空间:
PTHREAD_MUTEX_NORMAL 0: 普通锁(默认)。不提供死锁检测。尝试重新锁定互斥锁会导致死锁。如果某个线程尝试解除锁定的互斥锁不是由该线程锁定或未锁定,则将产生不确定的行为。
 
PTHREAD_MUTEX_ERRORCHECK 1: 检错锁,会提供错误检查。如果某个线程尝试重新锁定的互斥锁已经由该线程锁定,则将返回错误。如果某个线程尝试解除锁定的互斥锁不是由该线程锁定或者未锁定,则将返回错误。
 
PTHREAD_MUTEX_RECURSIVE 2: 嵌套锁/递归锁,该互斥锁会保留锁定计数这一概念。线程首次成功获取互斥锁时,锁定计数会设置为 1。线程每重新锁定该互斥锁一次,锁定计数就增加 1。线程每解除锁定该互斥锁一次,锁定计数就减小 1。 锁定计数达到 0 时,该互斥锁即可供其他线程获取。如果某个线程尝试解除锁定的互斥锁不是由该线程锁定或者未锁定,则将返回错误。
 
*/
pthread_mutexattr_settype(&mattr ,PTHREAD_MUTEX_NORMAL);
复制代码

pthread_mutex还有一种简便的调用方式,使用的是全局唯一互斥锁。实验表明,该锁是所有属性都是默认的,进程内可见,类型是普通锁

static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex);
block();
pthread_mutex_unlock(&mutex);
复制代码

同时它还提供了一种非阻塞版本pthread_mutex_trylock。若尝试获取锁时发现互斥锁已经被锁定,或则超出了递归锁定的最大次数,则立即返回,不会挂起。只有在锁未被占用时才能成功加锁。

static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int res = pthread_mutex_trylock(&mutex);
if(res == 0){
    block();
    pthread_mutex_unlock(&mutex);
}else if(res == EBUSY){
    printf("由于 mutex 所指向的互斥锁已锁定,因此无法获取该互斥锁。");
}else if (res == EAGAIN){
    printf("由于已超出了 mutex 的递归锁定最大次数,因此无法获取该互斥锁。");
}
复制代码
2.1.3 NSLock与NSRecursiveLock

NSLock是iOS中最常用的一种锁,对应着普通类型的互斥锁。另外一个可递归的子类为NSRecursiveLock; 我们先来看看它的官方文档:

An NSLock object can be used to mediate access to an application’s global data or to protect a critical section of code, allowing it to run atomically.

Warning

The NSLock class uses POSIX threads to implement its locking behavior. When sending an unlock message to an NSLock object, you must be sure that message is sent from the same thread that sent the initial lock message. Unlocking a lock from a different thread can result in undefined behavior.
You should not use this class to implement a recursive lock. Calling the lock method twice on the same thread will lock up your thread permanently. Use the NSRecursiveLock class to implement recursive locks instead.

Unlocking a lock that is not locked is considered a programmer error and should be fixed in your code. The NSLock class reports such errors by printing an error message to the console when they occur.
复制代码

从文档中我们可以知道:

  • 其实现是基于phthread的
  • 谁持有谁释放,试图释放由其他线程持有的锁是不合法的
  • 如果用在需要递归嵌套加锁的场景时,需要使用其子类NSRecursiveLock。不是所有情况下都会引发递归调用,而NSLock在性能上要优于NSRecursiveLock。而当我们使用NSLock不小心造成死锁时,可以尝试将其替换为NSRecursiveLock。
  • lock与unlock是一一对应的,如果试图释放一个没有加锁的锁,会发生异常崩溃。而lock始终等不到对应的unlock会进入饥饿状态,让当前线程一直挂起
2.1.4 @synchronized
@synchronized(self){
	// your code hear        
};
复制代码

@synchronized在运行时会在代码块前面加上objc_sync_enter,代码块最后插入objc_sync_exit。下面是这两个函数声明文件。

/** 
 * Begin synchronizing on 'obj'.  
 * Allocates recursive pthread_mutex associated with 'obj' if needed.
 * 
 * @param obj The object to begin synchronizing on.
 * 
 * @return OBJC_SYNC_SUCCESS once lock is acquired.  
 */
OBJC_EXPORT int
objc_sync_enter(id _Nonnull obj)
    OBJC_AVAILABLE(10.3, 2.0, 9.0, 1.0, 2.0);

/** 
 * End synchronizing on 'obj'. 
 * 
 * @param obj The object to end synchronizing on.
 * 
 * @return OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
 */
OBJC_EXPORT int
objc_sync_exit(id _Nonnull obj)
    OBJC_AVAILABLE(10.3, 2.0, 9.0, 1.0, 2.0);
复制代码

这两个函数位于runtime/objc-sync.mm中,而且是开源的,我们可以 这里看到具体的源码实现。源码中 当你调用 objc_sync_enter(obj) 时,它用 obj 内存地址的哈希值查找合适的 SyncData,然后将其上锁。当你调用 objc_sync_exit(obj) 时,它查找合适的 SyncData 并将其解锁。 SyncData其实是数据链表的一个节点,其数据结构如下:

typedef struct SyncData {
    struct SyncData* nextData;
    id               object;
    int              threadCount;  // number of THREADS using this block
    recursive_mutex_t        mutex;
} SyncData;

typedef struct {
    SyncData *data;
    unsigned int lockCount;  // number of times THIS THREAD locked this block
} SyncCacheItem;

typedef struct SyncCache {
    unsigned int allocated;
    unsigned int used;
    SyncCacheItem list[0];
} SyncCache;
复制代码

加锁代码如下:

/ Begin synchronizing on 'obj'. 
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.  
int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        SyncData* data = id2data(obj, ACQUIRE);
        require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_INITIALIZED, "id2data failed");
	
        result = recursive_mutex_lock(&data->mutex);
        require_noerr_string(result, done, "mutex_lock failed");
    } else {
        // @synchronized(nil) does nothing
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

done: 
    return result;
}
// End synchronizing on 'obj'. 
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) {
        SyncData* data = id2data(obj, RELEASE); 
        require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR, "id2data failed");
        
        result = recursive_mutex_unlock(&data->mutex);
        require_noerr_string(result, done, "mutex_unlock failed");
    } else {
        // @synchronized(nil) does nothing
    }
	
done:
    if ( result == RECURSIVE_MUTEX_NOT_LOCKED )
         result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;

    return result;
}

复制代码

可以看到,其核心逻辑是recursive_mutex_lock和recursive_mutex_unlock。这两个函数在苹果私有库当中,具体实现无从而知。但是从文档中得知是基于递归类型的pthread_mutex的,这个前文中我们已经讨论过。

需要注意的是,所传入的obj对象主要作用是生成链表节点的哈希索引。该对象的生命周期对代码块及加锁过程无任何影响。也就是说在传入之后,如论何时将对象释放或则置为nil,都是安全的。但是如果传入一个空对象,将不进行任何的加锁解锁操作。

2.2 自旋锁

自旋锁 与互斥锁有点类似,只是自旋锁被某线程占用时,其他线程不会进入睡眠(挂起)状态,而是一直运行(自旋/空转)直到锁被释放。由于不涉及用户态与内核态之间的切换,它的效率远远高于互斥锁。

虽然它的效率比互斥锁高,但是它也有些不足之处:

  • 自旋锁一直占用CPU,他在未获得锁的情况下,一直运行(自旋),所以占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低。在高并发执行(冲突概率大,竞争激烈)的时候,又或者代码片段比较耗时(比如涉及内核执行文件io、socket、thread等),就容易引发CPU占有率暴涨的风险
  • 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁。
  • 自旋锁可能会引起优先级反转问题。具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,自旋锁会处于忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。自旋锁OSSpinLock由于上述优先级反转问题,在新版iOS已经不在保证安全,除非开发者能保证访问锁的线程全部都处于同一优先级,否则 iOS 系统中所有类型的自旋锁都不能再使用了。在ios10中建议替换为os_unfair_lock

因此我们要慎重使用自旋锁,自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况下。

#import <libkern/OSAtomic.h>

OSSpinLock lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&lock);
OSSpinLockUnlock(&lock);
复制代码

2.3 信号量

信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。

信号量可以分为几类:

  • 二进制信号量(binary semaphore) / 二元信号量 :只允许信号量取0或1值,,只有两种状态:占用与非占用,其同时只能被一个线程获取。

  • 整型信号量(integer semaphore):信号量取值是整数,它可以被多个线程同时获得,直到信号量的值变为0。

  • 记录型信号量(record semaphore):每个信号量s除一个整数值value(计数)外,还有一个等待队列List,其中是阻塞在该信号量的各个线程的标识。当信号量被释放一个,值被加一后,系统自动从等待队列中唤醒一个等待中的线程,让其获得信号量,同时信号量再减一。

信号量通过一个计数器控制对共享资源的访问,信号量的值是一个非负整数,所有通过它的线程都会将该整数减一。如果计数器大于0,则访问被允许,计数器减1;如果为0,则访问被禁止,所有试图通过它的线程都将处于等待状态。

2.3.1 pthread中的sem_t

他的具体调用方式如下:

#include <semaphore.h>

// 初始化信号量:
// pshared 0进程内所有线程可用 1进程间可见
// val     信号量初始值
// 调用成功时返回0,失败返回-1
int sem_init(sem_t *sem, int pshared, unsigned int val);
        
// 信号量减1:
// 该函数申请一个信号量,当前无可用信号量则等待,有可用信号量时占用一个信号量,对信号量的值减1。
int sem_wait(sem_t *sem);
        
// 信号量加1:该函数释放一个信号量,信号量的值加1。
int sem_post(sem_t *sem);
        
// 销毁信号量:
int sem_destory(sem_t *sem);

复制代码

值得注意的是:上述初始化方法,已经被Apple弃用。在调用时基本返回的都是-1,调用失败。其后所有操作也是无效的。搜索了一下原因,iOS不支持创建无名的信号量所至,解决方案是造建有名的信号量。。换成下属方式,创建一个有名信号量,信号量初值为2。使用结束时,调用与之对应的unlick方法。

sem_t *semt = sem_open("sem name", O_CREAT,0664,2);


sem_unlink(semt);

复制代码

下面我们来看一个简单的例子。结果很明显可以看出,某一时刻,只有两个线程在输出了waite,其他线程都被挂起了,当1s后这两个线程都post之后。另外两个线程才被唤醒,继续运行。

func testSem_t(name:String){
    let semt = sem_open(name, O_CREAT,0664,2)
    if semt != SEM_FAILED {
        for i in 0...5 {
            DispatchQueue.global().async {
            	   sem_wait(semt)
                print("waite \(i)")
                sleep(1)
                sem_post(semt)
                print("post \(i)")
            }
        }
        sem_unlink(name)
    }else{
        if errno == EEXIST {
            print("Semaphore with name \(name) already exists.\n")
        }else{
            print( "Unhandled error: \(errno). name=\(name) \n")
        }
        let newName = name + "\(arc4random()%500)"
        print("new name = \(newName)")
        testSem_t(name: newName)
    }
}
复制代码

值得注意的是:当反复创建同一名字的信号量时,会返回错误。及时重新运行,也会几率性得到错误。因此,一方面我们尽量保证每次创建的信号量名字的唯一性,另一方面在重名返回错误时,也应该做相应的处理。本例中处理方式比较简单,只作为参考。(其中errno为全局变量,是内核<errno.h>返回的错误码)

2.3.2 dispatch_semaphore

dispatch_semaphore是GCD用于控制多线程并发的信号量,允许通过wait/signal的信号事件控制并发执行的最大线程数,当最大线程数降级为1的时候则可当作同步锁使用,注意该信号量并不支持递归;

2.3.1中的例子用dispatch_semaphore实现,代码如下:

let semt = DispatchSemaphore(value: 7)
for i in 0...20 {
    DispatchQueue.global().async {
        print(" \(i)")
        semt.wait()
        print("waite \(i)")
        sleep(1)
        semt.signal()
        print("post \(i) ")
    }
}
复制代码
2.3.2 信号量的用途
  • 二元信号量相当于互斥锁,也就是说当信号量初值为1时,wait相当于lock,signal相当于unlock。而它允许在一个线程加锁在另任一线程解锁,使用更加灵活,而带来的不确定性则相应增加。

下述代码中,线程A将等线程B调用之后再逐一运行。如果换成NSLock理论上由其他线程是不允许的,但运行结果一切正常。而换成NSRecursiveLock递归锁,所有加锁操作将失效,线程不会挂起。用pthread_mutex,也是在设置属性为可递归时,加锁才会失效。(普通互斥锁可能是由信号量实现的,具体原因不明,但不建议这样使用。)

let semt = DispatchSemaphore(value: 1)
let q1 = DispatchQueue(label:"A")
let q2 = DispatchQueue(label:"B")
for i in 0...20 {
   q1.async {
        print(" \(i)")
        semt.wait()
        print("waite \(i)")
    }
    q2.asyncAfter(deadline: .now() + .seconds(i * 1)){
        semt.signal()
        print("post \(i) ")
    }
}
复制代码
  • 控制某个代码块的最大并发数。通过设置信号量的初值,很容易实现某一段代码片段的执行的并发数。或者说控制某个资源最大同时访问量。

  • 当信号量的值为0,而waite/signal分属不同线程时,可以适用于经典的生产者-消费者模型。即一种一对一的观测监听方式。当生产者完成生产后,立刻通知消费者购买。而没有产品时,消费者只能等待。

var a : Int32 = 0
let semt = DispatchSemaphore(value:0)
for i in 0..<303 {
    DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) {
        print("task start \(i)  a= \(   OSAtomicAdd32(1, &a)     )")
        semt.signal()
    }
}
for i in 0..<5 {
    DispatchQueue.global().async {
        var count : Int32 = 0
        while(true){
            semt.wait()
            print("obsever \(i) finish a=\( OSAtomicAdd32(-1, &a)  )  一共抢到\( OSAtomicAdd32(1, &count) )")
        }
    }
}
复制代码

上述例子中,信号量的值相当于库存量。初始库存为0。生产者一共生产了303件商品,每生产一件都会及时对外销售。一共有5位消费者(或者经销商),每当有商品生产出来,都会不同的抢购。从结果中可以看出,由于并发比较高,最大库存存在波动,但是最终库存量是0。5位消费者抢购总数等于生产量。而且抢到的总数是一样的。由于余数是3,头三位多抢了一件。

上述生产者,消费者模型更加适合用条件变量来实现。下面让我们来仔细看看。

2.4 条件变量

条件变量 (Condition Variable) 作为一种同步手段类似于栅栏,允许线程以一种无竞争的方式等待某个条件的发生。当该条件没有发生时,线程会一直处于休眠状态。当被其它线程通知条件已经发生时,线程才会被唤醒从而继续向下执行。条件变量是比较底层的同步原语,直接使用的情况不多,往往用于实现高层之间的线程同步。使用条件变量的一个经典的例子就是线程池(Thread Pool)了。

NSCondition是条件变量在iOS上的一种实现,他是一种特殊类型的锁,通过它可以实现不同线程的调度。一个线程被某一个条件所阻塞,直到另一个线程满足该条件从而发送信号给该线程使得该线程可以正确的执行。比如说,你可以开启一个线程下载图片,一个线程处理图片。这样的话,需要处理图片的线程由于没有图片会阻塞,当下载线程下载完成之后,则满足了需要处理图片的线程的需求,这样可以给定一个信号,让处理图片的线程恢复运行。

func consumer() {
        DispatchQueue.global().async {
            print("start to track")
            while(true){
                self.conditionLock.wait()
                print("in  \(Thread.current)")
            }
        }
    }
    
func producer(){
    let queue1 = DispatchQueue.global()
    for i in 0...5 {
        queue1.asyncAfter(deadline: .now() + .milliseconds(i*300), execute: {
            print(i)
            self.conditionLock.signal()
        })
    }
}
复制代码
输出结果
start to track
0
in  <NSThread: 0x604000272dc0>{number = 3, name = (null)}
1
in  <NSThread: 0x604000272dc0>{number = 3, name = (null)}
2
in  <NSThread: 0x604000272dc0>{number = 3, name = (null)}
3
in  <NSThread: 0x604000272dc0>{number = 3, name = (null)}
4
in  <NSThread: 0x604000272dc0>{number = 3, name = (null)}
5
in  <NSThread: 0x604000272dc0>{number = 3, name = (null)}
复制代码

与lock和unlock一一对应相同的是,NSCondition中wait()与signal()也需要一一对应。多个线程waite()时,按顺序解锁。多出的wait()线程,如果一直等不到signal(),会造成死锁。同理同一时刻多个线程signal(),多余的将得不到处理。上述例子中,当时间延迟为0时,每次将只会执行一次,因为同一时间只有一把锁,多余的钥匙将被丢弃。

NSConditionLock 是另一种条件变量,唯一不同的是,它可以传入一个整型数,从而确定具体的条件。也就是具有处理多种条件的能力。与其他锁一样,**lock(whenCondition:)unlock(withCondition:)**是一一对应的,并且只有condition值相同时,才可以顺利解锁。由于继承NSLock,两者如lock()/unlock()类似,唯一不同是是否指定或修改condition值

let conditionLock = NSConditionLock()
let queue1 = DispatchQueue.global()
for i in 1...5 {
    queue1.asyncAfter(deadline: .now() + .milliseconds(0), execute: {
        conditionLock.lock()
        print("dosomthing thread1 cordition=\(i) ")
        if i == 3 {
            conditionLock.unlock(withCondition:3)
        }
        conditionLock.unlock()
    })
    DispatchQueue.global().async {
        conditionLock.lock(whenCondition:3)
        print("in \(Thread.current)")
        conditionLock.unlock()
    }
}
复制代码

上述代码几率性得到结果如下

dosomthing thread1 cordition=1 
dosomthing thread1 cordition=2 
dosomthing thread1 cordition=3 
in <NSThread: 0x604000663600>{number = 4, name = (null)}
in <NSThread: 0x604000663700>{number = 5, name = (null)}
dosomthing thread1 cordition=5 
in <NSThread: 0x6040006635c0>{number = 6, name = (null)}
in <NSThread: 0x60000026f340>{number = 3, name = (null)}
dosomthing thread1 cordition=4 
in <NSThread: 0x600000275780>{number = 7, name = (null)} 
复制代码

上述代码中,多个线程等到condition=3后才等以执行。

###2.4 读写锁 读写锁 从广义的逻辑上讲,也可以认为是一种共享版的互斥锁。如果对一个临界区大部分是读操作而只有少量的写操作,读写锁在一定程度上能够降低线程互斥产生的代价。

对于同一个锁,读写锁有两种获取锁的方式:共享(share)方式,独占(Exclusive)方式。写操作独占,读操作共享

读写锁状态以共享方式获取以独占方式获取
自由成功成功
共享成功等待
独占等待等待
NSString *path = [[NSBundle mainBundle] pathForResource:@"t.txt" ofType:nil];
    dispatch_group_t group = dispatch_group_create();
    __block double start = CFAbsoluteTimeGetCurrent();
    for (int k = 0; k <= 3000; k++) {
        dispatch_group_enter(group);
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self readBookWithPath:path];
            dispatch_group_leave(group);
        });
        dispatch_group_enter(group);
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self writeBook:path string:[NSString stringWithFormat:@"--i=%d--",k]];
            dispatch_group_leave(group);
        });
    }
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"----result=%@ time=%f",[self readBookWithPath:path],CFAbsoluteTimeGetCurrent()-start);
    });
复制代码
- (NSString *)readBookWithPath:(NSString *)path {
    pthread_rwlock_rdlock(&rwLock);
    NSLog(@"start read ---- ");
    NSString *contentString = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
    NSLog(@"end   read ---- %@",contentString);
    pthread_rwlock_unlock(&rwLock);
    return contentString;
}
- (void)writeBook:(NSString *)path string:(NSString *)string {
    pthread_rwlock_wrlock(&rwLock);
    NSLog(@"start wirte ---- ");
    [string writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];
    NSLog(@"end   wirte ---- %@",string);
    pthread_rwlock_unlock(&rwLock);
}
复制代码
输出结果:
......
2017-12-24 17:24:20.506522+0800 lock[8591:299152] start wirte ----
2017-12-24 17:24:20.507522+0800 lock[8591:299152] end   wirte ---- --i=2998--
2017-12-24 17:24:20.507685+0800 lock[8591:299162] start read ----
2017-12-24 17:24:20.507828+0800 lock[8591:299162] end   read ---- --i=2998--
2017-12-24 17:24:20.507943+0800 lock[8591:299154] start wirte ----
2017-12-24 17:24:20.508872+0800 lock[8591:299154] end   wirte ---- --i=2999--
2017-12-24 17:24:20.509065+0800 lock[8591:299161] start read ----
2017-12-24 17:24:20.509240+0800 lock[8591:299161] end   read ---- --i=2999--
2017-12-24 17:24:20.509358+0800 lock[8591:299157] start wirte ----
2017-12-24 17:24:20.510294+0800 lock[8591:299157] end   wirte ---- --i=3000--
2017-12-24 17:24:20.510443+0800 lock[8591:298979] start read ----
2017-12-24 17:24:20.510582+0800 lock[8591:298979] end   read ---- --i=3000--
2017-12-24 17:24:20.510686+0800 lock[8591:298979] ----result=--i=3000-- time=5.968375
复制代码

2.4 临界区

临界区 (Critical Section)是相较于互斥锁更为严格的同步手段。只对本进程可见,其他进程试图获取是非法的(信号量和互斥量可以)。获取锁被称为进入临界区,释放锁叫做离开临界区。除此之外,它具有和互斥锁相同的性质。

转载于:https://juejin.im/post/5a48c49d518825772a4b53fe

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值