线程同步及线程锁

1 资源竞争与线程同步

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

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

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

1.1 原子操做

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

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

某些简单的表达式能够算做现代编程语言的最小执行单元 某些简单的表达式,其实编译以后的获得的汇编指令,不止一条,因此他们并非真正意义原子的。以加法指令操做实现 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。bash

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

下边列举了不一样平台上原子操做API的部分例子多线程

windows API macOS/iOS API gcc内建函数 做用
InterlockExchange OSAtomicAdd32 AO_SWAP 原子的交换两个值
InterlockDecrement OSAtomicDecrement32 AO_DEC 原子的减小一个值
InterlockIncrement OSAtomicIncrement32 AO_INC 原子的增长一个值
InterlockXor OSAtomicXor32 AO_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)是相较于互斥锁更为严格的同步手段。只对本进程可见,其余进程试图获取是非法的(信号量和互斥量能够)。获取锁被称为进入临界区,释放锁叫作离开临界区。除此以外,它具备和互斥锁相同的性质。

相关文章
相关标签/搜索