SDWebimage相关知识点1-- 线程

你说的曾经没有我的故事 提交于 2020-04-25 13:15:04
 
线程(thread)是组成进程的子单元,操作系统的调度器可以对线程进行单独的调度。实际上,所有的并发编程 API 都是构建于线程之上的 —— 包括 GCD 和操作队列(operation queues)。
 
多线程可以在单核 CPU 上同时(或者至少看作同时)运行。操作系统将小的时间片分配给每一个线程,这样就能够让用户感觉到有多个任务在同时进行。如果 CPU 是多核的,那么线程就可以真正的以并发方式被执行,从而减少了完成某项操作所需要的总时间。
 
NSThread 是 Objective-C 对 pthread 的一个封装。通过封装
 
直接使用线程可能会引发的一个问题是,如果你的代码和所基于的框架代码都创建自己的线程时,那么活动的线程数量有可能以指数级增长。这在大型工程中是一个常见问题。例如,在 8 核 CPU 中,你创建了 8 个线程来完全发挥 CPU 性能。然而在这些线程中你的代码所调用的框架代码也做了同样事情(因为它并不知道你已经创建的这些线程),这样会很快产生成成百上千的线程。代码的每个部分自身都没有问题,然而最后却还是导致了问题。使用线程并不是没有代价的,每个线程都会消耗一些内存和内核资源。
 
GCD  Grand Central Dispatch
通过 GCD,开发者不用再直接跟线程打交道了,只需要向队列中添加代码块即可,GCD 在后端管理着一个 线程池。GCD 不仅决定着你的代码块将在哪个线程被执行,它还根据可用的系统资源对这些线程进行管理。这样可以将开发者从线程管理的工作中解放出来,通过集中的管理线程,来缓解大量线程被创建的问题。
 
GCD 带来的另一个重要改变是,作为开发者可以将工作考虑为一个队列,而不是一堆线程,这种并行的抽象模型更容易掌握和使用。
在绝大多数情况下使用默认的优先级队列就可以了。如果执行的任务需要访问一些共享的资源,那么在不同优先级的队列中调度这些任务很快就会造成不可预期的行为。这样可能会引起程序的完全挂起,因为低优先级的任务阻塞了高优先级任务,使它不能被执行。
 
 
操作队列(operation queue)是由 GCD 提供的一个队列模型的 Cocoa 抽象。GCD 提供了更加底层的控制,而操作队列则在 GCD 之上实现了一些方便的功能,这些功能对于 app 的开发者来说通常是最好最安全的选择。
 
NSOperationQueue 有两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。在两种类型中,这些队列所处理的任务都使用 NSOperation 的子类来表述。
 
你可以通过重写 main 或者 start 方法 来定义自己的 operations 。前一种方法非常简单,开发者不需要管理一些状态属性(例如 isExecuting 和 isFinished),当 main 方法返回的时候,这个 operation 就结束了。这种方式使用起来非常简单,但是灵活性相对重写 start 来说要少一些。
 
如果你希望拥有更多的控制权,以及在一个操作中可以执行异步任务,那么就重写 start 方法:
 
注意:这种情况下,你必须手动管理操作的状态。 为了让操作队列能够捕获到操作的改变,需要将状态的属性以配合 KVO 的方式进行实现。如果你不使用它们默认的 setter 来进行设置的话,你就需要在合适的时候发送合适的 KVO 消息。
 
为了能使用操作队列所提供的取消功能,你需要在长时间操作中时不时地检查 isCancelled 属性:
 
当你定义好 operation 类之后,就可以很容易的将一个 operation 添加到队列中:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
YourOperation *operation = [[YourOperation alloc] init];
[queue  addOperation:operation];
另外,你也可以将 block 添加到操作队列中。这有时候会非常的方便,比如你希望在主队列中调度一个一次性任务:
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
    // 代码...
}];
 
虽然通过这种的方式在队列中添加操作会非常方便,但是定义你自己的 NSOperation 子类会在调试时很有帮助。如果你重写 operation 的description 方法,就可以很容易的标示出在某个队列中当前被调度的所有操作 。
除了提供基本的调度操作或 block 外,操作队列还提供了在 GCD 中不太容易处理好的特性的功能。例如,你可以通过 maxConcurrentOperationCount 属性来控制一个特定队列中可以有多少个操作参与并发执行。将其设置为 1 的话,你将得到一个串行队列,这在以隔离为目的的时候会很有用。
另外还有一个方便的功能就是根据队列中 operation 的优先级对其进行排序,这不同于 GCD 的队列优先级,它只影响当前队列中所有被调度的 operation 的执行先后。如果你需要进一步在除了 5 个标准的优先级以外对 operation 的执行顺序进行控制的话,还可以在 operation 之间指定依赖关系,如下:
[intermediateOperation addDependency:operation1];
[intermediateOperation addDependency:operation2];
[finishedOperation addDependency:intermediateOperation];
 
这些简单的代码可以确保 operation1 和 operation2 在 intermediateOperation 之前执行. 对于需要明确的执行顺序时,操作依赖是非常强大的一个机制。它可以让你创建一些操作组,并确保这些操作组在依赖它们的操作被执行之前执行,或者在并发队列中以串行的方式执行操作。
 
互斥锁
互斥访问的意思就是同一时刻,只允许一个线程访问某个特定资源。为了保证这一点,每个希望访问共享资源的线程,首先需要获得一个共享资源的 互斥锁,一旦某个线程对资源完成了操作,就释放掉这个互斥锁,这样别的线程就有机会访问该共享资源了。
 
属性atomic 表示每次访问该属性都会进行隐式的加锁和解锁操作
 
在这里有一个东西需要进行权衡:获取和释放锁所是要带来开销的,因此你需要确保你不会频繁地进入和退出 临界区段(比如获取和释放锁)。同时,如果你获取锁之后要执行一大段代码,这将带来锁竞争的风险:其它线程可能必须等待获取资源锁而无法工作。这并不是一项容易解决的任务。
 
 
互斥锁解决了竞态条件的问题,但很不幸同时这也引入了一些 其他问题,其中一个就是 死锁。当多个线程在相互等待着对方的结束时,就会发生死锁,这时程序可能会被卡住。
你在线程之间共享的资源越多,你使用的锁也就越多,同时程序被死锁的概率也会变大。这也是为什么我们需要尽量减少线程间资源共享,并确保共享的资源尽量简单的原因之一
 
优先级翻转问题 、
优先级反转是指程序在运行时低优先级的任务阻塞了高优先级的任务,有效的反转了任务的优先级。
 
使用不同优先级的多个队列听起来虽然不错,但毕竟是纸上谈兵。它将让本来就复杂的并行编程变得更加复杂和不可预见。如果你在编程中,遇到高优先级的任务突然没理由地卡住了,可能你会想起本文,以及那个美国宇航局的工程师也遇到过的被称为优先级反转的问题。
 
UIKit 不是线程安全的,推荐只访问主线程,并且甚至是绘图方法他们都没有明确地表示保证线程安全。
在后台使用 UIKit 对象的的危险之处在于“内存回收问题”。要求 UI 对象应该在主线程中被回收,因为在它们的 dealloc方法被调用回收的时候,可能会去改变 view 的结构关系,而如我们所知,这种操作应该放在主线程来进行。
NSArry 这样不可变类是线程安全的
NSMutableArray 是线程不安全的
NSCache 使用一个可变的字典来存储不可变数据,它不仅会对访问加锁,更甚至在低内存情况下会清空自己的内容。
 
四 
原子属性 
一个非原子的 setter 看起来是这个样子的:
- (void)setUserName:(NSString *)userName {
      if (userName != _userName) {
          [userName retain];
          [_userName release];
          _userName = userName;
      }
}
要是 setUserName: 被并发调用的话会造成麻烦。我们可能会释放 _userName 两次,这回使内存错误,并且导致难以发现的 bug。
对于任何没有手动实现的属性,编译器都会生成一个  objc_setProperty_non_gc(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 的调用。在我们的例子中,这个调用的参数是这样的:
objc_setProperty_non_gc(self, _cmd,
  (ptrdiff_t)(&_userName) - (ptrdiff_t)(self), userName, NO, NO);`
objc_setProperty 调用的是如下方法:
static inline void reallySetProperty(id self, SEL _cmd, id newValue,
  ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    id oldValue;
    id *slot = (id*) ((char*)self + offset);
 
    if (copy) {
        newValue = [newValue copyWithZone:NULL];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:NULL];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }
 
    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spin_lock_t *slotlock = &PropertyLocks[GOODHASH(slot)];
        _spin_lock(slotlock);
        oldValue = *slot;
        *slot = newValue;       
        _spin_unlock(slotlock);
    }
 
    objc_release(oldValue);
}
除开方法名字很有趣以外,其实方法实际做的事情非常直接,它使用了在 PropertyLocks 中的 128 个自旋锁中的 1 个来给操作上锁。这是一种务实和快速的方式,最糟糕的情况下,如果遇到了哈希碰撞,那么 setter 需要等待另一个和它无关的 setter 完成之后再进行工作。
 
 
@synchonized(self) 更适合使用在你 需要确保在发生错误时代码不会死锁,而是抛出异常的时候。
 
对于那些肯定应该线程安全的代码(一个好例子是负责缓存的类)来说,一个不错的设计是使用并发的 dispatch_queue 作为读/写锁,并且确保只锁着那些真的需要被锁住的部分,以此来最大化性能。一旦你使用多个队列来给不同的部分上锁的话,整件事情很快就会变得难以控制了。
 
 
@property (nonatomic, strong) NSMutableSet *delegates;
 
// init方法中
_delegateQueue = dispatch_queue_create("com.PSPDFKit.cacheDelegateQueue",
  DISPATCH_QUEUE_CONCURRENT);
 
- (void)addDelegate:(id<PSPDFCacheDelegate>)delegate {
    dispatch_barrier_async(_delegateQueue, ^{
        [self.delegates addObject:delegate];
    });
}
除非 addDelegate: 或者 removeDelegate: 每秒要被调用上千次,否则我们可以使用一个相对简洁的实现方式:
// 头文件
@property (atomic, copy) NSSet *delegates;
 
- (void)addDelegate:(id<PSPDFCacheDelegate>)delegate {
    @synchronized(self) {
        self.delegates = [self.delegates setByAddingObject:delegate];
    }
}
 

dispatch_barrier_async和dispatch_barrier_sync

1.比较 
* 共同点:他之前的任务会在他之前完成,他之后的任务会等他在执行完后执行 
* 不同点:dispatch_barrier_async后面的任务不会等他执行完再 被添加进 队列;dispatch_barrier_sync后面的任务会等他再执行完以后再添加 进队列 
* 任务是 先添加进队列,但是并不是一添加进去就开始执行 
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!