Q:为什么出现多线程?
A:为了实现同时干多件事的需求(并发),同时进行着下载和页面UI刷新。对于处理器,为每个线程分配执行的时间片。在不同的线程之间切换。
Q:多线程和单线程,那个计算效率更高?
A:单线程,计算效率更高。cpu在从A线程切换到B线程,需要保存A线程的寄存器,栈信息,同时使用B线程的寄存器,栈信息。叫做切换上下文。这里就出现了线程切换的开销了,所以要合理的使用多线程,开启很多线程,会使运行效率大大下降。
Q:为什么会出现多线程的问题?
A:每个线程都有自己的栈信息,但是堆数据是共享的,线程之间存在共享资源。那么不同线程访问共享数据的时候,就出现了数据错乱的问题。
Q:解决办法?
A:使用锁
一、线程状态:
1、新建状态:线程创建
2、就绪状态:向线程对象发送start信息,线程对象被加入可调度线程池,等待获得CPU的使用权,获得执行的时间片,开始执行
3、运行状态:就绪状态获得CPU使用权,执行代码
4、阻塞状态:
(1)、同步阻塞,运行过程中要获得锁,才能执行下面的代码,但是获得失败,就会被加入锁池中,只有锁被别的线程释放,并且被本线程获得,才会从阻塞状态,变成就绪状态
(2)、其他阻塞,sleep,或是执行I/O 操作,或是barrier操作,线程会变成阻塞状态
对于不同线程之间的调度:
1、唤醒别的线程,通过释放锁(互斥量),释放条件变量,增加信号量,可以将别的线程从阻塞状态变成就绪状态,别的线程能继续执行
2、阻塞线程,通过锁,条件变量,信号量等方式,可以使线程进入阻塞状态。
多线程导致的问题:
多线程,对于共享数据而言,不同线程访问很可能导致数据错乱的问题。举例:一个线程通过条件判断已经进入后面的代码执行,但是这个值同时在别的线程被修改了。一个变量的赋值和写入,多线程操作也会导致问题。为了避免多线程的问题,出现了锁的概念,这个锁概念的基础就是原子操作,有了原子操作,保证了加锁的过程中,不会被多线程干扰,要么加锁成功,可以继续向下执行,要么加锁失败,线程进入阻塞状态。
注意:这里的加锁操作,是原子操作。但是之后执行的代码不是原子操作。类似于下面的代码,只有if条件判断是原子操作,临界代码执行并不是原子操作,还会被线程调度影响。
二、原子操作
不会被线程调度机制打断的操作,这个操作一旦开始,直到运行结束,中间不会切换到另外一个线程。原子操作是不可分割的,怎样实现这中不可分割那?对于单核处理器而言,指令就是原子操作,出现了test_and_set、test_and_clear这样的指令来保证临界资源互斥。(互斥量)
atomic_flag_test_set 保证了线程的安全
伪代码
对于多处理器而言,原子操作的实现更加复杂一些,单条指令已经不是原子操作了,例子:不同的cpu都在递减某一个共享的值
⒈ CPU A(CPU A上所运行的进程,以下同)从内存单元把当前计数值⑵装载进它的寄存器中;
⒉ CPU B从内存单元把当前计数值⑵装载进它的寄存器中。
⒊ CPU A在它的寄存器中将计数值递减为1;
⒋ CPU B在它的寄存器中将计数值递减为1;
⒌ CPU A把修改后的计数值⑴写回内存单元。
⒍ CPU B把修改后的计数值⑴写回内存单元。
我们看到,内存里的计数值应该是0,然而它却是1。如果该计数值是一个共享资源的引用计数,每个进程都在递减后把该值与0进行比较,从而确定是否需要释放该共享资源。这时,两个进程都去掉了对该共享资源的引用,但没有一个进程能够释放它--两个进程都推断出:计数值是1,共享资源仍然在被使用。
原子性不可能由软件单独保证--必须需要硬件的支持,因此是和架构相关的。在x86 平台上,CPU提供了在指令执行期间对总线加锁的手段。CPU芯片上有一条引线#HLOCK pin,如果汇编语言的程序中在一条指令前面加上前缀"LOCK",经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。
原子操作和线程安全的关系:
原子操作执行过程中,不会出现线程切换。但是线程安全的代码运行过程中可能出现线程切换。正式因为用原子操作保证线程安全,加锁和解锁操作是原子操作,但是对于共享资源的获取和修改,就是线程安全了。加锁代表着,加锁内部的代码在一个时刻只能被同一个线程访问,其他线程到了这个位置,会进入阻塞状态。只有等那个拿到锁的线程释放锁,这些阻塞线程才会被唤醒。尝试加锁,加锁成功,继续执行。
原子操作是锁功能实现的基础,因为有了锁,临界代码才线程安全。
三、锁
多线程安全问题:
看到结果,会发现出现了多线程数据错乱的问题:
不同种类的锁:
1、互斥锁,NSLock,pthread_mutex_t,最简单的加锁机制,当一个线程对一个代码区域加锁之后,其他线程当要进入这个区域的时候会阻塞,保证这段代码是线程安全的,这段区域,通常有获取共享资源,修改资源的操作
(1)、将耗时操作也上锁了:
没有出现数据错乱:执行时间 20s
(2)、只对于那些获得修改共享数据的操作加锁,无关的耗时操作不加锁。没有数据错乱的问题,执行时间15s
注意:对于无关的耗时操作不能加锁,不然耗时操作不执行完就不能释放锁。别的线程一直阻塞,等待锁的释放。就会导致性能问题。
Lock还有其他方法:
tryLock 尝试加锁,加锁成功,立刻返回ture,不能返回false,不会发生阻塞。
tryLock和自旋锁还是区别很大的,自旋锁,加锁失败,不会继续执行,而是一直获取,直到成功获得锁。而tryLock是立即返回结果并且向下执行。
lockBeforeDate 加锁失败,会被阻塞,获得锁或是超过时间,线程转成就绪状态。
使用pthread_mutex 互斥量,实现锁的功能。NSLock是基于pthread_mutex实现的。
// 初始化互斥锁 int pthread_mutex_init (pthread_mutex_t * restrict mutex , \ const pthread_mutexattr_t * restrict attr ) ;
int pthread_mutex_destroy (pthread_mutex_t * mutex ) ;
// 加锁 int pthread_mutex_lock (pthread_mutex_t *mutex) ;
// 解锁 int pthread_mutex_unlock (pthread_mutex_t *mutex) ;
// 尝试加锁 int pthread_mutex_trylock (pthread_mutex_t *mutex) ;
// 带超时的尝试加锁,防止死锁的一种方式 int pthread_mutex_timedlock (pthread_mutex_t * restrict mutex , \ const struct timespec * restrict abstime ) ;
查看使用NSLock调用堆栈:
(1)、可以看到其他线程都处在等待锁的状态,只有一个线程真正获得了锁
(2)、发现NSLock底层调用的就是pthread_mutex
2、自旋锁:
当线程尝试加锁,如果加锁失败,不会进入阻塞状态,而是会一直尝试加锁,直到加锁成功。使用OSSpinLock
自旋锁的忙等出现,是为了节省线程上下文快速切换带来的开销。比如A线程在甲处理器上运行,A线程获得了锁,B线程在乙处理器上运行,现在B线程要加锁,如果是传统的互斥锁,那么B线程会进入阻塞,并且乙处理器会快速切换到另一个线程。如果A线程获得锁之后执行的操作耗时非常少,之后会立即释放锁,互斥锁的性能就不好。这里就是忙等开销大,还是立刻进行上下文切换开销大的问题。
只有一个线程获得了锁,执行。其他线程在忙等:
注意:OSSpinLock已经在iOS10 之后废弃了,废弃的原因,是低优先级线程获得锁,但是低优先级线程执行的时间片比较短,任务迟迟不能完成,释放锁的时间也会延后。那么高优先级线程需要加锁,就需要一直忙等。导致优先级反转。
建议使用os_unfair_lock 代替。
在iOS中atomic修饰的属性,也是使用自旋锁保证线程安全,产生的getter和setter方法是线程安全的。(使用atomic,但是自己实现了getter,setter方法系统的实现就没有用了)
3、条件变量:(cond,类似于一个信号,也可以说条件,当condsignal或是broadcast的时候,等待着这个cond的线程都会结束条件等待)
NSCondition、NSConditonLock、pthread_cond_t 这三者都使用的是条件变量,NSCondition、NSConditonLock都是基于pthread_cond_t进行封装的。条件变量要和互斥锁配合使用。
pthread_cond_t 基本函数:
// 初始化条件变量 int pthread_cond_init (pthread_cond_t * restrict cond , \ pthread_condattr_t * restrict attr ) ;
// 销毁条件变量 int pthread_cond_destroy ( pthread_cond_t * cond ) ;
// 等待事件发生 int pthread_cond_wait (pthread_cond_t * restrict cond , \ pthread_mutex_t * restrict mutex )
// 带超时的等待,防止死锁的一种方式 int pthread_cond_timedwait (pthread_cond_t * restrict cond , \ pthread_mutex_t * restrict mutex , \ const struct timespec * restrict tsptr ) ;
// 向任意一个在等待的进程或线程通知锁可用 int pthread_cond_signal ( pthread_cond_t *cond ) ;
// 通知所有在等待的进程或者线程锁可用 int pthread_cond_broadcast ( pthread_cond_t *cond ) ;
主要函数:pthread_cond_wait 有两个参数一个是cond,另外一个是互斥锁mutex,也就是条件变量是和锁共同使用的。pthread_cond_wait时,会将mutext unlock,同时进入等待,只有等待条件实现,并且能够获得这个mutex 时,才会继续执行。
例子:存在两个生产者,存在两个消费者,消费物品为馒头,生产者不停生产,生产有随机的时间消耗,生产10个,不再生产,只要发现不满10个,就接续生产。消费者一直消费,消费会带来随机的时间消耗。
NSCondition 使用的仍然是pthread_cond_wait
注意:
(1)、为什么要使用条件变量?能不能用互斥锁代替条件变量,当消费者发现条件不满足wait的时候,让出了互斥锁,并且线程进入了阻塞,这里的等待不是等待一个另一个互斥锁,因为并没有任何线程获得了这个额外的锁。因为生产者不会被这个锁影响,生产者只负责生产,并且通知cond完成。
(2)、pthread_cond_wait 释放锁,并且对cond进行阻塞等待,是一个原子操作,才能保证这些操作是线程安全的。
(3)、NSCondition 实现了NSLocking协议,除了cond 外,还自带一个互斥锁,这个功能能通过一个NSCondition对象 实现.
可以只使用condition的wait和broadcast功能,但是因为没有使用互斥锁的功能,所以会出现数据错乱的问题,所以还是老老实实把锁功能也用上。这样就不会导致数据错乱的问题。
(4)、如果不适用condition的wait和broadCast功能,只使用互斥锁。那么结果就像是自旋锁一样,消费者者线程当food 不足的时候,一直处于忙等状态。知道food足够才开始工作。
4、NSConditionLock 提供了一个条件控制
5、NSRecursiveLock 递归锁,对于一些递归调用而言,使用普通的互斥锁会造成死锁,所以出现了递归锁。同一个线程可以多次获得递归锁,并不会出现问题,只要保证获得一次,释放一次。
NSRecursiveLock 就是一个pthread_mutex 的特殊类型。
注意:
常用的@synchronized 就是一种递归锁,@synchronized的好处就是,程序自己产生了锁对象,而不用我们产生。至于后面紧跟的参数,底层维护着一个锁表。能够通过后面紧跟参数的哈希值,迅速的拿到这个锁对象。
6、读写锁:
读操作,和写操作进行了区分。一个线程获得了读锁,其他线程仍能获得读锁,进行读操作。但是一旦一个线程获取了写锁,那么不再允许以后的线程获得读锁,或是写锁。知道前面获得读锁的线程执行完毕。进行写操作,写入完毕。释放读锁,或是写锁。
iOS没有直接提供除了pthread外更高程度的读写锁。读写锁一般用于数据库软件。
// 初始化读写锁 int pthread_rwlock_init ( pthread_rwlock_t * restrict rwlock , \ const pthread_rwlockattr_t * restrict attr ) ;
// 销毁读写锁 int pthread_rwlock_destroy (pthread_rwlock_t * rwlock ) ;
// 加读锁 int pthread_rwlock_rdlock ( pthread_rwlock_t * rwlock ) ;
// 加写锁 int pthread_rwlock_wrlock ( pthread_rwlock_t * rwlock ) ;
// 解锁 int pthread_rwlock_unlock ( pthread_rwlock_t * rwlock ) ;
// 尝试加读锁 int pthread_rwlock_tryrdlock ( pthread_rwlock_t * rwlock ) ;
// 尝试加写锁 int pthread_rwlock_trywrlock ( pthread_rwlock_t * rwlock ) ;
// 带有超时的读写锁,避免死锁的一种方式 int pthread_rwlock_timedrdlock ( pthread_rwlock_t * restrict rwlock ,\ const struct timespec * restrict tsptr ) ; int pthread_rwlock_timedwrlock ( pthread_rwlock_t * restrict rwlock , \ const struct timespec * restrict tsptr ) ;
7、信号量
让多个进程通过特殊变量展开交互,一个进程在某一个关键点上被迫停止执行直至接收到对应的特殊变量值,通过这一措施,任何复杂的进程交互要求均可得到满足,这种特殊的变量就是信号量。
设s为一个记录型数据结构,其中value为整型变量,系统初始化时为其赋值,PV操作的原语描述如下:
P(s):将信号量value值减1,若结果小于0,则执行P操作的进程被阻塞,若结果大于等于0,则执行P操作的进程将继续执行。
V(s):将信号量的值加1,若结果不大于0,则执行V操作的进程从信号量s有关的list所知队列中释放一个进程,使其转化为就绪态,自己则继续执行,若结果大于0,则执行V操作的进程继续执行。
不同操作系统有不同的实现
作用域
信号量: 进程间或线程间(linux仅线程间的无名信号量pthread semaphore)
互斥锁: 线程间
信号量: 只要信号量的value大于0,其他线程就可以sem_wait成功,成功后信号量的value减一。若value值不大于0,则sem_wait使得线程阻塞,直到sem_post释放后value值加一,但是sem_wait返回之前还是会将此value值减一
互斥锁: 只要被锁住,其他任何线程都不可以访问被保护的资源
信号量
dispatch_semaphore_create(信号量值)// 创建信号量
dispatch_semaphore_wait(信号量,等待时间)// 等待,并且降低信号量
dispatch_semaphore_signal(信号量) // 添加信号量
使用信号量完成生产者消费者模型,能保证一定程度上的安全。但是没有互斥锁那样对于共享资源的保护,信号量只能保护,消费者消费的时候一定是有food的,但是没办法保证共享资源的数据正确性。所以要实现线程安全,还是要使用锁。事实上,信号量功能主要体现在流程控制上,对于共享资源的互斥保护,并不是信号量的功能。当一个线程因为某个资源不足被阻塞,或是要等待某个其他线程的流程执行完毕。可以使用信号量。像是信号灯,告诉你能不能通行,要不要进入等待。
参考文章:
https://www.jianshu.com/p/eca71b7fda2c