C#中异步多线程的常见用法_c#异步线程的使用-程序员宅基地

技术标签: 技术  

先来看几个基本概念(纯属个人见解,可能不准确):

进程:程序运行时,占用的全部运行资源的总和。

线程:线程是隶属于操作系统管理的,也可以有自己的计算资源,是程序执行流的最小单位。任何的操作都是由线程来完成的。

每个线程都在操作系统的进程内执行,而操作系统进程提供了程序运行的独立环境。

多线程:多核cpu协同工作,多个执行流同时运行,是用资源换时间。(单核cpu,不存在所谓的多线程)。

单线程应用:在进程的独立环境中只跑一个线程,所以该线程拥有独立权。

多线程应用:单个进程中会跑多个线程,它们会共享当前的执行环境(尤其是内存)。

在单核计算机上,操作系统必须为每个线程分配“时间片”来模拟并发。而在多核或多处理器计算机上,多个线程可以真正的并行执行。(可能会受到计算机上其他活动进程的竞争)。

win10上的时间片(使用微软官方小工具测得):

Thread

  Thread的对象是非线程池中的线程,有自己的生命周期(有创建和销毁的过程),所以不可以被重复利用(一个操作中,不会出现二个相同Id的线程)。

Thread的常见属性:

  • 线程一旦开始执行,IsAlive就是true,线程结束就变成false。
  • 线程结束的条件是:线程构造函数传入的委托结束了执行。
  • 线程一旦结束,就无法再重启。
  • 每个线程都有一个Name属性,通常用于调试,线程的Name属性只能设置一次,以后更改会抛出异常。
  • 静态的Thread.CurrentThread属性,会返回当前线程。

Thread的常见用法:

join

调用join方法可以等待另一个线程结束。

private void button5_Click(object sender, EventArgs e) {
          
Console.WriteLine($
"===============Method start time is {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")},Thread ID is {Thread.CurrentThread.ManagedThreadId},is back ground: {Thread.CurrentThread.IsBackground}==================="); //开启一个线程,构造方法可重载两种委托,一个是无参无返回值,一个是带参无返回值 Thread thread = new Thread(a => DoSomeThing("Thread")); //当前线程状态 Console.WriteLine($"thread's state is {thread.ThreadState},thread's priority is {thread.Priority} ,thread is alived :{thread.IsAlive},thread is background:{thread.IsBackground},thread is pool threads: {thread.IsThreadPoolThread}"); //告知操作系统,当前线程可以被执行了。 thread.Start(); //阻塞当前执行线程,等待此thread线程实例执行完成。无返回值 thread.Join(); //最大等待的时间是5秒(不管是否执行完成,不再等待),返回一个bool值,如果是true,表示执行完成并终止。如果是false,表示已到指定事件但未执行完成。 thread.Join(5000); Console.WriteLine($"===============Method end time is {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")},,Thread ID is {Thread.CurrentThread.ManagedThreadId},is back ground: {Thread.CurrentThread.IsBackground}==================="); }
private void DoSomeThing(string name) { Console.WriteLine($"do some thing start time is {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")},Thread ID is {Thread.CurrentThread.ManagedThreadId},is back ground: {Thread.CurrentThread.IsBackground}"); long result = 0; for (long i = 0; i < 10000 * 10000; i++) { result += i; } Console.WriteLine($"do some thing end time is {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")},Thread ID is {Thread.CurrentThread.ManagedThreadId},is back ground: {Thread.CurrentThread.IsBackground}"); }

注意 :thread 默认是前台线程,启动后一定要完成任务的,即使程序关掉(进程退出)也要执行完。可以把thread 指定为后台线程,随着进程的退出而终止。

//false,默认是前台线程,启动后一定要完成任务的,即使程序关掉(进程退出)也要执行完。
Console.WriteLine(thread.IsBackground); 
thread.IsBackground = true;//指定为后台线程。(随着进程的退出而退出)

Sleep

Thread.Sleep()会暂停当前线程,,并等待一段时间。其实,Thread.Sleep只是放弃时间片的剩余时间,让系统重新调度并选择一个合适的线程。

在没有其他活动线程的情况下,使用Thread.Sleep(0)还是会选上自身,即连任,系统不会对其做上下文切换。

static void Main(string[] args)
{
    Stopwatch stopwatch=new Stopwatch();
    stopwatch.Start();
    Thread.Sleep(0);
    stopwatch.Stop();
    System.Console.WriteLine(stopwatch.ElapsedMilliseconds); //返回0
}

而Thread.Sleep(大于0)却让当前线程沉睡了,即使只有1ms也是沉睡了,也就是说当前线程放弃下次的竞选,所以不能连任,系统上下文必然发生切换。

阻塞

如果线程的执行由于某种原因导致暂停,那么就认为该线程被阻塞了。例如在Sleep或者Join等待其他线程结束。被阻塞的线程会立即将其处理器的时间片生成给其他线程,从此就不再消耗处理器时间。

 

 

 

 

Thread的回调用法:

Thread没有像Framework中的delegate的回调用法,如果需要回调得自动动手改造:

private void CallBack(Action action, Action calback)
{
    Thread thread = new Thread(() => { action(); calback(); });
    thread.Start();
}
//无参无返回值
CallBack(() => Console.WriteLine("好吗?"), () => Console.WriteLine("好的!"));
private Func<T> CallBackReturn<T>(Func<T> func)
{
    T t = default(T);
    Thread thread = new Thread(() =>
    {
        t = func();
    });
    thread.Start();
    return () =>
    {
        thread.Join();
        return t;
    };
}
//带返回值得用法
Func<int> func = CallBackReturn<int>(() => DateTime.Now.Second);
Console.WriteLine("线程未阻塞");
int result = func.Invoke();
Console.WriteLine("result:" + result);

ThreadPool 线程池

Thread的功能太过强大,像我这样的小白是用不好的(之前在项目中大量使用Thread的API,出现了许多意想不到的bug)。线程池中的线程在同一操作中可以被重复利用。

 //开启多线程
 ThreadPool.QueueUserWorkItem(n => DoSomeThing("ThreadPool"));

 个人觉得尽量不要阻塞线程池的线程,因为线程池里的线程数量是有限的,当线程池中没有线程可用时,会出现死锁。如果非要等待,用法如下:

ManualResetEvent manualResetEvent = new ManualResetEvent(false);
ThreadPool.QueueUserWorkItem(n =>
{
    DoSomethingLong("ThreadPool");
    manualResetEvent.Set();
});
//等待线程完成
manualResetEvent.WaitOne();

 Task

Task是基于ThreadPool的基础上做的封装,属于线程池中的线程。

Task启动多线程的方式:

方式一:指定任务的开始时机

/// <summary>
/// 使用Task或Task<T>创建任务,需指定任务的开始时机(任务调度)。
/// </summary>
public static void Demo1()
{
    Task task = new Task(() =>
    {
        Thread.Sleep(3000); 
Console.WriteLine($"Current thread id is {Thread.CurrentThread.ManagedThreadId}"); }); task.Start();//任务调度(开始任务) Console.WriteLine($"Current thread name is {Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"当前任务状态:{task.Status}"); task.Wait(); //等待任务执行完成 Console.WriteLine($"当前任务状态:{task.Status}"); }

 

 

方式二:一步完成多线程的创建和启动

/// <summary>
/// 使用Task.Run()方法一步完成多线程的创建和启动(当前线程立即准备启动任务)。
/// <remark>
/// 如果不需要对任务的创建和调度做更多操作,Task.Run()方法是创建和启动任务的首选方式。
/// </remark>
/// </summary>
public static void Demo2()
{
    Task task = Task.Run(() => { Thread.Sleep(3000); Console.WriteLine($"Current thread id is {Thread.CurrentThread.ManagedThreadId}"); });
    task.Wait(); //等待,直到任务完成
}

 

方式三:需要想多线程任务传递状态参数

/// <summary>
/// Task和Task<TResult>都有静态属性Factory,它返回默认的实例TaskFactory.
/// 使用Task.Factory.StartNew()方法也可以一步完成任务的创建和启动。
/// 当前需要向任务传递一个状态(参数)。可以使用此方法。
/// </summary>
public static void Demo3()
{
    Task[] tasks = new Task[10];
    for (int i = 0; i < tasks.Length; i++)
    {
        tasks[i] = Task.Factory.StartNew((obj) =>
        {
            CustomData data = obj as CustomData;
            data.ThreadId = Thread.CurrentThread.ManagedThreadId;
        }, new CustomData { CreationTime = DateTime.Now.Ticks, Index = i});
    }
//以阻塞当前线程的方式,等待所以子线程的完成 Task.WaitAll(tasks);
foreach (var task in tasks) { //通过任务的AsyncState属性,可以获取任务状态(提供给任务的参数). var data = task.AsyncState as CustomData; Console.WriteLine(JsonConvert.SerializeObject(data)); } } //Task.Factory.StartNew() 调用无返回值的任务 //Task<TResult>.Factory.StartNew() 调用有返回值的任务

 

Task<TResult>

public static void Demo4()
{
    Task<Double>[] tasks = {
Task<Double>.Factory.StartNew(() => DoComputation(1.0)),
Task<Double>.Factory.StartNew(() => DoComputation(100.0)),
Task<Double>.Factory.StartNew(() => DoComputation(1000.0)) };
    var results = new Double[tasks.Length];
    Double sum = 0;
    for (int i = 0; i < tasks.Length; i++)
    {
        //Task<TResult>.Result属性包含任务的计算结果,如果在任务完成之前调用,则会阻塞线程直到任务完成
        results[i] = tasks[i].Result; 
        Console.Write("{0:N1} {1}", results[i],
                          i == tasks.Length - 1 ? "= " : "+ ");
        sum += results[i];
    }
    Console.WriteLine("{0:N1}", sum);
}
private static Double DoComputation(Double start)
{
    Double sum = 0;
    for (var value = start; value <= start + 10; value += .1)
        sum += value;
    return sum;
}

 

Task的常用API

WaitAny和WaitAll,会阻塞当前线程(主线程)的执行:

List<Task> tasks = new List<Task>();
tasks.Add(Task.Run(() => DoSomeThing("Task1")));
tasks.Add(Task.Run(() => DoSomeThing("Task2")));
tasks.Add(Task.Run(() => DoSomeThing("Task3")));
//阻塞当前线程的执行,等待任意一个子线程任务完成后继续往下执行
Task.WaitAny(tasks.ToArray());
//阻塞当前线程的执行,等待所有子线程任务完成后继续往下执行
Task.WaitAll(tasks.ToArray());

 

WhenAll和WhenAny,是通过返回一个Task 对象的方式,来达到非阻塞式的等待

//不阻塞当前线程的执行,等待所有子线程任务完成后,异步执行后续的操作
Task.WhenAll(tasks).ContinueWith(t =>
{
    Console.WriteLine($"不阻塞,{Thread.CurrentThread.ManagedThreadId}");
});

//工厂模式的实现
Task.Factory.ContinueWhenAll(tasks.ToArray(), s =>
{
Console.WriteLine("不阻塞" + s.Length);
});

ContinueWith,是一个实例方式,并且返回Task实例,所以可以使用这种链式结构来完成按顺序执行。

public static void Demo8()
{
    var task = Task.Factory
        .StartNew(() => { Console.WriteLine("1"); return 10; })
        .ContinueWith(i => { Console.WriteLine("2"); return i.Result + 1; })
        .ContinueWith(i => { Console.WriteLine("3"); return i.Result + 1; });
    Console.WriteLine(task.Result);
}

控制线程数量的使用,(核心思想来自别人,我感觉控制的很好):

/// <summary>
/// 线程数量的控制
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Test(object sender, EventArgs e)
{
    //完成10000个任务,但只要11个线程。
    List<int> intList = new List<int>();
    for (int i = 0; i < 10000; i++)
    {
        intList.Add(i);
    }
    Action<int> action = i =>
    {
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(new Random(i).Next(100, 300));
    };
    List<Task> tasks = new List<Task>();
    foreach (var item in intList)
    {
        int i = item;
        tasks.Add(Task.Run(() => action(i)));
        //当已使用了11个线程的时候,即时释放已完成的线程。
        if (tasks.Count > 10)
        {
            Task.WaitAny(tasks.ToArray());
            tasks = tasks.Where(n => n.Status != TaskStatus.RanToCompletion).ToList();
        }
    }
    Task.WaitAll(tasks.ToArray());
}

 注意:应当避免在子线程委托的内部直接使用主线程变量(闭包的弊端问题)

 public static void Demo5()
 {
     Task[] taskArray = new Task[10];
     for (int i = 0; i < taskArray.Length; i++)
     {
         taskArray[i] = Task.Factory.StartNew(() =>
         {
             //当您使用lambda表达式创建委托时,虽然可以访问变量范围内可见的所有变量。
             //但是在某些情况下(最明显的是在循环中),lambda不能像预期的那样捕获变量
             //(本例中,它只能捕获最后一个值,而不每次迭代的值)。 
             //因为任务的运行时机不确定。可以通过传递参数的方式,避免此问题的发生。
             Console.WriteLine(i);//输出10个10
         });
     }
     Task.WaitAll(taskArray);
 }
创建分离的子任务

  在父任务中创建子任务,如果未指定AttachedToParent选项时,子任务不会与父任务同步。

public static void Demo9()
{
    //创建父任务
    var outer = Task.Run(() =>
    {
        Console.WriteLine("父任务开始启动!");
        //创建子任务
        var child = Task.Run(() =>
        {
            Thread.SpinWait(5000000);
            Console.WriteLine("分离的任务完成");
        });
    });
    outer.Wait(); //父任务不会等待子任务的完成
    Console.WriteLine("父任务完成.");
}

当在任务中运行的代码使用AttachedToParent选项创建新任务时,新任务称为父任务的附加子任务。可以使用AttachedToParent选项来表达结构化任务并行性,因为父任务隐式等待所有附加的子任务完成。

 public static void Demo10()
 {
     var parent = Task.Factory.StartNew(() => {
         Console.WriteLine("Parent task beginning.");
         for (int i = 0; i < 10; i++)
         {
             Task.Factory.StartNew((x) => {
                 Thread.SpinWait(5000000);
                 Console.WriteLine("Attached child #{0} completed.",x);
             }, i, TaskCreationOptions.AttachedToParent);  
         }
     });
     parent.Wait();
     Console.WriteLine("Parent task completed.");
 }

注意:如果父任务启动DenyChildAttach选项,子任务即时启用AttachedToParent选项也不会附加到父任务。

Parallel
parallel为并行计算,主线程也参与计算
 //Parallel.For:
public static void Main(string[] args)
 {
     //计算目录的大小
     long totalSize = 0;
     String[] files =Directory.GetFiles(@"C:\Users\Administrator\Desktop");
     Parallel.For(0, files.Length,
                  index => {
                      FileInfo fi = new FileInfo(files[index]);
                      long size = fi.Length;
                      Interlocked.Add(ref totalSize, size);  //将两个64位整数相加,并用和替换第一个整数,作为
                  });
     Console.WriteLine("{0:N0} files, {1:N0} bytes", files.Length, totalSize); 
 }
//旋转图片 
static void Main(string[] args)
 {
     // A simple source for demonstration purposes. Modify this path as necessary.
     string[] files = Directory.GetFiles(@"C:\Users\Administrator\Desktop\test");
     string newDir = @"C:\Users\Administrator\Desktop\test\Modified";
     if (!Directory.Exists(newDir))
         Directory.CreateDirectory(newDir);
     // Method signature: Parallel.ForEach(IEnumerable<TSource> source, Action<TSource> body)
     Parallel.ForEach(files, (currentFile) =>
     {
         // The more computational work you do here, the greater 
         // the speedup compared to a sequential foreach loop.
         string filename = Path.GetFileName(currentFile);
         var bitmap = new Bitmap(currentFile);
         bitmap.RotateFlip(RotateFlipType.Rotate180FlipNone);
         bitmap.Save(Path.Combine(newDir, filename));
         // Peek behind the scenes to see how work is parallelized.
         // But be aware: Thread contention for the Console slows down parallel loops!!!
         Console.WriteLine($"Processing {filename} on thread {Thread.CurrentThread.ManagedThreadId}");
         //close lambda expression and method invocation
     });
 }

 

 分区局部变量

 static void TestParallForeach()
 {
     int[] array = Enumerable.Range(1, 100).ToArray();
     long totalNum = 0;
     //int 为集合元素类型
     //long 为分区局部变量类型
     Parallel.ForEach<int, long>(array, //源集合
         () => 0, //初始化局部分区变量,每个分区执行一次
         (index, state, subtotal) => //每次迭代的时候执行
         {
             subtotal += index; //修改分区局部变量
             return subtotal; //传递给当前分区的下一次迭代
         },
         //每个分区结束的时间执行,并将该分区最后一次迭代的局部分区变量传递过来。
         (finalTotal) => Interlocked.Add(ref totalNum, finalTotal)
         );
     /*重载方式:public static ParallelLoopResult ForEach<TSource, TLocal>(IEnumerable<TSource> source, 
     Func<TLocal> localInit, Func<TSource, ParallelLoopState, TLocal, TLocal> body, Action<TLocal> localFinally);
     TSource:源数据类型。 source:源数据,必须实现 IEnumerable<T>接口。
     TLocal:局部分区变量类型。localInit:初始化局部分区变量的函数。每个分区都是执行此函数一次。
     body:并行循环的每次迭代都是调用此方法。
     body.TSource:当前元素。body.ParallelLoopState:ParallelLoopState类型的变量,可用来检索循环的状态。
     body.TLocal.1:局部分区变量。
     body.TLocal.2:返回值。将其传递给特定分区循环的下一个迭代。
     localFinally:每个分区的循环完成时调用此委托。*/
     Console.WriteLine(totalNum);

 

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_45534061/article/details/115007335

智能推荐

使用 ARFoundation 实现 AR 虚拟试戴_ar试帽项目-程序员宅基地

文章浏览阅读336次。在 Hierarchy 面板中右键点击 “AR Session” 对象,选择 “XR” -> “AR Session Origin”,将其添加为 “AR Session” 的子对象。首先,打开 Unity,并在 Hierarchy 面板中右键点击创建一个空对象,将其命名为 “AR Session”。在 Inspector 面板中选择 “AR Session Origin” 对象,然后在右侧的 Inspector 面板中找到 “AR Session Origin” 组件的 “AR Session” 字段。_ar试帽项目

探索T-Writer.js:一款强大的Web文本编辑器-程序员宅基地

文章浏览阅读320次,点赞3次,收藏6次。探索T-Writer.js:一款强大的Web文本编辑器项目地址:https://gitcode.com/ChrisCavs/t-writer.jsT-Writer.js 是一个基于JavaScript的开源富文本编辑器,旨在提供一种简洁、高效的在线写作体验。它具备现代Web应用所需的多种功能,并且易于集成到你的网站或应用中。技术分析T-Writer.js 使用了最新的Web技术栈,包括:...

[PHP]学生成绩管理系统_php成绩管理系统-程序员宅基地

文章浏览阅读1w次,点赞27次,收藏175次。[PHP]学生成绩管理系统其实,这是我大一的时候,数据库的课程设计,虽然现在回看也觉得代码凌乱,但也懒得改了,之前是发在了自己搭建的博客,现在发到CSDN上来。文章目录[PHP]学生成绩管理系统1 前言2 功能模块2.1学生模块2.2 教师模块2.3 主要的文件结构3 界面设计3.1 登录界面3.2 学生主页(我的成绩)3.3 各科成绩3.4 教师主页(学生管理)3.5 学生信息详情3.6 学生信息修改3.7 添加学生信息3.8 课程管理3.9 各科成绩3.10 一键管理4 数据库设计4.1 E-R图4_php成绩管理系统

基于ssm超市库存商品管理系统的设计与实现(源码+lw+部署文档+讲解等)-程序员宅基地

文章浏览阅读775次,点赞22次,收藏24次。功能对照表的目的是帮助开发团队了解软件的功能状况,及时修复功能缺陷和错误,并提高软件的质量和稳定性。功能编号功能名称功能描述功能状态备注1用户登录用户可以通过提供用户名和密码登录系统正常用户名和密码的验证机制安全性2用户注册用户可以通过提供用户名、密码和电子邮件地址注册新的账户正常无3密码修改用户可以通过提供原密码和新密码修改已有账户的密码正常用户密码的修改操作是否需要提供安全认证4用户信息查看用户可以查看自己的个人信息,如用户名、电子邮件地址、角色等正常无。

阿里云PAI大模型RAG对话系统最佳实践_rag最佳实践-程序员宅基地

文章浏览阅读745次,点赞12次,收藏21次。通过以上更具体的步骤和考虑因素,_rag最佳实践

python如何另存文件_python如何保存文本文件-程序员宅基地

文章浏览阅读3.1k次。python保存文本文件的方法:使用python内置的open()类可以打开文本文件,向文件里面写入数据可以用write()函数,写完之后,使用close()函数就可以关闭并保存文本文件了示例代码如下:执行结果如下:内容扩展:Python3将数据保存为txt文件的方法,具体内容如下所示:f = open("data/model_Weight.txt",'a') #若文件不存在,系统自动创建。'a'..._怎么保存python文件

随便推点

vue中的js文件如何使用i18n 国际化_vue中i18n.js文件中如何调用解决,并将结果赋值给i1 8n的message-程序员宅基地

文章浏览阅读5.8k次,点赞2次,收藏6次。1.在main.js文件中引入:方式1://main.jsimport VueI18n from 'vue-i18n'Vue.use(VueI18n) // 通过插件的形式挂载,通过全局方法 Vue.use() 使用插件const i18n = new VueI18n({ locale: 'zh', // 语言标识 //this.$i18n.locale // 通过切换loca..._vue中i18n.js文件中如何调用解决,并将结果赋值给i1 8n的message

linux驱动开发:ft5x06的touch screen的IIC驱动程序编写_触摸屏a类协议和b类协议-程序员宅基地

文章浏览阅读2.8k次。触摸屏属于一个标注的input dev.所以我们按照输入子系统的流程来实现驱动开发。 实际板子与CTPM的通讯连接基于IIC总线,所以我们需要把驱动挂载到IIC总线下面去,也就是注册IIC驱动到iic_core.c中去。 实例化一个IIC设备有多种方式,仿照上一次的24cxx IIC设备的创建,我们来实现ft5x06IIC设备的创建。 因实际板子上TS IC使用的是ft5x02,所以先实例化设_触摸屏a类协议和b类协议

关于html中下拉菜单select的样式的改变_html设置select选择显示和下来显示怎么能不一致-程序员宅基地

文章浏览阅读5.2k次。关于html中下拉菜单select的样式的改变作者:菩提树下的杨过 日期:2006-06-12字体大小: 小 中 大 首先要告诉大家,如果你是用css的方法,除了箭头部分,其他都可以改变,这是很令人别扭的事,因为其他的样式改了,箭头部分改不了等于无用。下面举个css改select的例子 HTML代码 .box{border:1px solid#C0C0_html设置select选择显示和下来显示怎么能不一致

ZedBoard-自定义IP核实现+PS成功调用【详细步骤+流程介绍+源码】_zedboard教程-程序员宅基地

文章浏览阅读3.4k次。软件环境:WIN7_64 + ISE 14.4 (system_edition)硬件:Zedboard、USB-Cable线搭建图: 经过前几天的学习,查看数据手册、官方例程,笔者已经对Zedboard有了基本的了解,但是怎样才能充分发挥ZYNQ的优势呢?这个就不得不说下ZYNQ的基本架构了,它分为PS(Processing System)和PL(Progr_zedboard教程

使用H2O机器学习"十分钟"提交天池练习赛--工业蒸汽量预测,超过86%的队伍_练习赛-蒸汽数据集-程序员宅基地

文章浏览阅读430次。试用一下H2O全自动机器学习下载数据集天池练习赛"工业蒸汽量预测",下个数据集:https://tianchi.aliyun.com/competition/entrance/231693/introduction安装H2OH2O requirements:pip install requestspip install tabulatepip install ..._练习赛-蒸汽数据集

VxLAN-分布式网关_vxlan分布式网关实验-程序员宅基地

文章浏览阅读603次。vLSW1、vLSW2充当虚拟交换机,给数据添加vlan tag。由于模拟器PC不会主动发送ARP,需要用PC ping 网关地址。查看VPN实例中的路由,学习到32位主机路由。查看OSPF邻居是否建立,路由是否学习到。CE1上学习到32位主机路由(回城路由)查看BGP EVPN学习的5类路由。测试PC访问外部网络Server。查看BGP EVPN邻居关系。同子网,PC1 访问PC2。跨子网,PC1 访问PC3。查看EVPN的3类路由。查看EVPN的2类路由。查看3层VxLan隧道。查看VPN实例内路由。_vxlan分布式网关实验

推荐文章

热门文章

相关标签