zoukankan      html  css  js  c++  java
  • 同步调用,异步调用,异步回调,Task任务

    一:创建加法类

    //定义委托
        public delegate int AddHandler(int a, int b);
    
        class AddMethod
        {
            public static int Add(int a, int b)
            {
                Console.WriteLine("开始计算:" + a + "+" + b);
                Thread.Sleep(3000); //模拟该方法运行三秒
                Console.WriteLine("计算完成!");
                return a + b;
            }
        }

    二.同步调用

    委托的Invoke方法用来进行同步调用。同步调用也可以叫阻塞调用,它将阻塞当前线程,然后执行调用,调用完毕后再继续向下进行。

    class Program
        {
            static void Main(string[] args)
            {
                Console.WriteLine("===== 同步调用 SyncInvokeTest =====");
                AddHandler handler = new AddHandler(AddMethod.Add);
                int result = handler.Invoke(1, 2);
    
                Console.WriteLine("继续做别的事情。。。");
    
                Console.WriteLine(result);
                Console.ReadKey();
            }
        

    运行结果:

    同步调用会阻塞线程,如果是要调用一项繁重的工作(如大量IO操作),可能会让程序停顿很长时间,造成糟糕的用户体验,这时候异步调用就很有必要了。

    三.异步调用

    1.BeginInvoke方法和EndInvoke方法

    BeginInvoke方法触发你的异步方法,它和你想要执行的异步方法有相同的参数。另外还有两个可选参数,第一个是AsyncCallback委托是异步完成的回调方法。第二个是用户自定义对象,该对象将传递到回调方法中。BeginInvoke立即返回并且不等待完成异步的调用(继续执行该下面的代码,不需要等待)。BeginInvoke返回IAsyncResult接口,可用于检测异步调用的过程。

    通过EndInvoke方法检测异步调用的结果。如果异步调用尚未完成,EndInvoke将阻塞调用线程,直到它完成。EndInvoke参数包括out和ref参数。

    下面代码演示使用BeginInvoke和EndInvoke进行异步调用的四种常见方式。在调用BeginInvoke可以做以下工作:

    1. 做一些其他操作,然后调用EndInvoke方法阻塞线程直到该方法完成。
    2. 使用IAsyncResult.AsyncWaitHandle属性,使用它的WaitOne方法阻塞线程直到收到WaitHandle信号,然后调用EndInvoke。
    3. 检查BeginInvoke返回值IAsyncResult的状态来决定方法是否完成,然后调用EndInvoke方法。
    4. 通过在BeginInvoke方法中传递该委托,在回调方法中调用该委托的EenInvoke方法。

    注意

    无论你怎么使用,都必须调用EndInvoke方法结束你的异步调用。

    2.异步调用

    异步调用不阻塞线程,而是把调用塞到线程池中,程序主线程或UI线程可以继续执行。
    委托的异步调用通过BeginInvoke和EndInvoke来实现。

    class Program
        {
            static void Main(string[] args)
            {
                Console.WriteLine("===== 异步调用 AsyncInvokeTest =====");
    
                AddHandler handler = new AddHandler(AddMethod.Add);//委托
    
                //IAsyncResult: 异步操作接口(interface)
                //BeginInvoke: 委托(delegate)的一个异步方法的开始,调用BeginInvoke方法,做一些其他操作(主线程继续),然后调用EndInvoke方法阻塞线程(阻塞主线程)直到该方法完成。
                IAsyncResult result = handler.BeginInvoke(1, 2, null, null);
    
                Console.WriteLine("继续做别的事情。。。");

           //调用EndInvoke,等待异步执行完成 Console.WriteLine("等待异步方法TestMethodAsync执行完成");
           //等待异步执行完毕信号
           //ressult.AsyncWaitHandle.WaitOne();
           //Console.WriterLine("收到WaitHandle信号");

    //异步操作返回,无论怎么使用,都必须调用EndInvoke方法结束你的异步调用 Console.WriteLine(handler.EndInvoke(result)); Console.ReadKey(); } }

    运行结果:

     可以看到,主线程并没有等待,而是直接向下运行了。
    但是问题依然存在,当主线程运行到EndInvoke时,如果这时调用没有结束(这种情况很可能出现),这时为了等待调用结果,线程依旧会被阻塞。

    注意:

      如果创建的是NET.Core项目,则会报一下错误:

    因为在.NET Core中不支持Action.BeginInvoke(null, null)的委托异步调用方法

    重新创建一个.NET Framework项目(测试用的是4.5.2),把代码重新复制过去后,就没有异常了。

    3.Task

      任务(Task表示一个通过或不通过线程实现的并发操作任务是可组合的使用延续(continuation可将它们串联在一起,它们可以使用线程池减少启动延迟,可使用回调方法避免多个线程同时等待I/O密集操作。

      Task 类的表示单个操作不返回一个值,通常以异步方式执行。 Task 对象是一个的中心思想 基于任务的异步模式

    3.1Task和ThreadPool的区别

      1.任务是架构在线程之上的,也就是说任务最终还是要抛给线程去执行;

      2.任务和线程不是一对一的关系,比如开10个任务并不是说会开10个线程,这点任务有点类似线程池,但是任务相比线城池有很小的开销和精确的控制;

    ThreadPool相对于Thread来说可以减少线程的创建,有效减小系统开销;但是ThreadPool不能控制线程的执行顺序,我们也不能获取线程池内线程取消/异常/完成的通知,即我们不能有效监控和控制线程池中的线程。

    3.2Task的创建和运行 

    class Program
        {
            static void Main(string[] args)
            {
                //1.new方式实例化一个Task,需要通过Start方法启动
                Task task = new Task(() =>
                {
                    Thread.Sleep(100);
                    Console.WriteLine($"hello, task1的线程ID为{Thread.CurrentThread.ManagedThreadId}");
                });
                task.Start();
    
                //2.Task.Factory.StartNew(Action action)创建和启动一个Task
                Task task2 = Task.Factory.StartNew(() =>
                {
                    Thread.Sleep(100);
                    Console.WriteLine($"hello, task2的线程ID为{ Thread.CurrentThread.ManagedThreadId}");
                });
    
                //3.Task.Run(Action action)将任务放在线程池队列,返回并启动一个Task
                Task task3 = Task.Run(() =>
                {
                    Thread.Sleep(100);
                    Console.WriteLine($"hello, task3的线程ID为{ Thread.CurrentThread.ManagedThreadId}");
                });
                Console.WriteLine("执行主线程!");
                Console.ReadKey();
            }
        }

    运行结果:

    我们看到先打印"执行主线程",然后再打印各个任务,说明了Task不会阻塞主线程。上边的例子Task都没有返回值,我们也可以创建有返回值的~task,用法和没有返回值的基本一致,我们简单修改一下上边的例子,代码如下:

    class Program
        {
            static void Main(string[] args)
            {
                //1.new方式实例化一个Task,需要通过Start方法启动
                Task<string> task = new Task<string>(() =>
                {
                    return $"hello, task1的ID为{Thread.CurrentThread.ManagedThreadId}";
                });
                task.Start();
    
                //2.Task.Factory.StartNew(Func func)创建和启动一个Task
               Task<string> task2 = Task.Factory.StartNew<string>(() =>
               {
                   return $"hello, task2的ID为{ Thread.CurrentThread.ManagedThreadId}";
               });
    
                //3.Task.Run(Func func)将任务放在线程池队列,返回并启动一个Task
               Task<string> task3 = Task.Run<string>(() =>
               {
                   return $"hello, task3的ID为{ Thread.CurrentThread.ManagedThreadId}";
               });
    
                Console.WriteLine("执行主线程!");
                Console.WriteLine(task.Result);
                Console.WriteLine(task2.Result);
                Console.WriteLine(task3.Result);
                Console.ReadKey();
            }
        }

    运行结果:

    注意task.Resut获取结果时会阻塞线程,即如果task没有执行完成,会等待task执行完成获取到Result,然后再执行后边的代码。

     上面实例中Task的执行都是异步,不会阻塞主线程。但有些场景想让Task同步执行,这时Task提供了task.RunSynchronously()用于同步执行Task任务,代码如下:

    class Program
        {
            static void Main(string[] args)
            {
                Task task = new Task(() =>
                {
                    Thread.Sleep(100);
                    Console.WriteLine("执行Task结束!");
                });
                //同步执行,task会阻塞主线程
                task.RunSynchronously();
                task.Start();
                Console.WriteLine("执行主线程结束!");
                Console.ReadKey();
            }
        }

    运行结果:

    异步执行:

    Task task = new Task(() =>
                {
                    Thread.Sleep(100);
                    Console.WriteLine("执行Task结束!");
                });
                //同步执行,task会阻塞主线程
                task.Start();
                Console.WriteLine("执行主线程结束!");
                Console.ReadKey();

    运行结果:

     3.3Task任务控制

     3.3.1 阻塞方法

    1.Thread阻塞线程的方法:

    static void Main(string[] args)
            {
                Thread th1 = new Thread(() => {
                    Thread.Sleep(500);
                    Console.WriteLine("线程1执行完毕!");
                });
                th1.Start();
    
                Thread th2 = new Thread(() => {
                    Thread.Sleep(500);
                    Console.WriteLine("线程2执行完毕");
                });
                th2.Start();
    
                th1.Join();
                th2.Join();
    
                Console.WriteLine("主线程执行完毕!");
                Console.ReadKey();
            }

    如果注释掉两个Join,执行结果是:先打印【主线程执行完毕】,而添加两个Join方法后执行结果如下,实现了线程阻塞:

    Thread使用join方法可以阻塞调用线程,但是有弊端:

      (1)如果实现很多线程的阻塞时,每个线程都要调用一次join方法;

      (2)如果想让所有线程执行完毕(或任一线程执行完毕时),立即解除阻塞,使用join方法不容易实现。

    2.Task阻塞线程的方法

    Task提供了 Wait/WaitAny/WaitAll 方法,可以更方便地控制线程阻塞。

    task.Wait() 表示等待task执行完毕,功能类似于thead.Join()Task.WaitAll(Task[] tasks) 表示只有所有的task都执行完成了再解除阻塞;Task.WaitAny(Task[] tasks)表示只要有一个task执行完毕就解除阻塞;

    static void Main(string[] args)
            {
                Task task1 = new Task(() => {
                    Thread.Sleep(500);
                    Console.WriteLine("线程1执行完毕!");
                });
                task1.Start();
                Task task2 = new Task(() => {
                    Thread.Sleep(1000);
                    Console.WriteLine("线程2执行完毕!");
                });
                task2.Start();
                //阻塞主线程。task1,task2都执行完毕再执行主线程
                //执行【task1.Wait();task2.Wait();】可以实现相同功能
                Task.WaitAll(new Task[] { task1, task2 });
                Console.WriteLine("主线程执行完毕!");
                Console.ReadKey();
            }

    运行结果:

     如果将上例中的WaitAll换成WaitAny,那么任一task执行完毕就会解除线程阻塞,执行结果是:先打印【线程1执行完毕】,然后打印【主线程执行完毕】,最后打印【线程2执行完毕】;

    3.阻塞方法:

    (1)Task.Wait()

    Task1.Wait():就是等待任务执行(task1)完成,task1的状态变为Completed。

    (2)Task.WaitAll()

    等待所有的任务都执行完成。即当task,task2,task3…N全部任务都执行完成之后才会往下执行代码(打印出:“主线程执行完毕!”)。

    (3)Task.WaitAny()

    同Task.WaitAll,就是等待任何一个任务完成就继续向下执行

    3.3.2 Task的延续操作

    Wait/WaitAny/WaitAll方法返回值为void,这些方法单纯的实现阻塞线程。我们现在想让所有task执行完毕(或者任一task执行完毕)后,开始执行后续操作,怎么实现呢?这时就可以用到WhenAny/WhenAll方法了,这些方法执行完成返回一个task实例。 task.WhenAll(Task[] tasks) 表示所有的task都执行完毕后再去执行后续的操作, task.WhenAny(Task[] tasks) 表示任一task执行完毕后就开始执行后续操作;

    class Program
        {
            static void Main(string[] args)
            {
                Task task1 = new Task(() => {
                    Thread.Sleep(500);
                    Console.WriteLine("线程1执行完毕!");
                });
                task1.Start();
                Task task2 = new Task(() => {
                    Thread.Sleep(1000);
                    Console.WriteLine("线程2执行完毕!");
                });
                task2.Start();
                //task1,task2执行完了后执行后续操作
                Task.WhenAll(task1, task2).ContinueWith((t) => {
                    Thread.Sleep(100);
                    Console.WriteLine("执行后续操作完毕!");
                });
    
                Console.WriteLine("主线程执行完毕!");
                Console.ReadKey();
            }
        }

    执行结果如下,我们看到WhenAll/WhenAny方法不会阻塞主线程,当使用WhenAll方法时所有的task都执行完毕才会执行后续操作;如果把例子中的WhenAll替换成WhenAny,则只要有一个线程执行完毕就会开始执行后续操作;

     上面的例子也可以通过 Task.Factory.ContinueWhenAll(Task[] tasks, Action continuationAction)Task.Factory.ContinueWhenAny(Task[] tasks, Action continuationAction) 来实现 ,修改上边例子代码如下,执行结果不变:

    class Program
        {
            static void Main(string[] args)
            {
                Task task1 = new Task(() => {
                    Thread.Sleep(500);
                    Console.WriteLine("线程1执行完毕!");
                });
                task1.Start();
                Task task2 = new Task(() => {
                    Thread.Sleep(1000);
                    Console.WriteLine("线程2执行完毕!");
                });
                task2.Start();
                //通过TaskFactroy实现
                Task.Factory.ContinueWhenAll(new Task[] { task1, task2 }, (t) =>
                {
                    Thread.Sleep(100);
                    Console.WriteLine("执行后续操作");
                });
    
                Console.WriteLine("主线程执行完毕!");
                Console.ReadKey();
            }
        }

    第一个Task完成后自动启动下一个Task,实现Task的延续;

    3.3.3Task取消

    Thread取消任务执行:Thread取消任务执行的一般流程:设置一个变量来控制任务是否停止,如设置一个变量isStop,然后线程轮询查看isStop,如果isStop为true就停止;

    static void Main(string[] args)
            {
                bool isStop = false;
                int index = 0;
                //开启一个线程执行任务
                Thread th1 = new Thread(() =>
                {
                    while (!isStop)
                    {
                        Thread.Sleep(1000);
                        Console.WriteLine($"第{++index}次执行,线程运行中...");
                    }
                });
                th1.Start();
                //五秒后取消任务执行
                Thread.Sleep(5000);
                isStop = true;
                Console.ReadKey();
            }

    运行结果:

    Task任务取消:

    Task中有一个专门的类 CancellationTokenSource 来取消任务执行;

    static void Main(string[] args)
            {
                CancellationTokenSource source = new CancellationTokenSource();
                int index = 0;
                //开启一个task执行任务
                Task task1 = new Task(() =>
                {
                    while (!source.IsCancellationRequested)
                    {
                        Thread.Sleep(1000);
                        Console.WriteLine($"第{++index}次执行,线程运行中...");
                    }
                });
                task1.Start();
                //五秒后取消任务执行
                Thread.Sleep(5000);
                //source.Cancel()方法请求取消任务,IsCancellationRequested会变成true
                source.Cancel();
                Console.ReadKey();
            }

    CancellationTokenSource的功能不仅仅是取消任务执行,我们可以使用 source.CancelAfter(5000)实现5秒后自动取消任务,也可以通过 source.Token.Register(Action action)注册取消任务触发的回调函数,即任务被取消时注册的action会被执行。

    static void Main(string[] args)
            {
                CancellationTokenSource source = new CancellationTokenSource();
                //注册任务取消的事件
                source.Token.Register(() =>
                {
                    Console.WriteLine("任务被取消后执行xx操作!");
                });
    
                int index = 0;
                //开启一个task执行任务
                Task task1 = new Task(() =>
                {
                    while (!source.IsCancellationRequested)
                    {
                        Thread.Sleep(1000);
                        Console.WriteLine($"第{++index}次执行,线程运行中...");
                    }
                });
                task1.Start();
                //延时取消,效果等同于Thread.Sleep(5000);source.Cancel();
                source.CancelAfter(5000);
                Console.ReadKey();
            }

    执行结果如下,第5次执行在取消回调后打印,这是因为,执行取消的时候第5次任务已经通过了while()判断,任务已经执行中了:

    跨线程实例:

    public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
    
            private void button1_Click(object sender, EventArgs e)
            {
                Task.Run(() =>
                {
                    Action<int> setValue = (i) => { myTxtbox.Text = i.ToString(); };
                    for (int i = 0; i < 1000000; i++)
                    {
                        myTxtbox.Invoke(setValue, i);
                    }
                });
            }
        }

    运行结果:

     3.4异步方法(asyc/await)

    获取文件内容示例:

    class Program
        {
            static void Main(string[] args)
            {
                string content = GetContentAsync(Environment.CurrentDirectory + @"/test.txt").Result;
                //调用同步方法
                //string content = GetContent(Environment.CurrentDirectory + @"/test.txt");
                Console.WriteLine(content);
                Console.ReadKey();
            }
            //异步读取文件内容
            async static Task<string> GetContentAsync(string filename)
            {
    
                FileStream fs = new FileStream(filename, FileMode.Open);
                var bytes = new byte[fs.Length];
                //ReadAync方法异步读取内容,不阻塞线程
                Console.WriteLine("开始读取文件");
                int len = await fs.ReadAsync(bytes, 0, bytes.Length);
                string result = Encoding.UTF8.GetString(bytes);
                return result;
            }
            //同步读取文件内容
            static string GetContent(string filename)
            {
                FileStream fs = new FileStream(filename, FileMode.Open);
                var bytes = new byte[fs.Length];
                //Read方法同步读取内容,阻塞线程
                int len = fs.Read(bytes, 0, bytes.Length);
                string result = Encoding.UTF8.GetString(bytes);
                return result;
            }
        }

    运行结果:

     注意一个小问题:异步方法中方法签名返回值为Task,代码中的返回值为T。上边栗子中GetContentAsync的签名返回值为Task,而代码中返回值为string。牢记这一细节对我们分析异步代码很有帮助。

    异步方法签名的返回值有以下三种:

    ① Task:如果调用方法想通过调用异步方法获取一个T类型的返回值,那么签名必须为Task;

    ② Task:如果调用方法不想通过异步方法获取一个值,仅仅想追踪异步方法的执行状态,那么我们可以设置异步方法签名的返回值为Task;

    ③ void:如果调用方法仅仅只是调用一下异步方法,不和异步方法做其他交互,我们可以设置异步方法签名的返回值为void,这种形式也叫做“调用并忘记”。

    3.5工作示例

    Task任务:

    //没有返回值得Task任务 
    private async Task GetDDPT(RequestPram reqPram, DataList repPram)
            {
                await Task.Run(() => {
                    try
                    {
                      //业务代码。。。
                 
                    }
                    catch (Exception ex)
                    { }
                });
            }    
    //没有返回值得Task任务 
    private async Task GetDDPT(RequestPram reqPram, DataList repPram)
            {
                await Task.Run(() => {
                    try
                    {
                      //业务代码。。。
                 
                    }
                    catch (Exception ex)
                    { }
                });
            }    
    //没有返回值得Task任务 
    private async Task GetDDPT(RequestPram reqPram, DataList repPram)
            {
                await Task.Run(() => {
                    try
                    {
                      //业务代码。。。
                 
                    }
                    catch (Exception ex)
                    { }
                });
            }    

    调用:

    [HttpPost]
    public async Task<ReponsePram> QueryInsulateData(RequestPram pram)
            {
                ReponsePram reponsePrams = new ReponsePram();
                reponsePrams.code = "200";
                reponsePrams.msg = "数据查询成功";
    
                DataList dataList = new DataList();
    
                //times.Start();
                var taskDdpt = GetDDPT(pram, dataList);
                var taskPangGu = GetPanGu(pram, dataList);
                var taskMoEr = GetMoEr(pram, dataList);
                var tasks = new[] { taskDdpt, taskPangGu, taskMoEr };//任务组
                var process = tasks.Select(async ts =>              
                {
                    await ts;
                });
                await Task.WhenAll(process);                         //等待处理完全
                                                                     // times.Stop();
    
              //业务代码
        
                return reponsePrams;
            }

    三.异步回调

     1.异步调用

      static void Main(string[] args)
            {
                Console.WriteLine("===== 异步调用 AsyncInvokeTest =====");
    
                AddHandler handler = new AddHandler(AddMethod.Add);//委托
    
                //IAsyncResult: 异步操作接口(interface)
                //BeginInvoke: 委托(delegate)的一个异步方法的开始,调用BeginInvoke方法,做一些其他操作(主线程继续),然后调用EndInvoke方法阻塞线程(阻塞主线程)直到该方法完成。
                IAsyncResult result = handler.BeginInvoke(1, 2, null, null);
    
                Console.WriteLine("继续做别的事情。。。");
    
                //异步操作返回,无论怎么使用,都必须调用EndInvoke方法结束你的异步调用
                Console.WriteLine(handler.EndInvoke(result));
                Console.WriteLine("异步调用方法结束后,继续做其他事!");
                Console.ReadKey();
            }

    运行结果:

     即调用EndInvoke方法时,这是会阻塞主线程,直到异步方法执行完毕,主线程才会继续执行下去!

    2.异步回调

    用回调函数,当调用结束时会自动调用回调函数,解决了为等待调用结果,而让线程依旧被阻塞的局面;

     static void Main(string[] args)
            {
                Console.WriteLine("===== 异步回调 AsyncInvokeTest =====");
                AddHandler handler = new AddHandler(AddMethod.Add);
                Console.WriteLine("主线程做第一件事!");
    
                //异步操作接口(注意BeginInvoke方法的不同!)
                IAsyncResult result = handler.BeginInvoke(1, 2, new AsyncCallback(CallBackMethod), "AsycState:OK");
    
                Console.WriteLine("继续做别的事情,主线程不会被阻塞。。。");
                Console.ReadKey();
            }
    
            static void CallBackMethod(IAsyncResult result)
            {      //result 是“加法类.Add()方法”的返回值
    
                //AsyncResult 是IAsyncResult接口的一个实现类,空间:System.Runtime.Remoting.Messaging
                //AsyncDelegate 属性可以强制转换为用户定义的委托的实际类。
                AddHandler handler = (AddHandler)((AsyncResult)result).AsyncDelegate;
                Console.WriteLine(handler.EndInvoke(result));
                Console.WriteLine(result.AsyncState);
            }

     运行结果:

      我定义的委托的类型为AddHandler,则为了访问 AddHandler.EndInvoke,必须将异步委托强制转换为 AddHandler。可以在异步回调函数(类型为 AsyncCallback)中调用 MAddHandler.EndInvoke,以获取最初提交的 AddHandler.BeginInvoke 的结果。

    问题:

    (1)int result = handler.Invoke(1,2);
    为什么Invoke的参数和返回值和AddHandler委托是一样的呢?
    答:
    Invoke方法的参数很简单,一个委托,一个参数表(可选),而Invoke方法的主要功能就是帮助你在UI线程上调用委托所指定的方法。Invoke方法首先检查发出调用的线程(即当前线程)是不是UI线程,如果是,直接执行委托指向的方法,如果不是,它将切换到UI线程,然后执行委托指向的方法。不管当前线程是不是UI线程,Invoke都阻塞直到委托指向的方法执行完毕,然后切换回发出调用的线程(如果需要的话),返回。
    所以Invoke方法的参数和返回值和调用他的委托应该是一致的。

    (2)IAsyncResult result = handler.BeginInvoke(1,2,null,null);

    BeginInvoke : 开始一个异步的请求,调用线程池中一个线程来执行,
    返回IAsyncResult 对象(异步的核心). IAsyncResult 简单的说,他存储异步操作的状态信息的一个接口,也可以用他来结束当前异步。
    注意: BeginInvoke和EndInvoke必须成对调用.即使不需要返回值,但EndInvoke还是必须调用,否则可能会造成内存泄漏。

    (3)IAsyncResult.AsyncState 属性:
    获取用户定义的对象,它限定或包含关于异步操作的信息。 例如:

    static void AddComplete(IAsyncResult result) 
    {   
          AddHandler handler = (AddHandler)result.AsyncState;    
          Console.WriteLine(handler.EndInvoke(result)); 
          。。。。。
    }
  • 相关阅读:
    SaltStack api使用
    saltstack批量管理文件和计划任务
    Kubernetes应用管理器OpenKruise之CloneSet
    Kubernetes日志系统新贵Loki-Stack
    Prometheus Operator自定义监控项
    MySQL错误修复:Table xx is marked as crashed and last (automatic?) repair failed
    kubernetes存储类与PV与PVC关系及实践
    手把手教你使用rpm部署ceph集群
    什么是dockerfile?
    RabbitMQ
  • 原文地址:https://www.cnblogs.com/chedahui/p/15419737.html
Copyright © 2011-2022 走看看