Skip to content
  • Home
  • Code
  • iOS & Swift
  • Combine
  • RxSwift
  • SwiftUI
  • Flutter & Dart
  • Tutorials
  • Art
  • Blog
Fx Studio
  • Home
  • Code
  • iOS & Swift
  • Combine
  • RxSwift
  • SwiftUI
  • Flutter & Dart
  • Tutorials
  • Art
  • Blog
MainActor
Written by chuotfx on July 6, 2021

MainActor và điều gì xảy ra với Data trên Main Thread

iOS & Swift

Contents

  • Chuẩn bị
  • Data trên Main Thread
    • Completion Handler
    • Publisher
  • Async/Await
    • Giải pháp mới
    • Vấn đề gặp phải
  • MainActor
    • On Type
    • Closure
    • Combine
  • Tạm kết

Chào mừng bạn đến với Fx Studio. Chúng ta vẫn tiếp tục với hành trình Concurrency trong Swift. Và ta sẽ khám phát thêm việc cập nhật dữ liệu trên Main Thread. Ngoài ra, ta học thêm các cách sử dụng khác của MainActor.

Đầu tiên, bạn cần biết được 2 khái niệm mới trong Swift 5.5. Đó là:

    • Cơ bản về Actor trong 10 phút – Swift 5.5
    • MainActor và điều gì xảy ra với UI trên Main Thread

Còn nếu mọi thứ đã ổn rồi, thì …

Bắt đầu thôi!

Chuẩn bị

Sự kiện lớn nhất của Apple vào đầu tháng 6/2021 vừa qua là WWDC 21. Đã có rất nhiều thứ mới mẻ được giới thiệu trong Swift 5.5. Và bây giờ, chúng ta sẽ tiếp tục tìm hiểu khái niệm thêm về MainActor.

Do đó, bạn cần phải chuẩn bị như sau:

    • Swift 5.5
    • Xcode 13 (beta)

Thật là khó khi Xcode 13 khá là nặng, nên có thể Macbook của bạn sẽ chạy không nỗi. Nên bạn cũng phải cân nhắc khi cập nhật macOS và Xcode 13 nhoé.

Về mặt lý thuyết, những gì mình trình bày ở dưới đây sẽ liên quan tới 2 khái niệm trình bày 2 vấn đề gặp phải khi lập trình bất đồng bộ.

    • Race Condition
    • Thread safety & data race

Về mặt demo, ta sẽ demo với một Project iOS nào đó cũng được. Giao diện cực kì đơn giản thôi. Bạn không cần lo lắng về nó quá. (Bạn có thể checkout code tại đây.)

Data trên Main Thread

Ở bài viết trước, bạn cũng đã biết là giao diện cũng không thoát khỏi Data Race trên chính Main Thread. Còn về mặt dữ liệu thì còn dễ bị rơi vào Data Race hơn nữa. Nhất là các dữ liệu mà bạn khai báo ra với mục đích phục vụ cho chính giao diện của bạn.

Mình sẽ lấy cho bạn một ví dụ rất là cơ bản cho mối quan hệ giữa giao diện và dữ liệu. Bạn muốn hiển thị 1 ảnh lên ứng dụng của bạn. Thì bạn sẽ dùng tới UIImageView. Class này sẽ có 2 ý nghĩa trong đó:

UIImageView = Image + View

Trong đó,

  • Image chính là dữ liệu của đối tượng ảnh mà ta muốn hiển thị. Trong iOS, dữ liệu ảnh sẽ đại diện bằng UIImage.
  • View chính là đối tượng UIImageView. Do có chữ View đó. Nó mang đầy đủ tính chất của một View và dùng để hiển thị dữ liệu ảnh cho người dùng thấy được.

Hầu như, các bạn sẽ không để ý tới imageView.image = motImageNaoDo. Đó là cách truyền thống để cập nhật dữ liệu cho một đối tượng UIImageView trên iOS.

Tất nhiên, bạn cần phải lưu trữ dữ liệu cho UIImageView trước đó đã. Biến mà để lưu trữ dữ liệu kia, nó chính là dữ liệu mà bạn cần phải đảm báo an toàn trên Main Thread.

Do đó, trong quán trình phát triển project của bạn, việc tách biệt ý nghĩa của các loại dữ liệu trong project với các mục đích dùng khác nhau. Hiểu được dữ liệu của mình sẽ biến đổi trên Thread nào và có an toàn khi thay đổi hay không … Là cực kỳ quan trong đối với việc tồn vong của một chương trình.

Completion Handler

Ngoài ra, bạn còn các dữ liệu khác như là:

  • Singleton
  • Biến Static
  • Các biến toàn cục
  • Các khai báo define, cấu hình, config ….

Bạn sẽ thao tác với tụi nó khá là nhiều. Việc truy cập và cập nhật giá trị cho chúng khá thường xuyên. Nhất là tương tác bất đồng bộ (như gọi một API). Và lúc đó, việc sử dụng một Completion Handler là điều hiển nhiên.

Bênh cạnh, việc phổ biến mô hình MVVM trong iOS cũng là tác nhân lớn gây ra việc sử dụng không an toàn cho các biến đó. Nhất là các dữ liệu trên Main Thread.

Ta hãy lấy ví dụ demo của chúng ta tại bài viết trước. Và thêm một ViewModel như sau:

class HomeViewModel {
    
    // MARK: Properties
    let imageURL = "https://photo-cms-viettimes.zadn.vn/w1280/Uploaded/2021/aohuooh/2019_03_16/chum_anh_chung_to_meo_bi_ngao_da_la_co_that_158278544_1632019.png"
    
    
    // MARK: Actions
    func loadImage(completion: @escaping (UIImage?) -> Void ) {
        fetchImage(url: URL(string: imageURL)!) { image in
            DispatchQueue.main.async {
                completion(image)
            }
        }
    }
    
    // MARK: API
    private func fetchImage(url: URL, completion: @escaping (UIImage?) -> ()) {
        URLSession.shared.dataTask(with: url) { data, response, error in
            if error != nil {
                completion(nil)
                return
            }
            
            guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let data = data else {
                completion(nil)
                return
            }
            
            let image = UIImage(data: data)
            completion(image)
        }
        .resume()
    }
}

Đây là một ViewModel điển hình trong việc tương tác với API để lấy dữ liệu về. ViewModel không lưu trữ dữ liệu, nhưng nó sẽ chuyển cho ViewController dữ liệu. Từ đó, dữ liệu sẽ dùng để cập nhật lên giao diện.

Bạn sẽ sử dụng nó như sau.

@IBAction func start(_ sender: Any) {
    viewmodel.loadImage { image in
        self.imageView.image = image
    }
}

Tuy nhiên, bạn sẽ để ý tới DispatchQueue.main đã xuất hiện trở lại. Và nó là nguyên nhân dẫn tới niềm đau. Chỉ cần bạn comment lại nó ở ViewModel. Sau đó, build project và cảm nhận kết quả nhoé.

(Mình đã đề cập về vấn đề này ở bài viết trước rồi.)

Như đã ví dụ ở trên, điểm yếu của chương trình nằm ở việc chuyển dữ liệu đi từ ViewModel sang ViewController. Tuy nhiên, cái cần chú ý là ta đang chuyển dữ liệu từ một Thread khác về lại Main Thread.

Publisher

Ta có thể có một giải pháp khác, nó cũng tiệm cận hoàn hảo. Khi nó có thể khắc phục được khâu truyền dữ liệu qua lại giữa các đối tượng. Đó là Reactive Programming, với framework sử dụng ở đây là Combine.

(Fx Studio có rất nhiều bài viết về Combine. Bạn có thể tham khảo thêm nha.)

Ta sẽ ví dụ bằng cách thêm Combine vào ViewModel như sau:

import Combine

class HomeViewModel {
  
    // ... 

    @Published var image: UIImage?

    func loadImage2() {
        fetchImage(url: URL(string: imageURL)!) { image in
            DispatchQueue.main.async {
                self.image = image
            }
        }
    }

    // ...
    
}

Trong đó:

  • Import thêm framework Combine
  • Thêm một thuộc tính với Wrapper là @Published. Có tác dụng vừa trữ và vừa phát dữ liệu
  • Function loadImage2() không có bất cứ closure nào.

Bạn chỉ cần gọi fetchImage, khi dữ liệu trả về. Bạn chỉ cần cập nhật lại dữ liệu cho thuộc tính @Published. Nó sẽ tự động phát dữ liệu sang ViewController. Bên ViewController, ta chỉ cần lắng nghe dữ liệu như sau:

var subscriptions = Set<AnyCancellable>()

// ....

override func viewDidLoad() {
    super.viewDidLoad()
    
    viewmodel.$image
        .assign(to: \.image, on: imageView)
        .store(in: &subscriptions)
}

Dữ liệu sẽ đưa thẳng tới .image của đối tượng imageView. Không có gì sai sót trong quá trình truyền dữ liệu đi. Tuy nhiên, bạn cũng sẽ nhận ra ở ViewModel có DispatchQueue.main.

Bạn vẫn không thoát được định mệnh với DispatchQueue.main này.

Async/Await

Để có được trải nhiệm tốt hơn với Combine cho việc truyền tải dữ liệu trên Main Thread, thì Swift đã ra mắt thêm async/await. Async/Await sẽ cung cấp thêm sức mạnh cho Combine rất nhiều. (Mình sẽ có các bài viết crossover giữa 2 thế lực mới đó)

Giải pháp mới

Nhiệm vụ của 2 thực thể mới như sau:

  • Async/Await sẽ lo phần bất đồng bộ với API
  • Combine sẽ lo phần truyền dữ liệu trên Main Thread

Nhưng để đảm bảo mọi thứ phối hợp được với nhau, thì ta cần cải tiến các function của mình lại trước đã. Bạn sẽ cần một hàm fetchImage mới với async nha. Xem code tham khảo sau:

func fetchImage(url: URL) async throws -> UIImage? {
    let (data, _)  = try await URLSession.shared.data(from: url)
    return UIImage(data: data)
}

Quá đơn giản (nếu bạn đã theo dõi các bài về async/await rồi). Chúng ta sẽ dừng chờ data từ việc triệu hồi 1 link url, bằng try await(có thể có lỗi trong quá trình tương tác, nên dùng try). Sau đó, return về một UIImage.

Cũng tại ViewModel, ta sẽ viết mới một function tương tác với ViewController cho việc cập nhật ảnh. Tham khảo code nha.

func loadImage3() async {
    do {
        image = try await fetchImage(url: URL(string: imageURL)!)
    } catch {
        print("ViewModel : lỗi nè")
    }
}

Cuối cùng, chính là việc triệu hồi tại ViewController.

@IBAction func start(_ sender: Any) {
    // Async/Await
    async {
        await viewmodel.loadImage3()
    }
}

Bạn để ý là chúng ta không sử dụng tới DispatchQueue.main, nên hạn chế đi nhiều lỗi gây ra khi cập nhật dữ liệu tại một thread khác Main.

Nhìn qua cũng ổn đó!

Vấn đề gặp phải

Tuy cách trên khá ổn, nhưng về bản chất là vẫn dừng chờ tại Thread nào đó. Nếu ta đang sử dụng Main Thread .thì sẽ dừng chờ tại Main Thread.

Mọi thứ sẽ không bị crash do bị cập nhật dữ liệu từ thread khác Main.

Và nếu bạn lạm dụng async quá. Thì rất có thể bạn rơi vào trường hợp sau:

func loadImage2() {
    fetchImage(url: URL(string: imageURL)!) { image in
        async {
            self.image = image
        }
    }
}

Trong đó:

  • Bạn đang kết hợp giữa Completion Handler và Async
  • Nhưng async { ... } sẽ dùng chờ tại thread lúc này đang là khác Main

Kết quả vẫn là crash như thường thôi. Và đôi khi, bạn cũng phải chấp nhận một điều là …

DispatchQueue.main như là một kim chỉ nam giúp bạn đang biết mình ở đâu và đang làm gì.

Cuối cùng, bạn cũng thể ngăn chặn Data Race cho dữ liệu trên Main Thread được. Vì chúng ta chỉ mới giải quyết vấn đề truyền dữ liệu mà thôi.

MainActor

Qua các phần trên, mình chỉ muốn cho bạn thấy sử ảnh hưởng của các Thread khác nhau sẽ tác động như thế nào đến dữ liệu. Mô hình mình sử dụng là MVVM cơ bản. Bên cạnh đó, cho dù bạn sử dụng kĩ thuật nào (Completion, Combine, Async …) thì hầu như không thoát khỏi Data Race. Các cách kết hợp chỉ giúp bạn đảm bảo việc cập nhật dữ liệu trên Main Thread mà thôi.

Mục đích thứ hai, chúng ta cần đảm bảo việc cập nhật dữ liệu được an toàn. Tránh các Data Race cho các dữ liệu dùng chung trên Main Thread. Vì tất cả mọi thứ vẫn đang là bất đồng bộ. Không có ai đảm bảo rằng việc cập nhật cùng một dữ liệu đồng thời từ nhiều nơi khác nhau.

Và từ bài trước, cách hiệu quả nhất chính là …

MainActor

MainActor sẽ giúp bạn đảm bảo việc cập nhật dữ liệu chỉ ở Main Thread mà thôi. Việc cập nhật dữ liệu sẽ diễn ra tuần tự và tránh đi Data Race không đáng có.

Ta sẽ khám thêm các cách dùng khác của MainActor trong các trường hợp lỗi ở trên nha.

On Type

On Type chính là cách khai báo một class/struct mà kèm theo từ khoá @MainActor trước đó. Nó sẽ đảm bảo rằng toàn bộ class/struct là một tác nhân toàn cục. Khi đó, các thuộc tính & phương thức của thể hiện class/struct đó là isolated (cách ly).

Ví dụ, với ViewModel của chúng ta như sau:

@MainActor
class HomeViewModel {
   // ....
}

Quá đơn giản cho đội Concurrency phải không nào. bây giờ ta sẽ sử dụng các function lỗi khi ta không sử dụng tới DispatchQueue.main.

func loadImage(completion: @escaping (UIImage?) -> Void ) {
    fetchImage(url: URL(string: imageURL)!) { image in
        //DispatchQueue.main.async {
            //completion(image)
        //}
        async {
            completion(image)
        }
    }
}

func loadImage2() {
    fetchImage(url: URL(string: imageURL)!) { image in
        //DispatchQueue.main.async {
            //self.image = image
        //}
        async {
            self.image = image
        }
    }
}

Lúc này, đối tượng viewmodel là một tác nhân toàn cục. Trạng thái của nó (thuộc tính image) chỉ được cập nhật tại Main Thread. Triệt tiêu đi việc Data Race cho các dữ liệu sử dụng ở Main Thread.

Việc sử dụng MainActor tại On Type cũng giúp bạn tránh đi việc khai báo 2 wrapper properties cùng nhau. Ví dụ như sau:

@MainActor
@Published var image: UIImage?

Closure

Bạn còn có một cách dùng khá đặc biệt nữa. Đó chính là khai báo một call back với @MainActor. Nghe qua hơi vô lý, nhưng mà cũng thực hiện được à.

Đầu tiên, bạn xoá đi @MainActor ở khai báo ViewModel. Bạn sẽ sử dụng khai báo closure với @MainActor, ví dụ như:

var callback : @MainActor (UIImage?) -> Void

Khá là ngầu đó!

Với khai báo đó, callback của bạn sẽ được thực thi tại Main Thread mà thôi. Nên bạn hoàn toàn yên tâm với việc dữ liệu mà nó mà đi. Còn áp dụng vào chương trình của chúng ta thì như sau:

func loadImage4(completion: @MainActor @escaping (UIImage?) -> Void ) {
    fetchImage(url: URL(string: imageURL)!) { image in
        detach {
            await completion(image)
        }
    }
}

Trong đó:

  • Vẫn dùng cách Completion Handler để tương tác giữa ViewController & ViewModel
  • Khai báo thêm @MainActor cho closure của function
  • detach để triệu hồi thread khác (cho vui thôi), hoặc bạn có thể dùng async để dừng chờ thực thi bất đồng bộ

Code của bạn lúc này sẽ không cần phải thay đổi hết các Completion Handler. Nhưng vẫn đảm bảo việc hoạt động bất đồng bộ và tránh được Data Race rồi đó.

Hãy build project và cảm nhận kết quả nhoé!

Combine

Mình sẽ bổ sung thêm các dùng MainActor với Combine một cách đơn giản và xịn sò hơn nhiều. Bạn tham khảo cách dùng sau cho function loadImage bằng Combine.

func loadImage() {
    URLSession.shared
        .dataTaskPublisher(for: URL(string: imageURL)!)
        //.receive(on: DispatchQueue.main)
        .map { data, _ in
            UIImage(data: data)
        }
        .replaceError(with: nil)
        .assign(to: \.image, on: self)
        .store(in: &subscriptions)
}

Chú ý, ta đã comment dòng code nhận về Main Thread .receive(on: DispatchQueue.main), vì với khai báo MainActor rồi thì sẽ không cần dùng nó nữa.

Bạn sẽ không cần quá lo lắng nhiều về truyền dữ liệu qua lại giữa các Thread khác nhau. Tất cả đều được MainActor lo rồi. EZ Game!

Tạm kết

  • Các loại dữ liệu trên Main Thread
  • Truyền tải và cập nhật dữ liệu trên Main Thread
  • Vấn đề Data Race cho dữ liệu
  • Các cách sử dụng MainActor để giải quyết vấn đề Data Race trên Main Thread
  • Sự kết hợp cách dùng giữa Completion Handler, Async, Combine và MainActor

 

Okay! Tới đây, mình xin kết thúc bài viết giới thiệu về MainActor trong Swift 5.5 . Nếu có gì thắc mắc hay góp ý cho mình thì bạn có thể để lại bình luận hoặc gởi email theo trang Contact.

  • Bài viết tiếp theo tại đây.
  • Bạn có thể checkout code tại đây.

Cảm ơn bạn đã đọc bài viết này!

FacebookTweetPinYummlyLinkedInPrintEmailShares22

Related Posts:

  • dart
    Data Type - Dart Tour
Tags: Actor, concurrency, Swift, Swift 5.5
Written by chuotfx

Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Donate – Buy me a coffee!

Fan page

Fx Studio

Tags

Actor Advanced Swift AI api AppDistribution autolayout basic ios tutorial blog ci/cd closure collectionview combine concurrency crashlytics dart dart basic dart tour Declarative delegate deploy design pattern fabric fastlane firebase flavor flutter GCD gradients iOS MVVM optional Prompt engineering protocol Python rxswift safearea Swift Swift 5.5 SwiftData SwiftUI SwiftUI Notes tableview testing TravisCI unittest

Recent Posts

  • Role-playing vs. Persona-based Prompting
  • [Swift 6.2] Raw Identifiers – Đặt tên hàm có dấu cách, tại sao không?
  • Vibe Coding là gì?
  • Cách Đọc Sách Lập Trình Nhanh và Hiệu Quả Bằng GEN AI
  • Nỗ Lực – Hành Trình Kiến Tạo Ý Nghĩa Cuộc Sống
  • Ai Sẽ Là Người Fix Bug Khi AI Thống Trị Lập Trình?
  • Thời Đại Của “Dev Tay To” Đã Qua Chưa?
  • Prompt Engineering – Con Đường Để Trở Thành Một Nghề Nghiệp
  • Vấn đề Ảo Giác (hallucination) khi tương tác với Gen AI và cách khắc phục nó qua Prompt
  • Điều Gì Xảy Ra Nếu… Những Người Dệt Mã Trở Thành Những Người Bảo Vệ Cuối Cùng Của Sự Sáng Tạo?

You may also like:

  • Data Type - Dart Tour
    dart

Archives

  • May 2025 (2)
  • April 2025 (1)
  • March 2025 (8)
  • January 2025 (7)
  • December 2024 (4)
  • September 2024 (1)
  • July 2024 (1)
  • June 2024 (1)
  • May 2024 (4)
  • April 2024 (2)
  • March 2024 (5)
  • January 2024 (4)
  • February 2023 (1)
  • January 2023 (2)
  • November 2022 (2)
  • October 2022 (1)
  • September 2022 (5)
  • August 2022 (6)
  • July 2022 (7)
  • June 2022 (8)
  • May 2022 (5)
  • April 2022 (1)
  • March 2022 (3)
  • February 2022 (5)
  • January 2022 (4)
  • December 2021 (6)
  • November 2021 (8)
  • October 2021 (8)
  • September 2021 (8)
  • August 2021 (8)
  • July 2021 (9)
  • June 2021 (8)
  • May 2021 (7)
  • April 2021 (11)
  • March 2021 (12)
  • February 2021 (3)
  • January 2021 (3)
  • December 2020 (3)
  • November 2020 (9)
  • October 2020 (7)
  • September 2020 (17)
  • August 2020 (1)
  • July 2020 (3)
  • June 2020 (1)
  • May 2020 (2)
  • April 2020 (3)
  • March 2020 (20)
  • February 2020 (5)
  • January 2020 (2)
  • December 2019 (12)
  • November 2019 (12)
  • October 2019 (19)
  • September 2019 (17)
  • August 2019 (10)

About me

Education, Mini Game, Digital Art & Life of coders
Contacts:
contacts@fxstudio.dev

Fx Studio

  • Home
  • About me
  • Contact us
  • Mail
  • Privacy Policy
  • Donate
  • Sitemap

Categories

  • Art (1)
  • Blog (44)
  • Code (11)
  • Combine (22)
  • Flutter & Dart (24)
  • iOS & Swift (102)
  • No Category (1)
  • RxSwift (37)
  • SwiftUI (80)
  • Tutorials (87)

Newsletter

Stay up to date with our latest news and posts.
Loading

    Copyright © 2025 Fx Studio - All rights reserved.