kotlin coroutine源码解析之suspend挂起函数原理

news2024/11/16 7:30:18

目录

  • suspend挂起函数
    • join原理
    • Await原理
    • Suspend函数
    • 总结

suspend挂起函数

在idea中写某些协程函数的时候,会有一个绿色箭头图标的出现,如下图:
在这里插入图片描述
而且这些方法不放在协程里面写的话,idea编辑器还会报错,如下图:
在这里插入图片描述
上面所说的这些方法就是挂起函数,挂起函数必须要在协程中调用,或者在挂起函数中调用;放在挂起函数中调用挂起函数调用,那么说明还是间接在协程中被调用,也就是挂起函数调用需要协程环境。
在说挂起函数原理之前,先复习下 launch启动过程 所说的三类continuation:

  1. DispatchedContinuation用于分发continuation到指定的线程池中;
  2. ContinuationImpl用于包装launch的lambda代码块作为业务代码代理类;
  3. StandAloneCoroutine协程管理类管理Job生命周期以及协程的状态父子Job关系维护等等。

join原理

启动一个launch的时候,返回一个Job对象,这个Job对象对应着上面三个continuation实例,其中我们的业务代码在ContinuationImpl中,例如下面的代码:

  val myScope = CoroutineScope(CoroutineName("name") + IO + Job())

  val job = myScope.launch(CoroutineName("job")) {

      var job2 : Job? = launch(CoroutineName("job2")) {
          Thread.sleep(20000)
      }

      job2?.join()

      printlnM("job has run to end")
  }

  printlnM("job has launch")

这段代码编译后的代码如下:

BuildersKt.launch$default(
   myScope,
   (CoroutineContext)(new CoroutineName("job")),
   (CoroutineStart)null,
   (Function2)(new Function2((Continuation)null) {
       // $FF: synthetic field
       private Object L$0;
       int label;

       public final Object invokeSuspend(@NotNull Object $result) {
           Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
           switch(this.label) {
               case 0:
                   ResultKt.throwOnFailure($result);
                   CoroutineScope $this$launch = (CoroutineScope)this.L$0;
                   //创建job2
                   Job job2 = BuildersKt.launch$default(/*省略。。。省略部分后面贴出*/)
                   
                   this.label = 1;
                   //调用join方法
                   if (job2.join(this) == var5) {
                       return var5;
                   }
                   break;

               case 1:
                   ResultKt.throwOnFailure($result);
                   break;

               default:
                   throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
           }

           MainActivity.Companion.printlnM("job has run to end");
           return Unit.INSTANCE;
       }
	    //省略。。。
   }), 
   2, (Object)null);

Job job2 = BuildersKt.launch$default(/*省略。。。省略部分后面贴出*/)job2的省略代码后面贴出,不影响代码流程。

可以看到,我们的业务代码被编译到invokeSuspend()函数中了,是通过switch case语句来运行对应的代码逻辑,
第一步:case 0执行的是Job job2 = BuildersKt.launch$default(/*省略。。。*/) 也就是逻辑代码里面的创建job2那句代码,然后this.label = 1; ,label设置为1,接着调用job2.join()方法,join方法一般都是返回COROUTINE_SUSPENDED的,那么在这里就返回结束了,除了立马就有返回值的挂起函数返回值,这个if判断就不成立了,直接走下面第三步;
第二步:label为1了,那么case 1的情况成立,入参设置一下异常,这个入参是上一步传递进来的,没有异常的话这里是空的,break;
第三步:调用printlnM("job has run to end");打印语句。

我们可以看到,遇到有函数返回COROUTINE_SUSPENDED的,那么invokeSuspend函数会立刻return结束掉。按照之前 launch启动过程 分析的,launch的lambda逻辑代码被调用的调用链是:

DispatchedContinuation -> ContinuationImpl(内部调用了invokeSuspend) -> StandAloneCoroutine

如果invokeSuspend函数因为返回了COROUTINE_SUSPENDED直接结束掉的话,岂不是这个Job2协程的代码还没有跑完,就结束了?显然是不行的,仔细观察发现,只需要多次调用invokeSuspend方法,label会随着每次调用都会递增或者变动,那么对应的case一定会让所有的代码都执行一遍的。
可以把这种switch case语句当成是人们常说的状态机模式,在安卓开发中,常用的handler.sendMessage就很类似状态机模式,根据不同的what去处理不同的逻辑,只是协程会将顺序代码,根据case分成一个接着一个连续的代码段。

这里我们直接去跟踪join源码看是怎么实现的:

job2.join(this)

public final override suspend fun join() {
    if (!joinInternal()) { // fast-path no wait
        coroutineContext.checkCompletion()
        return // do not suspend
    }
    return joinSuspend() // slow-path wait
}

private suspend fun joinSuspend() = suspendCancellableCoroutine<Unit> { cont ->
    // We have to invoke join() handler only on cancellation, on completion we will be resumed regularly without handlers
    cont.disposeOnCancellation(invokeOnCompletion(handler = ResumeOnCompletion(this, cont).asHandler))
}

主要看joinSuspend函数,里面有几个重要的点:

  1. suspendCancellableCoroutine<Unit> { cont -> 让当前协程在调用该函数处挂起,给当前协程的lambda代码块提供一个CancellableContinuation。
    suspendCoroutineUninterceptedOrReturn { uCont ->
        val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
        /*
         * For non-atomic cancellation we setup parent-child relationship immediately
         * in case when `block` blocks the current thread (e.g. Rx2 with trampoline scheduler), but
         * properly supports cancellation.
         */
        cancellable.initCancellability()
        block(cancellable)
        cancellable.getResult()
    }

首先创建一个CancellableContinuationImpl类型的continuation,入参是uCont.intercepted(),这个uCont是调用的协程,那么就是val job这个协程,uCont.intercepted()也就是 launch启动过程 那章分析的DispatchedContinuation对象,由于val job = myScope.launch(CoroutineName("job")) 这句话已经将DispatchedContinuation生成了,所有会复用之前的对象,代码如下:

    public fun intercepted(): Continuation<Any?> =
        intercepted
            ?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
                .also { intercepted = it }

那么cancellable对象持有的就是val job的DispatchedContinuation,

接着在调用 cancellable.initCancellability()

private fun setupCancellation() {
    //省略
    val parent = delegate.context[Job] ?: return // fast path 3 -- don't do anything without parent
    parent.start() // make sure the parent is started
    val handle = parent.invokeOnCompletion(
        onCancelling = true,
        handler = ChildContinuation(parent, this).asHandler
    )
    parentHandle = handle
	//省略
}

看代码,val handle = parent.invokeOnCompletion 之前分析过,是parent和当前job组成父子关系的作用,parent代表val job,当前job就是cancellable对象,意思就是父子Job可以相互取消对方,和之前父子Job关联的分析是差不多的,那么val jobcancellable组成了父子关系。

在调用 block(cancellable),这个就进入下面2了。

  1. invokeOnCompletion(handler = ResumeOnCompletion(this, cont).asHandler),这个方法是将入参ResumeOnCompletion和当前的JobSupport关联起来,加入到当前Jobsupport.state.list中,asHandler返回一个DisposableHandler然后将这个handler加入到cont的state.list中去,cont也是上面1中变量cancellable,是CancellableContinuationImpl类型的继承自continuation,这样val Job2和新创建的cancellable也组成了父子关系了。

  2. ResumeOnCompletion类型,传入的this,和cont参数,用来恢复cont协程的作用:

private class ResumeOnCompletion(
    job: Job,
    private val continuation: Continuation<Unit>
) : JobNode<Job>(job)  {
    override fun invoke(cause: Throwable?) = continuation.resume(Unit)
    override fun toString() = "ResumeOnCompletion[$continuation]"
}

调用了invoke -> continuation.resume(Unit),这样continuation就会继续执行了。

经过上面的分析之后,可以看出整个Job树的结构如下:
在这里插入图片描述
看图:其中cancellable是可以被job取消的,取消之后,会将cancellable移除掉,cancellable被取消后,会遍历自己的state.list列表调用invoke方法,那么就会调用DIsposableHandle的invoke方法,将自己从job2中移除掉,cancellable就失去了作用。

结合一下job2的被编译后的代码,Job2编译后的代码如下:

BuildersKt.launch$default(
	$this$launch, 
	(CoroutineContext)(new CoroutineName("job2")),
 	(CoroutineStart)null, 
	(Function2)(new Function2((Continuation)null) {
      int label;
      @Nullable
      public final Object invokeSuspend(@NotNull Object var1) {
          Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
          switch(this.label) {
              case 0:
                  ResultKt.throwOnFailure(var1);
                  Thread.sleep(1000L);
                  return Unit.INSTANCE;
              default:
                  throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
          }
      }
	  //省略。。。
  }), 
  2, (Object)null);

invokeSuspend方法被调用后,休眠了1s中,之后结束了,就会继续调用completion的resume方法,这样就会调用到job2对应的

AbstractCoroutine.resumeWith -> makeCompletingOnce -> 
notifyHandlers(list: NodeList, cause: Throwable?) -> 
(ResumeOnCompletion) node.invoke(cause) -> continuation.resume(Unit) ->

进入到CancellableContinuationImpl类中,

CancellableContinuationImpl.resumeWith -> resumeImpl -> 
dispatchResume -> dispatch -> dispatcher.dispatch(context, this)

上面已经分析了dispatcher.dispatch(context, this) dispatcher这个对象是根val job的dispatcher,在val joblaucnh的时候已经创建好了的,此时正在挂起中,所以这里调用dispatch就有下面的调用链:

BaseContinuationImpl..resumeWith() ->   val outcome = invokeSuspend(param)  ->
completion.resumeWith(outcome)

job之前在挂起点结束了,现在又再一次被CancellableContinuationImpl类型的continuation调用,那么job就从这个挂起点被唤醒了。这样就从case 0,运行到case 1了,然后job就结束了。

Await原理

将上面代码的Join换成await方法,代码如下:

  val myScope = CoroutineScope(CoroutineName("name") + IO + Job())

  val job = myScope.launch(CoroutineName("job")) {

       var deferred = async (CoroutineName("job2")) {
          Thread.sleep(20000)
      }

      job2?.await()

      printlnM("job has run to end")
  }

  printlnM("job has launch")

原理其实和Join方法差不多,流程是一样的,只是其中的某些节点的类型不一样,代码如下:

private suspend fun awaitSuspend(): Any? = suspendCoroutineUninterceptedOrReturn { uCont ->
    val cont = AwaitContinuation(uCont.intercepted(), this)
    
    cont.initCancellability()
 	cont.disposeOnCancellation(invokeOnCompletion(ResumeAwaitOnCompletion(cont).asHandler))
    cont.getResult()
}
  1. 创建一个AwaitContinuation类型的continuation,
  2. 然后调用initCancellability方法,这个在上一节讲过,是cont节点和自己的父节点产生父子关系关联,这个父节点也就是uCont,intercepted() 对象,也就是val job 的协程,
  3. 接着
cont.disposeOnCancellation(invokeOnCompletion(ResumeAwaitOnCompletion(cont).asHandler))

这句话在join方法里面看到了,是job2和AwaitContinuation产生父子关系的功能。
那么我们看下ResumeAwaitOnCompletion类型的节点是怎么实现的吧,

private class ResumeAwaitOnCompletion<T>(
    private val continuation: CancellableContinuationImpl<T>
) : JobNode() {
    override fun invoke(cause: Throwable?) {
        val state = job.state
        if (state is CompletedExceptionally) {
            continuation.resumeWithException(state.cause)
        } else {
            continuation.resume(state.unboxState() as T)
        }
    }
}

和ResumeOnCompletion节点其实差不多,只不过调用resume的时候参数值是await的返回值,所以在挂起点恢复的时候,还带有挂起函数执行完成的返回值;
而且在出现异常的时候,还可以将异常抛出去给父Job处理,这一点好像比Job功能更完善。
可以画个图描述一下await的Job树:
在这里插入图片描述
job2是通过AwaitContinuation完成挂起点恢复的,这个类也是继承子 CancellableContinuationImpl的,只是覆盖fun getContinuationCancellationCause(parent: Job): Throwable用于获取异常信息的接口。
其他流程完全和join方法一模一样、

Suspend函数

通过上面的Join和Await的分析,感觉挂起协程之后,似乎需要恢复协程才可以让协程执行完成未完成的代码的。如果Join和Await方法不创建CancellableContinuationImpl这个continuation节点的话,其实val job协程挂起后就结束了,剩下的代码是不会完成,后面的打印语句是不会执行的。这个直觉是正确的,Suspend函数就是有这个作用的,挂起协程之后,需要逻辑代码在挂起函数内部,主动去调用resume和resumeWithException()方法,用于恢复协程。

suspend函数挂起代码实例:

fun SuspendFunc() {
    val myScope = CoroutineScope(Dispatchers.IO)

    myScope.launch {
        printlnM("suspend func run before")

        testRemoteRequest()

        printlnM("suspend func run after")
    }
}

suspend fun testRemoteRequest():String {
    return suspendCancellableCoroutine { cancellableContinuation ->
        getUserInfo(object : Callback {
            override fun success() {
                printlnM("成功请求用户信息")
                cancellableContinuation.resume("成功")
            }
            override fun failed() {
                printlnM("请求用户信息失败")
                cancellableContinuation.resumeWithException(Exception("失败"))
            }
        })
    }
}

interface Callback{
    fun success()
    fun failed()
}

fun getUserInfo(callback: Callback?) {
    try {
        printlnM("getUserInfo")
        Thread.sleep(3000)
        callback?.success()
    } catch (e:Exception) {
        callback?.failed()
        e.printStackTrace()
    }
}

调用挂起函数,创建一个挂起点suspendCancellableCoroutine ,我们的逻辑代码写在suspendCancellableCoroutine 的lambda代码块中,内部调用模拟的网络请求,三秒后,主动调用resume方法,让挂起协程恢复执行。

打印结果如下:

2022-11-17 02:59:44.336  E/MainActivity: DefaultDispatcher-worker-1 : suspend func run before
2022-11-17 02:59:44.337  E/MainActivity: DefaultDispatcher-worker-1 : getUserInfo
2022-11-17 02:59:47.338  E/MainActivity: DefaultDispatcher-worker-1 : 成功请求用户信息
2022-11-17 02:59:47.339  E/MainActivity: DefaultDispatcher-worker-1 : suspend func run after

可以看到协程运行到getUserInfo,中间过了三秒后才继续执行,说明Job被挂起后恢复执行了。

现在修改代码,让网络请求失败,看看异常是谁来处理的:

val myScope = CoroutineScope(Dispatchers.IO + CoroutineExceptionHandler { c,e ->
    printlnM("scopeExceptionHandler : " + e.message)
})

getUserInfo(object : Callback {
    override fun success() {
        printlnM("成功请求用户信息")
        cancellableContinuation.resumeWithException(Exception("失败"))
    }
    override fun failed() {
        printlnM("请求用户信息失败")
        cancellableContinuation.resumeWithException(Exception("失败"))
    }
})

打印日志如下:

2022-11-17 03:10:07.952  E/MainActivity: DefaultDispatcher-worker-1 : suspend func run before
2022-11-17 03:10:07.953  E/MainActivity: DefaultDispatcher-worker-1 : getUserInfo
2022-11-17 03:10:10.954  E/MainActivity: DefaultDispatcher-worker-1 : 成功请求用户信息
2022-11-17 03:10:10.955  E/MainActivity: DefaultDispatcher-worker-1 : scopeExceptionHandler : 失败

可以看到,协程挂起后,三秒后,网路请求失败,挂起点抛出的异常,被根部的CoroutineExceptionHandler处理了,也就是说挂起函数的异常,也是遵循Job异常处理所说的链路传播。

根据上面的分析,可以给挂起流程画个时序图,用于展示线程是怎么切换的,怎么恢复协程的,如下图所示:
在这里插入图片描述

其中带颜色的方框,代表的是DisptacherContinuation,它持有的线程池执行它持有的的协程,所以可以知道挂起点恢复之后,协程所执行的线程池是保持一致的,对于不同的协程之间,由它继承的Disptahcer决定,或者通过launch传递disptahcer参数覆盖,这样就可以修改协程运行所在的线程了。

总结

1. 父JobA的lambda表达式中有挂起函数,协程会在父JobA的挂起点处创建一个CancellableContinuationImpl类型的continuation,这个Cancellable会和父JobA进行父子关联;
如果挂起函数本身是某个JobB的挂起函数,那么Cancellable还会和JobB组成父子关系,JobB在结束自己的时候,会通知Cancellable自己完成了,Cancellable又会继续通知JobA继续执行lambda的代码块,这样JobA就从挂起点恢复过来了。
如果挂起函数是由suspendCancellableCoroutine函数完成的,那么需要在lambda代码块中,收到调用resume函数去主动唤起协程。

2. suspendCancellableCoroutine是挂起函数的重点,这个函数才是挂起函数的实现成为可能。

3. 由于JobA恢复执行的dispatcher不变,所以JobA的lambda代码在挂起点前后所执行的线程池是一样的,如果是单线程的话,那么线程前后是一样的。而挂起点continuation本身的逻辑代码执行线程由挂起点自己决定。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/12254.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于python的人力资源管理系统

摘 要 随着当今社会的发展&#xff0c;时代的进步&#xff0c;各行各业也在发生着变化&#xff0c;比如人力资源管理这一方面&#xff0c;利用网络已经逐步进入人们的生活。传统的人力资源管理&#xff0c;都是员工去公司查看部门信息、招聘信息&#xff0c;这种传统方式局限性…

第一个 Go 程序,从 Hello World 开始

1、开发编辑器 Go 采用的是UTF-8编码的文本文件存放源代码&#xff0c;理论上使用任何一款文本编辑器都可以做 Go 语言开发&#xff0c;这里推荐使用 VS Code 和 Goland。 VS Code 是微软开源的编辑器&#xff0c;而 Goland 是 jetbrains 出品的付费IDE。GoLand 开发工具时收…

linux 安装微擎

前言 OS: CentOS Linux release 7.6.1810 (Core)nginx1.12.2微擎 v2.7.4 环境准备 PHP 7.0MYSQL 5.7 安装mysql 5.7 参考 【Docker】 安装 mysql 安装PHP 7.0 参考 Linux 利用yum源安装php7.0nginx PHP 支持 GD2 yum install php70w-gd*安装完成后重启php PHP 支持 D…

从感知机到神经网络

一、神经网络的一个重要性质 1.1 重要性质 自动从数据中学习到合适的权重参数 1.2 称呼 共n层神经元&#xff0c;称之为n-1层网络 输入层中间层&#xff08;隐藏层&#xff09;输出层 1.3计算神经网络 节点值*权重值偏置值输出值 根据输出值的大小计算出节点值 输出值…

ICV:全球首份量子重力测量仪器市场分析报告。传统测量行业地位正被量子传感器商业化严重威胁,中国有望成为量子重力测量仪器市场最大赢家!

本报告的主要内容是对量子重力测量仪器进行市场分析&#xff0c;并对量子重力测量仪器科研方向、主要企业、关键应用、产业现状&#xff08;科研需求、军用场景&#xff09;、未来趋势等方面进行分析及预测未来的市场发展。 量子重力传感器原理为在真空环境中利用激光和磁场捕获…

11、Service访问Pod、Service IP原理、DNS访问Service、外部访问service

Pod可能因为各种原因发生故障而死掉&#xff0c;Deployment等Controller会通过动态创建和销毁Pod来保障应用整体的健壮性。Pod是脆弱的&#xff0c;但应用是健壮的。每个Pod都有自己的IP地址&#xff0c;当controller用新的Pod替代发生故障的Pod时&#xff0c;新Pod会分配到新的…

【Linux】Ubuntu、Debian下对deb包进行修改后重新打包——以MySQL安装包为例

需求 关于MySQL的lower_case_table_names参数&#xff0c;在Windows系统下和在Linux系统下&#xff0c;默认值是不同的。 Unix&#xff0c;Linux下默认为0&#xff0c;大小写敏感。创建的库表名将原样保存在磁盘上。如create database TeSt;将会创建一个TeSt的目录&#xff0…

Java项目:SSH在线水果商城平台含管理系统

作者主页&#xff1a;源码空间站2022 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文末获取源码 项目介绍 本项目分为前后台&#xff0c;分为普通用户与管理员两个角色&#xff0c;前台为普通用户登录&#xff0c;后台为管理员登录&#xff1b; 管理员…

webpack5 Core-js解决async 函数、promise 对象等兼容问题

为什么Core-js 过去我们使用 babel 对 js 代码进行了兼容性处理&#xff0c;其中使用babel/preset-env 智能预设来处理兼容性问题。 它能将 ES6 的一些语法进行编译转换&#xff0c;比如箭头函数、点点点运算符等。但是如果是 async 函数、promise 对象、数组的一些方法&#x…

《FFmpeg Basics》中文版-06-填充视频

正文 填充视频意味着向视频帧添加额外的区域以包含额外的内容。当输入应在具有不同宽高比的显示器上播放时&#xff0c; 通常需要填充视频。 填充视频基础知识 对于视频填充&#xff0c;我们使用表格中描述的填充过滤器。 描述在输入视频帧中添加彩色填充&#xff0c;该帧位…

HTML+CSS+JS大作业:商城网购网站设计——淘宝1页

⛵ 源码获取 文末联系 ✈ Web前端开发技术 描述 网页设计题材&#xff0c;DIVCSS 布局制作,HTMLCSS网页设计期末课程大作业 | 在线商城购物 | 水果商城 | 商城系统建设 | 多平台移动商城 | H5微商城购物商城项目 | HTML期末大学生网页设计作业&#xff0c;Web大学生网页 HTML&a…

微信小程序获取用户头像昵称组件封装(最新版)

一、前言 微信小程序将在2022年11月08日对获取用户头像昵称信息的API再一次进行改动&#xff0c;这次的改动比较大。 更多详情查看公告&#xff1a;公告直达链接 我的项目比较多&#xff0c;而且大部分都是只需要获取用户的头像以及昵称&#xff0c;并不需要像官方的“最佳实…

袋鼠云数栈UI5.0体验升级背后的故事:可用性原则与交互升级

最近&#xff0c;我们袋鼠云的UED部⻔小伙伴们&#xff0c;不声不响地⼲了⼀件⼤事——升级了全新设计语言「数栈UI5.0」。 众所周知&#xff0c;用户在使用产品时&#xff0c;是一个动态的过程&#xff0c;用户和产品之间进行交互的可用性&#xff0c;能否让用户愉悦、快速地…

uni-app - H5 页面路由不存在时,跳转到自己定制的 404.vue 页面(当路由不存在时自动重定向到自定义的 404 组件)超详细简约高效的解决方案

前言 在 uni-app 开发 H5 移动端项目中,如果访问一个不存在的路由路径后,自定跳转重定向到自己定义的 404 页面。 本文带您从 0-1 一路复制,仅需 2 步完成 当页面 router 路由不存在,自动跳转到指定的 404 页面, 用最简单且最高效的方式完成,助您轻松完成该功能, 如下图…

【好文鉴赏】初创公司到底值不值得去?从以下几点考虑

关键词&#xff1a;【初创公司】【职业发展】【就业】 原文链接&#xff1a; https://tech.sina.cn/csj/2018-09-10/doc-ihiixyeu5565677.d.htmlhttps://www.zhihu.com/question/312725868/answer/693656577 有人说初创公司管理不成熟&#xff0c;未来不稳定&#xff1b; 有…

最小二乘法,加权最小二乘法,迭代重加权最小二乘法

文章目录一&#xff1a;最小二乘法&#xff08;OLS&#xff09;1&#xff1a;概述2&#xff1a;代数式3&#xff1a;矩阵式&#xff08;推荐&#xff09;3.1&#xff1a;实现代码二&#xff1a;加权最小二乘法&#xff08;WLS&#xff09;1&#xff1a;增加对角矩阵 W1.1&#…

oracle数据库控制语言—DCL

文章目录1、授予系统权限1.1 授予创建其他对象权限2、撤销系统权限2.1 示例3、oracle 中的角色3.1 什么时角色3.2 创建角色并且授予权限给角色3.2.1 创建角色3.2.1.1 示例3.2.2 授予权限给一个角色3.2.2.1 示例3.2.3 授予一个角色给用户3.2.3.1 示例一3.2.3.2 示例二3.2.3.3 示…

基于MySQL的事务管理

目录 概念&#xff1a;多条语句组成一个执行单位 事务的基本操作 MySQL中的事务必须满足A,C,I,D这四个基本特性 事务操作举例——&#xff08;转账&#xff09; 事务保存点——SAVEPOINT 事务隔离级别——多线程(并发同时访问) 总结 概念&#xff1a;多条语句组成一个执…

Mongo的数据操作

文章目录一&#xff0c;创建数据库二&#xff0c;插入数据&#xff08;一&#xff09;插入单条数据1&#xff0c;insert2&#xff0c;save&#xff08;二&#xff09;插入多条数据三&#xff0c;修改数据四&#xff0c; 更新所有找到匹配的数据五&#xff0c;数据删除&#xff…

极速Go语言入门(超全超详细)-基础篇

文章目录 GoLang概述 Go语言三大牛谷歌创造Golang的原因Golang 的发展历程Golang 的语言的特点 Go语言开发工具Go开发环境配置(sdk下载及配置) 使用开发工具创建第一个Go项目 Go 程序开发的注意事项 官方参考文档 Go学习 Go变量 数据类型 标识符 运算符 键盘输入语句 程序流程…