Chào mừng độc giả đến với Cafedev! Trong chuyên mục này, chúng ta sẽ khám phá về Kotlin và xử lý trạng thái chia sẻ và đồng thời. Kotlin không chỉ mạnh mẽ về tính năng ngôn ngữ mà còn cung cấp những giải pháp độc đáo cho vấn đề về trạng thái chia sẻ. Cùng với đó, chúng ta sẽ tìm hiểu về cách xử lý đồng thời một cách hiệu quả trong ngữ cảnh của Kotlin. Hãy bắt đầu hành trình khám phá cùng Cafedev!

Coroutine có thể được thực hiện song song bằng cách sử dụng bộ xử lý đa luồng như [Dispatchers.Default]. Điều này đặt ra tất cả những vấn đề thông thường về song song hóa. Vấn đề chính là đồng bộ hóa truy cập đến trạng thái chia sẻ có thể thay đổi. Một số giải pháp cho vấn đề này trong thế giới của coroutine tương tự như trong thế giới đa luồng, nhưng một số khác biệt.

1. Vấn đề

Hãy khởi chạy một trăm coroutine thực hiện cùng một hành động một nghìn lần. Chúng ta cũng sẽ đo thời gian hoàn thành chúng để so sánh sau này:

suspend fun massiveRun(action: suspend () -> Unit) {
    val n = 100  // number of coroutines to launch
    val k = 1000 // times an action is repeated by each coroutine
    val time = measureTimeMillis {
        coroutineScope { // scope for coroutines
            repeat(n) {
                launch {
                    repeat(k) { action() }
                }
            }
        }
    }
    println("Completed ${n * k} actions in $time ms")
}


Chúng ta bắt đầu với một hành động rất đơn giản là tăng giá trị của một biến có thể thay đổi chia sẻ bằng cách sử dụng đa luồng [Dispatchers.Default].

var counter = 0

fun main() = runBlocking {
    withContext(Dispatchers.Default) {
        massiveRun {
            counter++
        }
    }
    println("Counter = $counter")
}

Bạn có thể lấy toàn bộ mã nguồn tại đây.

Cuối cùng nó in ra gì? Rất ít khả năng để nó in ra “Counter = 100,000”, bởi vì một trăm coroutine tăng giá trị của biến counter đồng thời từ nhiều luồng mà không có bất kỳ đồng bộ hóa nào.

2. Biến volatile không giúp ích gì ?

Có một quan điểm sai lầm phổ biến là làm cho biến trở nên volatile giải quyết vấn đề đồng thời. Hãy thử xem nó hoạt động như thế nào:

@Volatile // in Kotlin `volatile` is an annotation 
var counter = 0

fun main() = runBlocking {
    withContext(Dispatchers.Default) {
        massiveRun {
            counter++
        }
    }
    println("Counter = $counter")
}

Bạn có thể lấy toàn bộ mã nguồn tại đây.

Mã nguồn này hoạt động chậm hơn, nhưng chúng ta vẫn không luôn nhận được “Counter = 100,000” ở cuối, bởi vì biến volatile đảm bảo đọc và ghi tuyến tính (đây là một thuật ngữ kỹ thuật cho “nguyên tử”) vào biến tương ứng, nhưng không cung cấp tính nguyên tử cho các hành động lớn hơn (ví dụ: tăng giá trị trong trường hợp của chúng ta).

3. Cấu trúc dữ liệu an toàn đa luồng

Giải pháp chung hoạt động cả cho luồng và coroutine là sử dụng một cấu trúc dữ liệu an toàn đa luồng (còn được gọi là đồng bộ, tuyến tính hóa hoặc nguyên tử) cung cấp tất cả đồng bộ hóa cần thiết cho các hoạt động tương ứng cần được thực hiện trên một trạng thái chia sẻ. Trong trường hợp của một bộ đếm đơn giản, chúng ta có thể sử dụng lớp AtomicInteger có các hoạt động incrementAndGet nguyên tử:

val counter = AtomicInteger()

fun main() = runBlocking {
    withContext(Dispatchers.Default) {
        massiveRun {
            counter.incrementAndGet()
        }
    }
    println("Counter = $counter")
}

Bạn có thể lấy toàn bộ mã nguồn tại đây.

Đây là giải pháp nhanh nhất cho vấn đề cụ thể này. Nó hoạt động cho bộ đếm đơn giản, bộ sưu tập, hàng đợi và các cấu trúc dữ liệu và các hoạt động cơ bản trên chúng. Tuy nhiên, nó không dễ dàng mở rộng cho trạng thái phức tạp hoặc cho các hoạt động phức tạp không có các triển khai an toàn đa luồng sẵn có.

4. Hạn chế luồng cấp độ tốt

Hạn chế luồng là một cách tiếp cận vấn đề về trạng thái chia sẻ có thể thay đổi khi tất cả truy cập vào trạng thái chia sẻ cụ thể đó đều được hạn chế trong một luồng duy nhất. Nó thường được sử dụng trong ứng dụng UI, nơi mà tất cả trạng thái UI được hạn chế trong một luồng sự kiện/ứng dụng duy nhất. Nó dễ áp dụng với coroutine bằng cách sử dụng một ngữ cảnh đơn luồng.

val counterContext = newSingleThreadContext("CounterContext")
var counter = 0

fun main() = runBlocking {
    withContext(Dispatchers.Default) {
        massiveRun {
            // confine each increment to a single-threaded context
            withContext(counterContext) {
                counter++
            }
        }
    }
    println("Counter = $counter")
}

Bạn có thể lấy toàn bộ mã nguồn tại đây.

Mã nguồn này hoạt động rất chậm, vì nó thực hiện hạn chế luồng cấp độ tốt. Mỗi việc tăng giá trị cá nhân chuyển từ ngữ cảnh đa luồng [Dispatchers.Default] sang ngữ cảnh đơn luồng sử dụng khối [withContext(counterContext)].

5. Hạn chế luồng cấp độ lớn

Trong thực tế, hạn chế luồng được thực hiện theo các khối lớn, ví dụ: các khối lớn của logic kinh doanh cập nhật trạng thái được hạn chế trong một luồng duy nhất. Ví dụ dưới đây thực hiện như vậy, chạy mỗi coroutine trong ngữ cảnh đơn luồng từ đầu.

val counterContext = newSingleThreadContext("CounterContext")
var counter = 0

fun main() = runBlocking {
    // confine everything to a single-threaded context
    withContext(counterContext) {
        massiveRun {
            counter++
        }
    }
    println("Counter = $counter")
}

Bạn có thể lấy toàn bộ mã nguồn tại đây.

Điều này hiện tại hoạt động nhanh hơn và tạo ra kết quả chính xác.

6. Loại trừ lẫn nhau

Giải pháp về loại trừ lẫn nhau cho vấn đề là bảo vệ tất cả các sửa đổi của trạng thái chia sẻ bằng một phần quan trọng không bao giờ thực thi đồng thời. Trong một thế giới chặn, bạn thường sử dụng synchronized hoặc ReentrantLock cho điều đó. Tùy chọn của Coroutine được gọi là [Mutex]. Nó có các hàm [lock] và [unlock] để đánh dấu một phần quan trọng. Điểm khác biệt chính là Mutex.lock() là một hàm treo. Nó không chặn một luồng.
Cũng có hàm mở rộng [withLock] tiện ích biểu diễn mẫu mutex.lock(); try { ... } finally { mutex.unlock() } một cách thuận tiện:

val mutex = Mutex()
var counter = 0

fun main() = runBlocking {
    withContext(Dispatchers.Default) {
        massiveRun {
            // protect each increment with lock
            mutex.withLock {
                counter++
            }
        }
    }
    println("Counter = $counter")
}

Bạn có thể nhận mã nguồn đầy đủ tại đây.

Việc khóa trong ví dụ này là tinh tế, vì vậy nó có giá. Tuy nhiên, đây là một lựa chọn tốt cho một số tình huống nơi bạn tuyệt đối phải sửa đổi một số trạng thái chia sẻ định kỳ, nhưng không có luồng tự nhiên nào mà trạng thái này được giới hạn vào.

Cảm ơn bạn đã đồng hành cùng Cafedev trong hành trình khám phá Kotlin và quản lý trạng thái chia sẻ cùng đồng thời. Chúng ta đã cùng nhau tìm hiểu những giải pháp độc đáo của Kotlin trong việc xử lý thách thức này. Nếu bạn muốn đào sâu hơn, đừng quên khám phá thêm trên Cafedev với nhiều thông tin hữu ích khác. Hãy tiếp tục theo dõi chúng tôi để không bỏ lỡ những nội dung mới và hấp dẫn tại Cafedev. Chúng tôi hy vọng rằng bạn sẽ tiếp tục tham gia cùng cộng đồng sáng tạo và đam mê tại Cafedev.

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của Cafedev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!