再C#里现在有3个Timer类:

  • System.Windows.Forms.Timer
  • System.Threading.Timer
  • System.Timers.Timer

这三个Timer我想大家对System.Windows.Forms.Timer已经很熟悉了,唯一我要说的就是这个Timer在激发Timer.Tick事件的时候,事件的处理函数是在程序主线程上执行的,所以在WinForm上面用这个Timer很方便,因为在From上的所有控件都是在程序主线程上创建的,那么在Tick的处理函数中可以对Form上的所有控件进行操作,不会造成WinForm控件的线程安全问题。

1、Timer运行的核心都是System.Threading.ThreadPool

在这里要提到ThreadPool(线程池)是因为,System.Threading.Timer 和System.Timers.Timer运行的核心都是线程池,Timer每到间隔时间后就会激发响应事件,因此要申请线程来执行对应的响应函数,Timer将获取线程的工作都交给了线程池来管理,每到一定的时间后它就去告诉线程池:“我现在激发了个事件要运行对应的响应函数,麻烦你给我向操作系统要个线程,申请交给你了,线程分配下来了你就运行我给你的响应函数,没分配下来先让响应函数在这儿排队(操作系统线程等待队列)”,消息已经传递给线程池了,Timer也就不管了,因为它还有其他的事要做(每隔一段时间它又要激发事件),至于提交的请求什么时候能够得到满足,要看线程池当前的状态:

  • 1、如果线程池现在有线程可用,那么申请马上就可以得到满足,有线程可用又可以分为两种情况:

    • <1>线程池现在有空闲线程,现在马上就可以用
    • <2>线程池本来现在没有线程了,但是刚好申请到达的时候,有线程运行完毕释放了,那么申请就可以用别人释放的线程。
    • 这两种情况情况就如同你去游乐园玩赛车,如果游乐园有10辆车,现在有3个人在玩,那么还剩7辆车,你去了当然可以选一辆开。另外还有一种情况就是你到达游乐园前10辆车都在开,但是你运气很好,刚到游乐园就有人不玩了,正好你坐上去就可以接着开。
  • 2、如果现在线程池现在没有线程可用,也分为两种情况:
    • <1>线程池现有线程数没有达到设置的最大工作线程数,那么隔半秒钟.net framework就会向操作系统申请一个新的线程(为避免向线程分配不必要的堆栈空间,线程池按照一定的时间间隔创建新的空闲线程。该时间间隔目前为半秒,但它在 .NET Framework 的以后版本中可能会更改)。
    • <2>线程池现有工作线程数达到了设置的最大工作线程数,那么申请只有在等待队列一直等下去,直到有线程执行完任务后被释放。

那么上面提到了线程池有最大工作线程数,其实还有最小空闲线程数,那么这两个关键字是什么意思呢:

  • 1、最大工作线程数:实际上就是指的线程池能够向操作系统申请的最大线程数,这个值在.net framework中有默认值,这个默认值是根据你计算机的配置来的,当人你可以用ThreadPool.GetMaxThreads返回线程池当前最大工作线程数,你也可以同ThreadPool.SetMaxThreads设置线程池当前最大工作线程数。
  • 2、最小空闲线程数:是指在程序开始后,线程池就默认向操作系统要最小空闲线程数个线程,另外这也是线程池维护的空闲线程数(如果线程池最小空闲线程数为3,当前因为一些线程执行完任务被释放,线程池现在实际上有10个空闲线程,那么线程池会让操作系统释放多余的7个线程,而只维持3个空闲线程供程序使用),因为上面说了,在执行程序的时候在要线程池申请线程有半秒的延迟时间,这也会影响程序的性能,所以把握好这个值很重要,用样你可以用ThreadPool.GetMinThreads返回线程池当前最小空闲线程数,你也可以同ThreadPool.SetMinThreads设置线程池当前最小空闲线程数。

下面是我给的例子,这个例子让线程池申请800个线程,其中设置最大工作线程数为500,800个线程任务每个都要执行100000000毫秒目的是让线程不会释放,并且让用户选择,是否预先申请500个空闲线程免受那半秒钟的延迟时间,其结果可想而知当线程申请到500的时候,线程池达到了最大工作线程数,剩余的300个申请进入漫长的等待时间:

/***************************************************
 * 项目:测试线程池
 * 描述:验证线程池的最大工作线程数和最小空闲线程数
 * 作者:@PowerCoder
 * 日期:2010-2-22
***************************************************/

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace ConsoleApplication1
{
    class Program
    {
        static int i=1;
        static int MaxThreadCount = 800;

static void OutPut(object obj)
        {
            Console.Write("\r申请了:{0}个工作线程",i);
            i++;
            Thread.Sleep(100000000);//设置一个很大的等待时间,让每个申请的线程都一直执行
        }

static void Main(string[] args)
        {
            int j;
            
            Console.Write("是否先申请500个空闲线程以保证前500个线程在线程池中开始就有线程用(Y/N)?");//如果这里选择N,那么前两个任务是用的线程池默认空闲线程(可以用ThreadPool.GetMinThreads得到系统默认最小空闲线程数为2)申请立即得到满足,然而由于每个线程等待时间非常大都不会释放当前自己持有的线程,因此线程池中已无空闲线程所用,后面的任务需要在线程池中申请新的线程,那么新申请的每个线程在线程池中都要隔半秒左右的时间才能得到申请(原因请见下面的注释)
            string key = Console.ReadLine();
            if(key.ToLower()=="y")
                ThreadPool.SetMinThreads(500, 10);//设置最大空闲线程为500,就好像我告诉系统给我预先准备500个线程我来了就直接用,因为这样就不用现去申请了,在线程池中每申请一个新的线程.NET Framework 会安排一个间隔时间,目前是半秒,以后的版本MS有可能会改
            
            int a, b;
            ThreadPool.GetMaxThreads(out a,out b);
            Console.WriteLine("线程池默认最大工作线程数:" + a.ToString() + "     默认最大异步 I/O 线程数:" + b.ToString());
            Console.WriteLine("需要向系统申请" + MaxThreadCount.ToString()+"个工作线程");

for (j = 0; j <= MaxThreadCount-1; j++)//由于ThreadPool.GetMaxThreads返回的默认最大工作线程数为500(这个值要根据你计算机的配置来决定),那么向线程池申请大于500个线程的时候,500之后的线程会进入线程池的等待队列,等待前面500个线程某个线程执行完后来唤醒等待队列的某个线程 
            {
                ThreadPool.QueueUserWorkItem(new WaitCallback(OutPut));
                Thread.Sleep(10);
            }

Console.ReadLine();
        }
    }
}

2、System.Threading.Timer

谈完了线程池,就可以开始讨论Timer,这里我们先从System.Threading.Timer开始,System.Threading.Timer的作用就是每到间隔时间后激发响应事件并执行相应函数,执行响应函数要向线程池申请线程,当然申请中会遇到一些情况在上面我们已经说了。值得注意的一点就是System.Threading.Timer在创建对象后立即开始执行,比如System.Threading.Timer timer = new System.Threading.Timer(Excute, null, 0, 10);这句执行完后每隔10毫秒就执行Excute函数不需要启动什么的。下面就举个例子,我先把代码贴出来:

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Diagnostics;

namespace ConsoleApplication1
{
    class UnSafeTimer
    {
        static int i = 0;
        static System.Threading.Timer timer;
        static object mylock = new object();
        static int sleep;
        static bool flag;
        public static Stopwatch sw = new Stopwatch();

static void Excute(object obj)
        {
            Thread.CurrentThread.IsBackground = false;
            int c;

lock (mylock)
            {
                i++;
                c = i;
            }

if (c == 80)
            {
                timer.Dispose();//执行Dispose后Timer就不会再申请新的线程了,但是还是会给Timmer已经激发的事件申请线程
                sw.Stop();
            }

if (c < 80)
                Console.WriteLine("Now:" + c.ToString());
            else
            {
                Console.WriteLine("Now:" + c.ToString()+"-----------Timer已经Dispose耗时:"+sw.ElapsedMilliseconds.ToString()+"毫秒");
            }

if (flag)
            {
                Thread.Sleep(sleep);//模拟花时间的代码
            }
            else
            {
                if(i<=80)
                    Thread.Sleep(sleep);//前80次模拟花时间的代码
            }
        }

public static void Init(int p_sleep,bool p_flag)
        {
            sleep = p_sleep;
            flag = p_flag;
            timer = new System.Threading.Timer(Excute, null, 0, 10);
        }
    }

class SafeTimer
    {
        static int i = 0;
        static System.Threading.Timer timer;

static bool flag = true;
        static object mylock = new object();

static void Excute(object obj)
        {
            Thread.CurrentThread.IsBackground = false;

lock (mylock)
            {
                if (!flag)
                {
                    return;
                }

i++;

if (i == 80)
                {
                    timer.Dispose();
                    flag = false;
                }
                Console.WriteLine("Now:" + i.ToString());
            }

Thread.Sleep(1000);//模拟花时间的代码
        }

public static void Init()
        {
            timer = new System.Threading.Timer(Excute, null, 0, 10);
        }
    }

class Program
    {
        static void Main(string[] args)
        {
            Console.Write("是否使用安全方法(Y/N)?");
            string key = Console.ReadLine();
            if (key.ToLower() == "y")
                SafeTimer.Init();
            else
            {
                Console.Write("请输入Timmer响应事件的等待时间(毫秒):");//这个时间直接决定了前80个任务的执行时间,因为等待时间越短,每个任务就可以越快执行完,那么80个任务中就有越多的任务可以用到前面任务执行完后释放掉的线程,也就有越多的任务不必去线程池申请新的线程避免多等待半秒钟的申请时间
                string sleep = Console.ReadLine();
                Console.Write("申请了80个线程后Timer剩余激发的线程请求是否需要等待时间(Y/N)?");//这里可以发现选Y或者N只要等待时间不变,最终Timer激发线程的次数都相近,说明Timer的确在执行80次的Dispose后就不再激发新的线程了
                key = Console.ReadLine();
                bool flag = false;
                if (key.ToLower() == "y")
                {
                    flag = true;
                }

UnSafeTimer.sw.Start();
                UnSafeTimer.Init(Convert.ToInt32(sleep), flag);
            }

Console.ReadLine();
        }
    }
}

这个例子包含了两个Timer的类UnSafeTimer和SafeTimer,两个类的代码的大致意思就是使用Timer每隔10毫秒就执行Excute函数,Excute函数会显示当前执行的次数,在80次的时候通过timer.Dispose()让Timer停止不再激发响应事件。

首先我们来分析下UnSafeTimer

class UnSafeTimer
{
    static int i = 0;
    static System.Threading.Timer timer;
    static object mylock = new object();
    static int sleep;
    static bool flag;
    public static Stopwatch sw = new Stopwatch();

static void Excute(object obj)
    {
        Thread.CurrentThread.IsBackground = false;
        int c;

lock (mylock)
        {
            i++;
            c = i;
        }

if (c == 80)
        {
            timer.Dispose();//执行Dispose后Timer就不会再申请新的线程了,但是还是会给Timmer已经激发的事件申请线程
            sw.Stop();
        }

if (c < 80)
            Console.WriteLine("Now:" + c.ToString());
        else
        {
            Console.WriteLine("Now:" + c.ToString() + "-----------Timer已经Dispose耗时:" + sw.ElapsedMilliseconds.ToString() + "毫秒");
        }

if (flag)
        {
            Thread.Sleep(sleep);//模拟花时间的代码
        }
        else
        {
            if (i <= 80)
                Thread.Sleep(sleep);//前80次模拟花时间的代码
        }
    }

public static void Init(int p_sleep, bool p_flag)
    {
        sleep = p_sleep;
        flag = p_flag;
        timer = new System.Threading.Timer(Excute, null, 0, 10);
    }
}

你可以执行试一试,在输入是否执行安全方法的时候选N,等待时间1000,申请了80个线程后Timer剩余激发的线程选N,本来想在80次的时候停下来,可是你会发现直到执行到660多次之后才停下来(具体看机器配置),申请前80个线程的时间为10532毫秒,反正执行的次数大大超出了限制的80次,回头想想让Timer不在激发事件的方法是调用timer.Dispose(),难不成是Dispose有延迟?延迟的过程中多执行了500多次?那么我们再来做个试验,我们在申请了80个线程后Timer剩余激发的线程选y,请耐心等待结果,在最后你会发现执行时间还是660次左右,这很显然是不合理的,如果Dispose有延迟时间造成所执行500多次,那么加长80次后面每个线程的申请时间在相同的延迟时间内申请的线程数应该减少,因为后面500多个线程每个线程都要执行1000毫秒,那么势必有些线程会去申请新的线程有半秒钟的等待时间(你会发现申请了80个线程后Timer剩余激发的线程选y明显比选n慢得多,就是因为这个原因),所以看来不是因为Dispose造成的。

那么会是什么呢?我们这次这样选在输入是否执行安全方法的时候选N,等待时间500,申请了80个线程后Timer剩余激发的线程选N

那么会是什么呢?我们这次这样选在输入是否执行安全方法的时候选N,等待时间50,申请了80个线程后Timer剩余激发的线程选N

我们发现随着每次任务等待时间的减少多执行的次数也在减少,最关键的一点我们从图中可以看到,前80次任务申请的时间也在减少,这是最关键的,根据上面线程池所讲的内容我们可以归纳出:每次任务的等待时间直接决定了前80个任务的执行时间,因为等待时间越短,每个任务就可以越快执行完,那么80个任务中就有越多的任务可以用到前面任务执行完后释放掉的线程,也就有越多的任务不必去线程池申请新的线程避免多等待半秒钟的申请时间,而Timer并不会去关心线程池申请前80个任务的时间长短,只要它没有执行到timer.Dispose(),它就会每隔10毫秒激发一次响应时间,不管前80次任务执行时间是长还是短,timer都在第80次任务才执行Dispose,执行Dispose后timer就不会激发新的事件了,但是如果前80次任务申请的时间越长,那么timer就会在前80次任务申请的时间内激发越多响应事件,那么线程池中等待队列中就会有越多的响应函数等待申请线程,System.Threading.Timer没有机制取消线程池等待队列中多余的申请数,所以导致等待时间越长,80次后执行的任务数越多。

由此只用timer.Dispose()来终止Timer激发事件是不安全的,所以又写了个安全的执行机制:

class SafeTimer
{
    static int i = 0;
    static System.Threading.Timer timer;

static bool flag = true;
    static object mylock = new object();

static void Excute(object obj)
    {
        Thread.CurrentThread.IsBackground = false;

lock (mylock)
        {
            if (!flag)
            {
                return;
            }

i++;

if (i == 80)
            {
                timer.Dispose();
                flag = false;
            }
            Console.WriteLine("Now:" + i.ToString());
        }

Thread.Sleep(1000);//模拟花时间的代码
    }

public static void Init()
    {
        timer = new System.Threading.Timer(Excute, null, 0, 10);
    }
}

安全类中我们用了个bool类型的变量flag来判断当前是否执行到80次了,执行到80次后将flag置为false,然后timer.Dispose,这时虽然任务还是要多执行很多次但是由于flag为false,Excute函数一开始就做了判断flag为false会立即退出,Excute函数80次后相当于就不执行了。

3、System.Timers.Timer

在上面的例子中我们看到System.Threading.Timer很不安全,即使在安全的方法类,也只能让事件响应函数在80次后立刻退出让其执行时间近似于0,但是还是浪费了系统不少的资源。

所以本人更推荐使用现在介绍的System.Timers.Timer,System.Timers.Timer大致原理和System.Threading.Timer差不多,唯一几处不同的就是:

  • 构造函数不同,构造函数可以什么事情也不做,也可以传入响应间隔时间:System.Timers.Timer timer = new System.Timers.Timer(10);
  • 响应事件的响应函数不在构造函数中设置:timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
  • 声明System.Timers.Timer对象后他不会自动执行,需要调用 timer.Start()或者timer.Enabled = true来启动它, timer.Start()的内部原理还是设置timer.Enabled = true
  • 调用 timer.Stop()或者timer.Enabled = false来停止引发Elapsed事件, timer.Stop()的内部原理还是设置timer.Enabled = false,最重要的是timer.Enabled = false后会取消线程池中当前等待队列中剩余任务的执行。

那么我们来看个例子:

代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Timers;
using System.Threading;

namespace ConsoleApplication2
{
    class UnSafeTimer
    {
        static int i = 0;
        static System.Timers.Timer timer;
        static object mylock = new object();

public static void Init()
        {
            timer = new System.Timers.Timer(10);
            timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
            timer.Start();
        }

static void timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            Thread.CurrentThread.IsBackground = false;
            int c;

lock (mylock)
            {
                i++;
                c = i;
            }

Console.WriteLine("Now:" + i.ToString());

if (c == 80)
            {
                timer.Stop();//可应看到System.Timers.Timer的叫停机制比System.Threading.Timer好得多,就算在不安全的代码下Timer也最多多执行一两次(我在试验中发现有时会执行到81或82),说明Stop方法在设置Timer的Enable为false后不仅让Timer不再激发响应事件,还取消了线程池等待队列中等待获得线程的任务,至于那多执行的一两次任务我个人认为是Stop执行过程中会耗费一段时间才将Timer的Enable设置为false,这段时间多余的一两个任务就获得了线程开始执行
            }

Thread.Sleep(1000);//等待1000毫秒模拟花时间的代码,注意:这里的等待时间直接决定了80(由于是不安全模式有时会是81或82、83)个任务的执行时间,因为等待时间越短,每个任务就可以越快执行完,那么80个任务中就有越多的任务可以用到前面任务执行完后释放掉的线程,也就有越多的任务不必去线程池申请新的线程避免多等待半秒钟的申请时间
        }
    }

class SafeTimer
    {
        static int i = 0;
        static System.Timers.Timer timer;

static bool flag = true;
        static object mylock = new object();

public static void Init()
        {
            timer = new System.Timers.Timer(10);
            timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
            timer.Start(); 
        }

static void timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            Thread.CurrentThread.IsBackground = false;

lock (mylock)
            {
                if (!flag)
                {
                    return;
                }
                i++;

Console.WriteLine("Now:" + i.ToString());

if (i == 80)
                {
                    timer.Stop();
                    flag = false;
                }
            }

Thread.Sleep(1000);//同UnSafeTimer
        }

class Program
        {
            static void Main(string[] args)
            {
                Console.Write("是否使用安全Timer>(Y/N)?");
                string Key = Console.ReadLine();

if (Key.ToLower() == "y")
                    SafeTimer.Init();
                else
                    UnSafeTimer.Init();

Console.ReadLine();
            }
        }
    }
}

这个例子和System.Threading.Timer差不多,这里也分为:安全类SafeTimer和不安全类UnSafeTimer,原因是 timer.Stop()有少许的延迟时间有时任务会执行到81~83,但是就算是不安全方法也就最多多执行几次,不像System.Threading.Timer多执行上百次...

所以我这里还是推荐大家使用System.Timers.Timer

发表评论

博主好文.谢谢分享! 
只是对于第三段System.Timers.Timer的使用有待商榷,MSDN曰:
如果 System.Timers.Timer 用于 WPF 应用程序,则值得注意的是 System.Timers.Timer 运行于不同于用户界面 (UI) 线程的其他线程上。为了访问用户界面 (UI) 线程上的对象,需要使用 Invoke 或 BeginInvoke 将操作发布到用户界面 (UI) 线程的 Dispatcher 上。使用 DispatcherTimer 而不是使用 System.Timers.Timer 的原因是 DispatcherTimer 与 Dispatcher 运行于相同的线程,并且可以在 DispatcherTimer 上设置 DispatcherPriority。

https://blog.csdn.net/henreash/article/details/41515807

C#的Timer(很多相关文章)的更多相关文章

  1. 使用 SendARP 获取 MAC 地址(使用SendARP API函数,很多相关文章)

    ARP 协议地址解析协议(ARP)是通过解析网路层地址来找寻数据链路层地址的一个在网络协议包中极其重要的网络传输协议.ARP 最初在 1982 年的 RFC 826 中提出并纳入互联网标准 STD 3 ...

  2. Qt 连接MySQL数据库(很多相关文章)

    今天想试试Qt如何连接数据库的. 谁知怎么写完了提示driver not loaded我就郁闷了. 我自己是 VS2010 + Qt4.8.4 + MySQL5.1 的环境 网上查到是 C:\Qt\4 ...

  3. dedecms调用全站相关文章怎么设置

    前面我们说了dedecms调用相关文章,但很多网友反映说调用的只是本栏目的相关文章,不是全站的相关文章,那么dedecms调用全站相关文章怎么设置呢?打开文件\include\taglib\likea ...

  4. ZBLOG PHP调用相关文章列表以及上一篇下一篇文章代码

    如果是比较小的个人博客.专题类网站项目,老蒋还是比较喜欢使用ZBLOG PHP程序的,无论是轻便度还是易用性上比WordPress简单很多,虽然WP的功能很强大,比如强大的插件和主题丰富功能是当前最为 ...

  5. 【目录】Newlife XCode组件相关文章目录

    本博客所有文章分类的总目录链接:本博客博文总目录-实时更新  1.Newlife XCode组件相关文章目录  1.Newlife XCode组件资源目录汇总[2013年版]    2.Newlife ...

  6. phpcms 移植【添加相关文章】功能

    添加相关文章功能相当有用,移植一个过来基本上可以实现比较复杂的页面内包含分类功能,做二次开发时可以省下不少力气. 用例:如果一个产品,属于一个厂家,而这个厂家是动态添加的,既不是一个分类,而是一个厂家 ...

  7. iOS - 直播相关文章

    直播相关文章 直播RTMP可用于测试的服务器地址 FFmpeg avdumpformat输出的tbn.tbc.tbr.PAR.DAR的含义 FFmpeg 3.0 计算视频时长 HLS Streamin ...

  8. ffrpc相关文章列表

    ffrpc 是异步c++通信库.可以说是传统rpc模式和zeromq模式的一个结合,采用broker模式封装client和server之间的拓扑关系,而client和server的通信仍然按照请求应答 ...

  9. 【汇总】涉及iOS&iPhone开发相关文章汇总

    此文章汇总本博客中有涉及iPhone开发的相关文章,不定时更新中... 1.NSUserDefaults快速存储数据: http://www.cnblogs.com/ios-wmm/archive/2 ...

随机推荐

  1. Java 学习(15):Java 数据结构

    Java 数据结构 Java工具包提供了强大的数据结构.在Java中的数据结构主要包括以下几种接口和类: 枚举(Enumeration) 位集合(BitSet) 向量(Vector) 栈(Stack) ...

  2. Ubuntu 美团sql优化工具SQLAdvisor的安装(转)

    by2009 by2009 发表于 3 个月前 SQLAdvisor简介 SQLAdvisor是由美团点评公司技术工程部DBA团队(北京)开发维护的一个分析SQL给出索引优化建议的工具.它基于MySQ ...

  3. NYOJ_75 日期计算 (推断这一天是这一年中的第几天)

    题目地址 如题,输入一个日期,格式如:2010 10 24 ,推断这一天是这一年中的第几天. 分析:   官方给的最优答案用了for 和switch语句结合,十分巧妙. 代码 /* 如题,输入一个日期 ...

  4. local-语言切换监听事件

    今天在更改时钟的问题的时候,需要监听语言切换来刷新时钟的显示.记录下监听方法 //注册监听事件 intentFilter.addAction(Intent.ACTION_LOCALE_CHANGED) ...

  5. 43.c++指针类型转换

    数据类型转换(static_cast) //数据类型转换 printf("%d\n", static_cast<int>(10.2)); 指针类型转换(reinterp ...

  6. C#解析HTML源码

    刚做了一个小任务,需要抓取其他网站的部分数据,这里就顺便介绍使用Winista.Text.HtmlParser这个类库如何解析HTML并抓取部分数据 1.获取指定网站的页面源码 string url ...

  7. vuex-store模块化配置

    一.目录结构: src -> js -> modules 1. 在modules下新建文件夹,文件夹名称按模块功能命名 如: modules ---- home -> homeMod ...

  8. liunx中安装禅道

    本文转自:https://www.cnblogs.com/bendouyao/p/10026746.html 一.准备工作 禅道安装包ZenTaoPMS.8.1.3.zbox_64.gz,上传至服务器 ...

  9. Flume Sink Processors官网剖析(博主推荐)

    不多说,直接上干货! Flume Sources官网剖析(博主推荐) Flume Channels官网剖析(博主推荐) Flume Channel Selectors官网剖析(博主推荐) Flume ...

  10. Solr 核心组成

     Solr 核心组成就是:SolrHome 和 SolrCore. SolrHome:SolrHome是Solr运行的主目录,该目录可以包含多个solrcore目录. SolrCore:每个solrc ...