Study

🍎 WWDC21 : Explore structured concurrency in Swift

_yunie 2024. 9. 7. 19:59

🍎 WWDC21 : Explore structured concurrency in Swift

Structured Programming

  • ꡬ쑰화 된 ν”„λ‘œκ·Έλž˜λ°
    • 정적 λ²”μœ„λ₯Ό μ‚¬μš©
      • ν•΄λ‹Ή λ²”μœ„ λ‚΄μ—μ„œλ§Œ λ³€μˆ˜ μ‚¬μš© κ°€λŠ₯
      • μ‘°κ±΄λΆ€λ‘œλ§Œ μ½”λ“œκ°€ 싀행됨
      • λ²”μœ„ λ²—μ–΄λ‚˜λ©΄ μ’…λ£Œ
    • μ œμ–΄νλ¦„μ„ 보닀 μ§κ΄€μ μœΌλ‘œ μ•Œ 수 있음
    • ν”„λ‘œκ·Έλž¨μ„ μœ„μ—μ„œ μ•„λž˜λ‘œ 읽을 수 있음

 

Completion Example

func fetchThumbnails(
    for ids: [String],
    completion handler: @escaping ([String: UIImage]?, Error?) -> Void
) {
    guard let id = ids.first else { return handler([:], nil) }
    let request = thumbnailURLRequest(for: id)
    let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
        // μ—λŸ¬ 핸듀링 λΆˆκ°€ 
        // - ν•˜λ‚˜μ˜ ν•¨μˆ˜κ°€ μ•„λ‹Œ ν•¨μˆ˜μ—μ„œ λ°œμƒν•œ 였λ₯˜λ§Œ μ²˜λ¦¬ν•˜λŠ” 것이 μ˜λ―ΈμžˆκΈ°λ•Œλ¬Έ 
        guard let response = response,
              let data = data
        else {
            return handler(nil, error)
        }
        // ... check response ...
        UIImage(data: data)?.prepareThumbnail(of: thumbSize) { image in
            guard let image = image else {
                return handler(nil, ThumbnailFailedError())
            }
            // ν•΄λ‹Ή νŒ¨ν„΄ μ‚¬μš© μ‹œ, 루프λ₯Ό μ‚¬μš©ν•˜μ—¬ 각 썸넀일을 μ²˜λ¦¬ν•  수 μ—†μŒ 
            // - ν•¨μˆ˜κ°€ μ™„λ£Œλœ ν›„ μ‹€ν–‰λ˜λŠ” μ½”λ“œκ°€ ν•Έλ“€λŸ¬ 내에 μ€‘μ²©λΌμ•Όν•˜λ―€λ‘œ μž¬κ·€κ°€ ν•„μš”ν•¨
            fetchThumbnails(for: Array(ids.dropFirst())) { thumbnails, error in
                // ... add image to thumbnails ...
            }
        }
    }
    dataTask.resume()
}

 

Async/await Example

// μ’‹μ•„λ³΄μ΄μ§€λ§Œ 수천 개의 이미지에 λŒ€ν•΄ 썸넀일을 μƒμ„±ν•œλ‹€λ©΄?
// - 각 썸넀일을 ν•œ λ²ˆμ— ν•˜λ‚˜μ”© μ²˜λ¦¬ν•˜λŠ” 것이 쒋아보이지 μ•ŠμŒ 
// - λ˜ν•œ κ³ μ •λœ 크기둜 λ°›μ•„μ˜€μ§€ μ•ŠλŠ”λ‹€λ©΄..?
func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
    var thumbnails: [String: UIImage] = [:]
    for id in ids {
        let request = thumbnailURLRequest(for: id)
        let (data, response) = try await URLSession.shared.data(for: request)
        try validateResponse(response)
        guard let image = await UIImage(data: data)?.byPreparingThumbnail(ofSize: thumbSize) else {
            throw ThumbnailFailedError()
        }
        thumbnails[id] = image
    }
    return thumbnails
}
  • Task
    • 비동기λ₯Ό μ‚¬μš©ν•  수 μžˆκ²Œν•˜λŠ” Swift의 μƒˆλ‘œμš΄ κΈ°λŠ₯
  • μ•ˆμ „ν•˜κ³  효율적일 λ•Œ λ³‘λ ¬λ‘œ μ‹€ν–‰λ˜λ„λ‘ 함
  • 비동기 ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•΄λ„ ν˜ΈμΆœμ— λŒ€ν•œ μƒˆλ‘œμš΄ μž‘μ—…μ΄ μƒμ„±λ˜μ§€ μ•ŠμŒ

 

Async-let

  • 일반적인 let 바인딩
  • ν•˜λ‚˜μ˜ 싀행흐름
  • let result = URLSession.shared.data(…)
    • 데이터λ₯Ό λ‹€μš΄λ°›μ•„μ˜€λŠ” λ™μ•ˆ λ‹€λ₯Έ μž‘μ—…μ„ ν•  수 있기λ₯Ό λ°”λžŒ
      • let μ•žμ— asyncλ₯Ό λͺ…μ‹œ

  • async-let 바인딩
  1. async let을 λ§Œλ‚˜κΈ° μ „ μƒˆλ‘œμš΄ ν•˜μœ„ μž‘μ—…μ„ 생성
    1. λͺ¨λ“  μž‘μ—…μ€ ν”„λ‘œκ·Έλž¨μ˜ 싀행을 λ‚˜νƒ€λ‚΄κΈ° λ•Œλ¬Έμ— ν•΄λ‹Ή 단계에선 흐름에 λŒ€ν•œ λ°©ν–₯이 두 κ°€μ§€λ‘œ λ‚˜λ‰¨
      1. URLSession.shared.data(…) : 데이터 λ‹€μš΄λ‘œλ“œλ₯Ό μœ„ν•œ ν•˜μœ„ μž‘μ—…
      2. result : λ³€μˆ˜μ˜ 결과값을 λ°”μΈλ”©ν•˜λŠ” μƒμœ„ μž‘μ—…
  2. μƒμœ„ μž‘μ—…μ—μ„œλŠ” ν•˜μœ„ μž‘μ—…μ΄ 데이터λ₯Ό λ°›μ•„μ˜€λŠ”λ™μ•ˆ 이후 μž‘μ—…μ„ μˆ˜ν–‰ν•¨
  3. 결과값이 ν•„μš”ν•œ μ½”λ“œμ— λ„λ‹¬ν•˜κ²Œ 되면 μƒμœ„ μž‘μ—…(result)λŠ” ν•˜μœ„ μž‘μ—…(데이터 λ‹€μš΄λ‘œλ“œ)κ°€ μ™„λ£Œλ  λ•ŒκΉŒμ§€ κΈ°λ‹€λ¦Ό
    1. URLSessionμ—μ„œ μ—λŸ¬κ°€ λ°œμƒν•  κ°€λŠ₯성이 있음 ⇒ try
    2. 결과값을 λ‹€μ‹œ μ½λŠ”λ‹€ν•˜μ—¬ 비동기 μž‘μ—…μ΄ 또 μΌμ–΄λ‚˜μ§€ μ•ŠμŒ

 

Task Tree Example

  • μž‘μ—… νŠΈλ¦¬λΌλŠ” 계측 ꡬ쑰의 일뢀
    • μž‘μ—…μ˜ μ·¨μ†Œ(cancellation), μž‘μ—…μ˜ μš°μ„ μˆœμœ„, μž‘μ—… λ‚΄ μ§€μ—­λ³€μˆ˜ 같은 것듀에 영ν–₯을 λ―ΈμΉ¨
    • ν•˜λ‚˜μ˜ 비동기 ν•¨μˆ˜μ—μ„œ λ‹€λ₯Έ ν•¨μˆ˜λ‘œ ν˜ΈμΆœν•  λ•Œλ§ˆλ‹€ λ™μΌν•œ μž‘μ—…μ΄ ν˜ΈμΆœμ„ μ‹€ν–‰ν•˜λŠ”λ° μ‚¬μš©λ¨
  • μƒˆλ‘œμš΄ ꡬ쑰화 된 μž‘μ—… 생성 μ‹œ, ν˜„μž¬ ν•¨μˆ˜κ°€ μ‹€ν–‰ 쀑인 μž‘μ—…μ˜ ν•˜μœ„ ν•­λͺ©μ΄ 됨
    • μž‘μ—…μ€ νŠΉμ • κΈ°λŠ₯의 ν•˜μœ„ ν•­λͺ©μ΄ μ•„λ‹ˆμ§€λ§Œ ν•΄λ‹Ή κΈ°λŠ₯의 수λͺ… λ²”μœ„λŠ” ν•΄λ‹Ή κΈ°λŠ₯으둜 μ œν•œλ  수 있음
  • νŠΈλ¦¬λŠ” 각 μƒμœ„ μž‘μ—…κ³Ό ν•΄λ‹Ή ν•˜μœ„ μž‘μ—… κ°„μ˜ 링크둜 ꡬ성됨
    • λ§ν¬λŠ” λͺ¨λ“  ν•˜μœ„μž‘μ—…μ΄ μ™„λ£Œλœ μ΄ν›„μ—λ§Œ μƒμœ„ μž‘μ—…μ΄ μ™„λ£Œλ  수 μžˆλ‹€λŠ” κ·œμΉ™μ„ μ μš©ν•¨
      • 이 κ·œμΉ™μ€ ν•˜μœ„μž‘μ—…μ΄ λŒ€κΈ°λ˜λŠ” 것을 λ°©μ§€ν•˜λŠ” 비정상적 μ œμ–΄ 흐름에도 λΆˆκ΅¬ν•˜κ³  적용됨
  • κ²°κ΅­ ν•˜μœ„μž‘μ—…λ“€μ΄ λͺ¨λ‘ μ™„λ£Œλ˜μ–΄μ•Ό μ΅œμƒμœ„ μž‘μ—…μ΄ μ΅œμ’…μ μœΌλ‘œ μ’…λ£Œλ¨
  • μž‘μ—… 수λͺ…을 κ΄€λ¦¬ν•˜λŠ”λ° 도움을 쀘 μ‹€μˆ˜λ‘œ μž‘μ—…μ΄ λ©”λͺ¨λ¦¬ λˆ„μˆ˜λ₯Ό λ°©μ§€ν•΄μ€Œ
func fetchOneThumbnail(withID id: String) async throws -> UIImage {
    let imageReq = imageRequest(for: id), metadataReq = metadataRequest(for: id)
    // async : 두 μž‘μ—…μ΄ λ™μ‹œμ— μΌμ–΄λ‚˜κΈ° μœ„ν•΄ 
    // -> λ‹€μš΄λ‘œλ“œ μž‘μ—…μ΄ ν•˜μœ„μ—μ„œ μΌμ–΄λ‚˜λ―€λ‘œ try await을 μ“°μ§€μ•ŠμŒ 
    // -> μ΄λŸ¬ν•œ κ²°κ³ΌλŠ” μƒμœ„ μž‘μ—…(data, metadata)μ—μ„œλ§Œ 관찰됨
    // 이미지 λ°›μ•„μ˜€κΈ°
    //let (data, _) = try await URLSession.shared.data(for: imageReq)
    async let (data, _) = URLSession.shared.data(for: imageReq)
    // 메타데이터 λ°›μ•„μ˜€κΈ° 
    // let (metadata, _) = try await URLSession.shared.data(for: metadataReq) 
    async let (metadata, _) = URLSession.shared.data(for: metadataReq)
    
    // μ—λŸ¬μ— λŒ€ν•œ 결과값도 metadata, dataμ—μ„œ κ΄€μ°°λ˜κΈ° λ•Œλ¬Έμ— ν•΄λ‹Ή 데이터λ₯Ό μ‚¬μš©ν•  λ•Œ try await을 μ‚¬μš©
    // - 이미지 μž‘μ—… 이전에 메타데이터 μž‘μ—…μ„ μš°μ„  처리 -> 였λ₯˜ λ°œμƒ μ‹œ ν•¨μˆ˜ λ°”λ‘œ μ’…λ£Œμ‹œμΌœμ•Όν•¨ 
    // -> 그럼 λ‘λ²ˆμ§Έ μž‘μ—…μ€? -> λ‘λ²ˆμ§Έ μž‘μ—…μ΄ μ·¨μ†Œλ˜λŠ” 것이 μ•„λ‹Œ ν•΄λ‹Ή κ²°κ³Όκ°€ λ”λŠ” ν•„μš”μ—†μŒμ„ μ•Œλ¦Ό 
    // -> ν•˜μœ„μž‘μ—…λ“€μ΄ μžλ™μœΌλ‘œ μ·¨μ†Œλ¨
    guard let size = parseSize(from: try await metadata),
          let image = try await UIImage(data: data)?.byPreparingThumbnail(ofSize: size)
    else {
        throw ThumbnailFailedError()
    }
    return image
}

 

Cancellation is Cooperative

  • μ½”λ“œμ—μ„œ λͺ…μ‹œμ μœΌλ‘œ μ·¨μ†Œλ₯Ό ν™•μΈν•˜κ³  μ μ ˆν•œ λ°©μ‹μœΌλ‘œ μ·¨μ†Œν•΄μ•Όν•¨
    • 비동기 여뢀와 κ΄€κ²Œμ—†μ΄ λͺ¨λ“  κΈ°λŠ₯μ—μ„œ ν˜„μž‘μ—…μ˜ μ·¨μ†Œ μƒνƒœλ₯Ό 확인할 수 있음
  • 특히 μž₯기적인 μž‘μ—…μ΄ ν¬ν•¨λœ 경우, μ·¨μ†Œλ₯Ό 염두해두어야함
    • 뢀뢄적인 κ²°κ³Όκ°€ λ°˜ν™˜λ  수 μžˆμŒμ„ λͺ…μ‹œν•΄μ•Όν•¨
func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
    var thumbnails: [String: UIImage] = [:]
    for id in ids {
		    // μ·¨μ†Œλœ 경우 μ—λŸ¬ λ˜μ§€κΈ° 
        try Task.checkCancellation()
        
        // μ·¨μ†Œ μƒνƒœλ₯Ό ν™•μΈν•΄μ„œ μ²˜λ¦¬ν•΄μ€„ μˆ˜λ„ 있음
        // if Task.isCancelled { break } 
       
        thumbnails[id] = try await fetchOneThumbnail(withID: id)
    }
    return thumbnails
}

 

Group Tasks

func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
    var thumbnails: [String: UIImage] = [:]
    for id in ids {
		    // λ§Œμ•½ λͺ¨λ“  썸넀일을 κ°€μ Έμ˜€λŠ” μž‘μ—…μ„ λ™μ‹œμ— ν•˜κ³ μ‹Άλ‹€λ©΄ ? => Group
        thumbnails[id] = try await fetchOneThumbnail(withID: id)
    }
    return thumbnails
}

func fetchOneThumbnail(withID id: String) async throws -> UIImage {
    // ...
		// 두 개의 ν•˜μœ„μž‘μ—… 
		// async let : μž‘μ—… λ²”μœ„κ°€ 지정됨 = λ‹€μŒ 루프 반볡이 μ‹œμž‘λ˜κΈ° μ „ 두 ν•˜μœ„ μž‘μ—…μ΄ μ™„λ£ŒλΌμ•Όν•¨ 
    async let (data, _) = URLSession.shared.data(for: imageReq)
    async let (metadata, _) = URLSession.shared.data(for: metadataReq)

    // ...
}
  • withThrowingTaskGroup
    • μ—λŸ¬λ₯Ό λ˜μ§€λŠ” μž‘μ—…λ“€μ˜ 묢음
    • 그룹에 μΆ”κ°€λœ μž‘μ—…μ€ 그룹이 μ •μ˜λœ λ²”μœ„λ³΄λ‹€ 였래 지속될 수 μ—†μŒ
func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
    var thumbnails: [String: UIImage] = [:]
    try await withThrowingTaskGroup(of: Void.self) { group in
        for id in ids {
		        // 그룹에 ν•˜μœ„ μž‘μ—…μ„ 생성 
		        // ν•˜μœ„μž‘μ—…μ΄ μ¦‰μ‹œ μž„μ˜μ˜ μˆœμ„œλ‘œ μ‹€ν–‰λ˜κΈ° μ‹œμž‘ 
		        // κ·Έλ£Ή κ°œμ²΄κ°€ λ²”μœ„λ₯Ό λ²—μ–΄λ‚˜λ©΄ κ·Έ μ•ˆμ— μžˆλŠ” λͺ¨λ“  μž‘μ—…μ˜ μ™„λ£Œκ°€ λŒ€κΈ° μƒνƒœλ‘œ 됨 
            group.async {
		            // μ—¬κΈ°μ„œλŠ” 또 metadata, data ν•˜μœ„ μž‘μ—… 생성 
		            // Data Race Error 
                // Error: Mutation of captured var 'thumbnails' in concurrently executing code
                thumbnails[id] = try await fetchOneThumbnail(withID: id)
            }
        }
    }
    return thumbnails
}
  • Data Raceλ₯Ό λ°©μ§€ν•˜κΈ° μœ„ν•΄ ν•˜μœ„ μž‘μ—…μ΄ 값을 λ°˜ν™˜ν•˜λ„λ‘ λ³€κ²½
func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
    var thumbnails: [String: UIImage] = [:]
    
    // withTrowingTaskGroup의 body
    try await withThrowingTaskGroup(of: (String, UIImage).self) { group in
        for id in ids {
            group.async {
		            // μƒμœ„ μž‘μ—…μ— κ²°κ³Ό μ²˜λ¦¬μ— λŒ€ν•œ 전적인 μ±…μž„ λΆ€μ—¬λ₯Ό μœ„ν•΄ ν•˜μœ„ μž‘μ—… κ²°κ³Ό λ°˜ν™˜
		            // Key - Value  
                return (id, try await fetchOneThumbnail(withID: id))
            }
        }
        // νƒ€μž…μ΄ AsyncSequence ν”„λ‘œν† μ½œμ„ μ±„νƒν•˜λŠ” 경우, for-await을 톡해 반볡 κ°€λŠ₯
        // ν•˜μœ„ μž‘μ—…μ˜ κ²°κ³Όλ₯Ό λ°˜λ³΅ν•˜λ©΄μ„œ κ²°κ³Ό 처리 
        // Obtain results from the child tasks, sequentially, in order of completion.
        for try await (id, thumbnail) in group {
            thumbnails[id] = thumbnail
        }
    }
    
    return thumbnails
}
  • Group TaskλŠ” ꡬ쑰화 된 λ™μ‹œμ„±μ˜ ν•œ ν˜•νƒœ
    • async-letκ³Ό 같은 μž‘μ—… νŠΈλ¦¬μ—μ„œλŠ” μ•½κ°„μ˜ 차이가 μžˆμ„ 수 있음
      • 예둜, κ·Έλ£Ή λ‚΄ ν•˜μœ„ μž‘μ—…μ—μ„œ μ—λŸ¬κ°€ λ°˜ν™˜λ˜μ—ˆμ„ λ•Œ κ·Έλ£Ή μ½”λ“œμ˜ μ’…λ£Œλ₯Ό 톡해 λ²”μœ„λ₯Ό λ²—μ–΄λ‚  λ•Œ λ°œμƒν•¨
        • μ·¨μ†Œκ°€ μ•”μ‹œμ μœΌλ‘œ λ°œμƒν•˜μ§€μ•Šκ³  였직 λŒ€κΈ°λ§Œ 일어남
  • 그룹의 cancelAll 을 톡해 κ·Έλ£Ή λ‚΄ λͺ¨λ“  μž‘μ—…μ„ μˆ˜λ™μœΌλ‘œ μ·¨μ†Œ κ°€λŠ₯
    • μ·¨μ†Œ 방법에 상관없이 μ·¨μ†Œ μ•Œλ¦Όμ€ ν•˜μœ„ μž‘μ—…μœΌλ‘œ μ•Œλ €μ§

 

Unstructured Tasks

  • λͺ…ν™•ν•œ 트리 ꡬ쑰가 μ•„λ‹Œ ꡬ쑰가 μžˆμ„ 수 있음
    • 동기 μ½”λ“œ λ‚΄ 비동기 μˆ˜ν–‰ μ‹œ, μƒμœ„ μž‘μ—…μ΄ μ—†λŠ” 경우
    • μ›ν•˜λŠ” μž‘μ—…μ˜ 수λͺ…이 단일 λ²”μœ„κ±°λ‚˜ 단일 ν•¨μˆ˜μ˜ λ²”μœ„μ— λ§žμ§€μ•Šμ„ λ•Œ
  • μž‘μ—…μ„ μ‹œμž‘ν•˜λŠ” 객체가 λ‹€λ₯Έ λ©”μ„œλ“œμ— λŒ€ν•œ 결과둜 μž‘μ—… μ·¨μ†Œ κ°€λŠ₯
  • UICollectionViewDelegate λ‚΄ λ©”μ„œλ“œμ—μ„œ 비동기 μž‘μ—…μ„ μˆ˜ν–‰ν•˜λ €κ³  ν•œλ‹€λ©΄
    • Main Threadμ—μ„œ λ™μž‘ν•˜λŠ” μž‘μ—… λ‚΄ 비동기 μž‘μ—…μ΄ λ“€μ–΄μžˆμ–΄ λΆˆκ°€λŠ₯함
      • Taskλ₯Ό 톡해 κ°μ‹Έμ€„μˆ˜ 있음
        • κ·Έλ£Ή μž‘μ—…μ΄λ‚˜ async-let처럼 μ›λ³Έμž‘μ—…μ˜ μš°μ„ μˆœμœ„ 및 기타 νŠΉμ„±μ„ μƒμ†λ°›μŒ
        • μž‘μ—…μ˜ 수λͺ…은 μ§€μ •λ˜μ§€μ•ŠμŒ
        • μ·¨μ†Œλ‚˜ λŒ€κΈ° μƒνƒœλ₯Ό 직접 관리해주어야함
  • μ·¨μ†Œλ₯Ό μœ„ν•œ μ½”λ“œ μˆ˜μ •

  • 화면이 사라진닀면 μž‘μ—… μ·¨μ†Œ

 

Detached Tasks

  • 수λͺ…이 μƒμœ„ μž‘μ—…μ˜ 수λͺ…μœΌλ‘œ μ œν•œλ˜μ§€ μ•ŠμŒ
  • μ›λž˜ μž‘μ—…μœΌλ‘œλΆ€ν„° μ–΄λ– ν•œ 것도 상속받지 μ•ŠμŒ
    • μ–΄λ–€ μŠ€λ ˆλ“œμ—μ„œ μΌμ–΄λ‚˜λ“ , μ–΄λ–€ μš°μ„ μˆœμœ„λ‘œ μž‘μ—…ν•˜λ“  상관무
  • λ§€κ°œλ³€μˆ˜λ₯Ό 톡해 μš°μ„ μˆœμœ„ 등을 지정해쀄 수 있음
  • detached(priority:operation:)
    • Runs the given throwing operation asynchronously as part of a new top-level task.
    • = μƒˆλ‘œμš΄ μ΅œμƒμœ„ μž‘μ—…μ˜ μΌν™˜μœΌλ‘œ 비동기 μž‘μ—… μˆ˜ν–‰
      • κ·Έλž˜μ„œ λ‹€λ₯Έ μž‘μ—…μ—μ„œ μ–΄λ– ν•œ 것도 상속받지 μ•Šκ³  수λͺ…을 이어받지 μ•ŠλŠ”κ²Œ μ•„λ‹κΉŒ μ‹ΆμŒ

이미지 캐싱

  • μž‘μ—…μ΄ μ—¬λŸ¬ 개인 경우
    • Task.detachedν•˜λ‚˜λ§Œ μ·¨μ†Œν•΄μ£Όλ©΄ ν•˜μœ„κ°€ λͺ¨λ‘ μ·¨μ†Œλ˜λ―€λ‘œ νŽΈλ¦¬ν•˜κ²Œ λ‹€μ–‘ν•œ λ°±κ·ΈλΌμš΄λ“œ μž‘μ—…μ„ ν•  수 있음

 

Summary

'Study' μΉ΄ν…Œκ³ λ¦¬μ˜ λ‹€λ₯Έ κΈ€

πŸ“š Closure  (0) 2025.06.02
πŸ“š Concurrency Docs λ‚΄ 일뢀  (0) 2024.09.11
πŸ‘€ Continuation μ•Œμ•„λ³΄κΈ°  (0) 2024.08.24
🍎 WWDC21: Meet async/await in Swift  (0) 2024.08.20
πŸ“š ARC Docs 정리  (0) 2024.08.06