Kotlin 協(xié)程入門

本文主要介紹協(xié)程長什么樣子, 協(xié)程是什么東西, 協(xié)程掛起的實(shí)現(xiàn)原理以及整理了協(xié)程學(xué)習(xí)的資料.

協(xié)程 HelloWorld

協(xié)程在官方指南中被稱為一種輕量級的線程, 所以在介紹協(xié)程是什么東西之前, 這里通過幾個與線程對比的小例子初步認(rèn)識協(xié)程.

啟動線程與啟動協(xié)程

/* Kotlin code - Example 1.1 */
// 創(chuàng)建一條新線程并輸出 Hello World.
thread {
    println("使用線程輸出 Hello World! Run in ${Thread.currentThread()}")
}

// 創(chuàng)建一個協(xié)程并使用協(xié)程輸出 Hello World.
GlobalScope.launch {
    println("使用協(xié)程輸出 Hello World! Run in ${Thread.currentThread()}")
}

/* output */
使用線程輸出 Hello World! Run in Thread[Thread-0,5,main]
使用協(xié)程輸出 Hello World! Run in Thread[DefaultDispatcher-worker-1,5,main]

上面的例子是一個簡單的輸出 Hello World 的程序. 在這個例子中, 我們可以看到創(chuàng)建并啟動一條協(xié)程和創(chuàng)建并啟動一條線程的代碼幾乎一致, 唯一不同的就是創(chuàng)建線程調(diào)用的是 #thread 方法, 而創(chuàng)建協(xié)程調(diào)用的是 GlobalScope#launch 方法.

暫停線程與暫停協(xié)程

/* Kotlin code - Example 1.2 */
fun demoSleep() {
    // 創(chuàng)建并運(yùn)行一條線程, 在線程中使用 Thread#sleep 暫停線程運(yùn)行 100ms.
    thread {
        val useTime = measureTimeMillis {
            println("線程啟動")
            println("線程 sleep 開始")
            Thread.sleep(100L)
            println("線程結(jié)束")
        }
        println("線程用時為 $useTime ms")
    }
}

fun demoDelay() {
    // 創(chuàng)建并運(yùn)行一條協(xié)程, 在協(xié)程中使用 #delay 暫停協(xié)程運(yùn)行 100 ms.
    GlobalScope.launch {
        val useTime = measureTimeMillis {
            println("協(xié)程啟動")
            println("協(xié)程 delay 開始")
            delay(100L)
            println("協(xié)程結(jié)束")
        }
        println("協(xié)程用時為 $useTime ms")
    }
}

/* output */
線程啟動
線程 sleep 開始
線程結(jié)束
線程用時為 102 ms

協(xié)程啟動
協(xié)程 delay 開始
協(xié)程結(jié)束
協(xié)程用時為 106 ms

上面例子展示了暫停線程和暫停協(xié)程的方法. 我們可以使用 Thread#sleep 方法暫停一條線程, 而暫停一條協(xié)程, 只需要把 Thread#sleep 直接替換成 #delay 就可以了.

等待線程執(zhí)行結(jié)束與等待協(xié)程執(zhí)行結(jié)束

/* Kotlin code - Example 1.3 */
/**
 * 線程等待另外一個線程任務(wù)完成的方法
 */
private fun waitOtherJobThread() {
    // 啟動線程 A
    thread {
        println("線程 A: 啟動")

        // 隨便定義一個變量用于阻塞線程 A
        val waitThreadB = Object()

        // 啟動線程 B
        val threadB = thread {
            println("線程 B: 啟動")
            println("線程 B: 開始執(zhí)行任務(wù)")
            for (i in 0..99) {
                Math.E * Math.PI
            }
            println("線程 B: 結(jié)束")
        }

        // 線程 A 等待線程 B 完成任務(wù)
        println("線程 A: 等待線程 B 完成")
        threadB.join()
        println("線程 A: 等待結(jié)束")

        println("線程 A: 結(jié)束")
    }
}

/**
 * 協(xié)程等待另外一個協(xié)程任務(wù)完成的方法
 */
private fun waitOtherJobCoroutine() {
    // 啟動協(xié)程 A
    GlobalScope.launch {
        println("協(xié)程 A: 啟動")

        // 啟動協(xié)程 B
        val coroutineB = GlobalScope.launch {
            println("協(xié)程 B: 啟動")
            println("協(xié)程 B: 開始執(zhí)行任務(wù)")
            for (i in 0..99) {
                Math.E * Math.PI
            }
            println("協(xié)程 B: 結(jié)束")
        }

        // 協(xié)程 A 等待協(xié)程 B 完成
        println("協(xié)程 A: 等待協(xié)程 B 完成")
        coroutineB.join()
        println("協(xié)程 A: 等待結(jié)束")

        println("協(xié)程 A: 結(jié)束")
    }
}

/* output */
線程 A: 啟動
線程 A: 等待線程 B 完成
線程 B: 啟動
線程 B: 開始執(zhí)行任務(wù)
線程 B: 結(jié)束
線程 A: 等待結(jié)束
線程 A: 結(jié)束

協(xié)程 A: 啟動
協(xié)程 A: 等待協(xié)程 B 完成
協(xié)程 B: 啟動
協(xié)程 B: 開始執(zhí)行任務(wù)
協(xié)程 B: 結(jié)束
協(xié)程 A: 等待結(jié)束
協(xié)程 A: 結(jié)束

在上面的例子中, 創(chuàng)建了一條線程 A(協(xié)程 A), 然后在線程 A(協(xié)程 A)中再創(chuàng)建一條線程 B(協(xié)程 B), 接著使用 #join 方法使線程 A(協(xié)程 A)等待線程 B(協(xié)程 B)執(zhí)行結(jié)束. 我們可以清楚的看到等待線程和等待協(xié)程的代碼幾乎是一致的, 甚至連等待的方法都是 #join.

中斷線程與中斷協(xié)程

/* Kotlin code -  Example 1.4 線程的中斷與協(xié)程的中斷. */
private fun cancelThread() {
    val job1 = thread {
        println("線程: 啟動")

        // 循環(huán)執(zhí)行 100 個耗時任務(wù).
        for (i in 0..99) {
            try {
                Thread.sleep(50L)
                println("線程: 正在執(zhí)行任務(wù) $i...")
            } catch (e: InterruptedException) {
                println("線程: 被中斷了")
                break
            }
        }

        println("線程: 結(jié)束")
    }

    // 延時 200ms 后中斷線程.
    Thread.sleep(200L)
    println("中斷線程!!!")
    job1.interrupt()
}

private fun cancelCoroutine() = runBlocking {
    val job1 = GlobalScope.launch {
        println("協(xié)程: 啟動")

        // 循環(huán)執(zhí)行 100 個耗時任務(wù).
        for (i in 0..99) {
            try {
                delay(50L)
                println("協(xié)程: 正在執(zhí)行任務(wù) $i...")
            } catch (cancelException: CancellationException) {
                println("協(xié)程: 被中斷了")
                break
            }
        }

        println("協(xié)程: 結(jié)束")
    }

    // 延時 200ms 后中斷協(xié)程.
    delay(200L)
    println("中斷協(xié)程!!!")
    job1.cancel()
}

/* output */
線程: 啟動
線程: 正在執(zhí)行任務(wù) 0...
線程: 正在執(zhí)行任務(wù) 1...
線程: 正在執(zhí)行任務(wù) 2...
中斷線程!!!
線程: 被中斷了
線程: 結(jié)束

協(xié)程: 啟動
協(xié)程: 正在執(zhí)行任務(wù) 0...
協(xié)程: 正在執(zhí)行任務(wù) 1...
協(xié)程: 正在執(zhí)行任務(wù) 2...
中斷協(xié)程!!!
協(xié)程: 被中斷了
協(xié)程: 結(jié)束

在上面例子中, 可以看到中斷線程調(diào)用的方法是 #interrupt, 當(dāng)線程被中斷后會拋出 InterruptedException. 中斷協(xié)程的方法為 #cancel. 協(xié)程被中斷后會拋出 CancellationException.

通過上面的幾個小例子, 我們可以看到幾乎每一個線程的方法在協(xié)程中都有一個方法與之對應(yīng). 除了調(diào)用的方法名稱不一樣, 協(xié)程在使用上可以說幾乎和線程沒有特別大的區(qū)別.


協(xié)程是什么?

可掛起的計算實(shí)例。 它在概念上類似于線程苍柏,在這個意義上,它需要一個代碼塊運(yùn)行嫌松,并具有類似的生命周期 —— 它可以被創(chuàng)建與啟動皂冰,但它不綁定到任何特定的線程贿讹。它可以在一個線程中掛起其執(zhí)行,并在另一個線程中恢復(fù)伐谈。而且烂完,像 future 或 promise 那樣,它在完結(jié)時可能伴隨著某種結(jié)果(值或異常)诵棵。

上面這段話引用自 Kotlin 官方協(xié)程設(shè)計文檔中對協(xié)程的描述. 那么這段話應(yīng)該怎么理解呢? 首先, 協(xié)程需要一個計算實(shí)例. 類比與線程, 創(chuàng)建和啟動線程同樣需要一個計算實(shí)例. 對于線程來說, 線程的計算實(shí)例是 Runnable, 我們需要把 Runnable 扔給線程才能在線程中完成計算任務(wù). 對于協(xié)程來說, 這個計算實(shí)例是 suspend 關(guān)鍵字修飾的方法或 lambda 表達(dá)式, 我們需要把這個 suspend 關(guān)鍵字修飾的方法或 lambda 表達(dá)式扔給協(xié)程才能在協(xié)程中完成計算任務(wù). 接著, 除了需要一個計算實(shí)例之外, 協(xié)程中的這個計算實(shí)例還必須是可掛起的, 這也是協(xié)程和線程的區(qū)別. 那么可掛起是什么意思呢? 比如在上面暫停線程與暫停協(xié)程的例子中, 線程和協(xié)程同樣是等待 100ms, 在線程的實(shí)現(xiàn)方式中, 是通過調(diào)用 Thread#sleep 方法阻塞線程來實(shí)現(xiàn)的, 而在協(xié)程的實(shí)現(xiàn)中, 調(diào)用 #delay 實(shí)現(xiàn)的等待是不會阻塞任何線程的(協(xié)程也是運(yùn)行在某一條線程上的). 同樣是等待, 線程等待的實(shí)現(xiàn)方式會阻塞線程, 而協(xié)程等待的實(shí)現(xiàn)方式不會阻塞線程, 所以就把線程的等待稱之為阻塞, 把協(xié)程的等待稱之為掛起. 同樣的, 在上面等待線程執(zhí)行結(jié)束與等待協(xié)程執(zhí)行結(jié)束例子中, 線程調(diào)用 threadB#join 勢必會造成線程 A 的阻塞, 而在協(xié)程中, 調(diào)用 coroutineB#join 也能實(shí)現(xiàn)同樣的功能卻不會造成任何線程的阻塞.

協(xié)程掛起的實(shí)現(xiàn)原理

經(jīng)過上文的簡單介紹, 我們知道了協(xié)程是什么, 協(xié)程和線程的區(qū)別是什么. 這里做個總結(jié), 協(xié)程是一個可掛起的計算實(shí)例, 和線程的區(qū)別就是協(xié)程的計算實(shí)例在執(zhí)行某些需要等待的任務(wù)時是可掛起的, 不阻塞線程的. 那么下面就開始介紹 Kotlin 協(xié)程是怎么實(shí)現(xiàn)等待某些任務(wù)而不阻塞線程的.

/* Kotlin code - Example 2.1*/

/**
 * 自定義一個 delay 掛起函數(shù). 功能和協(xié)程庫中的 [delay] 函數(shù)是一樣的.
 * 這里使用的是標(biāo)準(zhǔn)庫中定義掛起函數(shù)的方法.
 */
private suspend fun customDelay(delayInMillis: Long) = suspendCoroutine { complete: Continuation<Unit> ->
    // 創(chuàng)建一個可延時執(zhí)行任務(wù)的 Thread Executor.
    val executorService = Executors.newSingleThreadScheduledExecutor()

    // 延時 delayInMillis ms 后調(diào)用 complete#resume 方法通知該任務(wù)已經(jīng)執(zhí)行完成了.
    executorService.schedule({
        complete.resume(Unit)
        executorService.shutdown()
    }, delayInMillis, TimeUnit.MILLISECONDS)
}

/**
 * suspend 函數(shù)可以類比于 Thread 中的 Runnable.
 * 同時, suspend 函數(shù)還被看作掛起點(diǎn), 也就是說運(yùn)行到這個函數(shù)的時候
 * 可能會被切換到其他線程當(dāng)中運(yùn)行.
 */
private suspend fun doSomething() {
    println("A")
    customDelay(10L)  // 掛起點(diǎn)

    println("B")
    customDelay(10L) // 掛起點(diǎn)

    println("C")
}

/**
 * Example 2.1 使用標(biāo)準(zhǔn)庫啟動一個協(xié)程.
 */
fun main() {
    // 位于標(biāo)準(zhǔn)庫中的協(xié)程啟動函數(shù)
    ::doSomething.startCoroutine(Continuation(EmptyCoroutineContext) {
        println(">>> doSomething Completed <<<")
    })

    // 防止進(jìn)程退出.
    Thread.sleep(1000L)
}

/* output */
A
B
C
>>> doSomething Completed <<<

在正式介紹協(xié)程掛起原理之前, 需要先簡單介紹一下協(xié)程的幾個基本知識點(diǎn).

  1. 所有 suspend 修飾的函數(shù)或 lambda 表達(dá)式可以直接通過 public fun <T> (suspend () -> T).startCoroutine(completion: Continuation<T>) 這個拓展方法創(chuàng)建并啟動協(xié)程, 該方法在 suspend 修飾的計算實(shí)例(也就是前文提到的 suspend 修飾的函數(shù)或 lambda 表達(dá)式)完成計算后會回調(diào)參數(shù)的 #resumeWith 方法. 這個函數(shù)是最底層創(chuàng)建并啟動協(xié)程的函數(shù), 所有封裝的協(xié)程構(gòu)建器最終都要通過這個方法來創(chuàng)建并啟動一條協(xié)程. 在上面與線程對比的幾個小例子中使用到的 GlobalScope#launch 協(xié)程構(gòu)建器最終也會調(diào)用該方法來創(chuàng)建并啟動協(xié)程. 這也是在協(xié)程是什么這一節(jié)提到協(xié)程需要的計算實(shí)例是 suspend 關(guān)鍵字修飾的方法或 lambda 表達(dá)式的原因.
  2. suspend 修飾的函數(shù)被稱為掛起函數(shù). 調(diào)用掛起函數(shù)可能會掛起計算實(shí)例, 所以調(diào)用掛起函數(shù)的地方也被稱為掛起點(diǎn). 在上面代碼示例中, #customDelay#doSomething 都是掛起函數(shù). 在 #doSomething 中調(diào)用 #customDelay 的地方被稱為掛起點(diǎn).
  3. public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T 這個函數(shù)的作用是把被編譯器隱藏的 Continuation 參數(shù)暴露出來. 我們可以通過這個函數(shù)自定義自己的掛起函數(shù), 實(shí)現(xiàn)等待卻不阻塞線程的任務(wù).

在弄懂這幾個知識點(diǎn)之后, 上面的代碼就很容易知道是干什么的了. 上面的代碼實(shí)際上就是使用 #doSomething 創(chuàng)建并啟動一條協(xié)程, 在 #doSomething 中依次輸出 "A", "B", "C". 執(zhí)行完 #doSomething 之后輸出 "doSomething Completed". 在執(zhí)行 #doSomething 的過程中會調(diào)用自定義的 #customDelay 方法掛起等待 10ms.

說了這么多, 那么協(xié)程到底是如何實(shí)現(xiàn)等待而不阻塞線程的呢? 這里面的原理其實(shí)十分簡單. 協(xié)程實(shí)現(xiàn)等待而不阻塞線程的方法就是通過回調(diào), 只不過這個回調(diào)是 Kotlin 編譯器實(shí)現(xiàn)的. 既然是編譯器實(shí)現(xiàn)的, 那么我們就需要反編譯一下這段代碼看看 Kotlin 編譯器到底做了什么黑科技的東西. 在 idea 中, Kotlin 編譯后的代碼可以通過 Tools -> Kotlin -> show kotlin bytecode 這幾個步驟查看. 為了更加清晰的展示編譯器干了什么東西, 這里我就直接貼我整理過后的反編譯 Java 代碼了. 下面這段整理過的 Java 代碼和反編譯的代碼是等效的.

/* Java code - Example 2.2 */
public class StartCoroutineSimulation {

    /**
     * 一個可掛起的計算實(shí)例. 根據(jù)協(xié)程的定義, 這個接口的對象就是協(xié)程.
     */
    interface Continuation {

        /**
         * 喚醒被掛起的計算任務(wù), 繼續(xù)運(yùn)行.
         */
        void resume();
    }

    /**
     * 自定義一個 delay 掛起函數(shù). 功能和協(xié)程庫中的 [delay] 函數(shù)是一樣的.
     * 這里使用的是標(biāo)準(zhǔn)庫中定義掛起函數(shù)的方法.
     */
    private static void customDelay(Continuation complete, long delayInMillis) {
        Continuation continuation = new Continuation() {
            @Override
            public void resume() {
                // 創(chuàng)建一個可延時執(zhí)行任務(wù)的 Thread Executor.
                ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();

                // 延時 delayInMillis ms 后調(diào)用 complete#resume 方法通知該任務(wù)已經(jīng)執(zhí)行完成了.
                executorService.schedule(() -> {
                    complete.resume();
                    executorService.shutdown();
                }, delayInMillis, TimeUnit.MILLISECONDS);
            }
        };

        continuation.resume();
    }

    private static void doSomething(Continuation complete) {
        Continuation continuation = new Continuation() {

            int label = 0;

            @Override
            public void resume() {
                switch (label) {
                    case 0:
                        // 片段任務(wù) A
                        label = 1;
                        System.out.println("A");
                        customDelay(this, 10L);
                        return;

                    case 1:
                        // 片段任務(wù) B
                        label = 2;
                        System.out.println("B");
                        customDelay(this, 10L);
                        return;

                    case 2:
                        // 片段任務(wù) C
                        label = 3;
                        System.out.println("C");
                        break;
                }

                complete.resume();
            }
        };

        continuation.resume();
    }

    /**
     * Example 2.2 模擬 kotlin 協(xié)程標(biāo)準(zhǔn)庫啟動一個協(xié)程.
     */
    public static void main(String[] args) {
        doSomething(new Continuation() {
            @Override
            public void resume() {
                System.out.println(">>> doSomething Completed <<<");
            }
        });
    }
}

/* output */
A
B
C
>>> doSomething Completed <<<

通過反編譯的代碼, 我們可以看出 Kotlin 編譯器做了以下幾個點(diǎn).

  1. 每一個掛起函數(shù)都被編譯成了一個 Continuation.
  2. 每一個掛起函數(shù)都被編譯器添加了一個 Continuation 參數(shù). 在完成該函數(shù)的任務(wù)之后, 會回調(diào)該參數(shù)的 #resume 方法. 該參數(shù)在 Kotlin 的源碼中是被隱藏的, 所以自定義掛起函數(shù)的時候需要使用 public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T 函數(shù)把隱藏的 Continuation 參數(shù)暴露出來, 以便通知調(diào)用者任務(wù)已經(jīng)完成了.
  3. 如果掛起函數(shù)中有掛起點(diǎn), 被編譯成的 Continuation 中的 #resume 方法會被實(shí)現(xiàn)成狀態(tài)機(jī)模式. 兩兩掛起點(diǎn)之間組成一種狀態(tài). 在上面例子中, 我們可以清晰的看到 println("A") 到第一個 #costomDelay 方法之間組成了第一種狀態(tài), println("B") 到第二個 #customDelay 方法之間組成了第二種狀態(tài), 最后的 println("C") 組成最后一種狀態(tài).
  4. 在一個掛起函數(shù)調(diào)用另外一個掛起函數(shù)時, 需要把自身作為參數(shù)傳入另外一個掛起函數(shù)中. 當(dāng)另外一個掛起函數(shù)完成時, 會回調(diào)參數(shù)的 #resume 方法通知調(diào)用者繼續(xù)完成任務(wù). 例如在上面的例子中, 從 #doSomething函數(shù)調(diào)用 #customDelay 函數(shù)對應(yīng)代碼 customDelay(this, 10L); 中可以看出 #doSomething 把自身 this 作為參數(shù)傳給了 #customDelay 方法, 而最終自定義的 #customDelay 方法在等待任務(wù)結(jié)束后通過 complete.resume(); 這句代碼讓 #doSomething 函數(shù)繼續(xù)運(yùn)行. 順帶一提, Kotlin 規(guī)定 suspend 修飾的函數(shù)或 lambda 表達(dá)式只能在 suspend 修飾的函數(shù)或表達(dá)式中調(diào)用. 就是因?yàn)?suspend 修飾的函數(shù)或 lambda 表達(dá)式會編譯成需要 Continuation 參數(shù)的 Continuation, 調(diào)用另外一個 suspend 修飾的函數(shù)或 lambda 表達(dá)式需要傳入一個 Continuation 作為參數(shù), 而只有在 suspend 修飾的函數(shù)或 lambda 表達(dá)式中才有 Continuation (自身) 對象傳入另外一個 suspend 函數(shù)中.

至此, 我們已經(jīng)清晰的了解到了協(xié)程是怎么實(shí)現(xiàn)等待而不阻塞線程的了. 總結(jié)成一句話就是 suspend 關(guān)鍵字修飾的方法或 lambda 表達(dá)式會編譯成一個帶 Continuation 參數(shù)的 Continuation 對象, 當(dāng)一個 Continuation 調(diào)用另外一個 Continuation 時需要把自身作為參數(shù)傳入到另外一個 Continuation 中, 另外一個 Continuation 完成任務(wù)后會傳入的 Continuation 參數(shù)的 #resume 方法讓調(diào)用者繼續(xù)運(yùn)行. 更簡單的說, 協(xié)程的掛起就是通過 Continuation 這個回調(diào)對象實(shí)現(xiàn)的.


協(xié)程的使用指導(dǎo)

本片文章的初衷就是讓大家初步認(rèn)識一下協(xié)程長什么樣子, 協(xié)程是什么東西, 協(xié)程的掛起原理是什么. 搞明白了這個三個問題, 那么本片文章的目的也就達(dá)到了. 如果有興趣繼續(xù)學(xué)習(xí) Kotlin 協(xié)程相關(guān)內(nèi)容, 這里整理了一些 Kotlin 協(xié)程相關(guān)的官方文檔資料. 資料按易難程度順序排列.

最后的最后, 如果覺得本文對你有幫助, 請幫我點(diǎn)個??. 謝謝大家 ^ _ ^. 本文歡迎分享和轉(zhuǎn)載, 轉(zhuǎn)載請補(bǔ)上鏈接并注明出處.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末抠蚣,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子履澳,更是在濱河造成了極大的恐慌嘶窄,老刑警劉巖怀跛,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異柄冲,居然都是意外死亡吻谋,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進(jìn)店門现横,熙熙樓的掌柜王于貴愁眉苦臉地迎上來漓拾,“玉大人,你說我怎么就攤上這事戒祠』拊埽” “怎么了?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵得哆,是天一觀的道長。 經(jīng)常有香客問我哟旗,道長贩据,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任闸餐,我火速辦了婚禮饱亮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘舍沙。我一直安慰自己近上,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布拂铡。 她就那樣靜靜地躺著壹无,像睡著了一般。 火紅的嫁衣襯著肌膚如雪感帅。 梳的紋絲不亂的頭發(fā)上斗锭,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天,我揣著相機(jī)與錄音失球,去河邊找鬼岖是。 笑死,一個胖子當(dāng)著我的面吹牛实苞,可吹牛的內(nèi)容都是我干的豺撑。 我是一名探鬼主播,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼黔牵,長吁一口氣:“原來是場噩夢啊……” “哼聪轿!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起猾浦,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤屹电,失蹤者是張志新(化名)和其女友劉穎阶剑,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體危号,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡牧愁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了外莲。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片猪半。...
    茶點(diǎn)故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖偷线,靈堂內(nèi)的尸體忽然破棺而出磨确,到底是詐尸還是另有隱情,我是刑警寧澤声邦,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布乏奥,位于F島的核電站,受9級特大地震影響亥曹,放射性物質(zhì)發(fā)生泄漏邓了。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一媳瞪、第九天 我趴在偏房一處隱蔽的房頂上張望骗炉。 院中可真熱鬧,春花似錦蛇受、人聲如沸句葵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽乍丈。三九已至,卻和暖如春把将,著一層夾襖步出監(jiān)牢的瞬間诗赌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工秸弛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留铭若,地道東北人。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓递览,卻偏偏與公主長得像叼屠,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子绞铃,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評論 2 351

推薦閱讀更多精彩內(nèi)容