Is it ok to create JSONDecoder only once in withThrowingTaskGroup?

Hi,

Overview:

I have some asynchronous code (shown below) that uses withThrowingTaskGroup.

Note:

  • CarA is a struct
  • CarB is a class

Question:

  1. Is it ok to create JSONDecoder only once before withThrowingTaskGroup and reuse it inside withThrowingTaskGroup?
  2. Or should I be creating JSONDecoder every time?

Code

CarA

struct CarA: Identifiable, Codable {

    let id: Int
    
    static func convert(from carsB: [CarB]) async throws -> [CarA] {
        var result = [Int: CarA]()
        let decoder = JSONDecoder() //Is it safe to create JSONDecoder just once?
        
        try await withThrowingTaskGroup(of: (Int, CarA).self) { group in
            for carB in carsB {
                group.addTask(priority: .userInitiated) {
                    let data = try await carB.convertToData()
                    let carA = try decoder.decode(CarA.self, from: data)
                    return (carA.id, carA)
                }
            }
            
            for try await (id, carA) in group {
                result[id] = carA
            }
        }
        
        return result.map { $0.value }
    }
}

CarB

class CarB {
    func convertToData() async throws -> Data {
        return Data() //dummy code
    }
}

Thanks, any help would be much appreciated.

Thanks and Regards,
Somu

1 Like

No, you should create one for each task. There is mutable state associated with decoding, and there's no reason to think it's safe across Task or thread boundaries.

3 Likes

@QuinceyMorris Thank you so much!!

Just curious is it because JSONDecoder is a class and it would use the same instance across tasks?

Hi! You can find the definition of JSONDecoder in this source file from the swift open source project. As you can see, it is a class with some mutable stored properties, so it isn't safe to share an instance across Task boundaries.

However all of those stored properties are meant for configuration, so in theory as long as you don't change the configuration of the decoder until all of your tasks complete, this might totally work.

Still, I don't recommend it. It isn't explicitly Sendable, and JSONDecoder looks very cheap to initialize so you probably won't gain much from sharing an instance across tasks. Good question!

2 Likes

@crichez Thanks a lot for that clear explanation.

1 Like

In this case yes, but that is not always true!

You can sometimes share a class instance across Tasks, but only if the properties you access have some "synchronization" mechanism in their getter or setter. I don't see any in the code for JSONDecoder.

There is a new protocol in Swift called Sendable that means a value/instance is safe to share between actors (the things executing your Tasks), regardless of whether it is a class, enum or struct. This is a video from Apple that attempts to explain this.

1 Like

@crichez Thanks a lot for the clear explanation ... was very helpful.