剖析MapReduce 作业运行机制
包含四个独立的实体:
· Client Node 客户端:编写 MapReduce代码,配置作业,提交MapReduce作业。
· JobTracker :初始化作业,分配作业,与 TaskTracker通信,协调整个作业的运行。 jobtracker是一个Java 应用程序,它的主类是 JobTracker。
· TaskTracker :保持与 JobTracker通信,在分配的数据片段上执行 Map或Reduce 任务。tasktracker是 Java应用程序,它的主类是TaskTracker。
(一)作业的提交:
一个MapReduce 作业在提交到 Hadoop上之后,会进入完全地自动化执行过程。在这个过程中,用户除了监控程序的执行情况和强制中止作业之外,不能对作业的执行过程进行任何干扰。所以在作业提交之前,用户需要将所有应该配置的参数按照自己的意愿配置完毕。
需要配置的主要内容有:
public static void main(String[] args) throws Exception{
JobConf conf = new JobConf(WordCount.class);
conf.setJobName(“wordcount”); conf.setOutputKeyClass(Text.class);
conf.setOutputValueClass(IntWritable.class); conf.setMapperClass(Map.class);
conf.setReduceClass(Reduce.class); conf.setInputFormat(TextInputFormat.class);
conf.setOutputFormat(TextOutputFormat.class); FileInputFormat.setInputPaths(conf,new Path(args[0]));
FileOutputFormat.setOutputPath(conf,new Path(args[1])); JobClient.runJob(conf);
}
* 程序代码:这里主要是指 map函数和reduce 函数的具体代码,这是一个 MapReduce作业对应的程序必不可少的部分,并且这部分代码的逻辑正确与否与运行结果直接相关。
* Map接口和 Reduce接口的配置:在MapReduce中, Map接口需要派生自Mapper<k1,v1,k2,v2>接口,Reduce 接口则要派生自 Reduce<k2,v2,k3,v3>。它们都对应唯一一个方法,分别是 map函数和reduce 函数,也就是在上一点中所写的代码。在调用这两个方法时需要配置它们的四个参数,分别是输入 key的数据类型、输入value的数据类型、输出 key-value对的数据类型和Reporter实例,其中输入输出的数据类型要与继承时设置的数据类型相同,还有一个要求是 Map接口的输出key-value 类型要与Reduce的输入key-value对应,因为 map输出组合value 之后,它们会成为 reduce的输入内容。
* 输入输出路径:作业提交之前还需要在主函数中配置 MapReduce作业在Hadoop 集群上的输入路径和输出路径 (必须保证输出路径不存在,如果存在程序会报错,这也是初学者经常犯的错误 )。
* 其他类型设置,比如调用 runJob方法:先要在主函数中配置如 Output的key 和value类型、作业名称、InputFormat和OutputFormat 等,最后再调用 JobClient的runJob 方法。
配置完作业的所有内容并确认无误之后就可以运行作业了。( 步骤1:run job)
用户程序调用JobClient 的runJob方法,在提交 JobConf对象之后,runJob 方法会先调用 JobSubmissionProtocol接口所定义的submitJob 方法,并将作业提交给 JobTracker。紧接着,runJob 不断循环,并在循环中调用JobSubmissionProtocol的getTaskCompletionEvents 方法,获取 TaskCompletionEvent类的对象实例,了解作业的实时执行情况。如果发现作业运行状态有更新,就将状态报告给 JobTracker。作业完成后,如果成功就显示作业计数器,否则,将导致作业失败的错误记录到控制台。
上面介绍了作业提交的过程,可以看出,最关键的是 JobClient对象中submitJob() 方法的调用执行,那么 submitJob()方法具体是怎么做的呢?下面从 submitJob()方法的代码出发介绍作业提交的详细过程。
public RunningJob submitJob (JobConf job) throws FileNotFoundException, InvalidJobConfException, IOException{ // 从JobTracker 得到当前任务的 ID
JobID jobId = jobSubmitClient.getNewJobId(); // 获取HDFS 路径:
Path submitJobDir = new Path (getSystemDir(),jobId.toString()); // 获取提交作业的 JAR文件目录
Path submitJarFile = new Path( submitJobDir , “job.jar”); // 获取提交输入文件分割信息的路径
Path submitSplitFile = new Path (submitJobDir , “job.split”); // 此处将-libjars 命令行指定的 jar 上传至HDFS
configureCommandLineOprions (job , submitJobDir , submitJarFile); // 获取作业配置文档的路径
Path submitJobFile = new Path(submitJobDir , “job.xml”);
…….
// 通过input format 的格式获得相应的 input split ,默认类型为
// FileSplit InputSplit[] splits = job.getInputFormat().getSplits(job, job.getNumMapTasks());
// 从这里开始对文件进行分割
// 生成一个写入流,将 input split 的信息写入 job.split文件
FSDataOutputStream out = FileSystem.create( fs , submitSplitFile , new FsPermission(JOB_FILE_PERMISSION));
try{
writeSplitsFile(splits,out);
}finally{
out.close();
}
job.set(“mapred.job.split.file”,submitSplitFile.toString()); // 根据split 的个数设定 map task 的个数
job.setNumMapTask(splits.length); // 将job 的配置信息写入 job.xml 文件
out = FileSystem.create(fs , submitJobFile , new FsPermission(JOB_FILE_PERMISSION));
try{
job.writeXml(out);
}finally{
out.close();
} // 真正地调用JobTracker 来提交任务
JobStatus status = jobSubmitClient.submitJob(jobId);
……….
}
从上面的代码可以看出,整个提交过程包含以下步骤:
1) 通过调用 JobTracker对象的getNewJobId() 方法从JobTracker处获取当前作业的 ID号( 步骤2: get new job ID) 。
2) 检查作业相关路径。在代码中获取各个路径信息的时候会对作业的对应路径进行检查。比如,如果没有指定输出目录或它已经存在,作业就不会被提交,并且会给 MapReduce程序返回错误信息,再比如输入目录不存在也会返回错误等。
3) 计算作业的输入划分,并将划分信息写入 job.split文件,如果写入失败就会返回错误。 split文件的信息主要包括:split文件头、 split文件版本号、split 的个数。这些信息中每一条都会包括以下内容: split类型名( 默认FileSplit)、 split的大小、split 的内容(对于 FileSplit来说是写入的文件名,此 split在文件中的起始位置上)、 split的location 信息(即在哪个 DataNode上) 。
4) 将运行作业所需要的资源——包括作业 JAR文件、配置文件和计算所得的输入划分等——复制到一个以作业 ID命名的目录的HDFS 上。作业 JAR的副本较多( 由mapred.submit.replication属性控制,默认值为10),因此在运行作业的任务时,集群中有很多个副本可供 tasktracker访问。(步骤3:copy job resources)。(这里需要注意的是要处理的数据文件是事先先上传到HDFS 处,这里的copy job resources只是获取已经上传的数据的路径和配置文件的路径,还有分割文件的信息,在将分割的信息写入到配置信息中)。
5) 调用 JobTracker对象的submitJob() 方法来真正提交作业,告诉 JobTracker作业准备执行(步骤4: submit job)。
初始化作业:
在客户端用户作业调用JobTracker对象的 submitJob()方法后,JobTracker 会把此调用放入一个内部队列中,交由作业调度器(TaskScheduler)变量进行调度,默认的调度方法是 JobQueneTaskScheduler,也就是FIFO 调度方法。当客户作业被调度执行时,JobTracker会创建一个代表这个作业的 JobInProgress对象,并将任务和记录信息封装到这个对象中,以便跟踪任务的状态和进程。接下来 JobInProgress对象的initTasks 函数会对任务进行初始化操作( 步骤5:initialize job)。
下面仍然从initTasks 函数的代码出发详细讲解初始化过程。
public synchronized void initTasks() throws IOException{
………
// 从HDFS 中作业对应的路径读取 job.split 文件,生成 input
//splits 为下面 map的划分做好准备
String jobFile = profile.getJobFile();
Path sysDir = new Path(this.jobtracker.getSystemDir());
FileSystem fs = sysDir.getFileSystem(conf);
DataInputStream splitFile =
fs.open(new Path(conf.get(“mapred.job.split.file”)))
JobClient.RawSplit [] splits;
try{
splits = JobClient.readSplitFile(splitFile);
}finally{
splitFile.close();
} // 根据input split 设置 map task个数
numMapTasks = splits.length; //为每个 map tasks 生成一个 TaskInProgress来处理一个 input split
maps = new TaskInProgress[numMapTasks];
for( int i = 0 ; I < numMapTasks ; ++i ){
inputLength += splits[i] .getDataLength();
maps[i] = new TaskInProgress(jobId , jobFile , splits[i] , jobtracker , conf , this , i);
}
if (numMapTasks > 0){
//map task 放入 nonRunningMapCache,其将在 JobTracker 向
//TaskTracker 分配 map task 的时候使用
nonRunningMapCache = createCache(splits,maxLevel);
}
// 创建reduce task
this.reduces = new TaskInProgress[numReduceTasks];
for( int i = 0 ; i < numReduceTasks ; i++ ){
reduces[i] = new TaskInProgress( jobId , jobFile , numMapTasks , i , jobtracker , conf , this );
//reduce task 放入nonRunningReduces ,其将在 JobTracker 向
//TaskTracker 分配reduce task 的时候使用
nonRunningReduces.add(reduces[i]);
} // 清理map 和 reduce
cleanup = new TaskInProgress[2];
cleanup[0] = new TaskInProgress(jobId , jobFile , splits[0] , jobtracker , conf , this , numMapTasks);
cleanup[0].setJobCleanupTask();
cleanup[1] = new TaskInProgress(jobId , jobFile ,numMapTasks , numReduceTasks , jobtracker , conf , this);
cleanup[1].setJobCleanupTask(); // 创建两个初始化 task,一个初始化 map ,一个初始化 reduce
setup = new TaskInProgress[2];
setup[0] = new TaskInProgress(jobId , jobFile , splits[0] , jobtracker , conf , this , numMapTasks + 1 );
setup[0].setJobSetupTask();
setup[1] = new TaskInProgress(jobId , jobFile , numMapTasks , numReduceTasks + 1 , jobtracker , conf , this );
setup[1].setJobSetupTask();
taskInited.set(true); // 初始化完毕
}
从上面的代码可以看出在初始化过程中主要有以下步骤:
1)从 HDFS中读取作业对应的job.split( 步骤6:retrieve input splits)。JobTracker 从HDFS中作业对应的路径获取JobClient在步骤3 中写入的 job.split文件,得到输入数据的划分信息。为后面初始化过程中 map任务的分配做好准备。
2)创建并初始化 map任务和reduce 任务。initTasks先根据输入数据划分信息中的个数设定 map task的个数,然后为每个map tasks 生成一个 TaskInProgress来处理input split,并将 map task放入nonRunningMapCache 中,以便在 JobTasker向TaskTracker 分配map task的时候使用。接下来根据 JobConf中的mapred.reduce.tasks 属性利用 setNumReduceTasks()方法来设置reduce task 的个数,然后采用类似 map task的方式将reduce task 放入nonRunningReduces中,以便在向 TaskTracker分配reduce task 的时候使用。
3)最后就是创建两个初始化 task,根据个数和输入划分已经配置的信息,并分别初始化 map和reduce 。
分配任务:
在前面的介绍中已经知道, TaskTracker和JobTracker 之间的通信与任务的分配是通过心跳机制完成的。 TaskTracker作为一个单独的JVM执行一个简单的循环,主要实现每隔一段时间向 JobTracker发送心跳(heartbeat) :告诉JobTracker,此TaskTracker是否存活,是否准备执行新的任务。在 JobTracker接收到心跳信息后,如果有待分配的任务,它就会为 TaskTracker分配一个任务,并将分配信息封装在心跳通信的返回值中返回给 TaskTracker,TaskTracker 从心跳方法的 Response中得知此TaskTracker 需要做的事情,如果是一个新的 task则将task 加入本机的任务队列中( 步骤7:heartbeat returns task)。
下面从TaskTracker 中的transmitHeartBeat()方法和 JobTracker中的heartbeat() 方法的主要代码出发,介绍人任务分配的详细过程,以及在此过程中 TaskTracker和JobTracker 的通信。
TaskTracker中 transmitHeartBeat()方法的主要代码如下所示:
// 向 JobTracker报告 TaskTracker 的当前状态
if( status == null ){
synchronized( this ){
status = new TaskTrackerStatus( taskTrackerName , localHostname , httpPort , cloneAndResetRunningTaskStatuses(sendCounters) , failures , maxCurrentMapTasks , maxCurrentReduceTasks );
}
}
……..
//根据条件是否满足来确定此 TaskTracker 是否请求JobTracker
//为其分配新的 Task
Boolean askForNewTask ;
long localMinSpaceStart ;
synchronized ( this ) {
askForNewTask = (status.countMapTasks() < maxCurrentMapTasks || status.countReduceTasks() < maxCurrentReduceTasks) && acceptNewTasks;
localMinSpaceStart = minSpaceStart;
}
……..
//向 JobTracker 发送heartbeat
HeartbeatResponse heartbeatResponse = jobClient.heartbeat(status,justStarted,askForNewTask,heartbeatResponseId);
……….
JobTracker中 heartbeat()方法的主要代码如下所示:
……..
String trackerName = status.getTrackerName();
……..
//如果 TaskTracker 向JobTracker 请求一个 task 运行
if (acceptNewTasks){
TaskTrackerStatus taskTrackerStatus = getTaskTracker(trackerName);
if (taskTrackerStatus == null ){
LOG.warn(“Unknow task tracker polling; ignoring : ” + trackerName );
} else {
List<Task> tasks = getSetupAndCleanupTasks(taskTrackerStatus);
if (tasks == null ){
// 任务调度器分配任务
tasks = taskScheduler.assignTasks(taskTrackerStatus);
}
if (task != null ){
for (Task task : tasks ){
// 将任务返回给 TaskTracker
expireLaunchingTasks.addNewTask(task.getTaskID());
actions.add(new LaunchTaskAction(task));
}} }}………
上面两段代码展示了TaskTracker和 JobTracker之间通过心跳通信汇报状态与分配任务的详细过程。 TaskTracker首先发送自己的状态(主要是 map任务和reduce 任务的个数是否小于上限 ),并根据自身条件选择是否向 JobTracker请求新的Task ,最后发送心跳。 JobTracker接收到TaskTracker 的心跳之后首先分析心跳信息,如果发现 TaskTracker在请求一个task ,那么任务调度器就会将任务和任务信息封装起来返回给 TaskTracker。
在JobTracker 为TaskTracker选择任务之前, JobTracker必须先选定任务所在的作业,默认的调度算法是简单维护一个作业优先级列表。一旦选择好作业, JobTracker就可以为该作业选定一个作业。
针对map 任务和reduce任务, TaskTracker从JobTracker 有固定数量的任务槽 (map任务和reduce 任务的个数都有上限 )。当TaskTracker 从JobTracker返回的心跳信息中获取新的任务信息时,它会将 map任务或者reduce 任务加入到对应的任务槽中。需要注意的是,在 JobTracker为TaskTracker 分配map任务的时候,为了减小网络带宽会考虑将 map任务数据本地化。它会根据TaskTracker的网络位置,选取一个距离此 TaskTracker map任务最近的输入划分文件分配给此 TaskTracker。最好的情况是,划分文件就在 TaskTracker本地(TaskTracker 往往是HDFS的 DataNode中,所以这种情况是存在的 )。(体现移动计算比移动数据经济的好处)。
执行任务:
在TaskTracker 申请到新的任务之后,就要在本地运行任务了。运行任务的第一步是将任务本地化 (将任务运行所必需的数据、配置信息、程序代码从 HDFS复制到TaskTracker 本地(步骤8:retrieve job resources)。这主要是通过调用 localizeJob()方法来完成的。这个方法主要通过下面几个步骤来完成任务的本地化:
1)将 job.split拷贝到本地;
2)将 job.jar拷贝到本地;
3)将 job的配置信息写入job.xml;
4)创建本地任务目录,解压 job.jar;
5)调用 launchTaskForJob()方法发布任务(步骤9:launch)。
任务本地化之后,就可通过调用 launchTaskForJob()真正启动起来。接下来launchTaskForJob()又会调用 launchTask()方法启动任务。launchTask()方法的主要代码如下:
……..
//创建 task 本地运行目录
localizeTask(task);
if ( this.taskStatus.getRunState() == TaskStatus.State.UNASSIGNED ){
this.taskStatus.setRunState( TaskStatus.State.RUNNING );
}
//创建并启动 TaskRunner
this.runner = task.createRunner(TaskTracker.this , this);
this.runner.start()
this.taskStatus.setStartTime(System.currentTimeMillis());
………
从代码中可以看出launchTask()方法先会为任务创建本地目录,然后启动 TaskRunner。在启动TaskRunner 后,对于 map任务,会启动MapTaskRunner ;对于reduce任务则启动 ReduceTaskRunner。
这之后,TaskRunner 又会启动新的 Java虚拟机来运行每个任务( 步骤10:run)。以 map任务为例,任务执行的简单流程是:
1)配置任务的执行参数 (获取Java 程序的执行环境和配置参数等 );
2)在 Child临时文件表中添加map任务信息(运行 map和reduce 任务的主进程是 Child类);
3)配置 log文件夹,然后配置map任务的通信和输出参数;
4)读取 input split,生成RecordReader 读取数据;
5)为 map任务生成MapRunnable ,依次从 RecordReader中接收数据,并调用Mapper的 map函数进行处理;
6)最后将 map函数的输出调用collect收集到 MapOutputBuffer中。
更新任务执行进度和状态:
一个MapReduce 作业在提交到 Hadoop上之后,会进入完全自动化执行过程,用户只能监控程序的执行状态和强制中止作业。但是 MapReduce作业是一个长时间运行的批量作业,有时候可能需要运行数小时。所以对于用户而言,能够得知作业的运行状态是非常重要的。在 Linux终端运行MapReduce 作业时,可以看到在作业执行过程中有一些简单的作业执行状态报告,这能让用户大致了解作业的运行情况,并通过与预期运行情况进行对比来确定作业是否按照预定方式运行。
在MapReduce 作业中,作业的进度主要由一些可衡量可计数的小操作组成。比如在 map任务中,其任务进度就是已处理输入的百分比,比如完成 100条记录中的50 条,那么 map任务的进度就是50%(这里只针对一个 map任务举例,并不是指在Linux终端中执行 MapReduce任务时出现的map50% ,在终端中出现的 50% 是总体map 任务的进度,这是将所有 map任务的进度组合起来的结果)。总体来讲, MapReduce作业的进度由下面几项组成: mapper(或reducer) 读入或写出一条记录,在报告中设置状态描述,增加计数器,调用 Reporter对象的progress() 方法。
由MapReduce 作业分割的每个任务中都有一组计数器,它们对任务执行过程中的进度组成事件进行计数。如果任务要报告进度,它便会设置一个标志以表明状态变化将会发送到 TaskTracker上。另一个监听线程检查到这标志后,会告知 TaskTracker当前的任务状态。具体代码如下 (这是MapTask 中run函数的部分代码 ):
// 同 TaskTracker通信,汇报任务执行进度
final Reporter reporter = getReporter(umbilical);
startCommunicationThread(umbilical);
initialize(job , reporter);
同时,TaskTracker 每隔5秒在发送给 JobTracker的心跳中封装任务状态,报告自己任务执行状态。具体代码如下 (这是TaskTracker 中transmitHeartBeat()方法的部分代码 ):
// 每隔一段时间,向 JobTracker 返回一些统计信息
Boolean sendCounters;
if(now > (previousUpdate + COUNTER_UPDATE_INTERVAL )){
sendCounters = true;
previousUpdate = now;
}
else{
sendCounters = false;
}
通过心跳通信机制,所有 TaskTracker的统计信息都会汇总到JobTracker处。 JobTracker将这些统计信息合并起来,产生一个全局作业进度统计信息,用来表现正在运行的所有作业,以及其中所含任务的状态。最后, JobClient通过每秒查看JobTracker 来接收作业进度的最新状态。具体代码如下(这是 JobClient中用来提交作业的runJob()方法的部分代码):
// 首先生成一个 JobClient 对象
JobClient jc = new JobClient(job) ;
……..
//调用 submitJob 来提交一个任务
running = jc.submitJob(job);
JobID jobId = running.getID();
……..
//while循环中不断查看 JobTracker 来获取作业进度
while(true){
……..
}
完成作业:
所有TaskTracker 任务的执行进度信息都会汇总到 JobTracker处,当JobTracker 接收到最后一个任务的已完成通知后,便把作业的状态设置为“成功”。然后, JobClient也将及时得知任务已成功完成,它便会显示一条信息告知用户作业已完成,最后从runJob()方法处返回( 在返回后 JobTracker会清空作业的工作状态,并指示 TaskTracker也清空作业的工作状态,比如删除中间输出等 )。
剖析MapReduce 作业运行机制的更多相关文章
- 经典MapReduce作业和Yarn上MapReduce作业运行机制
一.经典MapReduce的作业运行机制 如下图是经典MapReduce作业的工作原理: 1.1 经典MapReduce作业的实体 经典MapReduce作业运行过程包含的实体: 客户端,提交MapR ...
- 【转】简易剖析Hadoop作业工作机制
原文地址:https://www.cnblogs.com/duma/p/10666269.html 建议:结合第四版Hadoop权威指南阅读,更有利于理解 运行机制 运行一个 MR 程序主要涉及以下 ...
- YARN作业运行机制
在传统的MapReduce中, Jobtracker同时负责作业调度(将任务调度给对应的tasktracker)和任务进度管理(监控任务, 重启失败的或者速度比较慢的任务等). YARN中将Jobtr ...
- 带你底层看Sqoop如何转换成MapReduce作业运行的(代码程序)
补充 其实啊,我们知道,sqoop在运行的时候,最终会去转换成mapreduce作业,这个很简单,不多赘述.直接贴出来. 具体这些怎么运行的,见我如下这篇博客.这里只做一个引子. Sqoop Impo ...
- 【Hadoop】MapReduce笔记(一):MapReduce作业运行过程、任务执行
一.MR作业运行过程 JobClient的runJob()方法:新建JobClient实例,并调用其submitJob()方法.提交作业后,runJob()每秒轮询作业进度,如果发现上次上报后信息有改 ...
- hadoop MapReduce Yarn运行机制
原 Hadoop MapReduce 框架的问题 原hadoop的MapReduce框架图 从上图中可以清楚的看出原 MapReduce 程序的流程及设计思路: 首先用户程序 (JobClient) ...
- 如果遇到Hadoop集群正常,MapReduce作业运行出现错误,如何来查看作业运行日志(图文详解)
不多说,直接上干货! 这个时候我们可以进入logs下的userlogs 备注:userlogs目录下有很多个以往运行的作业,我选择最新的最大编号的作业,就是我们当前运行作业的日志.然后找到stderr ...
- 王家林的“云计算分布式大数据Hadoop实战高手之路---从零开始”的第十一讲Hadoop图文训练课程:MapReduce的原理机制和流程图剖析
这一讲我们主要剖析MapReduce的原理机制和流程. “云计算分布式大数据Hadoop实战高手之路”之完整发布目录 云计算分布式大数据实战技术Hadoop交流群:312494188,每天都会在群中发 ...
- MapReduce 运行机制
Hadoop中的MapReduce是一个使用简单的软件框架,基于它写出来的应用程序能够运行在由上千个机器组成的大型集群上,并且以一种可靠容错并行处理TB级别的数据集. 一个MapReduce作业(jo ...
随机推荐
- Excel REPT函数使用
需要制作1K大小的数据 使用Excel REPT函数可以迅速制造 Excel REPT 函数 =REPT(1,1024) 结果直接黏贴进txt文件,注意删除尾空格.
- mysql导入的时候提示“1046-No Database selected”的解决办法
进入phpmyadmin后,先点击左边的要导入的数据库,进入后再点击右上角的“导入‘按钮即可 详细说明 http://www.xmxwl.net/help/member/20160325/13653. ...
- C#——中文转化成拼音
在KS系统中用到了中文转化成拼音的功能.通过查阅资料为下面是代码. /// <summary> /// MyConvert 的摘要说明 /// </summary> publi ...
- BootLoader作用
BootLoader 是系统加电后运行的第一段代码.一般它只在系统启动时非常短的时间内运行. 由OS Loader负责将所要引导的操作系统的内核映象从硬盘上读到系统RAM中,然后跳转到内核的入口点上. ...
- 从零开始学ios开发(十九):Application Settings and User Defaults(上)
在iphone和ipad中,有一个东西大家一定很熟悉,那个东西就是Settings. 这次要学习的东西说白了很简单,就是学习如何在Settings中对一个app的某些属性进行设置,反过来,在app中更 ...
- 指向const的指针和const指针
1.指向const的指针:const int *p 或 int const *p 解释:p是一个指针,指向const int类型的常量:指针指向的内容为常量,因此不能改变*p的值,但指针p可以改变,指 ...
- 使用go语言后的感受
前两天我说过为了学习go语言去学习了一遍python,当我完成了python的学习后,昨天中午就去学习了go语言.以下简称之为golang. 我用的操作系统是windows xp,golang对xp还 ...
- WPF 进程间通讯----inter-process communication
进程间通讯--inter-process communication 进程间相互通讯的方法有很多,如用web services,xml 等互相读取, 网络的可以使用socket 等. 2个WinFo ...
- Mysql几种索引类型的区别及适用情况
如大家所知道的,Mysql目前主要有以下几种索引类型:FULLTEXT,HASH,BTREE,RTREE. 那么,这几种索引有什么功能和性能上的不同呢? FULLTEXT 即为全文索引,目前只有MyI ...
- 使用XAMPP本地安装Wordpress博客
最近一直在研究博客,也知道了大名鼎鼎的wordpress,因此也希望动手尝试一下,看看跟网站提供的博客有何区别. 第一个问题:能什么安装wordPress,能否用tocmat? 虽然问题很可笑,但是之 ...