1.什么是保证线程的安全,线程为什么会不安全
多个线程同时修改同一资源时可能会以意想不到的方式造成互相干扰,比如一个线程可能覆盖了另一个线程改动的地方,造成数据的错乱。
在设计多线程操作的时候,应该尽量避免线程的交互,如果必须要交互,应该使用同步工具,保证线程交互的时候是安全的。
2.几种常见的同步工具
原子操作 atomic属性,默认系统生成的属性是原子的。原子属性在setter和getter方法上是线程安全的。如果是非原子属性,同时在不同的线程调用setter方法就会产生问题。原子操作其实是用自旋锁实现的。
锁 锁是最常见的同步工具,用来保护临界区(critical section),这些代码段在同一时间只能允许被一个线程访问。这些代码段可能要求同一时间只能有一个用户执行操作,这种代码就需要用锁去保护。
条件
3.同步会消耗性能
无论是加锁还是使用原子操作都会消耗系统的性能,因此讨论各种同步方法的性能消耗就变得非常重要。
加锁操作伴随的消耗:
从用户态切换到内核态。上下文切换。上下文切换的要点:
1)进程上下文切换可以描述为kernel执行下面的操作
a. 挂起一个进程,并储存该进程当时寄存器和程序计数器的状态
b. 从内存中恢复下一个要执行的进程,恢复该进程原来的状态到寄存器,返回到其上次暂停的执行代码然后继续执行
2)上下文切换只能发生在内核态,所以还会触发用户态与内核态切换
4.自旋锁
自旋锁是为了保护一小段临界区代码,保证这个临界区的操作是原子的,从而避免并发的竞争。如果内核控制路径发现自旋锁开着,就获得自旋锁并执行自己的操作,如果内核控制路径发现自旋锁被另一个cpu上的内核控制路径使用,就等待,等待的过程是忙等,直到自旋锁被释放。所以自旋锁保护的代码段必须非常小,否则等待自旋锁的释放会消耗很多时间。
⚠️:什么是忙等busy-waiting
假设有两个线程,线程a和线程b,分别运行在core0和core1上,如果线程a想通过pthread_spin_lock操纵去得到临界区的锁,而这个锁正在被线程b持有,那么这个时候,core0就会一直运行线程a,线程a一直进行锁请求,直到得到这个锁。
OSSpinLock
__block OSSpinLock theLock = OS_SPINLOCK_INIT;
OSSpinLockLock(&theLock);上锁 OSSpinLockUnlock(&theLock);开锁
⚠️:有人发现自旋锁是不再安全的,因为低优先级线程拿到锁时,高优先级线程进入忙等状态,消耗大量cpu时间,导致低优先级拿不到cpu时间,无法完成任务并释放锁,就一直持有锁,高优先级无法拿到锁。产生了优先级反转。
如果临界区的代码非常少,那么自旋锁的执行效率是很高的。
2.互斥锁pthread_mutex
⚠️:信号量用于线程的同步,互斥锁用于线程的互斥。
信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在semtake的时候,就阻塞在 哪里)。而互斥锁是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这个资源。
哈哈哈我今天终于明白了互斥锁和信号量是不一样的,互斥锁是为了把一个代码块锁住,上锁时,其他任何线程都不能访问被保护的资源。
但是信号量是不一样的,他可以让多个线程安全的同步执行。
互斥的值只能是0或1,信号量的值可以为非负整数。
也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。
互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。
互斥锁的使用:
创建:CreateMutex、加锁:pthread_mutex_lock、解锁:pthread_mutex_unlock、销毁pthread_mutex_destroy
3.dispatch_semaphore
dispatch_semaphore_create(long value)生成一个值为value的dispatch_semaphore_t类型的信号量。
dispatch_semaphore_signal(dispatch_semaphore_t deem)
这个方法会使输入的deem信号量的值加1
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
计数大于等于1时不等待并返回该函数,并且计数减1,计数等于0时等待,等待什么呢,如果在timeout时间内这个信号量的值大于0了(信号量值加1了),这个时候就返回该函数向下执行,如果等到了timeout信号量的值依然没有大于0,就继续执行下面的操作。
这个函数的返回结果是long类型的,如果返回的是0代表信号量的值大于0可以执行,如果非0,代表信号量的值等于0,不能执行。
信号量是睡眠等待的,假设有两个线程,线程a和线程b,分别运行在core0和core1上,线程a想要访问一段临界区的代码,但是锁正好被线程b拿着,这个时候线程a不会一直在corea上发请求,corea会把线程a放到等待队列,直到这个锁被b释放了再执行线程a。
4.NSLock
NSLock是oc以对象的形式暴露给开发者的一种锁,内部封装pthread_mutex。
常用的四种方法:
NSLock *lock = [[NSLock alloc] init];
[lock lock]; 加锁 [lock unlock]; 解锁
[lock tryLock]; 尝试获取锁
NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:3];
[lock lockBeforeDate:date];在所指定的date时间之前尝试获取锁
5.NSRecursiveLock递归锁
NSRecursiveLock实际上定义的是一个递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。它可以允许同一线程多次加锁,而不会造成死锁。递归锁会跟踪它被lock的次数。每次成功的lock都必须平衡调用unlock操作。只有所有达到这种平衡,锁最后才能被释放,以供其它线程使用。基于NSLock
6.NSConditionLock条件锁
基于NSLock
[lock lockWhenCondition:HAS_DATA];
[lock unlockWithCondition:NO_DATA];
只有满足一定条件的钥匙才能打开这个锁,也只有满足一定条件的锁才能锁上。
7.NSCondition
常用的方法:是一种基于信号量的实现方法
NSCondition *lock = [[NSCondition alloc] init];
[lock lock];上锁
[lock unlock];解锁
[lock wait];这个方法比较特殊,调用之后当前线程直接进入 wait 状态,当其它线程中的该锁执行 signal 或者 broadcast 方法时,线程被唤醒,继续运行之后的方法。
[lock signal]; 可以唤醒一个等待的线程
[lock broadcast];可以唤醒所有等待的线程
NSCondition和信号量的区别:
从上面的实例代码可以看到,一个 dispatch_semaphore_wait(signal, overTime); 方法会去对应一个 dispatch_semaphore_signal(signal); 看起来像NSLock的 lock 和 unlock,其实可以这样理解,区别只在于有信号量这个参数,lock unlock 只能同一时间,一个线程访问被保护的临界区,而如果 dispatch_semaphore 的信号量初始值为 x ,则可以有 x 个线程同时访问被保护的临界区。
8.synchronized
我们最常用的一种加锁机制,其实效率是最低的。他可以让我们不需要显示的去生成锁,而是系统自动生成锁。
[_lock lock];
[_elements addObject:element];
[_lock unlock];
用synchronized去实现:
@synchronized (obj 一个对象) {
[_elements addObject:element];
}
@synchronized 如何将一个锁和你正在同步的对象关联起来:
当你调用 objc_sync_enter(obj) 时,它用 obj 内存地址的哈希值查找合适的 SyncData,然后将其上锁。当你调用 objc_sync_exit(obj) 时,它查找合适的 SyncData 并将其解锁。