Concurrency By Turorials (5) - 并发问题

551 阅读3分钟

竞态条件 共享相同进程(包括应用程序本身)的线程共享相同的地址空间。

这意味着每个线程都试图读写相同的共享资源。

如果不小心,可能会遇到多个线程试图同时写入同一个变量的竞态条件。

考虑这样一个例子:有两个线程正在执行,它们都试图更新对象的count变量。

读和写是单独的任务,计算机不能作为单个操作执行。

count += 1

同时有两个线程都想更新这个状态:

  1. 将变量count的值加载到内存中。
  2. 在内存中增加一个count值。
  3. 将最新更新的计数写回磁盘。

结果:

• 线程1在线程2之前启动一个时钟周期,并从count中读取值1。

• 在第二个时钟周期中,线程1将内存中的值更新为2,线程2从count中读取值1。

• 在第三个时钟周期中,线程1现在将值2写回count变量。然而,线程2现在只是将内存中的值从1更新为2。

• 在第四个时钟周期,线程2现在也写值2计数…除非您希望看到值3,因为两个单独的线程都更新了值。

为了避免这种事情的发送,可以通过一个私有的队列去进行操作:

private let threadSafeCountQueue = DispatchQueue(label: “…”)
private var _count = 0
public var count: Int { 
  get {
    return threadSafeCountQueue.sync { 
     _count 
      } 
   } 
  set { 
     threadSafeCountQueue.sync { 
      _count = newValue
    }
} } 

barrier

有的时候为了尽可能的完成读的操作,可以使用并发队列,当需要写入变量时,您需要锁定队列,以便所有已提交的内容都完成,但是在更新完成之前不会运行新的提交。这时候需要使用barrier。

private let threadSafeCountQueue = DispatchQueue(label: “…”, 
attributes: .concurrent) 
private var _count = 0 
public var count: Int { 
	get {
		return threadSafeCountQueue.sync { 
      	return _count
    	}
	} 
	set { 
		threadSafeCountQueue.async(flags: .barrier) { [unowned self] in
			self._count = newValue 
		} 
	} 
} 

一旦到达barrier,队列就假装它是串行的,并且只有barrier中任务完成,在barrier任务之后提交的所有任务才可以再次并发运行。

死锁

死锁在Swift编程中很少发生,除非您使用信号量或其他显式锁定机制。 在当前DispatchQueue上意外调用sync是最常见的情况。

如果使用信号量来控制对多个资源的访问,请确保以相同的顺序请求资源。

如果线程1请求一个hammer,然后是一个saw,而线程2请求一个saw和一个hammer,你可以死锁。

线程1请求并同时接收一个资源1,线程2请求并接收一个资源2。

然后线程1请求一个资源2—没有释放资源1—但是线程2拥有资源2,所以线程1必须等待。

线程2请求使用资源1,但是线程1仍然拥有资源1,因此线程2必须等待资源1可用。

这两个线程现在都处于死锁状态,因为在释放它们所请求的资源之前,它们都无法继续前进,而这是永远不会发生的。

优先级翻转

从技术上讲,优先级反转发生在服务质量较低的队列获得比服务质量较高的队列更高的系统优先级时。

优先级反转最常见的情况是高优先级的队列与低优先级的队列共享一个资源。

当较低的队列获得对象上的锁时,较高的队列现在必须等待。

在释放锁之前,高优先级队列在低优先级任务运行时什么都不能做。