用于执行外部进程的 Kotlin 协程

dan*_*ito 5 kotlin kotlin-coroutines

在Java中的传统方法执行外部程序是开始一个新的Process,开始两个线程消耗其inputStreamerrorStream,然后调用其阻塞Process.waitFor()等到外部命令已退出。

如何使用 Kotlin 协程以(几乎)非阻塞方式完成此操作?

我是这样试的。你有什么建议可以改进它吗?

(如何异步读取流,也调用ProcessBuilder.start()in withContext(Dispatchers.IO),对 Dispatchers.IO 的调用是否过多,...?)

    suspend fun executeCommand(commandArgs: List<String>): ExecuteCommandResult {
        try {
            val process = ProcessBuilder(commandArgs).start()

            val outputStream = GlobalScope.async(Dispatchers.IO) { readStream(process.inputStream) }
            val errorStream = GlobalScope.async(Dispatchers.IO) { readStream(process.errorStream) }

            val exitCode = withContext(Dispatchers.IO) {
                process.waitFor()
            }

            return ExecuteCommandResult(exitCode, outputStream.await(), errorStream.await())
        } catch (e: Exception) {
            return ExecuteCommandResult(-1, "", e.localizedMessage)
        }
    }

    private suspend fun readStream(inputStream: InputStream): String {
        val readLines = mutableListOf<String>()

        withContext(Dispatchers.IO) {
            try {
                inputStream.bufferedReader().use { reader ->
                    var line: String?

                    do {
                        line = reader.readLine()

                        if (line != null) {
                            readLines.add(line)
                        }
                    } while (line != null)
                }
            } catch (e: Exception) {
                // ..
            }
        }

        return readLines.joinToString(System.lineSeparator())
    }
Run Code Online (Sandbox Code Playgroud)

Joa*_*nha 1

我刚刚遇到这个 3 年前的问题,并考虑改进这段代码。我认为这段代码已经是最优化的方法,因为当涉及到从源头阻止操作时,我们实际上无能为力,但我认为您可以节省一些上下文切换并改进代码,如下所示:

suspend fun executeCommand(commandArgs: List<String>): ProcessResult = withContext(
    Dispatchers.IO
) {
    runCatching {
        val process = ProcessBuilder(commandArgs).start()
        val outputStream = async {
            println("Context for output stream -> $coroutineContext -> Thread -> ${Thread.currentThread()}")
            readStream(process.inputStream) }
        val errorStream = async {
            println("Context for error stream -> $coroutineContext -> Thread -> ${Thread.currentThread()}")
            readStream(process.errorStream)
        }
        println("Context for exit code -> $coroutineContext -> Thread -> ${Thread.currentThread()}")
        val exitCode =  process.waitFor()
        ProcessResult(
            exitCode = exitCode,
            message = outputStream.await(),
            errorMessage = errorStream.await()
        )
    }.onFailure{
        ProcessResult(
            exitCode = -1,
            message = "",
            errorMessage = it.localizedMessage
        )
    }.getOrThrow()
}

private fun readStream(inputStream: InputStream) =
    inputStream.bufferedReader().use { reader -> reader.readText() }

data class ProcessResult(val exitCode: Int, val message: String, val errorMessage: String)
Run Code Online (Sandbox Code Playgroud)

忽略原理即可。如果您想清楚地看到协程在哪个上下文中运行,我将其留在那里。这也使得更明显地看到它们共享相同的作用域并且它们在不同的线程上运行。但在进入生产代码时应该将它们删除。