Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSAttributedString from HTML on main thread behaves as if multithreading

I'm converting some HTML to an NSAttributedString on the main thread (the way Apple tells you to). It takes some time and then it continues executing the rest of the block.

Now, if another block is queued to run in the main thread too (e.g. after getting a response from an HTTP request), I would expect it to run after everything else is finished, but that's not what happens: they run in parallel as if they were on different threads. I did put asserts everywhere making sure it's on the main thread.

I made an experiment "Single View App" project to test this, with a file containig a very long html string like <p>lorem</p> ipsum <b>dolor</b> <i><u>sit</u> amet</i> and a view controller with the following code:

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        dispatchStuff()
        for _ in 0..<10 {
            // slowOperation()
            parseHTML()
        }
    }

    func dispatchStuff() {
        for i in 0..<10 {
            let wait = Double(i) * 0.2
            DispatchQueue.main.asyncAfter(deadline: .now() + wait) {
                assert(Thread.isMainThread, "not main thread!")
                print("🔶 dispatched after \(wait) seconds")
            }
        }
    }

    // just loads a big lorem ipsum full of html tags
    let html: String = {
        let filepath = Bundle.main.path(forResource: "test", ofType: "txt")!
        return try! String(contentsOfFile: filepath)
    }()

    var n = 0
    func slowOperation() {
        n += 1
        assert(Thread.isMainThread, "not main thread!")
        print("slowOperation \(n) START")
        var x = [0]
        for i in 0..<10000 {
            x.removeAll()
            for j in 0..<i {
                x.append(j)
            }
        }
        print("slowOperation \(n) END")
        print("")
    }

    var m = 0
    func parseHTML() {
        m += 1
        assert(Thread.isMainThread, "not main thread!")
        print("parseHTML \(m) START")
        let options = [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html]
        let attrString = try! NSAttributedString(data: Data(html.utf8), options: options, documentAttributes: nil)
        print("parseHTML \(m) END")
        print("")
    }
}

if you run it, this is what the console looks like:

parseHTML() uncommented

...all mixed together, that's the surprising (to me) behavior.

But if in viewDidLoad() you comment the call to parseHTML() and uncomment slowOperation(), you'll get something like this instead:

slowOperation() uncommented

...which is what I'd expect. So, what's happening here? Is my understanding of how threads work horribly wrong?

like image 931
maltalef Avatar asked May 15 '19 17:05

maltalef


1 Answers

My original suspicion was correct. The implementation of NSAttributedString init(data:options:documentAttributes:) makes calls to CFRunLoopRun(). Doing so allows other queued up blocks/closures on the queue (main queue in this case) to run.

This is why you are seeing what appears to be asynchronous output on the main queue.

I put your code into a simple command line app and set a breakpoint on the print in dispatchStuff. The stack trace shows that during the call to the NSAttributedString init there is an internal call to _CGRunLoopRun which results in a call to one of the queued closures from dispatchStuff.

enter image description here

like image 190
rmaddy Avatar answered Nov 10 '22 23:11

rmaddy