iOS14开发-多线程

理论基础

进程与线程

进程

  • 进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动,它是操作系统分配资源的基本单元。
  • 进程是指在系统中正在运行的一个应用程序,就是一段程序的执行过程,可以理解为手机上一个正在运行的 App。
  • 每个进程之间是相互独立的,每个进程均运行在其专用且受保护的内存空间内,拥有独立运行所需的全部资源。

线程

  • 程序执行的最小单元,线程是进程中的一个实体。
  • 一个进程要想执行任务,必须至少有一个线程。应用程序启动的时候,系统会默认开启一个线程称之为主线程(又称为main线程、UI线程)

二者关系

  • 线程是进程的执行单元,进程的所有任务都在线程中执行。
  • 线程是 CPU 分配资源和调度的最小单位。
  • 一个程序可以对应多个进程(多进程),一个进程中可有多个线程但至少有一个主线程。
  • 同一个进程内的线程共享进程的资源。

多线程

  • 某个时刻在单个 CPU 的核心只能执行一个线程,多线程是指 CPU 快速的在多个线程之间进行切换(调度),形成多个线程同时执行的表象。现代 CPU 都是多核,此时可以真正同时处理多个线程。
  • 多线程的目的是为了同时完成多项任务,通过提高系统的资源利用率来提高系统的效率。

优缺点

优点

  • 提高程序的执行效率。
  • 提高资源利用率(CPU、内存利用率)。

缺点

  • 开启线程需要占用一定的内存空间,如果开启大量的线程,会占用大量的内存空间,降低程序的性能。
  • 线程越多,CPU 在调度时开销就越大。
  • 程序设计更加复杂:需要解决线程之间的通信、多线程的数据共享等问题。

线程安全

不论线程通过如何调度或线程如何交替执行,在不需要做任何干涉的情况下,其执行结果保持一致符合预期,则称之为线程安全。

串行、并行与并发

  • 串行:多个任务,执行完再执行另一个。(吃完饭再看电视)
  • 并行:每个线程分配给独立的 CPU 核心,线程同时运行。(一边吃饭一边看电视)
  • 并发:多个线程在单个 CPU 核心运行,同一时间一个线程运行,CPU 通过调度不断切换多个线程,形成多个线程同时执行的表象。(在餐厅吃饭,在客厅看电视)

同步与异步

同步和异步主要区别:是否开启新的线程。

  • 同步执行:在当前线程中执行任务,不会开启新线程。
  • 异步执行:可以在新的线程中执行任务,可以开启新的线程,但不是一定会开启新的线程。

多线程编程

iOS 中的多线程技术主要分为 3 种,分别为 Thread、GCD 和 Operation。

Thread

  • 面向对象。
  • 需要手动创建线程,但不需要手动销毁。

方式一

// Target-Action形式
let thread1 = Thread(target: self, selector: #selector(task), object: nil)
// 设置名字
thread1.name = "thread1"       
// 启动
thread1.start()       

方式二

// 闭包形式
let thread2 = Thread {
    sleep(1)

    print(Thread.current)
}

thread2.name = "thread2"
thread2.start()

方式三

// 类方法,也有3种形式,以闭包形式为例
// 会直接启动线程,不需要手动调用start方法来启动线程执行任务
Thread.detachNewThread {    
    sleep(1)
    
    print(Thread.current)  
}

线程休眠

  • sleep():休眠的时间只能为整数。
  • Thread.sleep(forTimeInterval: ):休眠的时间可以为浮点数。

GCD

  • Grand Central Dispatch(宏大、中央、调度)。
  • C 语言编写。
  • 充分利用了 CPU 多核特性,因此效率高。
  • 自动管理线程生命周期。
  • 核心概念 — 任务和队列,将任务放进队列即可执行。

队列

队列类型 功能描述
串行队列 按照任务添加到队列的顺序执行,一次只能执行一个任务
并发队列 同时执行一个或多个任务,但任务仍按其添加到队列的顺序启动。
主队列 特殊的串行队列,会在主线程上执行任务。

DispatchQueue

  • 主队列
// 主队列
let main = DispatchQueue.main  
  • 串行队列
// label:队列的名称
// 除label以外的参数都使用默认值时,返回的是串行队列。
let serialQueue = DispatchQueue(label: "serialQueue")   
  • 并发队列
// global并发队列
let defaultGlobalDipatchQueue =  DispatchQueue.global()

// 带qos的global并发队列
let globalDipatchQueue = DispatchQueue.global(qos: .default)

// 创建一个并发队列,参数attributes需要设置为.concurrent
let concurrentDispatchQueue = DispatchQueue(label: "concurrentQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)

sync与async

  • sync同步方法,执行时不会立即返回,它会阻塞当前线程,等待任务执行完毕后再执行后续任务。
  • async异步方法,执行时会立即返回然后执行后续任务, 任务会在子线程中执行。
  • async 方法有多个参数,其中有 2 个比较重要:
    (1)group:关联任务的 DispatchGroup。
    (2)flags:控制任务执行的环境。(该参数 sync 方法也有)
queue.sync {
    // 当前线程执行任务
}

queue.async {
    // 新线程执行任务
}

asyncAfter

在当前队列中延迟任务的执行时间,参数为DispatchTime,一般会在当前时间的基础上加上一个延迟时间(以秒为单位)。

func dispatchAfter() { 
    queue.asyncAfter(deadline: DispatchTime.now() + 2) {
        print("延迟2s执行")  
    }
    
    // 主队列延迟执行
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
        print("主队列延迟3s执行的任务")
    }  
}

concurrentPerform

  • 按指定次数异步执行任务,并且会等待指定次数的任务全部执行完毕才会执行后面的任务,即会阻塞当前线程直到全部任务完成。
  • 默认会开启多少个线程执行任务。
func concurrentPerform() {
    print("任务开始执行")

    DispatchQueue.concurrentPerform(iterations: 5) { index in
        for i in 0 ... 3 {
            Thread.sleep(forTimeInterval: 0.1)
            print("这是\(Thread.current)第\(index)次打印:\(i)")
        }
    }

    print("任务执行完毕")
}

barrier

  • 用于调整并发队列中任务之间的执行顺序。
  • 同一个队列中,barrier 之后的任务必须等其执行完才会执行。
func barrier() {
    let queue = DispatchQueue(label: "queue001", attributes: .concurrent)

    queue.async {
        sleep(1)
        print("\(Thread.current)执行任务一")
    }

    queue.async {
        sleep(1)
        print("\(Thread.current)执行任务二")
    }

    // 任务四和五会在三之后执行
    queue.async(flags: .barrier) {
        sleep(1)
        print("\(Thread.current)执行任务三")
    }

    queue.async {
        sleep(1)
        print("\(Thread.current)执行任务四")
    }

    queue.async {
        sleep(1)
        print("\(Thread.current)执行任务五")
    }
}

DispatchGroup

  • 用于需要在多个异步任务完成以后再处理后续任务的场景。
  • notify:等待 group 中的所有任务执行完以后才会执行的任务,该操作并不会阻塞当前线程。
  • notify 操作可以添加多次,也会执行多次。
func group() {
    let group = DispatchGroup()

    queue.async(group: group) {
        print("网络请求任务一")
    }

    queue.async(group: group) {
        print("网络请求任务二")
    }

    queue.async(group: group) {
        print("网络请求任务三")
    }

    // 执行完前面的任务后回到主线程执行后续任务
    group.notify(queue: DispatchQueue.main) {
        print("完成任务一、二、三, 更新UI")
    }

    queue.async {
        print("其他任务四")
    }
    
    group.notify(queue: DispatchQueue.main) {
        print("完成任务一、二、三、四, 更新UI")
    }
}
  • 可以通过enter()leave()方法显式表明任务是否执行完成,enter()必须在leave()之前且二者必须成对出现。
func group2() {
    let group = DispatchGroup()

    group.enter()
    queue.async(group: group) {
        print("网络请求任务一")
        group.leave()
    }

    group.enter()
    queue.async(group: group) {
        print("网络请求任务二")
        group.leave()
    }

    group.enter()
    queue.async(group: group) {
        print("网络请求任务三")
        group.leave()
    }

    group.notify(queue: DispatchQueue.main) {
        print("完成任务一、二、三, 更新UI")
    }

    queue.async {
        print("其他任务四")
    }
}

DispatchWorkItem

  • 任务的封装。
  • 单独使用时需要调用perform()方法执行任务。
func dispatchWorkItem() {    
    var value = 10
    // 初始化方法传入一个闭包,闭包中就是需要执行的任务
    let workItem = DispatchWorkItem {
        value += 5
        print(Thread.current) // 主线程
    }
    
    // 通过perform()方法来唤起DispatchWorkItem执行任务
    workItem.perform()
    
    print(value)
}
  • 队列中执行时不需要手动调用perform()方法。
let workItem = DispatchWorkItem {
    for i in 0 ... 10 {
        sleep(1)
        print(i)
        print(Thread.current) // 子线程
    }
}

DispatchQueue.global().async(execute: workItem)
  • cancel

(1)如果任务已经开始执行,即使取消也依然会执行。

let workItem = DispatchWorkItem {
    for i in 0 ... 10 {
        sleep(1)
        print(i)
        print(Thread.current)
    }
}

// 先执行
DispatchQueue.global().async(execute: workItem)
// 后取消
workItem.cancel()
// 查看取消状态
print(workItem.isCancelled)

(2)如果任务尚未开始执行,取消后则不会再执行。

let workItem = DispatchWorkItem {
    for i in 0 ... 10 {
        sleep(1)
        print(i)
        print(Thread.current)
    }
}
// 先取消
workItem.cancel()
// 再执行
DispatchQueue.global().async(execute: workItem)
// 查看取消状态
print(workItem.isCancelled)
  • wait

(1)无参数:阻塞当前线程直到任务完成。

let workItem = DispatchWorkItem {
    for i in 0 ... 10 {
        sleep(1)
        print(i)
        print(Thread.current)
    }
}

DispatchQueue.global().async(execute: workItem)
// 等待
workItem.wait()
// 任务完成后才会执行
print("继续执行任务")

(2)timeout 参数:阻塞当前线程直到 timeout,如果任务完成 timeoutResult 为 success,否则为 timeOut。

let workItem = DispatchWorkItem {
    for i in 0 ... 10 {
        sleep(1)
        print(i)
        print(Thread.current)
    }
}

DispatchQueue.global().async(execute: workItem)
// 设置等待时间
let timeoutResult = workItem.wait(timeout: .now() + 3)
// 3秒内执行完任务则为success,否则timeOut
switch timeoutResult {
case .success:
    print("success")
case .timedOut:
    print("timedOut")
}

// 3秒以后执行
print("继续执行任务")
  • notify:任务完成后需要执行的操作。
let workItem = DispatchWorkItem {
    for i in 0 ... 10 {
        sleep(1)
        print(i)
        print(Thread.current)
    }
}

DispatchQueue.global().async(execute: workItem)
// 任务完成以后回到指定队列执行任务
workItem.notify(queue: DispatchQueue.main) {
    print("任务完成")
}

print("继续执行任务")

Operation

  • 基于 GCD 的封装,更加面向对象,功能相对 GCD 也更加丰富。
  • 核心依然是任务和队列

OperationQueue

  • 方式一
func operationUseOne() {
    // 创建OperationQueue
    let operationQueue = OperationQueue()
    
    // 添加Operation
    operationQueue.addOperation {
        sleep(1)
        print("\(Thread.current)执行任务一")
    }

    operationQueue.addOperation {
        sleep(1)
        print("\(Thread.current)执行任务二")
    }

    operationQueue.addOperation {
        sleep(1)
        print("\(Thread.current)执行任务三")
    }
}
  • 方式二
func operationUseTwo() {
    let operationQueue = OperationQueue()

    // BlockOperation
    let operation1 = BlockOperation {
        print("\(Thread.current)执行任务一")
        sleep(1)
    }

    let operation2 = BlockOperation {
        print("\(Thread.current)执行任务二")
        sleep(1)
    }

    let operation3 = BlockOperation {
        print("\(Thread.current)执行任务三")
        sleep(1)
    }

    // 逐个添加到OperationQueue
    // operationQueue.addOperation(operation1)
    // operationQueue.addOperation(operation2)
    // operationQueue.addOperation(operation3)

    // 一次性添加到OperationQueue
    operationQueue.addOperations([operation1, operation2, operation3], waitUntilFinished: false)

    // waitUntilFinished
    // 如果为false,不会等任务完成再执行后续任务
    // 如果为true,阻塞当前线程,等待任务完成后再执行后续任务
    print("\(Thread.current)执行其他任务")
}
  • 主队列
let mainQueue = OperationQueue.main

// 在没有指定任何队列的情况下调用start方法启动的BlockOperation默认会在主线程执行任务
let op = BlockOperation {
    sleep(1)
    print("\(Thread.current)执行任务一")
}

op.start()

maxConcurrentOperationCount

设置 OperationQueue 的最大并发数,表示的是能同时执行的 Operation 的最大数量,而不是开启线程的最大数量

func setOperationQueue() {
      // 并发数
     operationQueue.maxConcurrentOperationCount = 2
}

queuePriority

  • 设置 Operation 的优先级。
  • 在同一个队列中等待调度的所有 Operation,会按照优先级排序执行,但实际执行的顺序还是依赖 CPU 的调度。
func setOperation(op:Operation){
    // 优先级
    op.queuePriority = .high
}

addDependency与completionBlock

  • addDependency 用于设置 Operation 之间的依赖关系。
  • 依赖操作必须在 Operation 添加到队列之前进行。
  • 可以跨队列进行依赖操作。
  • completionBlock 用于设置 Operation 完成时的回调。
func dependency() {
    let operationQueue = OperationQueue()

    let operation1 = BlockOperation {
        print("\(Thread.current)执行任务一")
        sleep(1)
    }

    // 监听Operation完成
    operation1.completionBlock = {
        print("\(Thread.current)完成任务一")
    }

    let operation2 = BlockOperation {
        print("\(Thread.current)执行任务二")
        sleep(1)
    }

    operation2.completionBlock = {
        print("\(Thread.current)完成任务二")
    }

    // 添加依赖
    // operation2在operation1执行完再执行(并不是等completionBlock执行完再执行,而是BlockOperation体执行完就开始执行)
    operation2.addDependency(operation1)

    let operation3 = BlockOperation {
        print("\(Thread.current)执行任务三")
        sleep(1)
    }

    operation3.completionBlock = {
        print("\(Thread.current)完成任务三")
    }

    // operation3在operation2执行完再执行
    operation3.addDependency(operation2)

    operationQueue.addOperations([operation1, operation2, operation3], waitUntilFinished: false)

    print("\(Thread.current)执行其他任务")
}

barrier

类似 GCD 的 barrier。

func barrier() {
    let operationQueue = OperationQueue()

    operationQueue.addOperation {
        sleep(1)
        print("\(Thread.current)执行任务一")
    }

    operationQueue.addOperation {
        sleep(1)
        print("\(Thread.current)执行任务二")
    }

    // 任务四和五会在三之后执行
    operationQueue.addBarrierBlock {
        sleep(1)
        print("\(Thread.current)执行任务三")
    }

    operationQueue.addOperation {
        sleep(1)
        print("\(Thread.current)执行任务四")
    }

    operationQueue.addOperation {
        sleep(1)
        print("\(Thread.current)执行任务五")
    }
}

suspend、resume与cancel

  • suspend:挂起,OperationQueue 中还没有被 CPU 调度的 Operation 才会被挂起,那些已经被 CPU 调度的 Operation 不会被挂起。
func suspend() {
    if operationQueue.operationCount != 0 && operationQueue.isSuspended == false {
        operationQueue.isSuspended = true
    }
}
  • resume:重启,OperationQueue 中被挂起的 Operation 可以继续执行。
func resume() {
    if operationQueue.operationCount != 0 && operationQueue.isSuspended == true {
        operationQueue.isSuspended = false
    }
}
  • cancel:取消,还没有被 CPU 调度的 Operation 才会被取消,但无法让其再次运行。分为 2 种:
    (1)取消单个。
    (2)取消所有。
func cancel() {
    //  Operation 取消 
    operation.cancel()
    // OperationQueue 取消所有
    operationQueue.cancelAllOperations()
}

安全性问题

多线程编程中,应该尽量避免资源在线程之间共享,以减少线程间的相互影响。有两个重要的概念:

  1. 临界资源:一次只能允许一个线程使用的共享资源。
  2. 临界区:访问临界资源的那段代码。

在实际开发中,经常存在多个线程访问同一个共享资源的情况,那么如何保证多线程执行结果的正确性?在 iOS 中主要提供了 2 种技术 — 锁和信号量

  • 互斥锁:保证在任何时候,都只有一个线程访问对象。当获取锁失败时,线程会进入睡眠,等待锁释放时被唤醒。
  • 递归锁:特殊的互斥锁。它的特点是同一个线程可以加锁 N 次而不会引发死锁。
  • 自旋锁 :它不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环尝试,直到该自旋锁的保持者已经释放了锁;因为不会引起调用者睡眠,所以效率高于互斥锁。 缺点:
    (1)调用者在未获得锁的情况下会一直运行,如果不能在很短的时间内获得锁,会使CPU效率降低。所以自旋锁就适用于临界区持锁时间非常短且CPU资源不紧张的场景。
    (2)在用自旋锁时(如递归调用)有可能造成死锁。

pthread

  • 比较底层,现在使用较少。
var mutex: pthread_mutex_t = {
    // 初始化锁属性
    var mutexattr = pthread_mutexattr_t()
    // 锁属性赋值
    pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_DEFAULT)
    // 初始化锁
    var mutex = pthread_mutex_t()
    // pthread_mutex_init(&mutex, nil)
    // mutexattr传nil表示default
    pthread_mutex_init(&mutex, &mutexattr)
    // 使用锁属性之后要释放
    pthread_mutexattr_destroy(&mutexattr)
    // 返回锁
    return mutex
}()

// 线程业务代码
DispatchQueue.global().async {
    // 加锁
    pthread_mutex_lock(&mutex)
    
    // 临界区

    // 解锁
    pthread_mutex_unlock(&mutex)
}
  • 销毁锁。
deinit {
    // 销毁锁
    pthread_mutex_destroy(&mutex)
}

NS系列锁

包括NSLock、NSCondition、NSConditionLock、NSRecursiveLock,都遵守了NSLocking协议,。

  • NSLocking协议。
public protocol NSLocking {
    func lock() // 加锁
    func unlock() // 解锁
}
  • NSLock:互斥锁。
// 初始化
let lock = NSLock() 
// 加锁
lock.lock()

// 临界区

// 解锁
lock.unlock()
  • NSCondition:常用于生产者消费者模式。
// 初始化
let lock = NSCondition()
var products = [Int]()

// 消费者
func consume() {
    DispatchQueue.global().async {
        // 加锁
        lock.lock()
        // 没有商品挂起线程
        while products.count == 0 {
            lock.wait()
        }
        // 消费产品
        let product = products.remove(at: 0)
        print("消费产品\(product)")
        // 解锁
        lock.unlock()
    }
}

// 生产者
func produce() {
    DispatchQueue.global().async {
        // 加锁
        lock.lock()
        // 生产产品
        let product = Int.random(in: 0 ... 100)
        products.append(product)
        print("生产产品\(product)")
        // 唤醒消费者
        lock.signal()
        // 解锁
        lock.unlock()
    }
}

while true {
    consume()
    sleep(1)
    produce()
}
  • NSConditionLock:条件锁,对 NSCondition 的进一步封装。
// 初始化时condition为0
let lock = NSConditionLock(condition: 0)
var products = [Int]()

// 消费者
func consume() {
    DispatchQueue.global().async {
        // 加锁,当参数与初始化时condition不一致时进行等待
        lock.lock(whenCondition: 1)
        // 消费产品
        let product = products.remove(at: 0)
        print("消费产品\(product)")
        // 解锁,修改condition的值为0
        lock.unlock(withCondition: 0)
    }
}

// 生产者
func produce() {
    DispatchQueue.global().async {
        // 加锁,与初始化时condition一致,继续执行
        lock.lock(whenCondition: 0)
        // 生产产品
        let product = Int.random(in: 0 ... 100)
        products.append(product)
        print("生产产品\(product)")
        // 解锁,修改condition的值为1
        lock.unlock(withCondition: 1)
    }
}

while true {
    consume()
    sleep(1)
    produce()
}
  • NSRecursiveLock:递归锁。
// 初始化
let lock = NSRecursiveLock()
var count = 5

func recursive(value: Int) {
    // 加锁(换成其他的锁会死锁)
    lock.lock()
    // 大于0才继续后面的操作
    guard value > 0 else {
        return
    }
    // 打印
    print(value)
    // 休眠
    sleep(1)
    // 递归次数减1
    count -= 1
    // 递归调用
    recursive(value: count)
    // 解锁
    lock.unlock()
}

DispatchQueue.global().async {
    print("开始")
    recursive(value: count)
    print("结束")
}

objc_sync


let lock: Int = 0
// 加锁
objc_sync_enter(lock) // 很多时候参数为self

// 临界区

// 解锁
objc_sync_exit(lock)

OSSpinLock自旋锁

由于存在因为低优先级争夺资源导致死锁的问题,所以在 iOS 10 之后已废弃,替换它的是 os_unfair_lock。

os_unfair_lock

一种互斥锁,内置于os模块。

var lock = os_unfair_lock()
// 加锁
os_unfair_lock_lock(&lock)

// 临界区

// 解锁
os_unfair_lock_unlock(&lock)

信号量

DispatchSemaphore 是一种基于计数的信号量。它可以设定一个阀值,多个线程竞争获取许可信号,超过阀值后,线程申请许可信号将会被阻塞。主要用于线程之间的数据同步。

  • DispatchSemaphore(value: ):创建信号量,value 为初始值。
  • wait:根据当前信号量的值进行判断:
    (1)若大于 0,则将信号量减 1 ,继续执行后续任务。
    (2)若小于等于 0,则阻塞当前线程,直到信号量大于 0 或者经过一个阈值时间才会执行后续任务。
  • signal:信号量加 1。

DispatchSemaphore

// 创建信号量,初始值为0
let semaphore = DispatchSemaphore(value: 0)

// 线程业务代码
DispatchQueue.global().async {
    // 临界区

    semaphore.signal()
}

semaphore.wait(timeout: .distantFuture)

UI更新问题

  • 当 App 运行以后,主线程随之启动。该线程需要接收用户的交互,完成界面的更新等操作,因此必须保证它的流畅性,耗时的操作不能放在主线程中执行,否则会造成界面的卡顿甚至崩溃。
  • iOS 规定不能在子线程中更新 UI 界面,更新 UI 的操作必须在主线程中进行。如果在子线程中更新了 UI,程序在编译时并不会报错,但运行时会出现意料不到的结果甚至崩溃,此时控制台和 Xcode 也会有相应的错误信息输出和提示。
  • 针对 3 种不同的线程实现方式,回到主线程也有 3 种方式。
import UIKit

// MARK:- Thread模式
func threadMode(){    
    let thread =  Thread {  
        print("\(Thread.current)执行任务")
        // 休眠
        sleep(3)
        // 更新UI
        self.perform(#selector(self.updateUI), on: Thread.main, with: nil, waitUntilDone: false)  
    }
    
    thread.start()    
}

@objc func updateUI() { 
    self.infoLb.text = "Thread方式更新UI"
}



// MARK:- GCD模式
func gcdMode(){    
    DispatchQueue.global().async {  
        print("\(Thread.current)执行任务")
        // 休眠
        sleep(3)
        // 更新UI
        DispatchQueue.main.async {          
            self.infoLb.text = "GCD方式更新UI"
        }  
    }   
}



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

推荐阅读更多精彩内容