Go语言基础06——并发编程

goroutine

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

有人把Go比作21世纪的C语言,第一是因为Go语言设计简单,第二,21世纪最重要的就是并行程序设计,而Go从语言层面就支持了并行。同时,并发程序的内存管理有时候是非常复杂的,而Go语言提供了自动垃圾回收机制。

Go语言为并发编程而内置的上层API基于CSP(communicating sequential processes, 顺序通信进程)模型。这就意味着显式锁都是可以避免的,因为Go语言通过相册安全的通道发送和接受数据以实现同步,这大大地简化了并发程序的编写。

一般情况下,一个普通的桌面计算机跑十几二十个线程就有点负载过大了,但是同样这台机器却可以轻松地让成百上千甚至过万个goroutine进行资源竞争。
goroutine是Go并行设计的核心。goroutine说到底其实就是协程,但是它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。

只需在函数调⽤语句前添加 go 关键字,就可创建并发执⾏单元。开发⼈员无需了解任何执⾏细节,调度器会自动将其安排到合适的系统线程上执行。

在并发编程里,我们通常想讲一个过程切分成几块,然后让每个goroutine各自负责一块工作。当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用go语句来创建。

package main

import (
    "fmt"
    "time"
)

func newTask() {
    for {
        fmt.Println("this is newTask thread")
        time.Sleep(time.Second)
    }
}

func main() {

    fmt.Println("并发编程演示案例")

    // 1. 创建goroutine
    go newTask()
    for {
        fmt.Println("this is main goroutine thread")
        time.Sleep(time.Second)
    }

}

主goroutine退出后,其它的工作goroutine也会自动退出,因此可能会导致:主协程先退出导致子协程没来得及调用

package main

import (
    "fmt"
    "time"
)

func newTask() {
    for {
        fmt.Println("this is newTask thread")
        time.Sleep(time.Second)
    }
}

func main() {

    fmt.Println("并发编程演示案例")

    // 1. 主goroutine退出后,其它的工作goroutine也会自动退出,因此可能会导致:主协程先退出导致子协程没来得及调用
    go newTask()
    var i int = 0
    for {
        if i < 5 {
            fmt.Println("this is main goroutine thread ", i)
            time.Sleep(time.Second)
            i++
        } else {
            break
        }

    }

}

控制台输出:

并发编程演示案例
this is main goroutine thread  0
this is newTask thread
this is main goroutine thread  1
this is newTask thread
this is main goroutine thread  2
this is newTask thread
this is newTask thread
this is main goroutine thread  3
this is newTask thread
this is main goroutine thread  4

runtime.Gosched() 用于让出CPU时间片,让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。

这就像跑接力赛,A跑了一会碰到代码runtime.Gosched() 就把接力棒交给B了,A歇着了,B继续跑。

package main

import (
    "fmt"
    "runtime"
)

func main() {

    fmt.Println("runtime.Gosched的使用演示案例")

    // 1. runtime.Gosched的使用
    go func() {
        for i := 0; i < 5; i++ {
            fmt.Println("this is newTask goroutine ", i)
        }
    }()

    for i := 0; i < 2; i++ {
        runtime.Gosched()
        fmt.Println("this is main goroutine ", i)
    }

    // 输出内容:
    // this is newTask goroutine  0
    // this is newTask goroutine  1
    // this is newTask goroutine  2
    // this is newTask goroutine  3
    // this is newTask goroutine  4
    // this is main goroutine  0
    // this is main goroutine  1
}

调用 runtime.Goexit() 将立即终止当前 goroutine 执⾏,调度器确保所有已注册 defer延迟调用被执行。

package main

import (
    "fmt"
    "runtime"
)

func test() {
    fmt.Println("dddddddddddd")
    runtime.Goexit() // 终止所在协程
    fmt.Println("eeeeeeeeeeee")
}

func main() {

    fmt.Println("runtime.Gosched的使用演示案例")

    // 1. runtime.Goexit的使用
    //调用 runtime.Goexit() 将立即终止当前 goroutine 执⾏,调度器确保所有已注册 defer延迟调用被执行。
    go func() {
        fmt.Println("aaaaaaaaaaaa")
        test()
        fmt.Println("bbbbbbbbbbbb")
    }()

    for { //死循环,目的是不让主协程结束

    }

    // 输出内容:
    // aaaaaaaaaaaa
    // dddddddddddd
}

调用 runtime.GOMAXPROCS() 用来设置可以并行计算的CPU核数的最大值,并返回之前的值。

package main

import (
    "fmt"
    "runtime"
)

func main() {

    fmt.Println("untime.GOMAXPROCS的使用演示案例")
    max := runtime.GOMAXPROCS(4) //调用 runtime.GOMAXPROCS() 用来设置可以并行计算的CPU核数的最大值,并返回之前的值。
    fmt.Println("max: ", max)    //max:  4

    for {
        go fmt.Print(1)
        fmt.Print(0)
    }

    // 输出内容:如果gomaxprocs设置为1则,
    // 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
    // 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
    // 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
    // 0011111111111111111111111111111111111111111111111111111111111111111111111111111111111111
    // 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
    // 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
    // 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
    // 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
    // 输出内容:如果gomaxprocs设置为4则,
    // 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
    // 0000000000000000000000000000000111111111111111111111111111111111111111111111111111111111
    // 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
    // 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
    // 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
    // 1111111111111000000000000000000000000000000000000000000000000000000000000000000000000000
    // 0000000000000000000000000000000000000000111111111111111111111111111111111111111111111111
}

多任务资源竞争问题:打印机问题

package main

import (
    "fmt"
    "time"
)

func HpPrinter(str string) { // 惠普打印机
    for _, c := range str {
        time.Sleep(time.Second)
        fmt.Printf("%c", c)
    }
}

func main() {
    go HpPrinter("hello") // 小明去打印hello
    go HpPrinter("ABC")   //小丽打印ABC
    for {

    }

    // 输出结果:并不是自己想要的结果
    //AheBCllo
    //hABelClo

}

为了解决这个问题,就引出了channel

channel

goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。goroutine 奉行通过通信来共享内存,而不是共享内存来通信。

引⽤类型 channel 是 CSP 模式的具体实现,用于多个 goroutine 通讯。其内部实现了同步,确保并发安全。
和map类似,channel也一个对应make创建的底层数据结构的引用。

当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者何被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil。

定义一个channel时,也需要定义发送到channel的值的类型。channel可以使用内置的make()函数来创建:
make(chan Type) //等价于make(chan Type, 0)
make(chan Type, capacity)

当 capacity= 0 时,channel 是无缓冲阻塞读写的,当capacity> 0 时,channel 有缓冲、是非阻塞的,直到写满 capacity个元素才阻塞写入。

channel通过操作符<-来接收和发送数据,发送和接收数据语法:
channel <- value //发送value到channel
<-channel //接收并将其丢弃
x := <-channel //从channel中接收数据,并赋值给x
x, ok := <-channel //功能同上,同时检查通道是否已关闭或者是否为空

默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得goroutine同步变的更加的简单,而不需要显式的lock。

package main

import (
    "fmt"
)

func main() {
    ch := make(chan string)

    defer fmt.Println("主线程调用结束")

    go func() {
        defer fmt.Println("子协程调用结束")
        for i := 0; i < 5; i++ {
            fmt.Println("子协程调用中 ", i)
        }
        ch <- "success"
    }()

    data := <-ch //没有数据前,阻塞
    fmt.Println("主线程取得数据:", data)

    // 输出结果:
    // 子协程调用中  0
    // 子协程调用中  1
    // 子协程调用中  2
    // 子协程调用中  3
    // 子协程调用中  4
    // 子协程调用结束
    // 主线程取得数据: success
    // 主线程调用结束
}

无缓冲channel
无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道。

这种类型的通道要求发送 goroutine 和接收 goroutine 同时准备好,才能完成发送和接收操作。如果两个goroutine没有同时准备好,通道会导致先执行发送或接收操作的 goroutine 阻塞等待。

这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。

下图展示两个 goroutine 如何利用无缓冲的通道来共享一个值:


image.png

l 在第 1 步,两个 goroutine 都到达通道,但哪个都没有开始执行发送或者接收。
l 在第 2 步,左侧的 goroutine 将它的手伸进了通道,这模拟了向通道发送数据的行为。这时,这个 goroutine 会在通道中被锁住,直到交换完成。
l 在第 3 步,右侧的 goroutine 将它的手放入通道,这模拟了从通道里接收数据。这个 goroutine 一样也会在通道中被锁住,直到交换完成。
l 在第 4 步和第 5 步,进行交换,并最终,在第 6 步,两个 goroutine 都将它们的手从通道里拿出来,这模拟了被锁住的 goroutine 得到释放。两个 goroutine 现在都可以去做别的事情了。

无缓冲的channel创建格式:
make(chan Type) //等价于make(chan Type, 0)

如果没有指定缓冲区容量,那么该通道就是同步的,因此会阻塞到发送者准备好发送和接收者准备好接收。

package main

import (
    "fmt"
    //"time"
)

func main() {
    ch := make(chan int, 0) //创建一个无缓存的channel
    //fmt.Printf("len(ch)=%d,cap(ch)=%d\n", len(ch), cap(ch)) //len(ch)=0,cap(ch)=0

    go func() {
        for i := 0; i < 3; i++ {
            fmt.Println("子协程:i =", i)
            data := <-ch //读管道中的内容,如果没有前,阻塞
            fmt.Println("子协程取得数据:data =", data)
        }
    }()
    //time.Sleep(2 * time.Second)
    for i := 0; i < 3; i++ {
        fmt.Println("main协程:i =", i)
        // 往chan里面写数据
        fmt.Println("main协程发送数据:data =", i)
        ch <- i

    }

    // 输出结果:
    // main协程:i = 0
    // main协程发送数据:data = 0
    // 子协程:i = 0
    // 子协程取得数据:data = 0
    // 子协程:i = 1
    // main协程:i = 1
    // main协程发送数据:data = 1
    // main协程:i = 2
    // main协程发送数据:data = 2
    // 子协程取得数据:data = 1
    // 子协程:i = 2
    // 子协程取得数据:data = 2
}

有缓冲channel
有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个值的通道。

这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。

这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。
示例图如下:


有缓存的channel.png

l 在第 1 步,右侧的 goroutine 正在从通道接收一个值。
l 在第 2 步,右侧的这个 goroutine独立完成了接收值的动作,而左侧的 goroutine 正在发送一个新值到通道里。
l 在第 3 步,左侧的goroutine 还在向通道发送新值,而右侧的 goroutine 正在从通道接收另外一个值。这个步骤里的两个操作既不是同步的,也不会互相阻塞。
l 最后,在第 4 步,所有的发送和接收都完成,而通道里还有几个值,也有一些空间可以存更多的值。

有缓冲的channel创建格式:
make(chan Type, capacity)

如果给定了一个缓冲区容量,通道就是异步的。只要缓冲区有未使用空间用于发送数据,或还包含可以接收的数据,那么其通信就会无阻塞地进行。

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int, 3)                                 //创建一个有缓存的channel
    fmt.Printf("len(ch)=%d,cap(ch)=%d\n", len(ch), cap(ch)) //len(ch)=0,cap(ch)=3

    go func() {
        for i := 0; i < 10; i++ {
            ch <- i
            fmt.Printf("子协程[%d],len(ch)=%d,cap(ch)=%d\n", i, len(ch), cap(ch))
        }
    }()
    time.Sleep(2 * time.Second)
    for i := 0; i < 10; i++ {
        num := <-ch
        fmt.Println("num = ", num)
    }

    // 输出结果:

    // 子协程[0],len(ch)=1,cap(ch)=3
    // 子协程[1],len(ch)=2,cap(ch)=3
    // 子协程[2],len(ch)=3,cap(ch)=3
    // num =  0
    // num =  1
    // num =  2
    // num =  3
    // 子协程[3],len(ch)=3,cap(ch)=3
    // 子协程[4],len(ch)=0,cap(ch)=3
    // 子协程[5],len(ch)=1,cap(ch)=3
    // 子协程[6],len(ch)=2,cap(ch)=3
    // 子协程[7],len(ch)=3,cap(ch)=3
    // num =  4
    // num =  5
    // num =  6
    // num =  7
    // num =  8
    // 子协程[8],len(ch)=3,cap(ch)=3
    // 子协程[9],len(ch)=0,cap(ch)=3
    // num =  9
}

关闭channel:
如果发送者知道,没有更多的值需要发送到channel的话,那么让接收者也能及时知道没有多余的值可接收将是有用的,因为接收者可以停止不必要的接收等待。这可以通过内置的close函数来关闭channel实现。
注意点:
l channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的,才去关闭channel;
l 关闭channel后,无法向channel 再发送数据(引发 panic 错误后导致接收立即返回零值);
l 关闭channel后,可以继续向channel接收数据;
l 对于nil channel,无论收发都会被阻塞。

package main

import (
    "fmt"
)

func main() {

    ch := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            ch <- i //往chan里面写数据
            if i == 5 {
                close(ch)
                //ch <- 10,err 关闭通道就不能写入东西了
                break
            }
        }

    }()

    for {
        if num, ok := <-ch; ok == true {
            fmt.Println("num = ", num)
        } else {
            fmt.Println("通道被关闭了,程序结束")
            break
        }

    }
    // 输出结果:
    // num =  0
    // num =  1
    // num =  2
    // num =  3
    // num =  4
    // num =  5
    // 通道被关闭了,程序结束
}

通过range遍历channel内容

package main

import (
    "fmt"
    "time"
)

func main() {

    ch := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println("往chan里面写数据")
            ch <- i //往chan里面写数据

            if i == 5 {
                close(ch)
                //ch <- 10,err 关闭通道就不能写入东西了
                break
            }
            time.Sleep(time.Second)
        }

    }()

    for num := range ch {
        fmt.Println("遍历channel:", num)
    }
    // 输出结果:

    // 往chan里面写数据
    // 遍历channel: 0
    // 往chan里面写数据
    // 遍历channel: 1
    // 往chan里面写数据
    // 遍历channel: 2
    // 往chan里面写数据
    // 遍历channel: 3
    // 往chan里面写数据
    // 遍历channel: 4
    // 往chan里面写数据
    // 遍历channel: 5

}

单向channel特点

默认情况下,通道是双向的,也就是,既可以往里面发送数据也可以同里面接收数据。

但是,我们经常见一个通道作为参数进行传递而值希望对方是单向使用的,要么只让它发送数据,要么只让它接收数据,这时候我们可以指定通道的方向。

单向channel变量的声明非常简单,如下:
var ch1 chan int // ch1是一个正常的channel,不是单向的
var ch2 chan<- float64 // ch2是单向channel,只用于写float64数据
var ch3 <-chan int // ch3是单向channel,只用于读取int数据

l chan<- 表示数据进入管道,要把数据写进管道,对于调用者就是输出。
l <-chan 表示数据从管道出来,对于调用者就是得到管道的数据,当然就是输入。

可以将 channel 隐式转换为单向队列,只收或只发,不能将单向 channel 转换为普通 channel:

package main

import (
    "fmt"
)

func main() {
    ch := make(chan int) //定义一个channel,默认是双向的

    var writeCh chan<- int = ch // 只能写,不能读
    var readCh <-chan int = ch  //只能读,不能写
    writeCh <- 666              //ok
    //n := <-writeCh              //err, invalid operation: <-writeCh (receive from send-only type chan<- int)

    num := <-readCh
    // readCh <- 777 //err,invalid operation: readCh <- 777 (send to receive-only type <-chan int)
    fmt.Println(num)
}

单向channel的应用:

package main

import (
    "fmt"
    "time"
)

// 生产者,写能写不能读
func producer(out chan<- int) {
    for i := 0; i < 5; i++ {
        fmt.Println("生产者producer:", (i * i))
        out <- i * i
        time.Sleep(time.Second)
    }
    close(out)
}

// 消费者,写能读不能写
func consumer(in <-chan int) {
    for num := range in {
        fmt.Println("消费者consumer:", num)
    }
}

func main() {
    ch := make(chan int) //定义一个channel,默认是双向的
    go producer(ch)
    consumer(ch)
    fmt.Println("程序结束")

    // 输出结果:
    // 生产者producer: 0
    // 消费者consumer: 0
    // 生产者producer: 1
    // 消费者consumer: 1
    // 生产者producer: 4
    // 消费者consumer: 4
    // 生产者producer: 9
    // 消费者consumer: 9
    // 生产者producer: 16
    // 消费者consumer: 16
    // 程序结束
}

Timer

Timer是一个定时器,代表未来的一个单一事件,你可以告诉timer你要等待多长时间,它提供一个channel,在将来的那个时间那个channel提供了一个时间值。

Timer实现延时功能、定时器停止、定时器重置

package main

import (
    "fmt"
    "time"
)

func main() {
    // 1. Timer的使用
    fmt.Println("------Timer的使用---------")
    fmt.Println("Timer的使用演示案例")
    t1 := time.NewTimer(2 * time.Second)
    fmt.Println("当前时间:", time.Now()) // 2019-03-20 14:00:41.9098831 +0800 CST m=+0.001982001
    t2 := <-t1.C
    fmt.Println("t2:", t2) //2019-03-20 14:00:43.9099262 +0800 CST m=+2.002025101

    fmt.Println("------Timer实现延时功能---------")

    // 2. Timer实现延时功能
    // 定时5秒
    t3 := time.NewTimer(5 * time.Second)
    fmt.Println("5s倒计时开始")
    <-t3.C
    fmt.Println("5s时间到")

    // 3. 定时器停止
    fmt.Println("------定时器停止---------")
    t4 := time.NewTimer(3 * time.Second)
    fmt.Println("3s后执行子协程")
    go func() {
        <-t4.C
        fmt.Println("时间到,子协程打印")
    }()

    t4.Stop() // 如果加了这句话,则子协程的任务就会取消,也就是不会打印

    // 4. 定时器重置
    fmt.Println("------定时器重置---------")
    fmt.Println("定时器重置5s->1s")
    t5 := time.NewTimer(5 * time.Second)
    t5.Reset(1 * time.Second) //重新设置为1s
    <-t5.C
    fmt.Println("定时器重置success")
    for {

    }

}

Ticker

package main

import (
    "fmt"
    "time"
)

func main() {
    // 1. Ticker的使用
    fmt.Println("Ticker的使用演示案例")

    t1 := time.NewTicker(1 * time.Second)

    i := 0
    for {
        <-t1.C
        fmt.Println("ticker:", i)
        if i > 10 {
            break
        } else {
            i++
        }
    }

    // 输出结果:
    // Ticker的使用演示案例
    // ticker: 0
    // ticker: 1
    // ticker: 2
    // ticker: 3
    // ticker: 4
    // ticker: 5
    // ticker: 6
    // ticker: 7
    // ticker: 8
    // ticker: 9
    // ticker: 10
    // ticker: 11

}

Select

Go里面提供了一个关键字select,通过select可以监听channel上的数据流动。

select的用法与switch语言非常类似,由select开始一个新的选择块,每个选择条件由case语句来描述。

与switch语句可以选择任何可使用相等比较的条件相比, select有比较多的限制,其中最大的一条限制就是每个case语句里必须是一个IO操作,大致的结构如下:
select {
case <-chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}

在一个select语句中,Go语言会按顺序从头至尾评估每一个发送和接收的语句。

如果其中的任意一语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。

如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况:
l 如果给出了default语句,那么就会执行default语句,同时程序的执行会从select语句后的语句中恢复。
l 如果没有default语句,那么select语句将被阻塞,直到至少有一个通信可以进行下去。

通过select实现斐波那契数列

package main

import (
    "fmt"
)

func fibonacci(ch chan<- int, quit <-chan bool) {

    x, y := 1, 1
    for {
        select {
        case ch <- x:
            x, y = y, x+y
        case flag := <-quit:
            fmt.Println("quit:", flag)
            return
        }

    }

}

func main() {
    // 1.通过select实现斐波那契数列 1、1、2、3、5、8、13、21、34、……
    fmt.Println("select的使用演示案例")

    ch := make(chan int)
    quit := make(chan bool)

    go func() {
        for i := 0; i < 8; i++ {
            num := <-ch
            fmt.Println("num = ", num)
        }
        quit <- true

    }()

    fibonacci(ch, quit)

    // 输出结果:
    // select的使用演示案例
    // num =  1
    // num =  1
    // num =  2
    // num =  3
    // num =  5
    // num =  8
    // num =  13
    // num =  21
    // quit: true
}

select实现的超时机制
有时候会出现goroutine阻塞的情况,那么我们如何避免整个程序进入阻塞的情况呢?我们可以利用select来设置超时,通过如下的方式实现:

package main

import (
    "fmt"
    "time"
)

func main() {
    // 1. select实现的超时机制
    fmt.Println("select实现的超时机制演示案例")

    ch := make(chan int)
    quit := make(chan bool)

    go func() {
        for {
            select {
            case num := <-ch:
                fmt.Println("程序执行中", num)
            case <-time.After(3 * time.Second):
                fmt.Println("程序超时")
                quit <- true

            }
        }
    }()

    for i := 0; i < 10; i++ {
        ch <- i
        time.Sleep(time.Second)
    }

    <-quit
    fmt.Println("程序结束")

    // 输出结果:
    // select实现的超时机制演示案例
    // 程序执行中 0
    // 程序执行中 1
    // 程序执行中 2
    // 程序执行中 3
    // 程序执行中 4
    // 程序执行中 5
    // 程序执行中 6
    // 程序执行中 7
    // 程序执行中 8
    // 程序执行中 9
    // 程序超时
    // 程序结束
}

END

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容