鸿蒙OpenHarmony多线程能力场景化示例实践

简介

在OpenHarmony应用中,每个 进程 都会有一个主线程,主线程主要承担执行UI绘制操作、管理ArkTS引擎实例的创建和销毁、分发和处理事件、管理Ability生命周期等职责,具体可参见 线程模型概述 。因此,开发应用时应当尽量避免将耗时的操作放在主线程中执行。ArkTS提供了WorkerTaskPool两种多线程并发能力,多线程并发允许在同一时间段内同时执行多段代码,这两个并发的基本能力可参见 TaskPool和Worker的对比 。

在介绍WorkerTaskPool的详细使用方法前,我们先简单介绍并发模型的相关概念,以便于大家的理解。

并发模型概述

并发的意思是多个任务同时执行。并发模型分为两大类:基于内存共享的并发模型和基于消息传递的并发模型。

在基于内存共享的并发模型中,并发线程通过读写内存中的共享对象来进行交互。基于共享内存的并发编程需要满足三条性质:

  • 原子性:指一个操作是不可中断的,要么全部执行成功要么全部执行失败。
  • 有序性:指程序执行的顺序必须符合预期,不能出现乱序的情况。
  • 可见性:指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。

现代程序语言一般通过锁、内存屏障、原子指令来满足这三条性质。基于内存共享的并发模型与底层硬件接近,在能正确撰写并发代码的情况下,可以最大发挥底层硬件性能,实现性能优秀的多线程程序。但是这种并发模型难以掌握,即使资深的程序员也非常容易犯错。典型的基于内存共享并发模型的程序语言有C++ 、Swift和Java等。

在基于消息传递的并发模型中,并发线程的内存相互隔离,需要通过通信通道相互发送消息来进行交互。典型的基于消息传递的并发模型一般有两种:CSP和Actor。

CSP(Communicating Sequential Processes,通信顺序进程)中的计算单元并不能直接互相发送信息。需要通过通道(Channel)作为媒介进行消息传递:发送方需要将消息发送到Channel,而接收方需要从Channel读取消息。与CSP不同,在Actor模型中,每个Actor可以看做一个独立的计算单元,并且相互之间内存隔离,每个Actor中存在信箱(Mail Box),Actor之间可以直接进行消息传递,如下图所示:

图1 Actor消息传递示意图

CSP与Actor之间的主要区别:

  • Actor需要明确指定消息接收方,而CSP中处理单元不用关心这些,只需要把消息发送给Channel,而接收方只需要从Channel读取消息。
  • 由于在默认情况下Channel是没有缓存的,因此对Channel的发送(Send)动作是同步阻塞的,直到另外一个持有该Channel引用的执行块取出消息,而Actor模型中信箱本质是队列,因此消息的发送和接收可以是异步的。

典型的基于消息传递的并发模型的程序语言有:Dart、JS和ArkTS。OpenHarmony中Worker和TaskPool都是基于Actor并发模型实现的并发能力。

Worker

基本概念和运作原理

OpenHarmony中的Worker是一个独立的线程,基本概念可参见 TaskPool和Worker的对比 。Worker拥有独立的运行环境,每个Worker线程和主线程一样拥有自己的内存空间、消息队列(MessageQueue)、事件轮询机制(EventLoop)、调用栈(CallStack)等。线程之间通过消息(Massage)进行交互,如下图所示:

图2 线程交互示意图

在多核的情况下(下图中的CPU 1和CPU 2同时工作),多个Worker线程(下图中的worker thread1和worker thread2)可以同时执行,因此Worker线程做到了真正的并发,如下图所示:

图3 Worker线程并发示意图

使用场景和开发示例

对于Worker,有以下适用场景:

  • 运行时间超过3分钟的任务,需要使用Worker。

  • 有关联的一系列同步任务,例如数据库增、删、改、查等,要保证同一个句柄,需要使用Worker。

以视频解压的场景为例,点击右上角下载按钮,该示例会执行网络下载并监听,下载完成后自动执行解压操作。当视频过大时,可能会出现解压时长超过3分钟耗时的情况,因此我们选用该场景来说明如何使用Worker。

场景预览图如下所示:

图4 场景预览图

使用步骤如下:

  1. 宿主线程创建一个Worker线程。通过new worker.ThreadWorker()创建Worker实例,示例代码如下:

    // 引入worker模块
    import worker, { MessageEvents } from '@ohos.worker';
    import type common from '@ohos.app.ability.common';let workerInstance: worker.ThreadWorker = new worker.ThreadWorker('entry/ets/pages/workers/worker.ts', { name: 'FriendsMoments Worker'
    });
    
  2. 宿主线程给Worker线程发送任务消息。宿主线程通过postMessage方法来发送消息给Worker线程,启动下载解压任务,示例代码如下:

    // 请求网络数据
    let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
    // 参数中mediaData和isImageData是根据开发者自己的业务需求添加的,其中mediaData为数据路径、isImageData为判断图片或视频的标识
    workerInstance.postMessage({ context, mediaData: this.mediaData, isImageData: this.isImageData });
    
  3. Worker线程监听宿主线程发送的消息。Worker线程在onmessage中接收到宿主线程的postMessage请求,执行下载解压任务,示例代码如下:

    // 引入worker模块
    import worker, { MessageEvents } from '@ohos.worker';let workerPort = worker.workerPort;
    // 接收宿主线程的postMessage请求
    workerPort.onmessage = (e: MessageEvents): void => {// 下载视频文件let context: common.UIAbilityContext = e.data.context;let filesDir: string = context.filesDir;let time: number = new Date().getTime();let inFilePath: string = `${filesDir}/${time.toString()}.zip`;let mediaDataUrl: string = e.data.mediaData;let urlPart: string = mediaDataUrl.split('.')[1];let length: number = urlPart.split('/').length;let fileName: string = urlPart.split('/')[length-1];let options: zlib.Options = {level: zlib.CompressLevel.COMPRESS_LEVEL_DEFAULT_COMPRESSION};request.downloadFile(context, {url: mediaDataUrl,filePath: inFilePath}).then((downloadTask) => {downloadTask.on('progress', (receivedSize: number, totalSize: number) => {Logger.info(`receivedSize:${receivedSize},totalSize:${totalSize}`);});downloadTask.on('complete', () => {// 下载完成之后执行解压操作zlib.decompressFile(inFilePath, filesDir, options, (errData: BusinessError) => {if (errData !== null) {...// 异常处理}let videoPath: string = `${filesDir}/${fileName}/${fileName}.mp4`;workerPort.postMessage({ 'isComplete': true, 'filePath': videoPath });})});downloadTask.on('fail', () => {...// 异常处理});}).catch((err) => {...// 异常处理});
    };
    
  4. 宿主线程监听Worker线程发送的信息。宿主线程通过onmessage接收到Worker线程发送的消息,并执行下载的结果通知。

  5. 释放Worker资源。在业务完成或者页面销毁时,调用workerPort.close()接口主动释放Worker资源,示例代码如下所示:

    workerInstance.onmessage = (e: MessageEvents): void => {if (e.data) {this.downComplete = e.data['isComplete'];this.filePath = e.data['filePath'];workerInstance.terminate();setTimeout(() => {this.downloadStatus = false;}, LOADING_DURATION_OPEN);}
    };
    

TaskPool

基本概念和运作原理

相比使用Worker实现多线程并发,TaskPool更加易于使用,创建开销也少于Worker,并且Worker线程有个数限制,需要开发者自己掌握,TaskPool的基本概念可参见 TaskPool和Worker的对比 。TaskPool作用是为应用程序提供一个多线程的运行环境。TaskPool在Worker之上实现了调度器和Worker线程池,TaskPool根据任务的优先级,将其放入不同的优先级队列,调度器会依据自己实现的调度算法(优先级,防饥饿),从优先级队列中取出任务,放入TaskPool中的Worker线程池,执行相关任务,流程图如下所示:

图5 TaskPool流程示意图

TaskPool有如下的特点:

  • 轻量化的并行机制。
  • 降低整体资源的消耗。
  • 提高系统的整体性能。
  • 无需关心线程实例的生命周期。
  • 可以使用TaskPool API创建后台任务(Task),并对所创建的任务进行如任务执行、任务取消的操作。
  • 根据任务负载动态调节TaskPool工作线程的数量,以使任务按照预期时间完成任务。
  • 可以设置任务的优先级。
  • 可以设置任务组(TaskGroup)将任务关联起来。

使用场景和开发示例

TaskPool的适用场景主要分为如下三类:

  • 需要设置优先级的任务。
  • 需要频繁取消的任务。
  • 大量或者调度点较分散的任务。

因为朋友圈场景存在不同好友同时上传视频图片,在频繁滑动时将多次触发下载任务,所以下面将以使用朋友圈加载网络数据并且进行解析和数据处理的场景为例,来演示如何使用TaskPool进行大量或调度点较分散的任务开发和处理。场景的预览图如下所示:

图6 朋友圈场景预览图

使用步骤如下:

  1. 首先import引入TaskPool模块,TaskPool的API介绍可参见 @ohos.taskpool(启动TaskPool) 。

    import taskpool from '@ohos.taskpool';
    
  2. new一个task对象,其中传入被调用的方法和参数。

    ... 
    // 创建task任务项,参数1.任务执行需要传入函数 参数2.任务执行传入函数的参数 (本示例中此参数为被调用的网络地址字符串)
    let task: taskpool.Task = new taskpool.Task(getWebData, jsonUrl);
    ...// 获取网络数据
    @Concurrent
    async function getWebData(url: string): Promise<Array<FriendMoment>> {try {let webData: http.HttpResponse = await http.createHttp().request(url,{ header: {'Content-Type': 'application/json'},connectTimeout: 60000, readTimeout: 60000})if (typeof (webData.result) === 'string') {// 解析json字符串let jsonObj: Array<FriendMoment> = await JSON.parse(webData.result).FriendMoment;let friendMomentBuckets: Array<FriendMoment> = new Array<FriendMoment>();// 下方源码省略,主要为数据解析和耗时操作处理...return friendMomentBuckets;} else {// 异常处理...}} catch (err) {// 异常处理...}
    }
    
  3. 之后使用taskpool.execute执行TaskPool任务,将待执行的函数放入TaskPool内部任务队列等待执行。execute需要两个参数:创建的任务对象、等待执行的任务组的优先级,默认值是Priority.MEDIUM。在TaskPool中执行完数据下载、解析和处理后,再返回给主线程中。

    let friendMomentArray: Array<FriendMoment> = await taskpool.execute(task, taskpool.Priority.MEDIUM) as Array<FriendMoment>;
    
  4. 将新获取的momentData通过AppStorage.setOrCreate传入页面组件中。

    // 获取页面组件中的momentData对象,其中是组件所需的username、image、video等数据
    let momentData = AppStorage.get<FriendMomentsData>('momentData');
    // 循环遍历对象并依次传入momentData
    for (let i = 0; i < friendMomentArray.length; i++) {momentData.pushData(friendMomentArray[i]);
    }
    // 将更新的momentData返回给页面组件
    AppStorage.setOrCreate('momentData', momentData);
    

其他场景示例和方案思考

在日常开发过程中,我们还会碰到一些其他并发场景问题,下面我们介绍了常用并发场景的示例方案推荐。

Worker线程调用主线程类型的方法

我们在主线程中创建了一个对象,假如类型为MyMath,我们需要把这个对象传递到Worker线程中,然后在Worker线程中执行该类型中的一些耗时操作方法,比如Math中的compute方法,类结构示例代码如下:

class MyMath {a: number = 0;b: number = 1;constructor(a: number, b: number) {this.a = a;this.b = b;}compute(): number {return this.a + this.b;}
}

主线程代码:

private math: MyMath = new MyMath(2, 3); // 初始化a和b的值为2和3
private workerInstance: worker.ThreadWorker;this.workerInstance = new worker.ThreadWorker("entry/ets/worker/MyWorker.ts");
this.workerInstance.postMessage(this.math); // 发送到Worker线程中,期望执行MyMath中的compute方法,预期值是2+3=5

MyMath对象在进行线程传递后,会丢失类中的方法属性,导致我们只是在Worker线程中可以获取到MyMath的数据,但是无法在子系统中直接调用MyMath的compute方法,示意代码如下:

const workerPort = worker.workerPort;
workerPort.onmessage = (e: MessageEvents): void => {let a = e.data.a;let b = e.data.b;
}

这种情况下我们可以怎么去实现在Worker线程中调用主线程中类的方法呢?

首先,我们尝试使用强制转换的方式把Worker线程接收到数据强制转换成MyMath类型,示例代码如下:

const workerPort = worker.workerPort;
workerPort.onmessage = (e: MessageEvents): void => {let math = e.data as MyMath; // 方法一:强制转换console.log('math compute:' + math.compute()); // 执行失败,不会打印此日志
}

强制转换后执行方法失败,不会打印此日志。因为序列化传输普通对象时,仅支持传递属性,不支持传递其原型及方法。接下来我们尝试第二种方法,根据数据重新初始化一个MyMath对象,然后执行compute方法,示例代码如下:

const workerPort = worker.workerPort;
workerPort.onmessage = (e: MessageEvents): void => {// 重新构造原类型的对象let math = new MyMath(0, 0);math.a = e.data.a;math.b = e.data.b;console.log('math compute:' + math.compute()); // 成功打印出结果:5
}

第二种方法成功在Worker线程中调用了MyMath的compute方法。但是这种方式还有弊端,比如每次使用到这个类进行传递,我们就得重新进行构造初始化,而且构造的代码会分散到工程的各处,很难进行维护,于是我们有了第三种改进方案。

第三种方法,我们需要构造一个接口类,包含了我们需要线程间调用的基础方法,这个接口类主要是管理和约束MyMath类的功能规格,保证MyMath类和它的代理类MyMathProxy类在主线程和子线程的功能一致性,示例代码如下:

interface MyMathInterface {compute():number;
}

然后,我们把MyMath类继承这个方法,并且额外构造一个代理类,继承MyMath类,示例代码如下:

class MyMath implements MyMathInterface {a: number = 0;b: number = 1;constructor(a: number, b: number) {console.log('MyMath constructor a:' + a + ' b:' + b)this.a = a;this.b = b;}compute(): number {return this.a + this.b;}
}class MyMathProxy implements MyMathInterface {private myMath: MyMath;constructor(math: MyMath) {this.myMath = new MyMath(math.a, math.b);}  // 代理MyMath类的compute方法compute(): number {return this.myMath.compute();}
}

我们在主线程构造并且传递MyMath对象后,在Worker线程中转换成MyMathProxy,即可调用到MyMath的compute方法了,并且无需在多处进行初始化构造,只要把构造逻辑放到MyMathProxy或者MyMath的构造函数中,Worker线程中的示例代码如下:

const workerPort = worker.workerPort;
workerPort.onmessage = (e: MessageEvents): void => {// 方法三:使用代理类构造对象let proxy = new MyMathProxy(e.data)console.log('math compute:' + proxy.compute()); // 成功打印出结果:5
}

大家可以根据实际场景选择第二种或者第三种方案。

为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

《鸿蒙开发学习手册》:https://qr21.cn/FV7h05

入门必看:https://qr21.cn/FV7h05
1.  应用开发导读(ArkTS)
2.  ……

HarmonyOS 概念:https://qr21.cn/FV7h05

  1. 系统定义
  2. 技术架构
  3. 技术特性
  4. 系统安全

如何快速入门:https://qr21.cn/FV7h05
1.  基本概念
2.  构建第一个ArkTS应用
3.  ……

开发基础知识:https://qr21.cn/FV7h05
1.  应用基础知识
2.  配置文件
3.  应用数据管理
4.  应用安全管理
5.  应用隐私保护
6.  三方应用调用管控机制
7.  资源分类与访问
8.  学习ArkTS语言
9.  ……

基于ArkTS 开发:https://qr21.cn/FV7h05
1.  Ability开发
2.  UI开发
3.  公共事件与通知
4.  窗口管理
5.  媒体
6.  安全
7.  网络与链接
8.  电话服务
9.  数据管理
10.  后台任务(Background Task)管理
11.  设备管理
12.  设备使用信息统计
13.  DFX
14.  国际化开发
15.  折叠屏系列
16.  ……

鸿蒙开发面试真题(含参考答案):https://qr21.cn/FV7h05

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

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

相关文章

Sora技术原理解析

1.Sora简介 Sora是一个基于大规模训练的文本控制视频生成扩散模型。 Sora能够生成高达1分钟的高清视频&#xff0c;涵盖广泛的视觉数据类型和分辨率。 Sora使用简单的文本描述&#xff0c;使得视频创作变得前所未有的简单和高效。 Sora的一些能力&#xff1a; Text-to-video…

windows server mysql 数据库停止 备份 恢复全流程操作方法

一,mysql备份 mysql最好是原工程文件备份.不需要sql查询的方式备份.安全高效. 比如,安装php与mysql组合后,我的mysql文件保存在: D:\phpstudy_pro\Extensions\MySQL5.7.26\data\dux 我只需要复制一份,保存起来就行. 二,mysql恢复 怎么恢复呢.我们一般是只恢复其中一个表,则找…

一个具有强大PDF处理能力的.Net开源项目

PDF具有跨平台、可读性强、不可修改性、无需特定阅读软件、内容安全等好处&#xff0c;在工作中经常都会用到。 所以&#xff0c;我们在项目开发中&#xff0c;经常需要生成PDF的文件&#xff0c;或者把Html、Xml等文件转化为PDF格式。 今天给大家推荐一个具有PDF处理能力的.…

优思学院|质量工程师需要学习什么软件?

初入职质量工程师的朋友常常会问&#xff1a;质量工程师需要学习什么软件&#xff1f;在质量控制和管理的世界里&#xff0c;拥有强大的数据分析工具是走向成功的关键&#xff0c;因此&#xff0c;对于质量工程师来说&#xff0c;掌握正确的软件不仅能提升工作效率&#xff0c;…

lv20 QT对话框3

1 内置对话框 标准对话框样式 内置对话框基类 QColorDialog, QErrorMessage QFileDialog QFontDialog QInputDialog QMessageBox QProgressDialogQDialog Class帮助文档 示例&#xff1a;各按钮激发对话框实现基类提供的各效果 第一步&#xff1a;实现组件布局&…

Pyhton的组合数据类型

归纳编程学习的感悟&#xff0c; 记录奋斗路上的点滴&#xff0c; 希望能帮到一样刻苦的你&#xff01; 如有不足欢迎指正&#xff01; 共同学习交流&#xff01; &#x1f30e;欢迎各位→点赞 &#x1f44d; 收藏⭐ 留言​&#x1f4dd; 生命对某些人来说是美丽的&#xff0c…

尝鲜18倍速大模型Groq和世界第二AI Mistral(Le Chat)

01 尝鲜 中午,一边吃饭,一边尝试一下最新的AI:Groq,它使用了重新设计的LPU,据说比英伟达的GPU快了18倍。 运行了开源的Mixtral-8x7b模型,屏幕上的文字回复几乎是瞬间的,那种速度感,让人心跳加速。 接着,我尝试了来自欧洲的新贵——Mistral AI的Le Chat。 这个三天前…

python学习笔记-内置异常

概述 Python 中的异常&#xff08;Exception&#xff09;是指在程序执行过程中遇到的错误或异常情况。当程序出现异常时&#xff0c;解释器会停止当前代码的执行&#xff0c;并试图找到匹配的异常处理器来处理异常。如果没有找到合适的异常处理器&#xff0c;程序就会终止并打…

go并发模式之----阻塞/屏障模式

常见模式之一&#xff1a;阻塞/屏障模式 定义 顾名思义&#xff0c;就是阻塞等待所有goroutine&#xff0c;直到所有goroutine完成&#xff0c;聚合所有结果 使用场景 多个网络请求&#xff0c;聚合结果 大任务拆分成多个子任务&#xff0c;聚合结果 示例 package main ​…

配电房轨道式巡检机器人方案

一、应用背景 在变电站、配电房、开关站等各种室内变配电场所内&#xff0c;由于变配电设备的数量众多、可能存在各类安全隐患&#xff0c;为了保证用电的安全可靠&#xff0c;都要进行日常巡检。 但目前配电房人工巡检方式有以下主要问题&#xff1a; 巡检工作量大、成本高 …

关于StartAI本地部署相关问题解答

很多小伙伴们都有接入自己本地SD的需求&#xff0c;对此小编整理了一些相关问题~ 一、本地部署相关条件 对于想要本地部署的小伙伴要了解&#xff0c;相对于使用StartAI试用引擎本地部署更加考验电脑硬件配置备噢~ 流畅使用要nvidia显卡&#xff0c;6g以上显存&#xff08;最…

图解知识蒸馏

soft labels与soft predictions越接近越好&#xff0c;通过Loss Fn来实现&#xff0c;产生的数值叫做distillation loss&#xff0c;也叫soft loss。 hard label y与hard prediction越接近越好&#xff0c;通过Loss Fn来实现&#xff0c;产生的数值叫做student loss&#xff0c…

什么是VR虚拟现实元宇宙|VR设备购买|VR设备零售店

VR&#xff08;Virtual Reality&#xff0c;虚拟现实&#xff09;元宇宙是指基于虚拟现实技术构建的一个虚拟世界&#xff0c;这个世界可以模拟现实世界或者超出现实世界的一切事物和活动。在这个虚拟世界里&#xff0c;用户可以通过虚拟现实设备&#xff08;如头戴式显示器、手…

本地搭建xss平台并获取cookie演练

前言 一般而言&#xff0c;搭建xss平台是不被允许的&#xff0c;但是由于教育的目的&#xff0c;搭建xss平台更能让学习者更加直观感受xss漏洞对我们的危害和它的重要性。 搭建xss平台 1.搭建xss平台的基础是在phpstudy一个集成环境上的&#xff0c;所有第一步要安装phpstudy&a…

XSS中级漏洞(靶场)

目录 一、环境 二、正式开始闯关 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x0B 0x0C 0x0D 0x0E ​ 0x0F 0x10 0x11 0x12 一、环境 在线环境&#xff08;gethub上面的&#xff09; alert(1) 二、正式开始闯关 0x01 源码&#xff1a; 思路&#xff1a;闭…

前后端依赖下载上传

在某些情况下&#xff0c;可能需要在没有互联网连接的环境中进行构建或部署。通过提前下载所有依赖&#xff0c;你可以将它们保存在本地&#xff0c;然后在没有网络连接时使用&#xff0c;提高构建或部署的效率。 前端下载依赖 脚本getTzgUrl.js const { readFileSync, writ…

SpringBoot整合rabbitmq-扇形交换机队列(三)

说明&#xff1a;本文章主要是Fanout 扇形交换机的使用&#xff0c;它路由键的概念&#xff0c;绑定了页无用&#xff0c;这个交换机在接收到消息后&#xff0c;会直接转发到绑定到它上面的所有队列。 大白话&#xff1a;广播模式&#xff0c;交换机会把消息发给绑定它的所有队…

异常网络下TCP的可靠服务机制(慢启动、拥塞避免、快重传、快恢复)

目录 TCP超时重传拥塞控制概述慢启动和拥塞避免下面讲解发送端如何判断拥塞发生。 快速重传和快速恢复 本文描述TCP在异常网络下的处理方式 以保证其可靠的数据传输的服务 TCP超时重传 tcp服务能够重传其超时时间内没有收到确认的TCP报文段&#xff0c;tcp模块为每一个报文段都…

看到极氪001这价格这配置,小米SU7我不等了

文 | AUTO芯球 作者 | 李诞 新款升级1100多项&#xff0c;还是原来老款的价格&#xff0c;新款极氪001你这样子卷&#xff0c;友商没法玩啊。 我惊呆了朋友们 不是极氪001一发布 第二天苹果就宣布造车失败 而是极氪001一直是30万以上中国品牌纯电轿车/SUV、高端猎装车销量…

MySQL 5.7.31详细下载安装配置

1、下载步骤 下载完毕后将文件解压到你想保存到的盘和目录内。我是将文件解压到D:\Mysql目录下面 2.配置环境变量 1.系统—>高级系统设置—>环境变量—>系统变量 在系统变量中点击新建&#xff0c;变量名为量名为&#xff1a;MYSQL_HOME&#xff0c;添加你的mysql…