気軽に楽しくプログラムと遊ぶ

自分が興味があってためになるかもって思う情報を提供しています。

Kotlin Coroutineの概念を理解する

以下でCoroutinの概要理解を行いました。
https://tamata78.hatenablog.com/entry/2021/09/06/174235

上記で理解を進める中でわからなかった概念を補足する情報を記載していきます。

CoroutineScope

  • CoroutineScopeは、コルーチンビルダー関数であるlaunchまたはasyncを使用して作成したコルーチンを全て追跡します。
  • 実行中のコルーチンはscope.cancel()を呼び出してキャンセル可能
  • キャンセルされたスコープは、コルーチンをそれ以上作成できません。
class ExampleClass {

    // Job and Dispatcher are combined into a CoroutineContext which
    // will be discussed shortly
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine within the scope
        scope.launch {
            // New coroutine that can call suspend functions
            fetchDocs()
        }
    }

    fun cleanUp() {
        // Cancel the scope to cancel ongoing coroutines work
        scope.cancel()
    }
}

CoroutineScopeの生成時には、ジョブとスレッドプールであるディスパッチャを結合した値を設定する必要があります。

ジョブ

  • ジョブはコルーチンのハンドル(管理側)です。
  • 各コルーチンは、コルーチンのライフサイクルを管理するJobインスタンスを返却します。
  • Jobを用いてコルーチンをキャンセルするなどのライフサイクル管理ができます。
class ExampleClass {
    ...
    fun exampleMethod() {
        // Handle to the coroutine, you can control its lifecycle
        val job = scope.launch {
            // New coroutine
        }

        if (...) {
            // Cancel the coroutine started above, this doesn't affect the scope
            // this coroutine was launched in
            job.cancel()
        }
    }
}

CoroutineContext

コルーチンの設定のようなもの。 以下のCoroutineContext要素(設定項目)があります。

  • Job: コルーチンのライフサイクルを制御
  • CoroutineDispatcher: 適切なスレッドに処理を送信
  • CoroutineName: コルーチンの名前。デバッグに用いる
  • CoroutineExceptionHandler: キャッチされない例外を処理

上記の要素は以下のように割り当てられます。

  • 新規コルーチン作成時にJobインスタンスが割り当てられます。
  • Job以外のCoroutineContext要素は、コルーチンを含むスコープから継承されます。
  • 継承された要素を変更するには、launchasyncに変更対象要素を渡す必要があります。
class ExampleClass {
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // 新しいコルーチンの開始。ディスパッチャとコルーチン名をlanchの引数に設定すると変更できる
        val job = scope.launch(Dispatchers.Default + "BackgroundCoroutine") {
            // New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
        }
    }
}

+演算子は文字列結合とは違う処理なんでしょうか。引数二つ指定ではないのが少し疑問。。

ディスパッチャ

  • コルーチンは、ディスパッチャでコルーチンの実行スレッドを決定します。
  • DBアクセス、外部API実行などメインスレッドの外部で実行する場合は、デフォルト or IOディスパッチャを指定します。

開発者が指定できるディスパッチャは以下の3つになります。

  • Dispatchers.Main:UI を操作して処理を手早く作業する場合にのみ使用(suspend 関数の呼び出しなど)
  • Dispatchers.IO:メインスレッドの外部でディスクまたはネットワークの I/O を実行する場合に使用(ファイルの読み書き、ネットワーク オペレーションの実行など)
  • Dispatchers.Default :メインスレッドの外部で CPU 負荷の高い作業を実行する場合に使用(リストの並べ替えや JSON の解析など)

withContext()におけるディスパッチャ切り替え

  • withContext(Dispatchers.IO)を呼び出すことで、IOスレッドプールで実行するブロックを生成します。
  • get関数内は、IOディスパッチャ経由で実行されるようになります。
  • withContext自体が中断関数であるため、ディスパッチャのメインスレッドを中断して、get関数内の処理を実行します。
suspend fun fetchDocs() {                      // Dispatchers.Main
    val result = get("developer.android.com")  // Dispatchers.Main
    show(result)                               // Dispatchers.Main
}

suspend fun get(url: String) =                 // Dispatchers.Main
    withContext(Dispatchers.IO) {              // Dispatchers.IO (main-safety block)
        /* perform network IO here */          // Dispatchers.IO (main-safety block)
    }                                          // Dispatchers.Main
}

withContext()コルーチンを作成せず、ディスパッチャの切り替えのみを行うため、パフォーマンスに優れている。
withContextブロックの処理が完了するとget以降の処理が再開されて処理が継続されます。

実装時の注意点

実装していて、気づいた実装上の注意点を少し記載しておきます。

  • コルーチン生成を乱立すると逆にスレッド切り替えで遅くなる可能性があります
  • ループ処理内でのDBアクセスなどは、コールーチン化のターゲットになりうる
  • asyncのネストなどはできるだけ避ける。コルーチンが作られすぎでしまう
  • launch、runblockなどのコルーチンビルダー関数は、ループ内で入れず、外に記載。これもコルーチンを大量に作ってしまい、逆に性能劣化を招く

参考URL

Javaバージョンを切り替えるjEnvの基本コマンド

jenvのインストール

brewでインストール

brew install jenv

設定

Bashを使っている場合

echo 'export PATH="$HOME/.jenv/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(jenv init -)"' >> ~/.bash_profile

Zshを使っている場合

echo 'export PATH="$HOME/.jenv/bin:$PATH"' >> ~/.zshrc
echo 'eval "$(jenv init -)"' >> ~/.zshrc

ディレクトリを作成

mkdir ~/.jenv
mkdir ~/.jenv/versions

#.bash_profile を再読み込み
source ~/.bash_profile # zshなら source ~/.zshrc

jenvのインストールに問題ないか確認

$ jenv doctor

インストール済のバージョンを確認

/usr/libexec/java_home -V
Matching Java Virtual Machines (3):
    16.0.2 (x86_64) "Oracle Corporation" - "OpenJDK 16.0.2" /Users/user/Library/Java/JavaVirtualMachines/openjdk-16.0.2/Contents/Home
    16.0.1 (x86_64) "AdoptOpenJDK" - "AdoptOpenJDK 16" /Library/Java/JavaVirtualMachines/adoptopenjdk-16.jdk/Contents/Home
    11.0.11 (x86_64) "AdoptOpenJDK" - "AdoptOpenJDK 11" /Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home

jenv環境へインストールJavaを追加

$ jenv add /Users/user/Library/Java/JavaVirtualMachines/openjdk-16.0.2/Contents/Home
openjdk64-16.0.2 added
16.0.2 added
16.0 added
16 added

$ jenv add /Library/Java/JavaVirtualMachines/adoptopenjdk-16.jdk/Contents/Home
openjdk64-16.0.1 added
16.0.1 added
 16.0 already present, skip installation
 16 already present, skip installation

$ jenv add /Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home
openjdk64-11.0.11 added
11.0.11 added
11.0 added
11 added

jenvに追加されているJDKの一覧

先頭に*が表示されているJDKが有効化されている。

$ jenv versions
* system (set by /Users/jpd20537/.jenv/version)
  11
  11.0
  11.0.11
  16
  16.0
  16.0.1
  16.0.2
  openjdk64-11.0.11
  openjdk64-16.0.1
  openjdk64-16.0.2

Javaの切り替え

# グローバル
$ jenv global 1.8.0.222

# ローカル(特定のディレクトリのみに適用)
$ jenv local 11.0

$jenv version
11.0.11 (set by /Users/user/workspace/project/.java-version)

設定されているJavaの確認

$ echo $JAVA_HOME
/Users/user/.jenv/versions/11.0.11
~/workspace/project

$ java -version 
openjdk version "11.0.11" 2021-04-20
OpenJDK Runtime Environment AdoptOpenJDK-11.0.11+9 (build 11.0.11+9)
OpenJDK 64-Bit Server VM AdoptOpenJDK-11.0.11+9 (build 11.0.11+9, mixed mode)

環境変数JAVA_HOMEの自動設定

$ jenv enable-plugin export
# 自動設定を解除するにはdisable-plugin export

Brewfile関連の基本コマンド

Mac OSの環境構築を自動化するツールの基本コマンドを記載していきます。

パッケージを探す

brew search # caskのパッケージも探せる
mas search

パッケージのアップデート

$ brew upgrade
$ brew upgrade --cask
$ mas upgrade

インストール済みパッケージ一覧

$ brew list
$ brew list --cask
$ mas list

診断

インストールしたパッケージの管理状態を診断します。

$ brew doctor
You have unlinked kags in your Cellarと表示されたらリンクを修正します。

リンクの修正

$ brew unlink (package name)
$ brew link (package name)
$ brew cleanup

環境移行

Brewfileの生成 新しい環境に引っ越す前に、インストール済みのパッケージを Brewfileに残します。

$ brew bundle dump

Brewfileからパッケージをインストール

新しい環境にBrewfileをコピーし、同じディレクトリ内に移動して下記を実行

$ brew bundle 

Mac OS 開発環境初期設定

ターミナル

brew install

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
brew install cask

クライアントアプリ

アプリランチャー、コピペアプリ、 エディタ、docker、メニューカレンダー表示、ブルーライトカット

brew install --cask alfred
brew install --cask clipy
brew install --cask atom
brew install --cask docker
brew install --cask flux

DBクライアントツール. https://dbeaver.io/files/dbeaver-ce-latest-macos.dmg

Google 日本語入力 https://tools.google.com/dlpage/japaneseinput/eula.html?platform=mac

IDE

intellij https://www.jetbrains.com/ja-jp/idea/download/#section=mac

STS Spring | Tools

Mybatis キャッシュ設定

Mybatis キャッシュ設定

同じクエリを複数回呼ぶ場合、キャッシュ設定を入れると高速に処理できる可能性があります。

mybatis:
  configuration:
    cache-enabled: true # デフォルト有効だが、明示的に記載。
    default-executor-type: reuse

PreparedStatementをキャッシュし再利用

同一トランザクション内で同じSQLを複数回実行する場合は、 REUSEモードで実行すると、SIMPLEモードと比較して性能向上が期待できる。

これは、SQLを解析してPreparedStatementを生成する処理の実行回数を減らす事ができるためである。

default-executor-type: reuse 

設定できるモードは、他にSIMPLE、BATCHなどがある。
同一トランザクション内で更新系のSQLを連続して大量に実行する場合は、BATCHモードで実行するパフォーマンスが上がる。

Kotlin Coroutineの概要を理解する

以下を写経しつつ、理解をしていきます。
Kotlin の Coroutine を概観する - Qiita

Coroutineとは?

  • 軽量なThreadのようなもの
  • 他の処理をブロックせずに並列処理を行える
  • Threadは無視できないコストがかかるが、Coroutineは気にしなくてもよいレベルのコストしかかからない

Coroutineの作成と利用

non-blocking関数とblocking関数があります。
違いは、main関数の処理をブロックするかどうか。
ブロックとは、一時停止してCoroutine処理を実行することを指す。

non-blocking関数 launch

以下のように実装することでCoroutineを生成。
Coroutineは、launch内の処理をmainスレッドをブロックすることなく、並列で処理することができる。

fun main(args: Array<String>) {
    println("start")
    launch {
        println("coroutine!")
    }
    // 以下がないとlaunchは実行スレッドをブロックしないため
    // coroutine!が表示されず、endが表示されて終了する
    Thread.sleep(1000)
    println("end")
}

実行結果は以下

start
coroutine!
end

他のコルーチン終了を待機する Job#join()

launchは戻り値でJobを返却するため、他のコルーチン終了を待機するには、以下のように実装可能です。

fun main(args: Array<String>) = runBlocking() {
    println("start")
    launch {
        println("coroutine!")
    }.join()
    println("end")
}

blocking関数 runBlocking

Coroutine(ラムダ)を生成し、実行すると実行スレッドをブロックしつつ、内部で並列処理を実行できる

fun main(args: Array<String>) {
    println("start")
    val text = runBlocking {
        "coroutine!"
    }
    println(text)
    println("end")
}

実行結果は以下。runBlockingは生成したラムダ内の処理結果を返却することができる。

start
coroutine!
end

注意点

  • runBlockingはCoroutineは、非同期処理内(Coroutine内)から呼び出してはいけない
  • 同期処理と非同期処理の境目で利用されることを想定しているため。

non-blocking関数 async/await

lauchのようにブロックしないで非同期処理を行いつつ、戻り値の結果を受け取りたい場合、asyncを利用する。

fun main(args: Array<String>) = runBlocking() {
    println("start")
    val text = async {
        "coroutine!"
    }.await()
    println(text)
    println("end")
}

asyncの戻り値のDeferred インタフェースに実装されているawaitを呼び出すと、Coroutine終了まで
現在のコルーチンを中断し、終了したCoroutineの戻り値を取得する。

Suspending Functionとは?

Coroutineを中断させることができる関数を「Suspending Function」と言う。

suspend fun doSomething() {
    delay(1000)
    println("something")
}

suspend modifierを付けることでCoroutineを中断できることを示します。
Coroutine内で呼び出す関数はsuspendをつけないとコンパイルエラーになります。

上記で呼び出しているdelayもSuspending Functionになります。

また、launch、runBlockingはラムダを用いてCoroutineを生成していましたが、これは無名のSuspending Lambdaになっています。

lauchの定義は以下の通りで、blockのところで無名のSuspending Lambdaを生成しています。

public fun launch(
    context: CoroutineContext = DefaultDispatcher,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

Suspending Functionの利用方法は以下

fun main(args: Array<String>) {
    println("start")
    runBlocking {
        println("coroutine start")
        greet()
        println("coroutine end")
    }
    println("end")
}

suspend fun greet() { // suspendがないとエラーになる
    delay(1000)
    println("Hello, Coroutine!")
}

greetは、runBlockingで生成したCoroutineを中断し、処理が終わったら、Coroutineを再開します。
そして、実行スレッドに結果があれば戻すことも可能です。

MySQL ギャップロックとは?

現場でギャップロックという聞き慣れない用語を耳にしたので、

以下の記事を参考に理解する。

MySQLのギャップロックとネクストキーロック - 41から始めました

MySQLロック

ロックの種類

  • 排他ロック
  • 共有ロック
    • 排他ロックがかかったデータを参照する際にかかるロック
    • ロックを保持するトランザクションによる行の読み取りが許可、書き込み不可
    • 共有ロック中データに対して、他トランザクションでも共有ロックは可能。そういう意味で共有ロック

ロック中データ(行)の扱い

InnoDBトランザクション分離レベルによって、ロック中の行の書き込み不可や読み取り不可などにできる。
MySQLInnoDBのデフォルト分離レベルはREPEATABLE READで、ロック中の行の書き込み不可かつ、読み取りは可能な状態になっている。

トランザクション分離レベル

下に行くほど分離レベルが高い

分離レベル 説明 デフォルト設定採用のDMBS
READ UNCOMMITTED コミットされていない変更を他のトランザクションから参照可 -
READ COMMITTED コミットされた変更を他のトランザクションから参照可 OraclePostgreSQLSQL Server
REPEATABEL READ コミットされ、トランザクション終了後の変更を他のトランザクションから参照可 MySQL
SERIALIZABLE 強制的にトランザクションを順序付けて処理。 -

ギャップロックとは?

InnoDBには3種類のロックがあります。

  • レコードロック:単一レコードのロック
  • ギャップロック:各種ギャップに対するロック
  • ネクスキーロック:レコードロックとそのレコードの直前のギャップロックの組み合わせ

ギャップロックのギャップは以下の三つで、MySQLでは対象レコードと以下のギャップにあるレコードにロックがかかります。

  • index値を持つ行と行の間にあるギャップ
    • レコードIDが1、3をロックしたときにID=2(間のギャップ)へのレコード挿入がロック待ちとなる
  • 先頭のindex値を持つ行の前のギャップ
    • レコードIDが4、5をロックしたとき、ID=3(先頭のギャップ)へのレコード挿入がロック待ちとなる。ID=2なら挿入可
  • 末尾のindex値を持つ行の後のギャップ
    • レコードIDが4、5をロックしたとき、ID=6(末尾のギャップ)へのレコード挿入がロック待ちとなる。ID=7なら挿入可

MySQLでデットロックや更新タイムアウトが発生した際に、ギャップロックを思い出すと問題解決になるかもしれない。