多线程应用初探(一)----(概念,安全)

多线程应用初探(一)----(概念,安全)

IOS小彩虹2021-07-16 15:17:52100A+A-

线程是进程的基本执行单元,一个进程的所有任务都在线程中执行 进程要想执行任务,必须得有线程,进程至少要有一条线程 程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程 进程是指在系统中正在运行的一个应用程序 每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存

线程与进程之间的关系

地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。
一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
线程是处理器调度的基本单位,但是进程不是

多线程

优点

  • 能适当提高程序的执行效率
  • 能适当提高资源的利用率(CPU,内存)
  • 线程上的任务执行完成后,线程会自动销毁

缺点

  • 开启线程需要占用一定的内存空间(默认情况下,每一个线程都占 512 KB)
  • 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
  • 线程越多,CPU 在调用线程上的开销就越大 * 程序设计更加复杂,比如线程间的通信、多线程的数据共享

原理

多线程在单位时间片内快速在各个线程之间切换

线程生命周期

start ==》 Runable ==》 Runing ==》 blocked(调用sleep方法等待同步锁可调度线程池里移出) ==》 dead
新建 ==》 就绪 ==》 运行(cpu调度当前线程) ==> 堵塞 ==》任务完成强制退出

线程和RunLoop的关系

  1. RunLoop与线程是一一对应的,一个Runloop对应一个核心线程,Runloop是可以嵌套的,他们的关系保持在一个全局的字典里
  2. RunLoop管理线程
  3. 第一获取时被创建,线程结束时被销毁
  4. 对于主线程Runloop默认创建好,Runloop在子线程懒加载使用时才创建

多线程安全 ---- 锁和性能

在多线程操作过程中,往往一个数据同时被多个线程读写,在这种情况下,如果没有相应的机制对数据进行保护,就很可能会发生数据污染的的问题,给程序造成各种难以重现的潜在bug。

多线程安全中相关术语及概念(假设操作的是数据库):

(1)脏读

指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中。这时,另外一个事务也访问这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是脏数据,依据脏数据所做的操作可能是不正确的。

(2)不可重复读

指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。

(3)幻觉读

指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。例如: 目前工资为5000的员工有10人,事务A读取所有工资为5000的人数为10人。此时,事务B插入一条工资也为5000的记录。这时,事务A再次读取工资为5000的员工,记录为11人。此时产生了幻读。


线程不安全:就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。
线程安全:简单来说就是多个线程同时对共享资源进行访问时,采用了加锁机制,当一个线程访问共享资源,对该资源进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。

多线程的锁

互斥锁

@synchronized(id anObject)

- (void)myMethod:(id)anObj
{
    @synchronized(anObj)
    {
        //do something here
    }
}

当一个线程在对某个资源进行读取时,另一条想要读取的线程将要进入等待状态,当该线程放问完毕时,等待的线程在对其进行访问,atomic就是对属性的Set方法加互斥锁,但这并不能完全保证线程安全,因为他仅仅对set方法进行了加锁

NSLock

NSLock对象实现了NSLocking protocol,包含几个方法: lock——加锁 unlock——解锁 tryLock——尝试加锁,如果失败了,并不会阻塞线程,只是立即返回NO lockBeforeDate:——在指定的date之前暂时阻塞线程(如果没有获取锁的话),如果到期还没有获取锁,则线程被唤醒,函数立即返回NO。 比如:

NSLock *theLock = [[NSLock alloc] init]; 
if ([theLock lock]) 
{
   //do something here
   [theLock unlock]; 
}

递归锁

NSRecursiveLock

多次调用不会阻塞已获取该锁的线程。

NSRecursiveLock *rcsLock = [[NSRecursiveLock alloc] init]; 

void recursiveLockTest(int value) 
{ 
  [rcsLock lock]; 
  if (value != 0) 
  { 
    --value; 
    recursiveLockTest(value); 
  }
  [rcsLock unlock]; 
} 

recursiveLockTest(5)

上面如果直接使用NSLock就会造成死锁。NSRecursiveLock类定义的锁可以在同一线程多次lock,而不会造成死锁。递归锁会跟踪它被多少次lock。每次成功的lock都必须平衡调用unlock操作。只有所有的锁住和解锁操作都平衡的时候,锁才真正被释放给其他线程获得。

NSConditionLock 条件锁

有时一把只会lock和unlock的锁未必就能完全满足我们的使用。因为普通的锁只能关心锁与不锁,而不在乎用什么钥匙才能开锁,而我们在处理资源共享的时候,多数情况是只有满足一定条件的情况下才能打开这把锁:

//主线程中
NSConditionLock *theLock = [[NSConditionLock alloc] init];

//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    for (int i=0;i<=2;i++)
    {
        [theLock lock];
        NSLog(@"thread1:%d",i);
        sleep(2);
        [theLock unlockWithCondition:i];
    }
});

//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [theLock lockWhenCondition:2];
    NSLog(@"thread2");
    [theLock unlock];
});

在线程1中的加锁使用了lock,是不需要条件的,所以顺利的就锁住了。但在unlock的使用了一个整型的条件,它可以开启其它线程中正在等待这把钥匙的临界地,而线程2则需要一把被标识为2的钥匙,所以当线程1循环到最后一次的时候,才最终打开了线程2中的阻塞。但即便如此,NSConditionLock也跟其它的锁一样,是需要lock与unlock对应的,只是lock、lockWhenCondition:与unlock,unlockWithCondition:是可以随意组合的.

分布锁:NSDistributedLock

以上所有的锁都是在解决多线程之间的冲突,但如果遇上多个进程或多个程序之间需要构建互斥的情景该怎么办呢?这个时候我们就需要使用到NSDistributedLock了,从它的类名就知道这是一个分布式的Lock,NSDistributedLock的实现是通过文件系统的,所以使用它才可以有效的实现不同进程之间的互斥,但NSDistributedLock并非继承于NSLock,它没有lock方法,它只实现了tryLock,unlock,breakLock,所以如果需要lock的话,你就必须自己实现一个tryLock的轮询。

GCD中信号量:dispatch_semaphore

假设现在系统有两个空闲资源可以被利用,但同一时间却有三个线程要进行访问,这种情况下,该如何处理呢?这里,我们就可以方便的利用信号量来解决这个问题。同样我们也可以用它来构建一把”锁”(从本质上讲,信号量与锁是有区别的,具体的请自行查阅资料)。

信号量:就是一种可用来控制访问资源的数量的标识。设定了一个信号量,在线程访问之前,加上信号量的处理,则可告知系统按照我们指定的信号量数量来执行多个线程。

在GCD中有三个函数是semaphore的操作: dispatch_semaphore_create 创建一个semaphore dispatch_semaphore_signal 发送一个信号 dispatch_semaphore_wait 等待信号

dispatch_semaphore_create函数有一个整形的参数,我们可以理解为信号的总量,dispatch_semaphore_signal是发送一个信号,自然会让信号总量+1,dispatch_semaphore_wait等待信号,当信号总量少于0的时候就会一直等待,否则就可以正常的执行,并让信号总量-1,根据这样的原理,我们便可以快速的创建一个并发控制来同步任务和有限资源访问控制。

GCD中“栅栏函数”:dispatch_barrier_async

dispatch_barrier_async函数的作用与barrier的意思相同,在进程管理中起到一个栅栏的作用,它等待所有位于barrier函数之前的操作执行完毕后执行,并且在barrier函数执行之后,barrier函数之后的操作才会得到执行,该函数需要同dispatch_queue_create函数生成的concurrent Dispatch Queue队列一起使用。

自旋锁

它的特点是在线程等待时会一直轮询,处于忙等状态。自旋锁由此得名。 自旋锁看起来是比较耗费cpu的,然而在互斥临界区计算量较小的场景下,它的效率远高于其它的锁。因为它是一直处于running状态,减少了线程切换上下文的消耗。OSSpinLock是一种自旋锁,但是这种锁是不安全的 关于 OSSpinLock 不再安全,原因就在于优先级反转问题。

优先级翻转:一个高优先级任务间接被一个低优先级任务所抢先(preemtped),使得两个任务的相对优先级被倒置。 这往往出现在一个高优先级任务等待访问一个被低优先级任务正在使用的临界资源,从而阻塞了高优先级任务;同时,该低优先级任务被一个次高优先级的任务所抢先,从而无法及时地释放该临界资源。这种情况下,该次高优先级任务获得执行权。
当一个高优先级的任务需要一个资源,但是此时这个资源正在被低优先级任务所占有,这种情况造成高优先级任务需要等待低优先级任务完成之后才能执行,但是次高级别任务并不需要资源,所以他可以在低优先级任务之前执行,所以间接的次优先级任务就在高优先级任务之前被执行了。使得优先级被倒置了。假设高优先级任务等待资源时不是堵塞等待,而是忙着循环,则可能永远无法获得资源,因为低优先级任务没有执行任务的时间片,进而无法释放资源,高优先级任务也永远不会推进。

性能对比

本节参考文章imlifengfeng

优先级反转解决的办法

  1. 优先级继承,故名思义,是将占有锁的线程优先级,继承等待该锁的线程高优先级,如果存在多个线程等待,就取其中之一最高的优先级继承。
  2. 优先级天花板,则是直接设置优先级上限,给临界区一个最高优先级,进入临界区的进程都将获得这个高优先级。
  3. 禁止中断的特点,在于任务只存在两种优先级:可被抢占的 / 禁止中断的 。 前者为一般任务运行时的优先级,后者为进入临界区的优先级。通过禁止中断来保护临界区,没有其它第三种的优先级,也就不可能发生反转了。

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1

联系我们