当前位置: 首页 > news >正文

GCD:异步同步?串行并发?一文轻松拿捏!

GCD

文章目录

  • GCD
      • 进程
      • 线程
      • 进程与线程的关系
      • 进程与线程的区别
    • 任务(执行的代码)
    • 队列
      • 线程与队列的关系
    • 队列任务
      • **同步执行任务(sync)**
      • 辅助方法
      • **异步执行任务(async)**
      • 总结
      • 栅栏任务
      • 迭代任务
    • 队列详细属性
      • QoS
      • Attributes
      • AutoreleaseFrequency
      • Target
    • 延迟加入队列
    • 挂起和唤醒队列
    • GCD Group(任务组)
      • 加入任务组
      • 任务组通知
    • 信号量
    • 任务对象
      • 创建任务
      • 执行任务
      • 取消任务
      • 任务通知
      • 任务等待
      • 代码示例
    • 其他

GCD:

全称是Grand Central Dispatch,底层是纯c语言,GCD 的核心就是为了解决如何让程序有序、高效的运行

GCD的优势:

GCD是苹果公司为多核的并行运算提出的解决方案

GCD会自动利用更多的CPU内核(比如双核、四核)

GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)

程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码

前导知识:

进程

进程是系统中正在运行的一个程序,程序一旦运行就是进程

线程

线程进程中执行运算的最小单位,负责当前进程中程序的执行。

进程与线程的关系

一个进程至少有一个线程,一个进程可以运行多个线程,同一进程的多个线程可共享数据。

进程与线程的区别

进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。

任务(执行的代码)

**任务:**一段代码、一个API调用、一个方法、函数、闭包等,一个应用就是由很多任务组成。

任务执行时间: 任务执行时间与线程状态、CPU调度、线程池调度、队列的优先级、任务的复杂度有关。

队列

**队列(排队处理的任务):**FIFO(先进先出)先排队的先受理。

线程与队列的关系

队列是用于保存以及管理任务的,线程负责去队列中取任务进行执行。也可以理解为队列调度任务给到线程中执行

1.串行队列:

​ 串行队列(DISPATCH_QUEUE_SERIAL) : 每次只有一个任务被执行。让任务一个接着一个地执行。一般只开启一个线程,一个任务执行完毕后,再执行下一个任务(特例在后面)。

重要特征:串行队列中执行任务不允许被当前队列中的任务阻塞(此时会死锁),但可以被别的队列任务阻塞。

304f3c68b6654676876391fd6aa199d9tplv-k3u1fbpfcp-zoom-in-crop-mark1512000

创建串行队列:

let serialQueue = DispatchQueue(label: "com.xxx.xxx.queueName")

2.并发队列:

​ 并发队列(DISPATCH_QUEUE_CONCURRENT) : 放到并发队列的任务,GCD也会 FIFO的取出来,放在多个线程中执行,看起来,所有的任务都是一起执行的。

**重要特征:**系统会为并行队列至少分配一个线程,队列允许被任何队列的任务阻塞。

aad73e9709f04f1eb06fe8d4872baea1tplv-k3u1fbpfcp-zoom-in-crop-mark1512000

创建并发队列:

let concurrent = DispatchQueue(label: "com.xxx.xxx.queueName", attributes: .concurrent)

在以上操作的手动创建队列之前,系统就已经默认建好了6条队列,1条系统主队列(串行),5条全局并发队列(不同优先级),它们是我们创建的所有队列的最终目标队列,这6个队列负责所有队列的线程调度。

3.系统主队列:

​ 系统主队列是一个串行队列,它主要处理UI相关任务和少量不耗时间和资源的操作,并且在主函数调用前生成,静态创建,UI只能在主线程更新。

(类属性)获取主队列:

let mainQueue = DispatchQueue.main

4.系统全局并发队列:

​ 全局并发队列:存在5个不同的QoS级别,可以使用默认优先级,也可以单独指定,全局队列底层由数组创建,平时使用网络请求(例如第三方包Alamofire)都是对全局并发队列进行了一个封装,所以看不到直接使用的代码。

获取全局并发队列:

let globalQueue = DispatchQueue.global() // qos: .default
let globalQueue = DispatchQueue.global(qos: .background) // 后台运行级别

全局并发队列的一个应用

此时需要在页面上放置一个网络图片,可以先用并发全局队列获取该图片(防止卡顿),再回到主队列渲染UI

DispatchQueue.global().async{let data = try! Data(contentsOf: URL(string: "aaa.jpg")!)let image = UIImage(data)!DispatchQueue.main.async{self.imageView.image = image}
}

队列任务

同步执行任务(sync)

  • 必须等待当前任务执行完毕,才会执行下一条任务
  • 任务一经提交就会阻塞当前队列(若是并发队列,理解为阻塞当前行),并请求队列立即安排执行该同步任务(即线程切换到另一个队列执行,执行完毕回到当前队列
  • 不具备开启多线程的能力

假设当前在主队列中执行,此时存在一个手动创建的串行队列serialQueue和一个并发队列concurrentQueue。

​ 1.serialQueue队列中没有任务在执行,那么提交一个同步任务在serialQueue队列,此时主队列阻塞,同步任务进入serialQueue队列执行,同步队列执行完后返回主队列执行,没有开辟新线程,是通过队列调度线程在两个队列中切换执行。

let serialQueue = DispatchQueue(label: ".com1")serialQueue.sync {print("同步1",Thread.current)}print("同步2", Thread.current)

输出结果:

同步1 <_NSMainThread: 0x600002770480>{number = 1, name = main}
同步2 <_NSMainThread: 0x600002770480>{number = 1, name = main}

​ 2.主队列中加入一个主队列同步任务,或者在serialQueue队列执行过程中再加入一个同步任务,这时会发生死锁,可以这样理解,一个串行队列中有一个代码块任务在执行,代码块中有一行代码要在当前队列加入一个同步任务,由于是串行队列,任务是按顺序一个一个执行的,同步任务想要执行就必须等待代码块任务执行完,而代码块任务又被要求等待同步任务执行完才能继续执行,此时就形成了一个互相等待的局面,造成死锁。

主队列死锁:

DispatchQueue.main.sync{print("同步", Thread.current)}

报错:

4febf9da44059bafecf904dce08e6089

serialQueue死锁:

let serialQueue = DispatchQueue(label: ".com1")serialQueue.sync {print("同步1",Thread.current)serialQueue.sync {print("同步2",Thread.current)}}

报错:

ade6d3e3d8d11ca24e6a7d450b23caa0

​ 3.并发队列中有任务在执行,此时加入一个同步任务,线程会转去执行该同步任务,结束后再回到原任务,可以把并行队列想象成很多行串行队列组成的队列,加入同步任务时,线程离开正在执行的一行转而去同步任务添加的那一行执行,此时没有创建新线程,所以一直是并发队列调度一个线程执行不同行的任务。

let concurrentQueue = DispatchQueue(label: ".com2", attributes: .concurrent)concurrentQueue.sync {print("同步1", Thread.current)concurrentQueue.sync {print("同步2", Thread.current)}print("同步3",Thread.current)}

输出结果:

同步1 <_NSMainThread: 0x600002710480>{number = 1, name = main}
同步2 <_NSMainThread: 0x600002710480>{number = 1, name = main}
同步3 <_NSMainThread: 0x600002710480>{number = 1, name = main}

辅助方法

测试任务是否在指定队列中,通过给队列一个标识,使用DispatchQueue.getSpecific方法来获取当前队列的标识,如果能获取到,说明任务在队列中。

//队列类型enum DispatchTaskType: String{case serialcase concurrentcase maincase global}//定义队列let serialQueue = DispatchQueue(label: "com.serialQueue")let concurrentQueue = DispatchQueue(label: "com.concurrentQueue", attributes: .concurrent)let mainQueue = DispatchQueue.mainlet globalQueue = DispatchQueue.global()//定义队列keylet serialQueueKey = DispatchSpecificKey<String>()let concurrentQueueKey = DispatchSpecificKey<String>()let mainQueueKey = DispatchSpecificKey<String>()let globalQueueKey = DispatchSpecificKey<String>()//初始化队列override func loadView() {super.loadView()serialQueue.setSpecific(key: serialQueueKey, value: DispatchTaskType.serial.rawValue)concurrentQueue.setSpecific(key: concurrentQueueKey, value: DispatchTaskType.concurrent.rawValue)mainQueue.setSpecific(key: mainQueueKey, value: DispatchTaskType.main.rawValue)globalQueue.setSpecific(key: globalQueueKey, value: DispatchTaskType.global.rawValue)}func testIsTaskInQueue(_ queueType: DispatchTaskType, key: DispatchSpecificKey<String>){let value = DispatchQueue.getSpecific(key: key)let opnValue: String? = queueType.rawValueprint("Is task in \(queueType.rawValue) queue: \(value == opnValue)")}override func viewDidLoad() {super.viewDidLoad()serialQueue.sync {self.testIsTaskInQueue(.serial, key: serialQueueKey)}}

输出结果:

Is task in serial queue: true

异步执行任务(async)

  • 不用等待当前任务执行完毕,就可以执行下一条任务
  • 具备开启多线程的能力
  • 特性:任务提交后不会阻塞当前队列,会由队列安排另一个线程执行

​ 1.并行队列中新增异步任务:此时会新开一个线程,任务同时执行在不同线程上。

concurrentQueue.async {print("异步1", Thread.current)}

执行结果:

异步1 <NSThread: 0x6000018d41c0>{number = 7, name = (null)}

​ 2.串行队列中新增异步任务:此时新开一个线程,串行队列的异步任务执行在新线程上

serialQueue.async {print("异步1", Thread.current)}

执行结果:

异步1 <NSThread: 0x6000018d41c0>{number = 6, name = (null)}

​ 3.串行队列任务中嵌套本队列的异步任务:先同步阻塞了主队列,在主线程中执行同步任务,执行到新增异步任务语句开辟一个新线程,但由于串行队列任务只能一个接一个执行,所以即使此刻有一个新线程,异步任务仍然要添加在串行队列的队尾,直到同步任务执行结束,该异步任务才通过新线程执行,注意此时若主队列也有新任务,两个串行队列的执行互不影响(不同线程),类似于并发队列的不同行通过不同线程执行。

//两个串行队列没有固定顺序let serialQueue = DispatchQueue(label: ".com1")serialQueue.sync {print("同步1",Thread.current)serialQueue.async {print("异步1",Thread.current)}print("同步2",Thread.current)}print("同步3", Thread.current)//这里后续的任务和serialQueue的异步任务互不影响

输出结果:

同步1 <_NSMainThread: 0x600003ebc000>{number = 1, name = main}
同步2 <_NSMainThread: 0x600003ebc000>{number = 1, name = main}
同步3 <_NSMainThread: 0x600003ebc000>{number = 1, name = main}
异步1 <NSThread: 0x600003ea1a00>{number = 7, name = (null)}

若在主队列后续任务前加一个延时:

//两个串行队列没有固定顺序let serialQueue = DispatchQueue(label: ".com1")serialQueue.sync {print("同步1",Thread.current)serialQueue.async {print("异步1",Thread.current)}print("同步2",Thread.current)}Thread.sleep(until: .now + 0.2)print("同步3", Thread.current)

输出结果:

同步1 <_NSMainThread: 0x600001ea8380>{number = 1, name = main}
同步2 <_NSMainThread: 0x600001ea8380>{number = 1, name = main}
异步1 <NSThread: 0x600001eada80>{number = 5, name = (null)}
同步3 <_NSMainThread: 0x600001ea8380>{number = 1, name = main}

总结

分类


  • sync + DISPATCH_QUEUE_SERIAL : 阻塞当前线程取出的任务一个一个执行 所以不会创建线程

  • sync + DISPATCH_QUEUE_CONCURRENT : 因为会阻塞当前线程 所以即使是并发队列 一样是一个一个任务执行 不会创建线程

  • async + DISPATCH_QUEUE_SERIAL : 不会阻塞当前的线程 但是任务是一个一个取出来执行的 所以会创建一个线程

  • async + DISPATCH_QUEUE_CONCURRENT : 不会阻塞当前线程 任务取出来放到其他线程中 所以会创建很多线程 由系统控制

默认代码为串行同步,网络请求为并发异步,这两个组合为常用组合

栅栏任务

162a8a44af2c49d2tplv-t2oaga2asx-jj-mark3024000q75

​ 栅栏任务的主要特性是可以对队列中的任务进行阻隔,执行栅栏任务时,它会先等待队列中已有的任务全部执行完成,然后它再执行,在它之后加入的任务也必须等栅栏任务执行完后才能执行。

​ 这个特性更适合并行队列,而且对栅栏任务使用同步或异步方法效果都相同。

创建方式,先创建 WorkItem,标记为:barrier,再添加至队列中:

let queue = DispatchQueue(label: "com.zhalan", attributes: .concurrent)let task = DispatchWorkItem(flags: .barrier) {print(Thread.current)}queue.async(execute: task)queue.sync(execute: task)

输出结果:

<NSThread: 0x6000000ec780>{number = 7, name = (null)}
<_NSMainThread: 0x6000000bc700>{number = 1, name = main}

示例

并行队列中执行栅栏任务

/// 栅栏任务func barrierTask() {let queue = concurrentQueuelet barrierTask = DispatchWorkItem(flags: .barrier) {print("栅栏任务", Thread.current)}queue.async {print("任务1", Thread.current)}queue.async {print("任务2", Thread.current)}queue.async {print("任务3", Thread.current)}queue.async(execute: barrierTask) // 栅栏任务queue.async {print("任务4", Thread.current)}queue.async {print("任务5", Thread.current)}queue.async {print("任务6", Thread.current)}}

输出结果:

任务2 <NSThread: 0x600001262100>{number = 4, name = (null)}
任务1 <NSThread: 0x600001239780>{number = 6, name = (null)}
任务3 <NSThread: 0x600001271b40>{number = 5, name = (null)}
栅栏任务 <NSThread: 0x600001271b40>{number = 5, name = (null)}
任务4 <NSThread: 0x600001271b40>{number = 5, name = (null)}
任务6 <NSThread: 0x600001262100>{number = 4, name = (null)}
任务5 <NSThread: 0x600001239780>{number = 6, name = (null)}

栅栏任务上下的任务输出顺序不确定

迭代任务

​ 并行队列利用多个线程执行任务,可以提高程序执行的效率。而迭代任务可以更高效地利用多核性能,它可以利用 CPU 当前所有可用线程进行计算(任务小也可能只用一个线程)。如果一个任务可以分解为多个相似但独立的子任务,那么迭代任务是提高性能最适合的选择。

​ 使用 concurrentPerform 方法执行迭代任务,迭代任务的后续任务需要等待它执行完成才会继续。本方法类似于 Objc 中的 dispatch_apply 方法,创建方式如下:

DispatchQueue.concurrentPerform(iterations: 10) {(index) -> Void in // 10 为迭代次数,可修改。// do something
}

迭代任务可以单独执行,也可以放在指定的队列中:

let queue = DispatchQueue.global() // 全局并发队列
queue.async {DispatchQueue.concurrentPerform(iterations: 100) {(index) -> Void in// do something}//可以转至主线程执行其他任务DispatchQueue.main.async {// do something}
}

示例:

本示例查找 1-100 之间能被 13 整除的整数,我们直接使用 10000 次迭代对每个数进行判断,符合的通过异步方法写入到结果数组中:

/// 迭代任务func concurrentPerformTask() {/// 判断一个数是否能被另一个数整除func isDividedExactlyBy(_ divisor: Int, with number: Int) -> Bool {return number % divisor == 0}let array = Array(1...100)var result: [Int] = []globalQueue.async {//通过concurrentPerform,循环变量数组print("迭代任务开始")DispatchQueue.concurrentPerform(iterations: 100) {(index) -> Void inif isDividedExactlyBy(13, with: array[index]) {print("find a match: \(array[index])", Thread.current)self.mainQueue.async {result.append(array[index])}}}print("迭代任务结束")//执行完毕,主线程更新结果。DispatchQueue.main.sync {print("回到主线程")print("result: 找到了 \(result.count) 个数字 - \(result)")}}}
迭代任务开始
find a match: 39 <NSThread: 0x600001f342c0>{number = 4, name = (null)}
find a match: 26 <NSThread: 0x600001f65c00>{number = 6, name = (null)}
find a match: 13 <NSThread: 0x600001f284c0>{number = 5, name = (null)}
find a match: 52 <NSThread: 0x600001f28340>{number = 8, name = (null)}
find a match: 78 <NSThread: 0x600001f2ee80>{number = 7, name = (null)}
find a match: 91 <NSThread: 0x600001f3c400>{number = 9, name = (null)}
find a match: 65 <NSThread: 0x600001f62580>{number = 3, name = (null)}
迭代任务结束
回到主线程
result: 找到了 7 个数字 - [39, 26, 13, 52, 78, 91, 65]

队列详细属性

创建队列的完整方法如下:

public convenience init(label: String, qos: DispatchQoS = .unspecified, attributes: DispatchQueue.Attributes = [], autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = .inherit, target: DispatchQueue? = nil)

QoS

队列在执行上是有优先级的,更高的优先级可以享受更多的计算资源,从高到低包含以下几个等级:


  • userInteractive
  • userInitiated
  • default
  • utility
  • background

Attributes

包含两个属性:

  • concurrent:标识队列为并行队列
  • initiallyInactive:标识运行队列中的任务需要动手触发(未添加此标识时,向队列中添加任务会自动运行),触发时通过 queue.activate() 方法。

AutoreleaseFrequency

这个属性表示 autorelease pool 的自动释放频率, autorelease pool 管理着任务对象的内存周期。

包含三个属性:

  • inherit:继承目标队列的该属性
  • workItem:跟随每个任务的执行周期进行自动创建和释放
  • never:不会自动创建 autorelease pool,需要手动管理。

一般任务采用 .workItem 属性就够了,特殊任务如在任务内部大量重复创建对象的操作可选择 .never 属性手动创建 autorelease pool

Target

这个属性设置的是一个队列的目标队列,即实际将该队列的任务放入指定队列中运行。目标队列最终约束了队列优先级等属性。

在程序中手动创建的队列,其实最后都指向系统自带的 主队列全局并发队列

手动创建队列的好处是可以将任务进行分组管理。如单独阻塞队列中的任务,而不是阻塞系统队列中的全部任务。如果阻塞了目标队列,所有指向它的原队列也将被阻塞。

在 Swift 3 及之后,对目标队列的设置进行了约束,只有两种情况可以显式地设置目标队列(原因参考):

  • 初始化方法中,指定目标队列。
  • 初始化方法中,attributes 设定为 initiallyInactive,然后在队列执行 activate() 之前可以指定目标队列。

在其他地方都不能再改变目标队列。

延迟加入队列

等待一段时间后再进入队列中,这时候可以使用 asyncAfter 方法.

class AsyncAfter {/// 延迟执行闭包static func dispatch_later(_ time: TimeInterval, block: @escaping ()->()) {let t = DispatchTime.now() + timeDispatchQueue.main.asyncAfter(deadline: t, execute: block)}
}AsyncAfter.dispatch_later(2) {print("打个电话 at: \(Date())") // 将在 2 秒后执行
}

示例:封装一个方法,可以延迟执行任务,在计时结束前还可以取消任务或者将原任务替换为一个新任务。主要的思路是,将延迟后实际执行的任务代码进行替换,替换为空闭包则相当于取消了任务,或者替换为你想执行的其他任务:

AfterTask.swift文件

class AsyncAfter {typealias ExchangableTask = (_ newDelayTime: TimeInterval?,_ anotherTask:@escaping (() -> ())) -> Void/// 延迟执行一个任务,并支持在实际执行前替换为新的任务,并设定新的延迟时间。////// - Parameters:///   - time: 延迟时间///   - yourTask: 要执行的任务/// - Returns: 可替换原任务的闭包static func delay(_ time: TimeInterval, yourTask: @escaping ()->()) -> ExchangableTask {var exchangingTask: (() -> ())? // 备用替代任务var newDelayTime: TimeInterval? // 新的延迟时间let finalClosure = { () -> Void in //最后会执行的闭包if exchangingTask == nil { //如果没有传入新的任务(替换原任务的更改任务)DispatchQueue.main.async(execute: yourTask)//执行原任务} else {if newDelayTime == nil {//如果需要执行新任务,且没有延迟,立刻执行DispatchQueue.main.async {print("任务已更改,现在是:\(Date())")exchangingTask!()//执行新任务(exchangingTask在新任务调用时被赋值成新闭包了)}}print("原任务取消了,现在是:\(Date())")}}dispatch_later(time) { finalClosure() }//原任务经过原延迟时间后执行let exchangableTask: ExchangableTask = //返回给用户的可添加新任务的闭包{ delayTime, anotherTask in //新的延迟时间和新任务exchangingTask = anotherTask //赋值新任务newDelayTime = delayTime //赋值新的延迟时间(可为nil,立刻执行)if delayTime != nil { //如果有新的延迟时间self.dispatch_later(delayTime!) { //经过新的延迟时间后anotherTask()	//执行新任务print("任务已更改,现在是:\(Date())")}}}return exchangableTask}
}

ViewController.swift文件

override func viewDidLoad() {super.viewDidLoad()let newTask = AsyncAfter.delay(2) {print("OldTask")}newTask(2) {//若闭包为空则表示为取消任务,newTask(nil){}(nil可以替换成别的时间,表示更改任务的时间)取消任务时间按原计划print("NewTask")}}

输出结果:

NewTask
原任务取消了,现在是:2023-11-18 06:24:58 +0000
任务已更改,现在是:2023-11-18 06:24:58 +0000

挂起和唤醒队列

​ GCD 提供了一套机制,可以挂起队列中尚未执行的任务,已经在执行的任务会继续执行完,后续还可以手动再唤醒队列。

​ 挂起使用 suspend(),唤醒使用 resume()。对于队列,这两个方法调用时需配对,因为可以多次挂起,调用唤醒的次数应等于挂起的次数才能生效,唤醒的次数更多则会报错,所以使用时最好设置一个计数器,或者封装一个挂起、唤醒的方法,在方法内部进行检查。

而对于 DispatchSource 则有所不同,它必须先调用 resume() 才能接收消息,所以此时唤醒的数量等于挂起的数量加一。

示例:

//
//  SusResume.swift
//  test123
//
//  Created by 李跃行 on 2023/11/18.
//import Foundationclass CreateQueueWithTask{let concurrentQueue = DispatchQueue(label: "com.concurrentQueue", attributes: .concurrent)func printCurrentThread(with: String){print(with, Thread.current)}
}/// 挂起、唤醒测试类
class SuspendAndResum {let createQueueWithTask = CreateQueueWithTask()var concurrentQueue: DispatchQueue {return createQueueWithTask.concurrentQueue}var suspendCount = 0 // 队列挂起的次数// MARK: ---------队列方法------------/// 挂起测试func suspendQueue() {createQueueWithTask.printCurrentThread(with: "start test\n")concurrentQueue.async {self.createQueueWithTask.printCurrentThread(with: "concurrentQueue async task1\n")}concurrentQueue.async {self.createQueueWithTask.printCurrentThread(with: "concurrentQueue async task2\n")}————————————————————————————————可替换区域——————————————————————————————————————————// 通过栅栏挂起任务let barrierTask = DispatchWorkItem(flags: .barrier) {self.safeSuspend(self.concurrentQueue)}concurrentQueue.async(execute: barrierTask)print(123)//通过同步挂起任务concurrentQueue.sync {self.safeSuspend(self.concurrentQueue)}print(456)————————————————————————————————————————————————————————————————————————————————————concurrentQueue.async {self.createQueueWithTask.printCurrentThread(with: "concurrentQueue async task3\n")}concurrentQueue.async {self.createQueueWithTask.printCurrentThread(with: "concurrentQueue async task4\n")}concurrentQueue.async {self.createQueueWithTask.printCurrentThread(with: "concurrentQueue async task5\n")}createQueueWithTask.printCurrentThread(with: "end test")}/// 唤醒测试func resumeQueue() {self.safeResume(self.concurrentQueue)}/// 安全的挂起操作func safeSuspend(_ queue: DispatchQueue) {suspendCount += 1queue.suspend()print("任务挂起了")}/// 安全的唤醒操作func safeResume(_ queue: DispatchQueue) {if suspendCount == 1 {queue.resume()suspendCount = 0print("任务唤醒了")} else if suspendCount < 1 {print("唤醒的次数过多")} else {queue.resume()suspendCount -= 1print("唤醒的次数不够,还需要 \(suspendCount) 次唤醒。")}}}

调用代码:

@IBAction func resume(_ sender: UIButton) {suspendAndResum.resumeQueue()}@IBAction func suspend(_ sender: UIButton) {suspendAndResum.suspendQueue()}

UI:

这里要注意可替换区域三种情况:

1.若直接挂起任务,连续点击2次挂起,会把每次点击产生的任务直接添加到concurrentQueue队列中,需要点击对应次数的唤醒,才能执行后续任务,可替换区域代码如下。

self.safeSuspend(self.concurrentQueue)

输出结果:

start test<_NSMainThread: 0x60000079c540>{number = 1, name = main}
任务挂起了
end test <_NSMainThread: 0x60000079c540>{number = 1, name = main}
concurrentQueue async task1<NSThread: 0x6000007d9100>{number = 6, name = (null)}
concurrentQueue async task2<NSThread: 0x6000007d5100>{number = 3, name = (null)}start test<_NSMainThread: 0x60000079c540>{number = 1, name = main}
任务挂起了
end test <_NSMainThread: 0x60000079c540>{number = 1, name = main}唤醒的次数不够,还需要 1 次唤醒。任务唤醒了
concurrentQueue async task3<NSThread: 0x600000797300>{number = 4, name = (null)}
concurrentQueue async task5<NSThread: 0x6000007f7e40>{number = 9, name = (null)}
concurrentQueue async task4<NSThread: 0x6000007d1640>{number = 8, name = (null)}
concurrentQueue async task1<NSThread: 0x600000797300>{number = 4, name = (null)}
concurrentQueue async task2<NSThread: 0x6000007d1380>{number = 10, name = (null)}
concurrentQueue async task3<NSThread: 0x6000007f7e40>{number = 9, name = (null)}
concurrentQueue async task4<NSThread: 0x6000007d11c0>{number = 11, name = (null)}
concurrentQueue async task5<NSThread: 0x6000007f7dc0>{number = 12, name = (null)}

2.若使用栅栏任务来挂起任务,连续点击两次挂起,在第一次挂起时,栅栏任务所在队列的剩余任务就被挂起了,再次点击挂起,由于前面的任务还是挂起状态,所以此时新添加的任务(包括栅栏任务)也处于挂起状态,栅栏任务没有被第二次执行,所以唤醒只用点击一次即可执行队列中的任务,直到再次执行到队列中添加的栅栏任务,此时再次被挂起,可替换区域代码如下。

// 通过栅栏挂起任务
let barrierTask = DispatchWorkItem(flags: .barrier) {self.safeSuspend(self.concurrentQueue)
}
concurrentQueue.async(execute: barrierTask)

运行结果:

start test<_NSMainThread: 0x600003c68480>{number = 1, name = main}
concurrentQueue async task1<NSThread: 0x600003c38280>{number = 3, name = (null)}
end test <_NSMainThread: 0x600003c68480>{number = 1, name = main}
concurrentQueue async task2<NSThread: 0x600003c11440>{number = 8, name = (null)}
任务挂起了start test<_NSMainThread: 0x600003c68480>{number = 1, name = main}
end test <_NSMainThread: 0x600003c68480>{number = 1, name = main}任务唤醒了
concurrentQueue async task3<NSThread: 0x600003c11440>{number = 8, name = (null)}
concurrentQueue async task4<NSThread: 0x600003c0fe80>{number = 9, name = (null)}
concurrentQueue async task5<NSThread: 0x600003c38280>{number = 3, name = (null)}
concurrentQueue async task1<NSThread: 0x600003c0f600>{number = 10, name = (null)}
concurrentQueue async task2<NSThread: 0x600003c11440>{number = 8, name = (null)}
任务挂起了任务唤醒了
concurrentQueue async task3<NSThread: 0x600003c11440>{number = 8, name = (null)}
concurrentQueue async task4<NSThread: 0x600003c0f600>{number = 10, name = (null)}
concurrentQueue async task5<NSThread: 0x600003c38280>{number = 3, name = (null)}

3.若使用同步任务挂起任务,连续点击两次挂起,在第一次挂起时,同步任务所在队列的剩余任务就被挂起了,再次点击挂起,由于前面的任务还是挂起状态,所以此时新添加的任务(包括同步任务)也处于挂起状态,当主队列执行到加入同步任务的那行代码时,同步任务加入到concurrentQueue队列中,而主队列此时被阻塞(同步任务的特点),且concurrentQueue队列中的任务都处于挂起状态,没有任务可以执行,就陷入了一个类似死锁的状态,点击挂起和唤醒都无效(因为主队列被阻塞了),可以观察结果输出的123和456来判断,可替换区域代码如下。

//通过同步挂起任务
print(123)
concurrentQueue.sync {self.safeSuspend(self.concurrentQueue)
}
print(456)

输出结果:

start test<_NSMainThread: 0x6000030f8000>{number = 1, name = main}
123
concurrentQueue async task1<NSThread: 0x60000309f000>{number = 7, name = (null)}
concurrentQueue async task2<NSThread: 0x6000030b5240>{number = 5, name = (null)}
任务挂起了
456
end test <_NSMainThread: 0x6000030f8000>{number = 1, name = main}start test<_NSMainThread: 0x6000030f8000>{number = 1, name = main}
123

GCD Group(任务组)

​ 任务组相当于一系列任务的松散集合,它可以来自相同或不同队列,扮演着组织者的角色。它可以通知外部队列,组内的任务是否都已完成。或者阻塞当前的线程,直到组内的任务都完成。所有适合组队执行的任务都可以使用任务组,且任务组更适合集合异步任务(如果都是同步任务,直接使用串行队列即可)。

加入任务组

把一个task加入一个DispatchGroup有两种方式

方式一:通过enter()和leave()

let group = DispatchGroup()
let queue1 = DispatchQueue(label: "com.1")
let queue2 = DispatchQueue(label: "com.2")
let queue3 = DispatchQueue(label: "com.3")group.enter()
queue1.async(){for i in 0...10{print("i = \(i)",Thread.current)}group.leave()
}group.enter()
queue2.async(){for j in 11...20{print("j = \(j)",Thread.current)}group.leave()
}group.enter()
queue3.async(){for n in 21...30{print("n = \(n)",Thread.current)}group.leave()
}group.notify(queue: .main){print("ok")
}

方式二:直接把task加入group

let group = DispatchGroup()
let queue1 = DispatchQueue(label: "com.1")
let queue2 = DispatchQueue(label: "com.2")
let queue3 = DispatchQueue(label: "com.3")queue1.async(group: group){for i in 0...10{print("i = \(i)",Thread.current)}
}queue2.async(group: group){for j in 11...20{print("j = \(j)",Thread.current)}
}queue3.async(group: group){for n in 21...30{print("n = \(n)",Thread.current)}
}group.notify(queue: .main){print("ok")
}

如果想让上面异步任务按顺序执行,可以加入信号量机制

let group = DispatchGroup()
let queue1 = DispatchQueue(label: "com.1")
let queue2 = DispatchQueue(label: "com.2")
let queue3 = DispatchQueue(label: "com.3")
let semaphore = DispatchSemaphore(value: 1)
semaphore.wait()
group.enter()
queue1.async(group: group){for i in 0...10{print("i = \(i)",Thread.current)}group.leave()semaphore.signal()
}
semaphore.wait()
group.enter()
queue2.async(group: group){for j in 11...20{print("j = \(j)",Thread.current)}group.leave()semaphore.signal()
}
semaphore.wait()
group.enter()
queue3.async(group: group){for n in 21...30{print("n = \(n)",Thread.current)}group.leave()semaphore.signal()
}group.notify(queue: .main){print("ok")
}

​ 两种加入方式在对任务处理的特性上是没有区别的,只是便利之处不同。如果任务所在的队列是自己创建或引用的系统队列,那么直接使用第一种方式直接加入即可。如果任务是由系统或第三方的 API 创建的,由于无法获取到对应的队列,只能使用第二种方式将任务加入组内,例如将 URLSessionaddDataTask 方法加入任务组中:

extension URLSession {func addDataTask(to group: DispatchGroup,with request: URLRequest,completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void)-> URLSessionDataTask {group.enter() // 进入任务组return dataTask(with: request) { (data, response, error) incompletionHandler(data, response, error)group.leave() // 离开任务组}}
}

任务组通知

等待任务组中的任务全部完成后,可以统一对外发送通知,有两种方式:

1.group.notify 方法,它可以在所有任务完成后通知指定队列并执行一个指定任务,这个通知的操作是异步的(意味着通知后续的代码不需要等待任务,可以继续执行):

let group = DispatchGroup()let queueBook = DispatchQueue(label: "book")
queueBook.async(group: group) {// do something 1
}
let queueVideo = DispatchQueue(label: "video")
queueVideo.async(group: group) {// do something 2
}group.notify(queue: DispatchQueue.main) {print("all task done")
}print("do something else.")// 执行结果
// do something else.
// do something 1(任务 1、2 完成顺序不固定)
// do something 2
// all task done

2.group.wait 方法,它会在所有任务完成后再执行当前线程中后续的代码,因此这个操作是起到阻塞的作用:

let group = DispatchGroup()let queueBook = DispatchQueue(label: "book")
queueBook.async(group: group) {// do something 1
}
let queueVideo = DispatchQueue(label: "video")
queueVideo.async(group: group) {// do something 2
}group.wait()print("do something else.")// 执行结果
// do something 1(任务 1、2 完成顺序不固定)
// do something 2
// do something else.

wait方法中还可以指定具体的时间,它表示将等待不超过这个时间,如果任务组在指定时间之内完成则立即恢复当前线程,否则将等到时间结束时再恢复当前线程。

  • 方式1,使用 DispatchTime,它表示一个时间间隔,精确到纳秒(1/1000,000,000 秒):

    let waitTime = DispatchTime.now() + 2.0 // 表示从当前时间开始后 2 秒,数字字面量也可以改为使用 TimeInterval 类型变量
    group.wait(timeout: waitTime)

    方式2,使用 DispatchWallTime,它表示当前的绝对时间戳,精确到微秒(1/1000,000 秒),通常使用字面量即可设置延时时间,也可以使用 timespec 结构体来设置一个精确的时间戳。

    // 使用字面量设置
    var wallTime = DispatchWallTime.now() + 2.0 // 表示从当前时间开始后 2 秒,数字字面量也可以改为使用 TimeInterval 类型变量

信号量

DispatchSemaphore,通常称作信号量,顾名思义,它可以通过计数来标识一个信号,这个信号怎么用呢,取决于任务的性质。通常用于对同一个资源访问的任务数进行限制。

例如,控制同一时间写文件的任务数量、控制端口访问数量、控制下载任务数量等。

信号量的使用非常的简单:

  • 首先创建一个初始数量的信号对象
  • 使用 wait 方法让信号量减 1,再安排任务。如果此时信号量仍大于或等于 0,则任务可执行,如果信号量小于 0,则任务需要等待其他地方释放信号。
  • 任务完成后,使用 signal 方法增加一个信号量。
  • 等待信号有两种方式:永久等待、可超时的等待(同上)。

示例:限制同时运行的任务数

/// 信号量测试类
class DispatchSemaphoreTest {/// 限制同时运行的任务数static func limitTaskNumber() {let queue = DispatchQueue(label: "com.sinkingsoul.DispatchQueueTest.concurrentQueue",attributes: .concurrent)let semaphore = DispatchSemaphore(value: 2) // 设置数量为 2 的信号量semaphore.wait()queue.async {task(index: 1)semaphore.signal()}semaphore.wait()queue.async {task(index: 2)semaphore.signal()}semaphore.wait()queue.async {task(index: 3)semaphore.signal()}}/// 任务static func task(index: Int) {print("Begin task \(index) --->")Thread.sleep(forTimeInterval: 2)print("Sleep for 2 seconds in task \(index).")print("--->End task \(index).")}}

输出结果:示例中设置了同时只能运行 2 个任务,可以看到任务 3 在前两个任务完成后才开始运行(仅当任务执行时间差不多的情况)

Begin task 2 --->
Begin task 1 --->
Sleep for 2 seconds in task 2.
Sleep for 2 seconds in task 1.
--->End task 2.
--->End task 1.
Begin task 3 --->
Sleep for 2 seconds in task 3.
--->End task 3.

任务对象

在队列和任务组中,任务实际上是被封装为一个 DispatchWorkItem 对象的。任务封装最直接的好处就是可以取消任务。

前面提到的栅栏任务就是通过封装任务对象实现的。

创建任务

先看看它的创建,其中 qosflags 参数都有默认值,可以不填:

let workItem = DispatchWorkItem(qos: .default, flags: DispatchWorkItemFlags()) {// Do something
}

qos 前面提到过了,这里说一下 DispatchWorkItemFlags,它有以下几个静态属性(详细解释可参考 官方源码 ):

  • assignCurrentContext: 标记应该为任务分配创建它时的上下文属性(例如:QoS、os_activity_t、可能存在的当前 IPC 请求属性)。如果直接调用任务,任务对象将在它的持续时间内在调用线程中应用这些属性。如果提交任务至队列中,则会替换提交任务时的上下文属性默认值。
  • barrier: 标记任务为栅栏任务,提交至并行队列时生效,如果直接运行该任务对象则无此效果。
  • detached: 标记任务在执行时应该剥离当前执行上下文属性(例如:QoS、os_activity_t、可能存在的当前 IPC 请求属性)。如果直接调用任务,任务对象将在它的持续时间内从调用线程中删除这些属性(如果存在属性,且应用于任务之前)。如果提交任务至队列中,将使用队列的属性(或专门分配给任务对象的任何属性)进行执行。如果创建任务时指定了 QoS,则该 QoS 将优先于 flag 对应的 QoS 值。
  • enforceQoS: 标记任务提交至队列执行时,任务对象被分配的 QoS (提交任务时的值)应优先于队列的 QoS,这样做不会降低 QoS。当任务提交至队列同步执行时,或则直接执行任务时,这个 flag 是默认值。
  • inheritQoS: 标记任务提交至队列执行时,队列的 QoS 应优先于任务对象被分配的 QoS (提交任务时的值),后一个 QoS 值只会在队列的 QoS 有问题时才会采用,这样做会导致 QoS 不会低于继承自队列的 QoS。当任务提交至队列异步执行时,这个 flag 是默认值,且直接执行任务时该标志无效。
  • noQoS: 标记任务不应指定 QoS,如果直接执行,将以调用线程的 QoS 执行。如果提交至队列,则会替换提交任务时的 QoS 默认值。

执行任务

执行任务时,调用任务项对象的 perform() 方法,这个调用是同步执行的:

workItem.perform()

或则在队列中执行:

let queue = DispatchQueue.global()
queue.async(execute: workItem)

取消任务

在任务未实际执行之前可以取消任务,调用 cancel() 方法,这个调用是异步执行的:

workItem.cancel()

取消任务将会带来以下结果:

  • 取消将导致 任何 将来的任务在执行时立即返回,但不会影响已在执行的任务。
  • 与任务对象关联的任何资源的释放都会延迟,直到下一次尝试执行任务对象(或者任何正在进行中的执行已完成)。因此需要注意确保可能被取消的任务对象不要捕获任何需要实际执行才能释放的资源,例如使用 malloc(3) 进行内存分配,而在任务中调用 free(3) 释放。 如果由于取消而从未执行任务,则会导致内存泄露。

任务通知

任务对象也有一个通知方法,在任务执行完成后可以向指定队列发送一个异步调用闭包:

workItem.notify(queue: queue) {// Do something
}

这个通知方法有一些地方需要注意:

  • 任务不支持在被多次调用结束后再发出通知,运行时将会报错,通知只能响应一次完整的调用(如果在发出通知时,还有另一次执行未完成,这种情况也视为只有一次调用)。需要在多次执行结束后发出通知,使用任务组的通知更合适。
  • 可以多次发出通知,但通知执行的顺序是不确定的。
  • 任务只要提交至队列中,即使调用 cancel() 方法被取消了,通知也可以生效。

任务等待

任务对象支持等待方法,类似于任务组的等待,也是阻塞型的,需要等待已有的任务完成才能继续执行,也可以指定等待时间:

workItem.perform()
workItem.wait()
workItem.wait(timeout: DispatchTime) // 指定等待时间
workItem.wait(wallTimeout: DispatchWallTime) // 指定等待时间
// 等待任务完成
// do something

下面看个完整的例子:

代码示例

示例 12.1:任务对象测试。

/// 任务对象测试
@IBAction func dispatchWorkItemTestButtonTapped(_ sender: Any) {DispatchWorkItemTest.workItemTest()
}/// 任务对象测试类
class DispatchWorkItemTest {static func workItemTest() {var value = 10let workItem = DispatchWorkItem {print("workItem running start.--->")value += 5print("value = ", value)print("--->workItem running end.")}let queue = DispatchQueue.global()queue.async(execute: workItem)queue.async {print("异步执行 workItem")workItem.perform()print("任务2取消了吗:\(workItem.isCancelled)")workItem.cancel()print("异步执行 workItem end")}workItem.notify(queue: queue) {print("notify 1: value = ", value)}workItem.notify(queue: queue) {print("notify 2: value = ", value)}workItem.notify(queue: queue) {print("notify 3: value = ", value)}queue.async {print("异步执行2 workItem")Thread.sleep(forTimeInterval: 2)print("任务3取消了吗:\(workItem.isCancelled)")workItem.perform()print("异步执行2 workItem end")}}}

执行结果,可以看到任务第一次执行完成后,发出了 3 次通知,而且未按照代码的顺序。在发出通知前,任务还有一次执行未完成,并未造成通知报错。第二次执行任务后,取消了任务,因此任务第三次未正常执行:

workItem running start.--->
异步执行 workItem
异步执行2 workItem
value =  15
workItem running start.--->
value =  20
--->workItem running end.
任务2取消了吗:false
异步执行 workItem end
notify 2: value =  20
notify 3: value =  20
notify 1: value =  20
--->workItem running end.
任务3取消了吗:true
异步执行2 workItem end

其他

另外关于DispatchSource、DispatchIO、DispatchData、时间相关结构体说明可以参考这篇文章,本文结构参考了很多大佬的文章,属实是站在大佬肩膀上了,但其中有很多自己的思考和对其他文章的改正,值得一看。

相关文章:

GCD:异步同步?串行并发?一文轻松拿捏!

GCD 文章目录 GCD进程线程进程与线程的关系进程与线程的区别 任务&#xff08;执行的代码&#xff09;队列线程与队列的关系 队列任务**同步执行任务&#xff08;sync&#xff09;**辅助方法**异步执行任务&#xff08;async)**总结栅栏任务迭代任务 队列详细属性QoSAttributes…...

学习c#的第十七天

目录 C# 异常处理 异常的原因 System.Exception 类 如何处理异常 常见的异常类 throw 语句 throw 表达式 try 语句 try-catch 语句 try-finally 语句 try-catch-finally 语句 when 异常筛选器 异步和迭代器方法中的异常 C# 异常处理 C # 中的异常提供了结构化、统…...

龙芯 操作系统选择和安装

龙芯3a5000及之后的cpu底层架构已经从mips64el改为了loongarch64 所以这里分了2种来说明&#xff0c;分别对应3a4000之前的和3a5000之后的 龙芯的系统安装难点在于操作系统的选取和引导 一、烧录工具 制作安装盘使用常规的烧录工具是不行滴&#xff0c;会提示没有\boot\initrd…...

【开源】基于JAVA的智能停车场管理系统

项目编号&#xff1a; S 005 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S005&#xff0c;文末获取源码。} 项目编号&#xff1a;S005&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、研究内容A. 车主端功能B. 停车工作人员功能C. 系…...

使用IDEA 将Eclipse java工程转为maven格式

使用IDEA 将Eclipse java工程转为maven格式 ①使用idea打开项目&#xff0c;在项目根目录下右键选择 Add Framework Support 选择 maven &#xff0c;引入maven ②找到项目中的.classpath文件或者lib目录 根据.classpath文件或者lib目录中列举的jar包名&#xff0c;将其依次手…...

CCF CSP认证 历年题目自练Day47

题目 试题编号&#xff1a; 201712-3 试题名称&#xff1a; Crontab 时间限制&#xff1a; 10.0s 内存限制&#xff1a; 256.0MB 样例输入 3 201711170032 201711222352 0 7 * * 1,3-5 get_up 30 23 * * Sat,Sun go_to_bed 15 12,18 * * * have_dinner 样例输出 201711170…...

LeetCode Hot100之十:239.滑动窗口最大值

题目 给你一个整数数组 nums&#xff0c;有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回 滑动窗口中的最大值 。 提示&#xff1a; 1 < nums.length < 10^5 -10^4 < nums[i…...

x264、x265、OpenH264 简要对比

一&#xff1a; x264、x265、OpenH264&#xff0c;都是开源代码库&#xff1b;二&#xff1a; H264(MPEG-4/AVC)、H265(HEVC)&#xff0c;是视频编码格式。是视频标准&#xff1b; H264(MPEG-4/AVC) 简称: H264 或 AVC&#xff1b; H265(HEVC) 简称: H265 …...

二维码智慧门牌管理系统升级解决方案:门牌聚合,让管理更便捷!

文章目录 前言一、传统门牌管理系统的瓶颈二、地图门牌聚合展示的优势三、地图门牌聚合展示的实现方法四、智慧门牌管理系统的未来发展 前言 随着城市的发展和建设&#xff0c;对于地址信息的管理变得越来越重要。而智慧门牌管理系统作为管理地址信息的重要工具&#xff0c;其…...

物联网AI MicroPython学习之语法UART通用异步通信

学物联网&#xff0c;来万物简单IoT物联网&#xff01;&#xff01; UART 介绍 模块功能: UART通过串行异步收发通信 接口说明 UART - 构建UART对象 函数原型&#xff1a;UART(id, baudrate&#xff0c;bits, parity&#xff0c;stop, tx, rx)参数说明&#xff1a; 参数类…...

Git企业开发级讲解(四)

&#x1f4d8;北尘_&#xff1a;个人主页 &#x1f30e;个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上&#xff0c;不忘来时的初心 文章目录 一、理解分⽀二、创建分支三、切换分⽀四、合并分⽀五、删除分⽀六、合并冲突七、分⽀管理策略…...

pytorch 安装 2023年

pytorch网址&#xff1a;https://pytorch.org/get-started/locally/ conda install pytorch torchvision torchaudio pytorch-cuda11.8 -c pytorch -c nvidia我在自己电脑上用这个pip命令完全安装不了&#xff0c;只能用conda安装。复制上面提供的命令&#xff0c;在cmd中直接运…...

人工智能基础_机器学习040_Sigmoid函数详解_单位阶跃函数与对数几率函数_伯努利分布---人工智能工作笔记0080

然后我们再来详细说一下Sigmoid函数,上面的函数的公式 我们要知道这里的,Sigmoid函数的意义,这逻辑斯蒂回归的意义就是,在多元线性回归的基础上,把 多元线性回归的结果,缩放到0到1之间对吧,根据中间的0.5为分类,小于0.5的一类,大于的一类, 这里的h theta(x) 就是概率函数 然…...

Scala---迭代器模式+Trait特质特性

Scala迭代器模式处理数据 scala中创建集合需要内存&#xff0c;集合与集合之间的转换时&#xff0c;每次转换生成新的集合时&#xff0c;新的集合也需要内存。如果有一个非常大的初始集合&#xff0c;需要经过多次转换&#xff0c;每次转换都生成一个新的集合&#xff0c;才能…...

labview运行速度太慢

找到labview程序运行速度的瓶颈 - 百度文库 LabVIEW执行速度 - 北京瀚文网星科技有限公司 性能和内存信息窗口 必需&#xff1a;基础版开发系统 选择工具性能分析性能和内存&#xff0c;可显示该窗口。 该窗口用于采集和显示VI的执行时间和内存使用信息。如在不属于项目的…...

QT基础入门【QSS】继承、命名空间中的小部件、QObject 属性介绍

继承 在经典 CSS 中,当项目的字体和颜色没有显式设置时,它会自动从父级继承。但是在使用 Qt 样式表时,默认情况下,部件不会从其父部件自动继承其字体和颜色设置。 例如,考虑一个 QPushButton 在 QGroupBox 内部: qApp->setStyleSheet("QGroupBox { color: red…...

Ubuntu18.04安装IgH主站

EtherCAT主站是EtherCAT网络中的中央控制单元,负责协调和管理连接到网络的所有从站设备。EtherCAT(Ethernet for Control Automation Technology)是一种高性能、实时的工业以太网通信协议,广泛应用于自动化和控制领域。 一、安装依赖包 sudo apt install autoconf automa…...

HTML5-原生History

更多内容&#xff0c;访问: history hash 单页面应用和多页面应用 React-Router源码分析-History库 History库源码分析-Action 动作类型 History库源码分析-createLocation History库源码分析-createPath History库源码分析-parsePath history 浏览器历史记录对象 属性: le…...

无需公网IP,使用MCSM面板一键搭建我的世界Minecraft服务器联机游戏

文章目录 前言1.Mcsmanager安装2.创建Minecraft服务器3.本地测试联机4. 内网穿透4.1 安装cpolar内网穿透4.2 创建隧道映射内网端口 5.远程联机测试6. 配置固定远程联机端口地址6.1 保留一个固定TCP地址6.2 配置固定TCP地址 7. 使用固定公网地址远程联机 前言 MCSManager是一个…...

高斯积分-Gaussian Quadrature

https://mathworld.wolfram.com/GaussianQuadrature.html...

Linux下非root用户安装CUDA

目录 前言 参考链接 步骤 一. 首先&#xff0c;需要查看系统版本&#xff1a; 二. 安装包下载。 下载CUDA&#xff1a; cuDNN下载 三. 开始安装CUDA和cuDNN 安装CUDA 修改环境变量 安装 cuDNN 查看是否安装成功&#xff0c;输入nvcc -V 前言 由于一些代码实现&…...

【bugfix】安装 flash-attn 报错

目录 1. 报错信息 2. 解决方法 安装 flash attention 报错 1. 报错信息 Building wheel for flash-attn (setup.py) ... error error: subprocess-exited-with-error 或者 Building wheel for flash-attn (pyproject.toml) did not run successfully 甚至更多问题。 2. 解…...

技术实践|高斯集群服务器双缺省网关故障分析

导语&#xff1a;当前国产化数据库使用范围越来越广泛&#xff0c;在GaussDB数据库的使用过程中难免会遇到一些问题&#xff0c;有的问题是由于在安装过程中没有注意细节而产生的&#xff0c;多数隐患问题都是在特定场景下才会暴露出来&#xff0c;且暴露的时间未知&#xff0c…...

手把手教你搭建Maven私服

Java全能学习面试指南&#xff1a;https://javaxiaobear.cn 1. Maven私服简介 ①私服简介 Maven 私服是一种特殊的Maven远程仓库&#xff0c;它是架设在局域网内的仓库服务&#xff0c;用来代理位于外部的远程仓库&#xff08;中央仓库、其他远程公共仓库&#xff09;。 当然…...

LeetCode 面试题 16.25. LRU 缓存

文章目录 一、题目二、C# 题解 一、题目 设计和构建一个“最近最少使用”缓存&#xff0c;该缓存会删除最近最少使用的项目。缓存应该从键映射到值(允许你插入和检索特定键对应的值)&#xff0c;并在初始化时指定最大容量。当缓存被填满时&#xff0c;它应该删除最近最少使用的…...

LaTeX 数学公式常见问题及解决方案

本文汇总了博主在使用 LaTeX 写文档过程中遇到的所有数学公式常见问题及对应的 LaTeX 解决方案 持续更新... 目录 1. 连等式2. 公式重新开始编号2.1 图片/表格重新编号 1. 连等式 在数学公式推导过程中常常会遇到如 Figure 1 所示的连等式&#xff0c;一般需要保证等号或者不等…...

2023最新软件测试20个基础面试题及答案

什么是软件测试&#xff1f; 答案&#xff1a;软件测试是指在预定的环境中运行程序&#xff0c;为了发现软件存在的错误、缺陷以及其他不符合要求的行为的过程。 软件测试的目的是什么&#xff1f; 答案&#xff1a;软件测试的主要目的是保证软件的质量&#xff0c;并尽可能大…...

JMeter-BeanShell预处理程序和BeanShell后置处理程序的应用

一、什么是BeanShell&#xff1f; BeanShell是用Java写成的,一个小型的、免费的、可以下载的、嵌入式的Java源代码解释器&#xff0c;JMeter性能测试工具也充分接纳了BeanShell解释器&#xff0c;封装成了可配置的BeanShell前置和后置处理器&#xff0c;分别是 BeanShell Pre…...

Java声明式事务实战!工作中用这几种就够了!

文章目录 1.几种常用的事务传播行为1.1 REQUIRED1.2 REQUIRES_NEW1.2 NESTED 2. 事务问题2.1 事务不生效&#xff1f;2.2 事务不回滚&#xff1f; 文章会分为两个部分来讲解&#xff0c;第一部分是声明式事务的几种使用场景。第二部分包含事务没有生效&#xff0c;没有回滚的情…...

Abp6.0 使用 appsettings.json配置Serilog.Sinks.MariaDB

Abp6.0中已经启用Serilog,使用Serilog.Sinks.MariaDB包可以保存到MariaDB&#xff0c;mysql中 一种做法是在var loggerConfiguration new LoggerConfiguration( )后使用WriteTo.MariaDB扩展方法来配置&#xff0c;这样在代码中配置不够灵活&#xff0c;修改起来也不方便 其实…...

关于Flume-Kafka-Flume的模式进行数据采集操作

测试是否连接成功&#xff1a; 在主节点flume目录下输入命令: bin/flume-ng agent -n a1 -c conf/ -f job/file_to_kafka.conf -Dflume.root.loggerinfo,console # 这个file_to_kafka.conf文件就是我们的配置文件 然后在另一台节点输入命令进行消费数据&#xff1a; kafka-cons…...

WeTab--颜值与实力并存的浏览器插件

一.前言 现在的浏览器花花绿绿&#xff0c;有大量的广告与信息&#xff0c;令人目不暇接。有没有一款好用的浏览器插件可以解决这个问题呢&#xff1f;我愿称WeTab为版本答案。 WeTab的界面&#xff1a; 干净又整洁。最最关键的是还有智能AI供你服务。 这个WeTabAI就像chatgp…...

2023/11/15JAVA学习(线程池,Executors,网络编程,InetAddress,UDP,TCP,DatagramSocket)

如何多开一个程序...

【整理】HTTP相关版本对比

1. HTTP/1 超文本传输协议&#xff0c;处于计算机网络中的应用层&#xff0c;HTTP是建立在TCP协议之上&#xff0c;所以HTTP协议的瓶颈及其优化技巧都是基于TCP协议本身的特性。 缺陷&#xff1a; 连接无法复用 ---------- 每次请求经历三次握手和慢启动HOLB&#xff08;队头…...

spark性能调优 | 默认并行度

Spark Sql默认并行度 看官网&#xff0c;默认并行度200 https://spark.apache.org/docs/2.4.5/sql-performance-tuning.html#other-configuration-options 优化 在数仓中 task最好是cpu的两倍或者3倍(最好是倍数&#xff0c;不要使基数) 拓展 在本地 task需要自己设置&a…...

Python-pptx教程之二操作已有PPT模板文件

文章目录 简单的案例找到要修改的元素修改幻灯片中的文本代码使用示例 修改幻灯片的图片代码使用示例 删除幻灯片代码使用示例 获取PPT中所有的文本内容获取PPT中所有的图片总结 在上一篇中我们已经学会了如何从零开始生成PPT文件&#xff0c;从零开始生成较为复杂的PPT是非常消…...

生活总是自己的,请尽情打扮,尽情可爱,,

同色系拼接羽绒服了解一下 穿上时尚感一下子就突显出来了 90白鸭绒填充&#xff0c;不仅时尚还保暖 设计感满满的羽绒服不考虑一下吗?...

栈和队列的初始化,插入,删除,销毁。

目录 题外话 顺序表和链表优缺点以及特点 一.栈的特点 二. 栈的操作 2.1初始化 2.2 栈的销毁 2.3 栈的插入 2.3 输出top 2.4 栈的删除 2.5 输出栈 题外话 顺序表和链表优缺点以及特点 特点&#xff1a;顺序表&#xff0c;逻辑地址物理地址。可以任意访问&#xff0c…...

重温《Unix设计哲学》

重温Unix设计哲学 这个世界是复杂的&#xff0c;但往往本质的东西都是简单的。这些原则&#xff0c;不光是用在程序开发&#xff0c;也适用于架构设计&#xff0c;产品设计等等地方。 简洁原则&#xff1a;以简洁为美 不要为了满足自己的虚荣心&#xff0c;企图搞一些花哨的东…...

AIGC创作系统ChatGPT源码,AI绘画源码,支持最新GPT-4-Turbo模型,支持DALL-E3文生图

一、AI创作系统 SparkAi创作系统是基于OpenAI很火的ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如…...

Spring条件注解@Conditoinal+ Profile环境切换应用@Profile

Spring条件注解 一、创建一个maven项目 <dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.1.5.RELEASE</version></dependency> </dependenc…...

Scrum框架中的Sprint

上图就是sprint里要做的事。Sprint是scrum框架的核心&#xff0c;是所有的想法、主意转换为价值的地方。所有实现产品目标的必要工作都在sprint里完成&#xff0c;这些工作主要包括Sprint 计划&#xff08;Sprint planning&#xff09;、每日站会&#xff08;Daily Scrum&#…...

openfeign、nacos获取接口提供方真实IP

源码分析 client 是 LoadBalancerFeignClient org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient#execute public Response execute(Request request, Request.Options options) throws IOException {try {URI asUri URI.create(request.url());String c…...

Linux系统编程学习 NO.9——git、gdb

前言 本篇文章简单介绍了Linux操作系统中两个实用的开发工具git版本控制器和gdb调试器。 git 什么是git&#xff1f; git是一款开源的分布式版本控制软件。它不仅具有网络功能&#xff0c;还是服务端与客户端一体的软件。它可以高效的处理程序项目中的版本管理。它是Linux内…...

【联邦学习+区块链】TORR: A Lightweight Blockchain for Decentralized Federated Learning

文章目录 I.CONTRIBUTIONII. ASSUMPTIONS AND THREAT MODELA. AssumptionsB. Threat Model III. SYSTEM DESIGNA. Design OverviewB. Block DesignC. InitializationD. Role SelectionE. Storage ProtocolF. Aggregation ProtocolG. Proof of ReliabilityH. Blockchain Consens…...

《网络协议》08. 概念补充

title: 《网络协议》08. 概念补充 date: 2022-10-06 18:33:04 updated: 2023-11-17 10:35:52 categories: 学习记录&#xff1a;网络协议 excerpt: 代理、VPN、CDN、网络爬虫、无线网络、缓存、Cookie & Session、RESTful。 comments: false tags: top_image: /images/back…...

利用NVIDIA DALI读取视频帧

1. NVIDIA DALI简介 NVIDIA DALI全称是NVIDIA Data Loading Library&#xff0c;是一个用GPU加速的数据加载和预处理库&#xff0c;可用于图像、视频和语音数据的加载和处理&#xff0c;从而为深度学习的训练和推理加速。 NVIDIA DALI库的出发点是&#xff0c;深度学习应用中…...

TSINGSEE青犀AI智能分析+视频监控工业园区周界安全防范方案

一、背景需求分析 在工业产业园、化工园或生产制造园区中&#xff0c;周界防范意义重大&#xff0c;对园区的安全起到重要的作用。常规的安防方式是采用人员巡查&#xff0c;人力投入成本大而且效率低。周界一旦被破坏或入侵&#xff0c;会影响园区人员和资产安全&#xff0c;…...

【算法每日一练]-图论(保姆级教程 篇5(LCA,最短路,分层图)) #LCA #最短路计数 #社交网络 #飞行路线 # 第二短路

今天讲最短路统计和分层图 目录 题目&#xff1a;LCA 思路&#xff1a; 题目&#xff1a;最短路计数 思路&#xff1a; 题目&#xff1a;社交网络 思路&#xff1a; 题目&#xff1a;飞行路线 思路&#xff1a; 题目&#xff1a;第二短路 思路&#xff1a; 题目&a…...

德迅云安全为您介绍关于抗D盾的一些事

抗D盾概述&#xff1a; 抗D盾是新一代的智能分布式云接入系统&#xff0c;接入节点采用多机房集群部署模式&#xff0c;隐藏真实服务器IP&#xff0c;类似于网站CDN的节点接入&#xff0c;但是“抗D盾”是比CDN应用范围更广的接入方式&#xff0c;适合任何TCP 端类应用包括&am…...