阻塞一个线程到底意味着什么?

3
给定
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    GlobalScope.launch {
        doNetworkCall()
        doNetworkCall2()
    }
}

suspend fun doNetworkCall() {
   delay(3000)
  Log.d("TRACE", "doNetworkCall1 in ${Thread.currentThread().name}")
}

suspend fun doNetworkCall2() {
  delay(3000)
  Log.d("TRACE", "doNetworkCall2 in ${Thread.currentThread().name}")
}

结果是睡眠3秒钟,然后打印"在DefaultDispatcher-worker-1中的doNetworkCall1",再睡眠3秒钟,然后打印"在DefaultDispatcher-worker-1中的doNetworkCall2"。

根据延迟的Javadoc,"延迟协程一段时间而不阻塞线程"。

在我的例子中,我启动了一个协程。这个协程被延迟了两次,首先在打印"doNetworkCall1..."之前延迟3秒钟,然后在打印"doNetworkCall2..."之前延迟。但是我们在同一个线程worker-1中,很明显这个线程被阻塞了,因为这些日志是按顺序打印的。

另外,假设我有一种方法可以在这个协程之后启动另一个在同一个线程(线程1)上运行的协程。第二个协程不会等待第一个协程,对吗?换句话说,虽然协程体内的代码按顺序执行,但是不同协程之间的调用是异步执行的,即无序执行。

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  GlobalScope.launch {
    doNetworkCall()
    doNetworkCall2()
  }
  // pretend this runs in the same thread as coroutine 1, I just dont know how to do that
  GlobalScope.launch {
    doNetworkCall3()
    doNetworkCall4()
  }
}

suspend fun doNetworkCall() {
  delay(3000)
  Log.d("TRACE", "doNetworkCall1 in 
  ${Thread.currentThread().name}")
}

suspend fun doNetworkCall2() {
  delay(3000)
  Log.d("TRACE", "doNetworkCall2 in 
  ${Thread.currentThread().name}")
}

所以doNetworkCall1()必须在doNetworkCall2()开始之前执行,doNetworkCall3()必须在doNetworkCall4()开始之前执行,但是不知道doNetworkCall1()或doNetworkCall3()哪个先运行,因为协程不会阻塞线程,特别是线程1。

你只启动了一个协程。同时启动两个协程,你就会得到并发的延迟。 - undefined
2个回答

3
但是我们在同一个线程中,worker-1,这个线程明显被阻塞,因为这些日志是按顺序打印的。
协程以以下方式违反了您的训练直觉:当您的代码似乎调用delay(3000)并等待其完成时,实际上编译后的代码从delay()返回一个特殊的结果值COROUTINE_SUSPENDED,并且所有调用函数也将其返回,直到最内层的不可挂起的调用函数(在您的情况下是launch)。该函数的内部将特殊的Continuation对象(在所有可挂起函数中作为隐藏参数出现)附加到Runnable上,并将其传递给负责线程的计划任务执行器。执行器将在3000毫秒后运行该任务。
当涉及的线程是GUI线程时,精确的技术细节有些不同,但本质上没有什么变化。 这里有另一个带有代码的答案,应该有助于揭示这个故事的一些方面。

这个回答很有帮助,但并没有解决标题提出的问题... - undefined
我的结论是这是 OP 困惑的本质。这在他们指出的“明显阻塞线程”中是显而易见的。这解释了协程被阻塞而不阻塞其线程的看似矛盾之处。 - undefined

3

看起来你大致上理解了。

当我们说线程被阻塞时,意味着它被当前任务独占,无法用于其他任何事情。通常情况下,我们使用这个术语是指“任务”实际上正在等待某些东西,所以线程并没有做任何实际的工作,但它仍然被完全占用,无法利用。

但我们在同一个线程中,worker-1,正如这些日志按顺序打印出来的那样,很明显是被阻塞了。

不,线程并没有被阻塞。协程是“被阻塞”的,它在原地等待,但底层的线程并没有被阻塞,它可以在这个协程等待的同时执行另一个协程。

展示阻塞和挂起的最简单方法是在单个线程上安排并发任务。我使用了类似于你的示例,先使用协程,然后使用更经典的Java执行器方法。协程:

fun main() {
    val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()

    GlobalScope.launch(dispatcher) {
        println("1 in ${Thread.currentThread().name}")
        delay(1000)
        println("2 in ${Thread.currentThread().name}")
    }

    Thread.sleep(500)

    GlobalScope.launch(dispatcher) {
        println("3 in ${Thread.currentThread().name}")
        delay(1000)
        println("4 in ${Thread.currentThread().name}")
    }
}

输出结果为:1 3 2 4,每500毫秒显示一行,总共1500毫秒。
现在执行者,几乎相同的代码:
fun main() {
    val executor = Executors.newSingleThreadExecutor()

    executor.submit {
        println("1 in ${Thread.currentThread().name}")
        Thread.sleep(1000)
        println("2 in ${Thread.currentThread().name}")
    }

    Thread.sleep(500)

    executor.submit {
        println("3 in ${Thread.currentThread().name}")
        Thread.sleep(1000)
        println("4 in ${Thread.currentThread().name}")
    }
}

输出为:1 2 3 4,第二个任务必须等待第一个任务完成,所以总共需要2000毫秒。
结论:如果使用协程,即使是单线程,我们可以在延迟、等待I/O或等待另一个协程时重用线程。

1
首个任务在T+0时运行,所以在T+0时显示1,在T+1000时显示2。第二个任务在T+500开始,所以在T+500时显示3,在T+1500时显示4。因此,我们在0、500、1000和1500时分别看到1 3 2 4。如果使用Java执行器,则在0、1000、1000和2000时分别显示1 2 3 4 - undefined
1
另外,我认为你过于关注线程在做什么。相反,你应该考虑协程在做什么,并将线程留给协程机制。你上面说的并不完全正确:协程的执行始终是顺序的,无论我们是否运行另一个协程。此外,即使我们只启动了一个协程,在delay()之后我们也不知道哪个线程会接手它。在你的例子中,它被之前的线程接手了,但实际上可以是任何其他线程。 - undefined
抱歉之前我用错了调度程序。我们的执行顺序是:
  1. 打印1
  2. 开始协程1的延迟(1000毫秒)
  3. 线程休眠500毫秒
  4. 协程1暂停,等待协程2打印3
  5. 开始协程2的延迟(1000毫秒)
  6. 返回协程1,从步骤2继续延迟,剩余500毫秒
  7. 打印2
  8. 在协程2中延迟,剩余500毫秒
  9. 打印4
由于协程1和协程2之间的切换,我的断言是你不能按顺序读取它,即协程1中的行必须在协程2中的行之前开始,因为如果是这样的话,它会打印1 2 3 4。
- undefined
这基本上是正确的,但我觉得你假设一个协程需要另一个协程来挂起/恢复。"co 1 挂起以便 coroutine 2 打印 3" - co 1 无论是否有另一个协程,都会挂起。它甚至不知道有另一个协程的存在。"返回到 co 1 以恢复从步骤 2 的延迟,剩余 500 毫秒" - 它不是恢复延迟,而是在延迟之后恢复。 - undefined
我们在这里无法百分之百确定执行顺序,因为我们进行并发处理,但我可以说:1. 主线程启动协程1。2. 主线程开始休眠500毫秒。3. 协程1启动,打印1并开始等待1000毫秒。4. 主线程恢复并启动协程2。5. 协程2启动,打印3并开始等待1000毫秒。6. 协程1等待后恢复,打印2并完成。7. 协程2等待后恢复,打印4并完成。 - undefined
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接