admin管理员组

文章数量:1394593

I have two use cases: offloading heavy work from the UI thread to keep the UI smooth.

Perform searching while user is typing.

extension MoveNoteViewController: UISearchBarDelegate {

    // Busy function.
    private func filterNotes(_ text: String) async -> [Note] {
        let filteredNotes: [Note] = await Task.detached { [weak self] in
            guard let self else { return [] }
            
            let idToFolderMap = await idToFolderMap!
            
            if text.isEmpty {
                return await notes
            } else {
                return await notes.filter { [weak self] in
                    guard let self else { return false }
                    
                    let emoji = $0.emoji
                    let title = $0.title
                    var folderName: String? = nil
                    if let folderId = $0.folderId {
                        folderName = idToFolderMap[folderId]?.name ?? ""
                    }
                    
                    return
                        emoji.localizedCaseInsensitiveContains(text) ||
                        title.localizedCaseInsensitiveContains(text) ||
                        (folderName?.localizedCaseInsensitiveContains(text) ?? false)
                }
            }
        }.value
        
        return filteredNotes
    }
    
    @MainActor
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        let text = searchText.trim()
        
        if text.isEmpty {
            applySnapshot(snapshot: getSnapshot(notes: notes))
        } else {
            Task {
                let filteredNotes = await filterNotes(text)
                
                if searchBar.text?.trim() == text {
                    applySnapshot(snapshot: getSnapshot(notes: filteredNotes))
                }
            }
        }
    }
}

Perform list of file iteration I/O

// Busy function.

private static func fetchRecentLocalFailedNoteCountAsync() async -> Int {
    return await Task.detached { () -> Int in
        let fileManager = FileManager.default
        
        guard let enumerator = fileManager.enumerator(at: UploadDataDirectory.audio.url, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) else { return 0 }
        
        var count = 0
        for case let fileURL as URL in enumerator {
            if !RecordingUtils.isValidAudioFileExtension(fileURL.pathExtension) {
                continue
            }
            
            if let fileCreationTimestamp = FileUtils.getFileCreationTimestamp(from: fileURL) {
                if await fileCreationTimestamp > MainViewController.createdTimeStampConstraint {
                    count += 1
                }
            }
        }
        
        return count
    }.value
}

I was wondering, am I using Task.detached in a correct and good practice way?

I have two use cases: offloading heavy work from the UI thread to keep the UI smooth.

Perform searching while user is typing.

extension MoveNoteViewController: UISearchBarDelegate {

    // Busy function.
    private func filterNotes(_ text: String) async -> [Note] {
        let filteredNotes: [Note] = await Task.detached { [weak self] in
            guard let self else { return [] }
            
            let idToFolderMap = await idToFolderMap!
            
            if text.isEmpty {
                return await notes
            } else {
                return await notes.filter { [weak self] in
                    guard let self else { return false }
                    
                    let emoji = $0.emoji
                    let title = $0.title
                    var folderName: String? = nil
                    if let folderId = $0.folderId {
                        folderName = idToFolderMap[folderId]?.name ?? ""
                    }
                    
                    return
                        emoji.localizedCaseInsensitiveContains(text) ||
                        title.localizedCaseInsensitiveContains(text) ||
                        (folderName?.localizedCaseInsensitiveContains(text) ?? false)
                }
            }
        }.value
        
        return filteredNotes
    }
    
    @MainActor
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        let text = searchText.trim()
        
        if text.isEmpty {
            applySnapshot(snapshot: getSnapshot(notes: notes))
        } else {
            Task {
                let filteredNotes = await filterNotes(text)
                
                if searchBar.text?.trim() == text {
                    applySnapshot(snapshot: getSnapshot(notes: filteredNotes))
                }
            }
        }
    }
}

Perform list of file iteration I/O

// Busy function.

private static func fetchRecentLocalFailedNoteCountAsync() async -> Int {
    return await Task.detached { () -> Int in
        let fileManager = FileManager.default
        
        guard let enumerator = fileManager.enumerator(at: UploadDataDirectory.audio.url, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) else { return 0 }
        
        var count = 0
        for case let fileURL as URL in enumerator {
            if !RecordingUtils.isValidAudioFileExtension(fileURL.pathExtension) {
                continue
            }
            
            if let fileCreationTimestamp = FileUtils.getFileCreationTimestamp(from: fileURL) {
                if await fileCreationTimestamp > MainViewController.createdTimeStampConstraint {
                    count += 1
                }
            }
        }
        
        return count
    }.value
}

I was wondering, am I using Task.detached in a correct and good practice way?

Share Improve this question asked Mar 27 at 10:30 Cheok Yan ChengCheok Yan Cheng 43k139 gold badges496 silver badges948 bronze badges 1
  • You probably won't use a detached task. In that case, its closure runs on an unspecified isolation domain (aka actor). You rather want the closure execute on the same isolation domain as the caller, which is associated to the MainActor, since the extension methods of MoveNoteViewController should run on the MainActor, correct? At least, your code using self within the closure indicates this, even though it's not clear whether you use self actually. But if, do not use a detached Task, and also ensure the extension/delegate methods run on the MainActor. – CouchDeveloper Commented Mar 27 at 11:03
Add a comment  | 

2 Answers 2

Reset to default 2

To offload some CPU intensive work from the main thread, there are many ways to accomplish this.

One approach, is defining a global actor specifically for running CPU intensive work. Then, associate CPU intensive functions with it:

Your custom minimalistic Global Actor:

@globalActor actor MyGlobalActor: GlobalActor {
    static let shared = MyGlobalActor()
}

CPU intensive synchronous function:

@MyGlobalActor
func compute(input: sending Input) -> sending Output {
   ...
}

Note: you may require Input and Output to conform to Sendable.

Then, use it like

@MainActor
func foo() async throws {
    let result = await compute(input: 100.0)
    print(result)
}

Why does this work:

"By default, actors execute tasks on a shared global concurrency thread pool."

The MyGlobalActor global actor above, does not specify a custom executor.

See: https://developer.apple/documentation/swift/actor

Update

Another approach is, to associate the CPU intensive function with an actor instance, which then becomes actor isolated:

func compute(
    isolated: isolated any Actor = #isolation,
    input: sending Input
) -> sending Output {
   ...
}

You then call it like:

Task { @MyGlobalActor in 
    let result = compute(input: 100.0)
    print(result)
}

The actor instance will be implicitly passed as an argument. The compute() function executes on MyGlobalActor.

Note, that you don't need to use a Task and a closure which specifies the actor. It's sufficient, that the compiler can infer an actor. Note also, that the compiler requires to infer the actor, otherwise it ungracefully fails to compile (I hope this will be improved in the future).

One can make the parameter isolated also optional. In this case, the compiler wouldn't crash when there is no actor. However, I would rather prefer the compiler to crash, to indicate there's an actor missing, than running the code on an unspecified isolation domain, which would missing the original goal.

Detached Tasks should be your last resort, according to documentation

Don’t use a detached task if it’s possible to model the operation using structured concurrency features like child tasks. Child tasks inherit the parent task’s priority and task-local storage, and canceling a parent task automatically cancels all of its child tasks. You need to handle these considerations manually with a detached task.

In your case this could lead to worse performance, as every detached Task creates a new root Task (afaik leads to a new Thread being created).

Try using Task(priority: .low) { ... } or just Task { ... }. This will inherit parent Actor, in your case the MainActor. So essentially your code runs on the main thread, but is deferred into the pauses of high priority Tasks like handling your touch events. This is faster than moving data between threads.

Hint: If you have worker classes, that do heavy lifting, you can make them actors.

本文标签: