多线程和锁的学习

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

https://bestswifter.com/ios-lock/

https://blog.csdn.net/tietao/article/details/7367827

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • linux编程-线程 MUTEX 一.概述 互斥量是线程同步的一...
    Aska偶阵雨阅读 454评论 0 0
  • 作者: 一字马胡 转载标志 【2018-03-27】 更新日志 日期更新内容备注2018-03-27回顾以前的知...
    一字马胡阅读 429评论 0 3
  • 线程安全是怎么产生的 常见比如线程内操作了一个线程外的非线程安全变量,这个时候一定要考虑线程安全和同步。 - (v...
    幽城88阅读 615评论 0 0
  • linux线程同步 信号灯:与互斥锁和条件变量的主要不同在于"灯"的概念,灯亮则意味着资源可用,灯灭则意味着不可用...
    鲍陈飞阅读 657评论 0 2
  • 这是在简书发布的第五十篇文章了。最近很长一段时间心情都不是很好,也没有想要写东西的想法。想写点自己内心想对自己说的...
    小橙小橙小橙阅读 152评论 0 0