从异步和并发推导Kotlin的协程以及共享资源控制

1,860 阅读5分钟

同步和阻塞的区别

  • 同步:描述的是一种行为,当执行IO操作的时候,代码层面等待操作的结果,直到结果返回这一种行为叫做同步
  • 阻塞:是一种状态,当执行IO操作的时候,线程处于挂起的状态,就是该线程没有执行了

异步

  • 指的是使用多线程来避免同步等待的方式

异步所造成的的回调地狱

  • 指的是多个异步线程等待结果,下一个的执行要等上一个的结果回调,一直嵌套下去的操作

多线程一定优于多线程么?

  • 首先,我们知道多线程的实现原理,其实就是操作系统的层面通过CPU频繁切换线程实现的
  • 但是,当线程过多的时候,CPU频繁切换线程本身所消耗的资源太大,这就造成了多线程的实现方式不优于单线程

Kotlin的协程

  • 正是由于,上面多线程的回调地狱,以及大量线程最后的性能问题,Kotlin的协程应运而生

协程:一个更轻量级的“线程”

  • 协程是一个无优先级的程序调度组件
  • 允许子程序在特定的地方挂起和恢复
  • 进程包含线程
  • 线程包含协程
  • 只要内存足够用,一个线程中可以有任意多个协程,但是同一时刻只能有一个协程在运行
  • 多个协程分享该线程分配到的计算机资源
  • 协程是工作在线程之上的,协程的切换可以由程序自己来控制,而不是由操作系统去进行调度,这样就大大降低了开销
  • 我们在协程域中启动10万个协程是没有问题的,但是如果你直接启动10万个线程,程序将会直接OOM

合理使用线程

  • delay只能在协程内部使用,它用于挂起协程,不会阻塞线程
  • sleep会阻塞线程

用协程实现同步方式写异步代码

  • 同步的方式的代码
package com.example.kotlincore

import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking

class KotlinCoroutine {

    private suspend fun searchOne(): String {
        delay(1000)
        return "item_one"
    }

    private suspend fun searchTwo(): String {
        delay(1000)
        return "item_two"
    }

    fun main(args: Array<String>) = runBlocking<Unit> {
        val one = searchOne()
        val two = searchTwo()
        println("The items is $one and $two")
    }
}
  • 上面的代码执行顺序是依次执行的
  • 如果我们需要并行执行,可以使用async+awiat
fun main1() = runBlocking<Unit> { 
        val one = async { 
            searchOne()
        }
        val two = async { 
            searchTwo()
        }
        val def1 = one.await()
        val def2 = two.await()
    }
  • 经测试,使用并行执行的方式,时间上节省了一半。

共享资源控制

  • 共享资源控制就是对程序中的一个变量或者是数据库中的数据保证共享资源的正确性
  • 在并发编程中尤为重要

锁模式

  • 对共享资源加锁 比如 Java的 synchronized
  • kotlin中的@synchronized方法注解来声明一个同步方法
  • 代码块中的synchronized()来对一段代码进行加锁
  • 在并发激烈的情况下,加锁不是一个好的选择,但是在实际开发中,要根据具体的场景来设计方案
  • 当我们知道并发量不太大的情况下,去追求高并发实际上是没有意义的
  • 过早的优化是万恶之源
  • 在竞争不激烈的情况下,使用synchronized比较简单,也更为语义化
  • kotlin除了@synchronized之外,还有@Volatile对变量进行注解
  • 还可以是使用lock的方式,kotlin为此还对其进行了类库封装
package com.example.kotlincore

import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

class Sync {

    private val goods = hashMapOf<Long, Int>()

    @Volatile private var running = false

    init {
        goods[1] = 10
        goods[2] = 15
    }

    /**
     * 写法一
     */
    @Synchronized
    fun buyGoods(id: Long) {
        val stock = goods.getValue(id)
        if (stock > 0) {
            goods[id] = stock
        }
    }

    /**
     * 写法二
     */
    fun buyGoods2(id: Long) {
        synchronized(this) {
            val stock = goods.getValue(id)
            goods.put(id, stock)
        }
    }

    /**
     * 写法三
     */
    fun buyGood3(id: Long) {
        val lock: Lock = ReentrantLock()
        lock.withLock {
            val stock = goods.getValue(id)
            goods.put(id, stock)
        }
    }
}

通过一个实例来对锁模式进行分析

问题:上面我们的代码其实就是一个买商品同步锁的表现,现在有另一个需求就是,有两家商店,A商店和B商店,他们卖的商品是不一样的,这个时候,如果只用一个同步锁来解决问题显然是不妥的,因为两家的商品是没有关系的,这个时候,我们就需要用多个锁来实现了并行了

package com.example.kotlincore

import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

/**
 * 通过Shop类传递id的方式初始化不同的Shop类
 * 实现了业务锁分离
 */
class Shop(private var goods: HashMap<Long, Int>) {

    private val lock: Lock = ReentrantLock()

    fun buyGoods(id: Long) {
        lock.withLock {
            val stock = goods.getValue(id)
            goods.put(id, stock - 1)
        }
    }
}

class ShopApi {
   private val A_goods = hashMapOf<Long, Int>()
    private val B_goods = hashMapOf<Long, Int>()
    
    private var shopA: Shop
    private var shopB: Shop
    
    init {
        A_goods[1] = 10
        A_goods[2] = 15
        
        B_goods[1] = 20
        B_goods[2] = 10
        
        shopA = Shop(A_goods)
        shopB = Shop(B_goods)
    }
    
    fun buyGoods(shopName: String, id: Long) {
        when (shopName) {
            "A" -> shopA.buyGoods(id)
            "B" -> shopB.buyGoods(id)
            else -> {}
        }
    }
}

fun main() {
    val shopApi = ShopApi()
    shopApi.buyGoods("A", 1)
    shopApi.buyGoods("B", 2)
}

上面的方式,我们实现了不用商家的锁分离,但是存在一种情况就是,当商家不止两个,或者说有成千上万个的时候,使用when表达式来匹配就显得非常鸡肋了,那怎么办呢?

Actor : 有状态的并行计算单元

  • 这里暂时先说一下思想吧:传统的锁原理是通过共享内存,来实现资源共享的安全
  • 但是Actor的思想是,通过通信来实现共享内存,而不是通过共享内存来实现通讯

Actor的两条原则

  • 消息的发送必须先于消息的接受
  • 同一个Actor对一条消息的处理先于对下一条消息的处理
  • 具体等后面有时间再来分析