読み込み中...

Kotlinで学ぶ!別スレッドでの実行のたった10の方法

Kotlinで別スレッド実行の方法を学ぶイメージ Kotlin
この記事は約23分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

Kotlinでの別スレッド実行方法に関心を持たれているあなたへ。

この記事を通して、Kotlinでの別スレッド実行の基本から応用に至るまでのステップを一緒に学んでいきましょう。

別スレッド実行は、アプリケーションのパフォーマンスを向上させるために非常に重要な要素です。

特に、重たい処理をメインスレッド外で行いたいときや、同時に複数の処理を行いたい場合には欠かせません。

この記事では、Kotlinでの別スレッド実行方法を10の具体的なステップとサンプルコードとともに解説します。

初心者の方でも安心して学べる内容となっていますので、最後までしっかりとお付き合いください。

●Kotlinとは

Kotlinは、2011年にJetBrains社によって開発された静的型付けのプログラミング言語です。

Javaとの互換性が高く、Androidの公式言語としても採用されています。

しかし、それだけがKotlinの魅力ではありません。

簡潔で表現力が高く、安全性を持ち合わせた言語設計になっているため、多くの開発者から支持を受けています。

○Kotlinの特徴と強み

  1. 簡潔性:Kotlinは、冗長なコードを減少させるための機能が豊富に用意されています。これにより、コード量が減少し、読みやすいコードを書くことが可能です。
  2. 安全性:Kotlinは、Null安全をはじめとする多くの安全性に関する機能を提供しています。これにより、ランタイムエラーの発生を大幅に削減することができます。
  3. Java互換性:KotlinはJavaとの高い互換性を持っているため、Javaのライブラリやフレームワークをそのまま利用することができます。
  4. モダンな言語設計:ラムダ式や拡張関数、データクラスなど、現代のプログラミングに適した機能が備わっています。

このような特徴と強みを持つKotlinは、新しいプロジェクトだけでなく、既存のJavaプロジェクトにも積極的に取り入れられています。

特に、Android開発においては、Kotlinの採用が進められており、今後さらなる普及が期待されています。

●別スレッドでの実行とは

別スレッドでの実行とは、主となるメインスレッド以外のスレッドでプログラムの一部を実行することを指します。

多くのプログラムは、ユーザーの操作を受け取ったり、結果を表示したりするためのメインスレッドがあります。

しかし、メインスレッドだけで重たい処理を行うと、アプリケーションが停止したり、応答しなくなることがあります。

このような状況を避けるために、別のスレッドで処理を行うことが推奨されています。

○なぜ別スレッドでの実行が必要なのか

メインスレッドはGUI(グラフィカルユーザーインターフェース)の更新やユーザーからの入力を受け付ける役割があります。

このメインスレッド上で時間がかかる処理を行うと、アプリケーションの動作が遅くなったり、完全に停止してしまったりする可能性があります。

例えば、大量のデータを処理する場合や、インターネットからのデータ取得などの非同期処理を行う場合には、これをメインスレッドで行うとアプリケーションの反応が鈍くなってしまいます。

このような問題を避けるために、時間のかかる処理は別のスレッドで行い、メインスレッドは軽快に動作させることが重要です。

別スレッドでの処理を行うことで、メインスレッドはユーザーの操作をスムーズに受け付け、応答することができるようになります。

○スレッドとプロセスの違い

スレッドとプロセスは、プログラムの実行単位としてよく聞かれる用語ですが、その違いを理解することは非常に重要です。

簡単に言えば、プロセスはアプリケーションの実行単位で、それぞれ独立したメモリ領域を持っています。

一方、スレッドはプロセス内の実行単位で、同じプロセス内のスレッド同士はメモリ領域を共有しています。

具体的には、例えばブラウザやエディタなどのアプリケーションが1つのプロセスとして動作します。

そして、そのアプリケーション内で複数のタスクを並行して実行するためのものがスレッドです。

複数のスレッドが同じメモリ領域を共有することで、データのやり取りが効率的に行われますが、その分、データの不整合などの問題に注意が必要です。

●Kotlinでの別スレッド実行の基本

Kotlinでは、別スレッドでの実行を容易にする多数の機能やライブラリが提供されています。

これにより、開発者は複雑なスレッド管理を意識せずに非同期処理を行うことができるようになりました。

○別スレッドのメリットとデメリット

別スレッドでの実行には多くのメリットがあります。

一つ目は、メインスレッドをブロックすることなく時間のかかる処理をバックグラウンドで行えることです。

これにより、アプリケーションの応答性が向上します。

二つ目のメリットとして、マルチコアのCPUを効果的に利用できる点が挙げられます。

これにより、処理速度の向上が期待できます。

一方で、別スレッドの実行にはいくつかのデメリットも存在します。

メインスレッドとは異なるスレッドでの処理は、データの競合やスレッド間の同期の問題を引き起こす可能性があります。

また、多数のスレッドを作成することは、システムのオーバーヘッドを増加させるリスクがあります。

○Kotlinでのスレッドの基本構造

Kotlinでは、Javaと同じくThreadクラスを利用してスレッドを作成することができます。

ここでは、新しいスレッドを作成し、実行する基本的なコードのサンプルを紹介します。

// 新しいスレッドを作成
val thread = Thread {
    println("これは別スレッドでの処理です。")
}

// スレッドの開始
thread.start()

// スレッドが終了するまで待機
thread.join()

このコードでは、Threadクラスのインスタンスを作成する際に、ラムダ式を使ってスレッドで実行される処理を指定しています。

thread.start()でスレッドの実行を開始し、thread.join()でスレッドが終了するまでメインスレッドを待機させています。

●別スレッドでの実行の方法10選

Kotlinでの別スレッド実行方法にはさまざまな方法が存在します。

多様なアプリケーションニーズに応じて適切な方法を選択し、効率的な非同期処理を実装することが求められます。

○サンプルコード1:Kotlinでの基本的な別スレッドの起動

Kotlinで新しいスレッドを起動する基本的な方法は、Javaと非常に似ています。

ここでは、Kotlinで新しいスレッドを起動し、そのスレッドでの処理を表示するサンプルコードを紹介します。

val thread = Thread {
    println("別スレッドでの処理です。")
}
thread.start()

このコードを実行すると、”別スレッドでの処理です。”というメッセージがコンソールに表示されます。

こちらは非常にシンプルな例ですが、新しいスレッドを起動する基本的な手法を示しています。

○サンプルコード2:Kotlinのコルーチンを用いた非同期処理

Kotlinの強力な機能の一つに「コルーチン」があります。

コルーチンは、軽量なスレッドのように動作し、非同期処理を簡単に実装できるツールです。

ここでは、コルーチンを用いた非同期処理のサンプルコードを紹介します。

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch {
        delay(1000L)  // 1秒待機
        println("コルーチンでの非同期処理です。")
    }
    println("メインスレッドの処理")
    Thread.sleep(2000L)  // 2秒待機
}

このコードでは、GlobalScope.launchを使って非同期処理を開始しています。

delay関数で1秒待機した後、”コルーチンでの非同期処理です。”というメッセージを表示します。

一方、メインスレッドでは直ちに”メインスレッドの処理”というメッセージを表示します。

○サンプルコード3:Dispatchersの種類と使い方

コルーチンの世界では、Dispatcherが非常に重要な役割を果たしています。

Dispatcherは、コルーチンがどのスレッドで実行されるかを制御し、それによって非同期処理の動作が大きく変わることがあります。

ここでは、Dispatchersの主要な種類とそれぞれの使い方について説明します。

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch(Dispatchers.Default) {
        println("Default: ${Thread.currentThread().name}")
    }

    launch(Dispatchers.IO) {
        println("IO: ${Thread.currentThread().name}")
    }

    launch(Dispatchers.Unconfined) {
        println("Unconfined: ${Thread.currentThread().name}")
    }

    launch(newSingleThreadContext("MyOwnThread")) {
        println("newSingleThreadContext: ${Thread.currentThread().name}")
    }
}

このコードでは、それぞれ異なるDispatcherを使ってコルーチンを起動しています。

それぞれのDispatcherがどのように動作するかを見てみましょう。

  • Dispatchers.Defaultは、CPUの利用が多いタスクに最適です。これはデフォルトのDispatcherで、コンピューティング処理に適しています。
  • Dispatchers.IOは、I/O処理に最適化されています。ファイルの読み書き、ネットワーク通信など、ブロッキングI/Oに対して効率的です。
  • Dispatchers.Unconfinedは、制限がなく、呼び出されたスレッドでコルーチンを開始し、その後は第一の一時停止ポイントまで実行します。
  • newSingleThreadContextは、新しいスレッドを作成します。名前を指定できるので、デバッグ時にどのスレッドで実行されているかを識別しやすくなります。

このサンプルコードを実行すると、それぞれのDispatcherが表示され、それぞれのコルーチンがどのスレッドで実行されるかを確認することができます。

○サンプルコード4:非同期タスクの結果をメインスレッドで取得

非同期処理を行う際、バックグラウンドで処理を行い、その結果をメインスレッドでUIに反映させる、というケースは非常に多いです。

下記のコードは、非同期処理の結果をメインスレッドで取得し、それを利用する一例です。

import kotlinx.coroutines.*

fun main() = runBlocking {
    val data = async(Dispatchers.IO) {
        // 非同期でデータを取得
        "取得したデータ"
    }

    println("メインスレッド: ${Thread.currentThread().name}")
    val result = data.await()
    println("非同期タスクの結果: $result")
}

このコードではasyncを用いてIOスレッドで非同期にデータを取得しています。

そしてawaitメソッドを用いて非同期タスクの結果をメインスレッドで取得しています。

このようにasyncawaitを用いて、簡潔に非同期処理の結果をメインスレッドで取得し、利用することができます。

実行すると、メインスレッドで非同期処理の結果が取得され、”非同期タスクの結果: 取得したデータ”と表示されます。

○サンプルコード5:スレッドプールの利用方法

スレッドプールは、複数のスレッドを効率的に管理し、再利用するための仕組みです。

大量のタスクを同時に実行する際や、リソースを効率的に使用したい場合に、スレッドプールを利用するとパフォーマンスの向上やリソースの節約が期待できます。

Kotlinでは、Executorsクラスを利用してスレッドプールを作成・利用することができます。

下記のサンプルコードは、固定数のスレッドを持つスレッドプールを作成し、その中でタスクを実行する例です。

import java.util.concurrent.Executors
import kotlinx.coroutines.*

fun main() {
    // スレッドプールを作成(3つのスレッドを持つ)
    val customDispatcher = Executors.newFixedThreadPool(3).asCoroutineDispatcher()

    runBlocking {
        repeat(10) { i ->
            launch(customDispatcher) {
                println("タスク $i は ${Thread.currentThread().name} で実行されました。")
                delay(1000L)
            }
        }
    }

    customDispatcher.close()
}

このコードでは、3つのスレッドを持つスレッドプールを作成しています。

その後、10のタスクをそのスレッドプール上で実行しており、タスクは3つのスレッド間で分散されて実行されます。

タスクが実行されると、それぞれのタスクがどのスレッドで実行されたかが表示されます。

このようにスレッドプールを使用すると、スレッドの作成と破棄のオーバーヘッドを減少させ、リソースを効率的に使用することができます。

○サンプルコード6:Kotlinでのエラーハンドリング

非同期処理中にエラーが発生する可能性は常に考慮する必要があります。Kotlinのコルーチンでは、エラーハンドリングも簡潔に行うことができます。

ここでは、非同期処理中にエラーが発生した場合のハンドリング方法を表すサンプルコードを紹介します。

import kotlinx.coroutines.*

fun main() = runBlocking {
    val deferred = async {
        println("非同期処理を開始します。")
        throw Exception("エラーが発生しました!")
    }

    try {
        deferred.await()
    } catch (e: Exception) {
        println("エラーハンドリング: ${e.message}")
    }
}

このコードでは、非同期処理の中で意図的に例外をスローしています。

そして、awaitメソッドを用いて結果を取得する際に、try-catchブロックを使用してエラーをハンドリングしています。

このように、通常の同期処理と同様の方法で、エラーハンドリングを行うことができます。

実行すると、「エラーハンドリング: エラーが発生しました!」というメッセージが表示され、適切にエラーが捕捉されていることが確認できます。

○サンプルコード7:非同期処理のキャンセル方法

非同期処理を開始した後でも、状況や要件によっては途中で処理をキャンセルしたい場面が生じることがあります。

Kotlinのコルーチンを使用して非同期処理を行う際も、このようなキャンセルの要求に柔軟に対応することができます。

Kotlinでは、Jobオブジェクトを用いてコルーチンの生存期間やキャンセル操作を管理します。

下記のサンプルコードは、非同期処理を開始した後に一定時間が経過した時点でその処理をキャンセルする方法を表しています。

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            println("タスク $i は進行中")
            delay(500L)
        }
    }

    delay(3000L)  // 3秒待機
    println("非同期処理をキャンセルします。")
    job.cancel()
    job.join()
    println("非同期処理はキャンセルされました。")
}

上記のコードを実行すると、タスクが進行中であることを示すメッセージが何度か出力されます。

しかし、3秒経過後に非同期処理はキャンセルされ、「非同期処理はキャンセルされました」というメッセージが表示されます。

このようにjob.cancel()メソッドを使用することで、実行中の非同期処理を安全にキャンセルすることが可能です。

○サンプルコード8:スレッド間のデータ共有

非同期処理の中で生成されたデータを、他のスレッドやメインスレッドで使用する必要が生じる場面も考えられます。

このような場面でデータを安全に共有するための仕組みとして、KotlinではMutableSharedFlowStateFlowといったフローを提供しています。

下記のサンプルコードでは、非同期処理で生成されたデータをメインスレッドで受け取り、表示する方法を表しています。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val sharedFlow = MutableSharedFlow<String>()

    // 非同期処理でデータを生成
    launch {
        for (i in 1..5) {
            sharedFlow.emit("データ $i")
            delay(1000L)
        }
    }

    // メインスレッドでデータを受け取り表示
    sharedFlow.collect { data ->
        println("受け取ったデータ: $data")
    }
}

上記のコードを実行すると、非同期処理で生成されたデータが順次メインスレッドに渡され、「受け取ったデータ: データ X」という形で出力されます。

MutableSharedFlowを使用することで、非同期処理の間でデータを安全にやり取りすることができます。

○サンプルコード9:高度な非同期処理の実装

Kotlinでの非同期処理は非常に柔軟性が高く、さまざまなパターンでの実装が可能です。

ここでは、高度な非同期処理の一例として、複数の非同期タスクを並列に実行し、その結果を統合する方法について解説します。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val result1 = async { task1() }
    val result2 = async { task2() }

    println("タスク1とタスク2の結果の合計: ${result1.await() + result2.await()}")
}

suspend fun task1(): Int {
    delay(2000L)
    return 100
}

suspend fun task2(): Int {
    delay(1500L)
    return 200
}

上記のサンプルコードでは、task1task2という2つの非同期タスクを並列に実行しています。

async関数を使用することで、各タスクは非同期に実行され、await関数を用いてその結果を取得しています。

このコードを実行すると、各タスクから返された結果が合計されて出力されます。

具体的には、task1は2秒かかって100を返し、task2は1.5秒かかって200を返します。

これらのタスクは並列に実行されるので、全体の実行時間は2秒となります。

その結果、”タスク1とタスク2の結果の合計: 300″というメッセージが表示されます。

○サンプルコード10:別スレッドでのDB操作

データベース操作は、メインスレッドで実行するとアプリケーションの応答性に影響を与える可能性があります。

そのため、Kotlinでは別スレッドでデータベース操作を行うことが推奨されています。

ここでは、非同期でデータベースにアクセスし、データを取得するサンプルコードを紹介します。

import kotlinx.coroutines.*
import java.sql.Connection
import java.sql.DriverManager
import java.sql.ResultSet

fun main() = runBlocking {
    val data = async(Dispatchers.IO) { fetchFromDatabase() }
    println("データベースから取得したデータ: ${data.await()}")
}

fun fetchFromDatabase(): List<String> {
    val connection: Connection = DriverManager.getConnection("jdbc:your_database_url")
    val statement = connection.createStatement()
    val resultSet: ResultSet = statement.executeQuery("SELECT * FROM your_table")

    val dataList = mutableListOf<String>()
    while (resultSet.next()) {
        dataList.add(resultSet.getString("your_column_name"))
    }

    connection.close()
    return dataList
}

このコードのfetchFromDatabase関数は、データベースにアクセスしてデータを取得する処理を行っています。

この関数は、async関数とDispatchers.IOを用いて非同期で実行されます。

その結果、データベースから取得したデータがコンソールに出力されます。

注意点として、このコードはデータベース接続のためのJDBCドライバや接続情報を前提としています。

具体的な実装前に、適切な設定やライブラリの追加が必要です。

●注意点と対処法

Kotlinで非同期処理や別スレッドでの実行を行う際には、いくつかの注意点や問題点が存在します。

適切な知識を持つことで、これらの問題を回避や対処することができます。

ここでは、非同期処理や別スレッド実行における主要な注意点とその対処法について詳しく解説します。

○別スレッドでの処理時の一般的な問題点

別スレッドでの処理はアプリケーションのパフォーマンス向上に寄与する反面、次のような問題が生じる可能性があります。

  1. 競合状態(Race Condition):複数のスレッドが同時にデータにアクセスし、データの不整合が生じること。
  2. デッドロック:複数のスレッドがリソースを待ち合う状態で、どのスレッドも進行できなくなること。

このような問題を解消するためのサンプルコードを紹介します。

import kotlinx.coroutines.*
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

val lock = ReentrantLock()

fun main() = runBlocking {
    val job1 = launch {
        lock.withLock {
            // クリティカルセクションの処理
        }
    }
    val job2 = launch {
        lock.withLock {
            // クリティカルセクションの処理
        }
    }

    job1.join()
    job2.join()
}

このコードでは、ReentrantLockを利用して、クリティカルセクションの処理をロックしています。

これにより、一度に一つのスレッドだけがこのセクションを実行し、競合状態やデッドロックのリスクを減少させることができます。

○メモリリークの予防と対処法

非同期処理やスレッドの操作中にオブジェクトが不適切に保持され続けると、メモリリークが発生する可能性があります。

これは、アプリケーションのパフォーマンス低下やクラッシュの原因となり得ます。

ここでは、メモリリークを回避するための簡単なサンプルコードを紹介します。

import kotlinx.coroutines.*

var globalScope: CoroutineScope? = CoroutineScope(Job())

fun main() = runBlocking {
    globalScope?.launch {
        // 何らかの長時間の処理
    }

    // その他の処理

    // CoroutineScopeを終了
    globalScope?.cancel()
}

このコードのポイントは、CoroutineScopeを使って非同期タスクを制御し、不要になった場合にcancel()メソッドでタスクを終了することです。

これにより、メモリリークを予防しつつ、リソースの適切な解放ができます。

●別スレッドでのカスタマイズ方法

Kotlinでの別スレッド処理において、実行環境や要件に応じてカスタマイズを行うことはよくあるシチュエーションです。

ここでは、別スレッドの実行優先度の変更方法やスレッドの拡張方法について、サンプルコードを交えて詳しく解説します。

○別スレッドの実行優先度の変更方法

別スレッドでの処理は、その優先度によって実行速度やリソースの取得順が異なります。

Kotlinでのスレッドの優先度を変更する方法を示します。

import kotlin.concurrent.thread

fun main() {
    val threadA = thread(start = true, priority = 10) {
        // 高優先度の処理
    }

    val threadB = thread(start = true, priority = 1) {
        // 低優先度の処理
    }

    threadA.join()
    threadB.join()
}

このコードは、2つのスレッドを作成しています。

priority パラメータを使って、それぞれのスレッドの優先度を指定しています。

数字が大きいほど、優先度が高くなります。

○スレッドの拡張方法

Kotlinの拡張関数を利用することで、スレッドの機能を簡単に拡張することができます。

例えば、スレッドが終了した際に何らかの処理を追加する方法を紹介します。

import kotlin.concurrent.thread

fun Thread.onFinish(action: () -> Unit): Thread {
    thread(start = true) {
        this.join()
        action()
    }
    return this
}

fun main() {
    val myThread = thread(start = true) {
        // 何らかの処理
    }.onFinish {
        println("スレッドが終了しました。")
    }

    myThread.join()
}

上記のコードでは、onFinishという拡張関数をThreadクラスに追加しています。

この関数は、スレッドが終了した際に指定された処理を実行します。

上記のサンプルコードの結果、main関数内でmyThreadが終了した際、コンソールに”スレッドが終了しました。”と表示されます。

まとめ

Kotlinを使用して別スレッドでの処理を実行する方法は、アプリケーションのパフォーマンスや応答性を向上させるために非常に重要です。

この記事を通じて、Kotlinの非同期処理やスレッド関連の基本的な概念、その使い方やカスタマイズ方法を解説しました。

特に、非同期処理の基本から高度な実装、そして注意点やカスタマイズ方法まで、幅広く詳しく解説しました。

Kotlinにおける別スレッドでの実行方法やカスタマイズは、プログラミングの幅を広げ、より高品質なアプリケーションを作成する上でのキーとなります。

今後は、この知識を活用して、ユーザーにとって快適なアプリケーションを開発していくことが期待されます。

新たな技術や手法が日々進化しているため、常に最新の情報をキャッチアップし、技術の深化を図ることが大切です。

Kotlinでのプログラミングを楽しみながら、さらに高度な技術を習得していきましょう。