Android(Kotlin)

Threading models in Coroutines and Android SQLite API

----___<<<<< 2023. 1. 19. 11:05

Threading models in Coroutines and Android SQLite API 에 대한 번역입니다.

 

원문은 여기서 보세요.

https://medium.com/androiddevelopers/threading-models-in-coroutines-and-android-sqlite-api-6cab11f7eb90

 

Threading models in Coroutines and Android SQLite API

Implementing suspending transactions in Room

medium.com

 

Implementing suspending transactions in Room

Room에서 suspneding 트랜젝션 적용

 

Room 2.1 now lets you use Kotlin Coroutines by defining suspending DAO functions. Coroutines are great for performing asynchronous operations. They allow you to write natural, sequential code that operates on expensive resources like the database, without having to explicitly pass tasks, results, and errors between threads. Room’s Coroutine support gives you the benefits of concurrency scoping, lifecycle, and nesting in your database operations.

Room 2.1 에서는 suspending function 을 통해서 코루틴을 사용해서 대충 좋다는 내용.


While developing Coroutines support in Room we ended up encountering some unforeseen problems between the threading models in Coroutines and Android’s SQL API. Read on to find out more about these problems, our solutions, and implementation.

Room에서 코루틴 지원 개발하고 있는데 SQL API에서 문제가 생김. 밑에서 문제를 알아보자

 

Unforeseen Problems
Take a look at the following snippet of code, which might seem safe, but is actually broken 😮:

아래 코드를 보자. 대충 괜찮아보이지만 잘 안된다

 

 

Android’s SQLite transactions are thread confined

안드로이드 SQLite는 쓰레드가 갇혀있다. -> 한마디로 좀 쓰기 힘들다.

 

The issue is that Android’s SQLite transactions are thread confined. When a query is executed within an ongoing transaction on the current thread, then it is considered part of that transaction and can proceed. If the query was instead executed on a different thread, then it is not considered part of that transaction and will block until the transaction on the other thread ends. That is how the beginTransaction and endTransaction API allows atomicity. It’s a reasonable API when database tasks run entirely on one thread; however, this a problem for coroutines because they are not bound to any particular thread. There is no guarantee that the thread continuing a coroutine after it was suspended is the same as the thread that executed before the suspension point.

이 부분은 번역보다 아래 그림을 보면 바로 이해가 갈텐데, 아래의 그림처럼 다른 쓰레드에서 SQLite 트랜잭션을 호출하면 데드락이 걸린다.

 

Database transactions in coroutines can lead to deadlock.

 

A simple implementation

간단한 구현

 

To work around Android’s SQLite limitation we needed an API similar to runInTransaction that accepts a suspending block. A naive implementation of this API can be as simple as using a single thread dispatcher:
SQLite 제한을 해결하려면 runInTransaction과 같이 suspending block이 필요하다. single 쓰레드 디스패처를 이용하는 것처럼.

 

The above implementation is a start, but it quickly falls apart when a different dispatcher is used within the suspending block:
위의 예제는 다른 디스패처를 사용합니다. 

 

By accepting a suspend block there is a possibility that a child coroutine will get launched using a different dispatcher, which may execute a database operation in an unexpected, different thread. Therefore, an appropriate implementation should allow the usage of the standard coroutine builders, such as async, launch, and withContext. In practice, it is only the database operations that need to be dispatched to a single transaction thread.

suspend block을 사용하면 자식 코루틴이 다른 디스패처를 사용할 수 있으니 async / launch / withContext를 통해서 작업해줘야 한다 -> 비동기 작업이 아닌 동기식으로 작업하라는 느낌적인 느낌.

 

Introducing withTransaction

withTransaction 보기 

 

To accomplish this, we’ve built the withTransaction API, which mimics the withContext API but provides a coroutine context specifically built for safe Room transactions. This allows you to write code such as:

withContext API 를 흉내내서 withRansaction 이라는 것을 만들었다.

 

 

As we dive into the implementation of Room’s withTransaction API, let’s review some of the coroutine concepts that have been mentioned. A CoroutineContext holds information a Coroutine needs to dispatch work. It carries the current CoroutineDispatcher, Job, and maybe some additional data; but it can also be extended to contain more elements. One important feature of a CoroutineContext is that they are inherited by child coroutines within the same coroutine scope, such as the scope in the withContext block. This mechanism allows for child coroutines to continue using the same dispatcher, or for them to get cancelled when the parent coroutine Job is cancelled. In essence, Room’s suspending transaction API creates a specialized coroutine context for performing database operations in a transaction scope.

 

There are three key elements in the context created by the withTransaction API:

  • A single threaded dispatcher used to perform database operations.
  • A context element that helps DAO functions identify that they are in a transaction.
  • A ThreadContextElement that marks dispatched threads used during the transaction coroutine.

번역을 요약하면 한 블록에서 로직을 실행해서 동일한 디스패처를 사용하도록 한다.

 

Transaction Dispatcher
A CoroutineDispatcher dictates in which thread a coroutine will execute. For example, Dispatchers.IO uses a shared pool of threads recommended for off-loading blocking operations, while Dispatchers.Main will execute coroutines in Android’s main thread. The transaction dispatcher created by Room is able to dispatch to a single thread taken from Room’s Executor — it is not using some arbitrary new thread. This is important since the executor is configurable by the user and is instrumentable for tests. At the start of a transaction, Room will take ownership of one of the threads in the executor until the transaction is completed. Database operations performed during the transaction will be dispatched into the transaction thread, even if the dispatcher was changed for a child coroutine.

Acquiring a transaction thread is not a blocking operation — it shouldn’t be, since if no threads are available, we should suspend and yield the caller so that other coroutines can proceed. It also involves enqueuing a runnable and waiting for it to actually execute, which is an indicator that a thread has become available. The function suspendCancellableCoroutine helps us bridge between a callback-based API and coroutines. In this case, our callback for when a thread is available is the execution of the enqueued runnable. Once our runnable executes we use runBlocking to start an event loop that takes ownership of the thread. The dispatcher created by runBlocking is then extracted out and used for dispatching blocks into the acquired thread. Additionally, a Job is used to suspend and hold the thread until the transaction is done. Note that precautions are taken for when the coroutine is cancelled or is unable to acquire a thread. The snippet of code that acquires a transaction thread is as follows:

 

대충 코루틴이 어떻게 쓰레드를 사용하는가에 대한 내용과 코드

 

 

나머지는 아래 링크 참고

 

https://medium.com/androiddevelopers/threading-models-in-coroutines-and-android-sqlite-api-6cab11f7eb90

 

Threading models in Coroutines and Android SQLite API

Implementing suspending transactions in Room

medium.com

 

'Android(Kotlin)' 카테고리의 다른 글

ROOM + Coroutine Flow - 5 (Flow)  (0) 2023.01.28
Android Horizontal ProgressBar  (0) 2023.01.21
local.properties  (0) 2023.01.17
Unix Time to SimpleDataFormat  (0) 2023.01.09
Paging Advanced  (0) 2022.12.26