(15)线程的实例认识:同步,异步,并发,并发回调,事件,异步线程,UI线程

参看:https://www.bilibili.com/video/BV1xA411671D/?spm_id_from=333.880.my_history.page.click\&vd_source=2a0404a7c8f40ef37a32eed32030aa18

下面是net framework版本

一、文件构成

1、界面如下。

(1)同步与异步有什么区别?

同步和异步是描述任务完成方式的两个概念。

同步是指任务按照顺序依次执行,每个任务需要等待前一个任务完成后才能开始执行。在同步任务中,任务的执行顺序是确定的,任务之间存在依赖关系,后续任务必须等待前面的任务完成才能继续执行。同步任务通常会阻塞当前线程,直到任务完成。

异步是指任务可以独立于其他任务执行,不需要等待前一个任务完成。在异步任务中,任务的执行顺序是不确定的,任务之间不存在依赖关系,可以同时执行多个任务。异步任务通常不会阻塞当前线程,而是通过回调、事件或者其他机制在任务完成后通知调用者。

区别总结如下:

执行方式:同步任务按照顺序依次执行,而异步任务可以独立于其他任务执行。

任务顺序:同步任务的执行顺序是确定的,而异步任务的执行顺序是不确定的。

依赖关系:同步任务之间存在依赖关系,后续任务必须等待前面的任务完成才能继续执行,而异步任务之间不存在依赖关系。

阻塞:同步任务会阻塞当前线程,直到任务完成,而异步任务不会阻塞当前线程。

通知机制:异步任务通常通过回调、事件或其他机制在任务完成后通知调用者。

在编程中,可以根据任务的性质和需求选择使用同步或异步方式来执行任务。同步方式适用于任务之间有明确依赖关系的情况,而异步方式适用于任务之间相互独立且需要提高系统性能和响应能力的情况。

(2)并发与并行有什么区别?

并行和并发是计算机领域中两个相关但不同的概念。

并行是指在同一时间点上执行多个任务,通常使用多个处理器或多个计算机来同时处理不同的任务。并行计算通常用于处理大规模的计算问题,例如科学计算、大数据处理等。在大型计算机或分布式系统中,可以利用并行计算来加速任务的执行速度。

而并发是指在同一时间段内执行多个任务,这些任务可以是交替执行的,每个任务都在一段时间内执行,然后切换到另一个任务。并发通常用于个人计算机或多任务操作系统中,以提高系统的资源利用率和响应能力。在并发模型中,多个任务可以同时运行,但每个任务的执行进度可能会受到其他任务的影响。

个人台式机通常使用并发来处理多个任务,例如同时运行多个应用程序、处理用户的输入输出等。并发可以提供更好的用户体验和系统性能。

注意,并行和并发并不是互斥的概念,有些情况下可以同时使用并行和并发来提高系统的性能和效率。

总结,大型计算机通常使用并行来处理大规模的计算问题,而个人台式机通常使用并发来处理多个任务和提高系统性能。

2、Book类

cs 复制代码
        public class Book
        {
            public class BookEventArgs : EventArgs//a
            {
                public BookEventArgs(string name, string result)
                { Name = name; Result = result; }

                public string Name { get; }
                public string Result { get; }
            }

            public event EventHandler<BookEventArgs> EventCompleted;//b

            public string Name { get; set; }
            public int Duration { get; set; }

            public Book(string name, int second)
            { Name = name; Duration = second; }

            private string Result(long m)
            { return $"{Name.PadRight(12, '-')}用时:{Convert.ToSingle(m) / 1000}"; }//c 用全角,不然对不齐

            public string Search()//d
            {
                Stopwatch sw = Stopwatch.StartNew();
                Thread.Sleep(Duration * 1000);
                sw.Stop();
                return Result(sw.ElapsedMilliseconds);
            }

            public void SearchEvent()//e
            {
                Stopwatch sw = Stopwatch.StartNew();
                Thread.Sleep(Duration * 1000);
                sw.Stop();
                EventCompleted(this, new BookEventArgs(Name, Result(sw.ElapsedMilliseconds)));
            }

            public async Task<string> SearchAsync()//f 异步检索

            {
                Stopwatch sw = Stopwatch.StartNew();
                await Task.Delay(Duration * 1000).ConfigureAwait(false);
                sw.Stop();
                return Result(sw.ElapsedMilliseconds);
            }
        }

(1)复习EvenHandler类

EventHandler 是一个委托类型,用于处理事件的订阅和触发。EventHandler 是一个泛型委托,可以用于处理不带参数的事件,也可以用于处理带有 EventArgs 参数的事件。

public delegate void EventHandler(object sender, EventArgs e);

EventHandler 委托有两个参数:sender 和 e。sender 参数表示事件的发送者,通常是引发事件的对象。EventArgs 参数是一个包含事件数据的对象,它可以是预定义的 EventArgs 子类,也可以是自定义的派生类。

使用 EventHandler 委托可以实现事件的订阅和触发。在事件的发布者类中,可以定义一个事件,并使用 EventHandler 委托作为事件的类型。然后,在其他类中可以订阅该事件,并提供一个事件处理方法来处理事件发生时的逻辑。

cs 复制代码
        internal class Program
        {
            private static void Main(string[] args)
            {
                EventPublisher publisher = new EventPublisher();
                EventSubscriber subscriber = new EventSubscriber();
                publisher.MyEvent += subscriber.ExecutionMethod;//a
                publisher.OnMyEvent();
                Console.ReadKey();
            }
        }
        public class EventPublisher//事件发布者
        {
            public event EventHandler MyEvent;

            public void OnMyEvent()
            { MyEvent?.Invoke(this, EventArgs.Empty); }//b
        }
        public class EventSubscriber//事件订阅者,用户
        {
            public void ExecutionMethod(object sender, EventArgs e)//c
            { Console.WriteLine("这里是实际被调用处理方法"); }
        }

上面a处用MyEvent而不用OnMyEvent是为了解耦。同时b,c的参数应与EventHandler保持一致.

问:b的 EventHandler<BookEventArgs>是什么?

答:EventHandler 委托的实际参数是两个:object 类型的发送者(即事件的来源对象)和 EventArgs 类型的事件参数。

而 EventHandler<T> 是 EventHandler 的泛型版本,其中 T 是自定义的事件参数类型。它的实际参数也是两个:object 类型的发送者和 T 类型的事件参数。

通过使用泛型委托 EventHandler<T>,可以在定义事件时指定自定义的事件参数类型,从而使事件处理方法能够接收到特定类型的事件参数。

上面的a处就是对b的具体处理(第二参数,一般在类后都继承EventArgs)。

(2)c处的排版用全角,不然因为汉字与英文的不同宽度,而对不齐。

(3)Thread.Sleep()与Task.Delay()的区别是什么?

答:Thread.Sleep是同步等待,会阻塞当前线程,看上去当前线程好像"死了",但当等待的时间到了后,又继续向下执行,看上去又"活了"。

Task.Delay是异步等待。小心!当前线程会异步调用一个线程来执行Task.Delay来等待,当前线程自己调用完后不会等待,而立即向下执行,所以当前线程不会在在地进行等待,所以当前线程看似是"活的",但它没起到等待的作用。

若要当前线程看似活了,又要阻塞,则要用await task.delay(同时方法添加async),await表示"等会儿",等异步线程task.deley执行完成后,再继续执行await下面的语句,而且当前线程一直看似是"活的"。

3、Data类

cs 复制代码
        public class Data
        {
            public static readonly List<Book> Books = new List<Book>()
            {
                new Book("封神演义",1),
                new Book("三国演义",2),
                new Book("水浒传",1),
                new Book("西游记",1),
                new Book("聊斋志异",1),
                new Book("儒林外史",2),
                new Book("隋唐演义",1),
            };
        }

4、程序

cs 复制代码
        public Form1()
        {
            InitializeComponent();
            Data.Books.ForEach(b => { b.EventCompleted += Book_EventCompleted; });
        }

        private void Book_EventCompleted(object sender, Book.BookEventArgs e)
        {
            TxtInfo.Invoke(new Action(() => AppendLine(e.Result)));
        }

        private void AppendLine(string s)
        {
            TxtInfo.AppendText(s + $"{Environment.NewLine}");
            TxtInfo.ScrollToCaret(); TxtInfo.Refresh();
        }

        private async void BtnEvent_Click(object sender, EventArgs e)//事件
        {
            Stopwatch sw = Stopwatch.StartNew();
            TxtInfo.Clear();
            AppendLine("检索开始...");
            List<Task> ts = new List<Task>();
            foreach (var b in Data.Books)
            { ts.Add(Task.Run(b.SearchEvent)); }

            await Task.WhenAll(ts);
            sw.Stop();
            AppendLine($"检索完成:{Convert.ToSingle(sw.ElapsedMilliseconds) / 1000}秒");
        }

        private async void BtnAsync_Click(object sender, EventArgs e)//异步
        {
            Stopwatch sw = Stopwatch.StartNew();
            TxtInfo.Clear();
            AppendLine("异步检索开始...");
            AppendLine($"当前线程Id:{Environment.CurrentManagedThreadId}");
            int idx = 0;
            foreach (var b in Data.Books)
            {
                string t = await Task.Run(b.Search).ConfigureAwait(false);
                AppendLineThread($"{++idx}.{t}--线程Id:{Environment.CurrentManagedThreadId}");
            }
            sw.Stop();
            AppendLineThread($"异步检索完成:{Convert.ToSingle(sw.ElapsedMilliseconds) / 1000}秒");
        }

        private async void BtnCon_Click(object sender, EventArgs e)//并发
        {
            Stopwatch sw = Stopwatch.StartNew();
            TxtInfo.Clear();
            AppendLine("并发检索开始...");
            int idx = 0;
            List<Task<string>> ts = new List<Task<string>>();
            Data.Books.ForEach(b => ts.Add(Task.Run(b.Search)));
            全部完成才返回信息
            //var rs = await Task.WhenAll(ts);//Task.WhenAll(ts)返回的类型是Task<string[]>,但是通过使用await关键字等待任务完成后,会获取到Task<string[]>的结果,即string[]类型的值。
            //foreach (var s in rs)
            //{
            //    AppendLine($"{++idx}.{s}");
            //}
            //完成一个就显示一个,直到所有完成才向下执行
            while (ts.Count > 0)
            {
                Task<string> com = await Task.WhenAny(ts);
                AppendLine($"{++idx}.{com.Result}");
                ts.Remove(com);
            }

            sw.Stop();
            AppendLine($"并发检索完成:{Convert.ToSingle(sw.ElapsedMilliseconds) / 1000}秒");
        }

        private void BtnSync_Click(object sender, EventArgs e)//同步
        {
            Stopwatch sw = Stopwatch.StartNew();
            TxtInfo.Clear();
            AppendLine("同步检索开始...");
            int idx = 0;
            Data.Books.ForEach(book => AppendLine($"{++idx}.{book.Search()}"));
            sw.Stop();
            AppendLine($"同步检索完成:{Convert.ToSingle(sw.ElapsedMilliseconds) / 1000}秒");
        }

        private void BtnConCallback_Click(object sender, EventArgs e)//回调
        {
            Stopwatch sw = Stopwatch.StartNew();
            TxtInfo.Clear();
            AppendLine("异步回调检索开始...");
            int idx = 0;
            foreach (var b in Data.Books)
            {
                Task.Run(b.Search).ContinueWith(t => AppendLineThread($"{++idx}.{t.Result}"));
            }

            sw.Stop();
            AppendLine($"异步检索完成:{Convert.ToSingle(sw.ElapsedMilliseconds) / 1000}秒");
        }

        private void AppendLineThread(string s)
        { this.Invoke(new Action(() => AppendLine(s))); }

二、同步

cs 复制代码
        private void BtnSync_Click(object sender, EventArgs e)//同步
        {
            Stopwatch sw = Stopwatch.StartNew();
            TxtInfo.Clear();
            AppendLine("同步检索开始...");
            int idx = 0;
            Data.Books.ForEach(book => AppendLine($"{++idx}.{book.Search()}"));
            sw.Stop();
            AppendLine($"同步检索完成:{Convert.ToSingle(sw.ElapsedMilliseconds) / 1000}秒");
        }

1、AppendLine主要用于向信息文本框TxtInfo追加信息,方便查看效果。

2、private void BtnSync_Click(object sender, EventArgs e)//同步

Data.Books.ForEach(book => AppendLine($"{++idx}.{book.Search()}"));

foreach的使用原型:

List<T>.ForEach(Action<T>)

后面跟一个带一个参数的无返回值的Action委托。

注意:

foreach 循环在遍历集合时,会创建一个迭代器来依次访问集合中的元素。在 foreach 循环中,不能对原集合的元素进行增加、删除或修改操作。

这是因为 foreach 循环是基于集合的迭代器实现的,它会在每次迭代时返回集合中的下一个元素。如果在循环过程中修改了集合的元素,会导致迭代器的状态不一致,可能会引发异常或产生意外的结果。

另外,对于 List<T>.ForEach(Action<T>) 方法,它接受一个 Action<T> 委托作为参数,用于对集合中的每个元素执行特定的操作。在这个方法中,也不应该对原集合的元素进行修改,因为它会遍历集合并按顺序对每个元素执行操作,如果在操作过程中修改了集合的元素,可能会导致意外的结果。

如果需要对集合进行修改,应该使用其他适当的方法,例如使用 for 循环来遍历集合并对元素进行修改,或者使用 LINQ 查询来筛选出需要修改的元素并进行相应的操作。在这些情况下,需要小心处理索引和集合的状态,以避免出现意外的结果。

3、因为是同步,所以一本书检索完成后,根据foreach再检索下一本书。所以耗时较长。

同时,由于是同步,检索中(Thread.Sleep())会阻塞当前线程,这样当前界面看似"死了",无法拖动,点击等操作。只有所有检索完成后,整个界面才"活"过来了。

三、异步

cs 复制代码
        private async void BtnAsync_Click(object sender, EventArgs e)//异步
        {
            Stopwatch sw = Stopwatch.StartNew();
            TxtInfo.Clear();
            AppendLine("异步检索开始...");
            AppendLine($"当前线程Id:{Environment.CurrentManagedThreadId}");
            int idx = 0;
            foreach (var b in Data.Books)
            {
                string t = await Task.Run(b.Search).ConfigureAwait(false);
                AppendLineThread($"{++idx}.{t}--线程Id:{Environment.CurrentManagedThreadId}");
            }
            sw.Stop();
            AppendLineThread($"异步检索完成:{Convert.ToSingle(sw.ElapsedMilliseconds) / 1000}秒");
        }

1、使用Task.Run(...)表示异步执行括号内语句, 不会等task返回结果到当前线程(UI)会立即向下执行。这样我们可能无法预知结果。

因此,前面使用一个await来阻塞当前线程,以便等待task异步的结果回来,同时又不会让当前UI线程看上去"死了"一样.

2、await/async一般配套使用。

await 是一个用于异步编程的关键字,用于在异步方法中等待一个异步操作的完成。

在异步编程中,有些操作可能会花费较长时间来完成,例如网络请求、文件读写或数据库查询等。为了避免阻塞主线程或当前线程,我们可以使用异步操作来执行这些耗时的任务。

在使用 await 关键字时,它会在遇到异步操作时暂停当前方法的执行,并在等待异步操作完成后继续执行后续的代码。这种暂停和继续执行的机制被称为异步等待。

注意,Task.Run 返回的是一个 Task<T> 对象,而使用 await 关键字后,返回的类型会变成异步操作的结果类型 T。await 关键字的目的是等待异步操作的完成,并返回异步操作的结果。

await 关键字可以与多种异步操作一起使用,包括 Task、Task<T>、ValueTask、ValueTask<T>、TaskCompletionSource 等。它提供了一种简洁和直观的方式来处理异步操作的结果,并使代码更具可读性和可维护性。

注意,虽然 await 关键字会暂停当前方法的执行,但不会阻塞主线程或当前线程。它允许其他代码在等待异步操作完成时继续执行,从而提高程序的并发性和响应性。

3、为什么使用AppendLineThread,而不使用AppendLine呢?

在多线程应用程序中,UI控件通常只能在创建它们的UI线程上进行访问和更新。当我们在非UI线程上尝试直接访问或更新UI控件时,会抛出一个异常,称为"跨线程操作无效"。

为了解决这个问题,我们可以使用Control.BeginInvoke或Control.Invoke方法将UI操作委托到UI线程上执行。这些方法会将操作添加到UI线程的消息队列中,然后由UI线程按顺序处理消息并执行操作。这样可以确保UI操作在UI线程上执行,避免了多线程操作UI控件的冲突。

这相相当于委托UI操作控件。特别注意的是,外线程对UI线程中的控件或窗体是可见的,但对它的属性或方法是不可见的(不可操控的),这就是为什么要委托原因,你可以触碰到大门(UI控件或窗体),但你不能进行大门里去取或放东西(控件的属性或方法),因此你需要委托一个有权限的人(如保安)到室内去拿或放东西(属性或方法)。所以在AppendLine里有控件,所以外线程不能直接操作,需要转个弯,套个保安invoke去作这个事。所以用了AppendLineThread。这个this就是指代的是UI线程里的form1(当然this也是可以省略的,表示同样的意思)

在我们的代码示例中,我们使用了BeginInvoke方法来将UI操作委托到UI线程上执行。这样,当后台任务中的Print方法被调用时,UI操作将会在UI线程上执行,避免了跨线程操作无效的异常。

4、invoke与begininvoke的区别是什么?

Invoke 方法和 BeginInvoke 方法都用于将委托调度到 UI 线程上执行,但它们之间有一些区别。

  1. Invoke 方法:Invoke 方法会将委托的执行请求发送到 UI 线程,并等待直到委托执行完成。在等待期间,调用 Invoke 方法的线程会被阻塞,无法继续执行其他操作。这意味着 Invoke 方法是同步的,它会阻塞调用线程直到 UI 线程执行完成。

  2. BeginInvoke 方法:BeginInvoke 方法也会将委托的执行请求发送到 UI 线程,但它是异步的。BeginInvoke 方法会立即返回,而不会等待委托执行完成。这意味着调用 BeginInvoke 方法的线程可以继续执行其他操作,而不必等待 UI 线程执行完成。

通过 BeginInvoke 方法调度的委托会在 UI 线程上异步执行。当委托执行完成后,可以使用 EndInvoke 方法获取委托的返回值(如果有的话)。

总结来说,Invoke 方法是同步的,会阻塞调用线程直到委托执行完成,而 BeginInvoke 方法是异步的,会立即返回并允许调用线程继续执行其他操作。

下面看一下EndInvoke的效果:

cs 复制代码
            Func<int, int, int> f = (a, b) => { Thread.Sleep(2000); return a + b; };
            IAsyncResult result = f.BeginInvoke(3, 5, null, null);
            int n = f.EndInvoke(result);
            Console.WriteLine(n);
            Console.ReadKey();

(1)IAsyncResult 接口是异步操作的结果,它提供了一种在异步操作完成时获取结果的机制。在 BeginInvoke 方法中,它返回一个实现了 IAsyncResult 接口的对象,该对象可以用于在稍后的时间点调用 EndInvoke 方法来获取委托的返回值。

(2)在 BeginInvoke 方法中,最后两个参数 null, null 分别表示异步回调方法和用户定义的状态对象。这两个参数的作用如下:

异步回调方法:在异步操作完成时,可以通过异步回调方法来处理结果或执行其他操作。这个参数允许您指定一个回调方法,当异步操作完成时,系统会调用该回调方法。上面将其设置为 null,表示不需要使用异步回调方法。 用户定义的状态对象(object):这个参数允许您传递一个自定义的对象,用于在异步操作期间传递一些额外的信息。上面将其设置为 null,表示不需要使用用户定义的状态对象。如果是多参数也可以用这个参数来传递,比如数组,只是进入后再转为数组。这个参数主要是参与到回调函数中,起到补充辅助作用。

3.回调函数的形式是固定的, 并且必须符合:void CallbackMethod(IAsyncResult result)。参数类型必须是IAsyncResult接口或其派生类型。这是因为BeginInvoke方法返回一个IAsyncResult对象,该对象用于跟异步操作的状态。回调函数被调用时,它将接收到该IAsyncResult对象作为参数,以便在回调函数中处理异步操作的结果或状态。

在回调函数内部,您可以使用AsyncState属性来获取传递给BeginInvoke方法的任何自定义参数(如下面的"处理完毕")。这样可以在异步操作完成后,通过回调函数获取传递的参数并进行处理。

注意,回调函数的具体参数形式可能因使用的异步编程模型或委托类型而有所不同。上述提到的委托签名是最常见的形式,但也可以根据具体需求使用其他类型的委托签名。

cs 复制代码
        private static void Main(string[] args)
        {
            Func<int, int, int> f = (a, b) => { Thread.Sleep(2000); return a + b; };
            IAsyncResult result = f.BeginInvoke(5, 10, CallbackMethod, "处理完毕");// 开始异步调用
            Console.WriteLine("前面");
            int n = f.EndInvoke(result); // 等待异步调用完成
            Console.WriteLine("后面");
            Console.WriteLine(n);
            Console.ReadLine();
        }

        private static void CallbackMethod(IAsyncResult result)
        {
            var asyncResult = (AsyncResult)result; // a 异步操作结果的类
            var d = (Func<int, int, int>)asyncResult.AsyncDelegate;//c
            var p1 = result.AsyncState.ToString();// b
            Console.WriteLine($"状态: {p1}");
        }

a处(AsyncResult)result 表示将 result 转换为 AsyncResult 类型,AsyncResult 是一个代表异步操作结果的类。

b处result.AsyncState 表示异步调用时传递的状态对象,即在调用 f.BeginInvoke 方法时传递的 "处理完毕"。

c处(Func<int, int, int>)asyncResult.AsyncDelegate 表示从异步结果中获取原始的委托对象,即 f。

通过 f.BeginInvoke 方法开始异步调用,然后继续执行后面的代码,不会阻塞。而在调用 f.EndInvoke 方法时,会等待异步调用完成才会继续执行后面的代码,所以可以说 f.EndInvoke 是同步等待调用结果的。

注意:

回调函数在异步线程中的执行过程是这样:

(1)首先,异步操作通过 BeginInvoke 方法提交给异步线程执行。

(2)异步线程开始执行异步操作,并在执行完成后通知调用线程。

(3)调用线程收到通知后,会调用回调函数。

(4)回调函数在调用线程中同步执行,可以处理异步操作的结果或执行其他操作。

(5)当回调函数执行完毕后,异步线程才会结束。

回调函数的执行是在调用线程中执行的,而不是在异步线程中执行。异步线程执行完异步操作后,会通知调用线程去执行回调函数。

当回调函数执行完毕时,异步线程可能会继续执行其他任务,或者结束并释放资源,这取决于具体的实现和使用场景。在某些情况下,异步线程可能会等待所有回调函数执行完毕后再结束,以确保所有异步操作都已完成。但也有可能在回调函数执行完毕后立即结束异步线程。

总之,回调函数的执行和异步线程的结束或释放是两个独立的过程,具体的行为取决于实现和使用方式。

注意,尽量避免在UI操作频繁且耗时较长的情况下使用BeginInvoke或Invoke方法,以免影响UI的响应性能。最好的做法是将耗时的操作放在后台线程中执行,并在必要时使用BeginInvoke或Invoke方法将结果更新到UI控件上。

5.invoke,begininvoke与普通的委托有区别吗

Control.BeginInvoke或Control.Invoke方法实际上是一种特殊的委托机制,用于在UI线程上执行操作。它们的使用方式与普通委托类似,但它们的执行方式有一些区别。

通常的委托是在当前线程上同步执行的,即委托被调用时,执行委托所关联的方法。这种同步执行可能会导致阻塞当前线程,直到委托执行完成。

而Control.BeginInvoke或Control.Invoke方法是将委托添加到UI线程的消息队列中,并由UI线程按顺序执行。这种异步执行方式不会阻塞当前线程,而是将委托的执行推迟到UI线程上。

这种异步执行的好处是,在多线程场景下,我们可以在非UI线程上执行操作,并通过Control.BeginInvoke或Control.Invoke方法将结果更新到UI控件上,从而避免了跨线程操作无效的异常。

总之,Control.BeginInvoke或Control.Invoke方法是一种特殊的委托机制,用于在UI线程上执行操作,以确保UI控件的访问和更新在UI线程上进行。

虽然 Invoke 方法是同步执行的,但在 UI 中仍然要按照消息的顺序来执行。当调用 Invoke 方法时,调用线程会被阻塞,直到 UI 线程处理完前面的消息并到达委托的调用请求时才会执行委托。这意味着即使 Invoke 方法是同步的,它仍然需要等待前面的消息处理完毕,按照消息的顺序排队执行。

这种机制确保了在 UI 中的操作按照顺序执行,避免了多个操作之间的竞态条件和不确定性。无论是使用 Invoke 还是 BeginInvoke,都会确保 UI 中的操作按照消息的顺序来执行,以保持 UI 的一致性和可预测性。

使用Invoke或BeginInvoke方法来在UI上进行显示时,延迟是由消息队列中的位置决定的。这是因为在UI线程中执行的操作是顺序排队的,并按照它们添加到消息队列的顺序执行。延迟是由消息队列中的其他操作和UI线程的工作负载决定的。如果您的操作对于实时性非常重要,可能需要考虑使用其他方法来处理UI更新,例如使用Dispatcher类的InvokeAsync方法,该方法提供更灵活的控制,并可以提高响应性能。

四、并发

cs 复制代码
        private async void BtnCon_Click(object sender, EventArgs e)//并发
        {
            Stopwatch sw = Stopwatch.StartNew();
            TxtInfo.Clear();
            AppendLine("并发检索开始...");
            int idx = 0;
            List<Task<string>> ts = new List<Task<string>>();
            Data.Books.ForEach(b => ts.Add(Task.Run(b.Search)));
            全部完成才返回信息
            //var rs = await Task.WhenAll(ts);
            //foreach (var s in rs)
            //{
            //    AppendLine($"{++idx}.{s}");
            //}
            //完成一个就显示一个,直到所有完成才向下执行
            while (ts.Count > 0)
            {
                Task<string> com = await Task.WhenAny(ts);
                AppendLine($"{++idx}.{com.Result}");
                ts.Remove(com);
            }

            sw.Stop();
            AppendLine($"并发检索完成:{Convert.ToSingle(sw.ElapsedMilliseconds) / 1000}秒");
        }

1、Task.Run(b.Search)返回的是Task<stirng>, 把这个类型作为List<T>中的类型,这样通过linq中的foreach就把每个异步执行的任务逐个添加到ts中了。当foreach添加完成后不会等待返回结果,而是直接向下执行,遇到while判断ts是否还有元素(即异步任务),若有遇异步没有完全完成,则进入循环中用whenany(ts)去等待任一一个任务的完成,一旦完成就将这个刚完成的任务进行输出,然后删除这个任务,继续循环判断是否还有异步任务在执行,若没有就退出,若有继续前面等待最先完成的任务返回。

2、task.whenany与task.waitany有什么区别?

答:Task.WhenAny 和 Task.WaitAny 都是用于等待多个任务完成的方法:

Task.WhenAny 是一个异步方法,它接收一个 Task 数组(或可变参数列表)作为参数,并返回一个 Task<Task>,表示已完成的任务中的第一个任务。当任何一个任务完成时,Task.WhenAny 就会返回。

Task.WaitAny 是一个同步方法,它接收一个 Task 数组(或可变参数列表)作为参数,并阻塞当前线程,直到任何一个任务完成。它返回一个表示已完成任务的索引值。

主要区别如下:

异步 vs 同步:Task.WhenAny 是异步方法,不会阻塞当前线程,而 Task.WaitAny 是同步方法,会阻塞当前线程。

返回类型:Task.WhenAny 返回一个 Task<Task>,表示已完成的任务中的第一个任务。Task.WaitAny 返回一个表示已完成任务的索引值。

List<Task<string>> ts = new List<Task<string>>();

Data.Books.ForEach(b => ts.Add(Task.Run(b.Search)));

int completedTaskIndex = Task.WaitAny(ts);//返回ts中的索引,索引指向的Task异步任务已经完成。

上面Task.WhenAll(ts)返回的类型是Task<string[]>,但是通过使用await关键字等待任务完成后,会获取到Task<string[]>的结果,即string[]类型的值。因此 var是string[]

使用方式:Task.WhenAny 通常与异步编程模型(如 async/await)一起使用,以便在多个任务中获取第一个完成的任务。Task.WaitAny 通常在同步代码中使用,以等待多个任务中的任何一个完成。

总之,Task.WhenAny 适用于异步编程模型,而 Task.WaitAny 适用于同步代码。您可以根据自己的需求选择适合的方法。

扩展:

(1)Task.WaitAll()与Task.WaitAny()的区别

Task.WaitAll 方法是一个同步方法,它会阻塞当前线程,直到所有的任务都完成为止。它会等待所有的任务都完成后,才会继续执行后续的代码。

Task.WaitAny 方法也是一个同步方法,它会阻塞当前线程,直到任意一个任务完成为止。它会等待任意一个任务完成后,就会继续执行后续的代码。

(2)Task.WhenAll()与Task.WhenAny()的区别

Task.WhenAll 方法是一个异步方法,它会返回一个 Task 对象,表示当所有的任务都完成时的一个任务。它会等待所有的任务都完成后,才会返回。返回的任务的结果是一个包含所有任务结果的数组。

Task.WhenAny 方法也是一个异步方法,它会返回一个 Task<Task> 对象,表示当任意一个任务完成时的一个任务。它会等待任意一个任务完成后,就会返回。返回的任务的结果是一个已完成的任务对象,它表示第一个完成的任务。

3、task实际上就是操作的是线程池,但是更智能化的, 无需列细节地操作线程池。相当于开汽车时的自动档。

Task提供了一种高级的抽象来处理并发和异步操作,而无需直接操作线程池。Task实际上是对线程池中的线程进行智能管理的一种方式。它允许你以一种更高级的方式编写并发代码,而无需显式地管理线程的创建和调度。

通过使用Task,你可以将工作发给线程池,而不必关心线程的创建和销毁,以及线程的调度和管理。Task会根据系统资源和可用线程池线程的数量来自动决定如何调度和执行任务。

这种自动化的线程管理类似于自动档车辆的运行方式。在自动档车辆中,你只需要踩下油门和刹车,车辆会自动根据当前的速度和负载条件来选择合适的档位。类似地,使用Task 时,你只需定义任务,并让线程池自动选择合适的线程来执行任务。

这种自动化的线程管理使得并发编程更加简单可靠,同时也提高了性能和资源利用率。

五、异步回调

cs 复制代码
        private void BtnConCallback_Click(object sender, EventArgs e)//回调
        {
            Stopwatch sw = Stopwatch.StartNew();
            TxtInfo.Clear();
            AppendLine("异步回调检索开始...");
            int idx = 0;
            foreach (var b in Data.Books)
            {
                Task.Run(b.Search).ContinueWith(t => AppendLineThread($"{++idx}.{t.Result}"));
            }

            sw.Stop();
            AppendLine($"异步检索完成:{Convert.ToSingle(sw.ElapsedMilliseconds) / 1000}秒");
        }    
    

1、上面Task是异步,所以当foreach把所以异步任务添加后 ,不管它们死活,直接执行sw.stop以及的后面,所以看到的结果是先出总时间,后面的异步执行,谁先完成就完成后继任务(continuewith)的委托任务,所以看到逐个后面任务的显示。

2.ContinueWith()介绍

ContinueWith是C#中的一个方法,用于为异步操作指定一个或多个后续任务(Continuations)。它是Task类的一个扩展方法,可以在任务完成后执行指定的操作。

使用ContinueWith方法,您可以在任务(Task)完成后执行一些额外的逻辑,例如处理任务的结果、记录日志、更新用户界面等。下面是ContinueWith方法的一般语法:

task.ContinueWith(Action<Task<TResult> continuationAction);

其中,task是要延续的任务,continuationAction是要执行的后续操作。ContinueWith方法还支持其他重载形式,可以传递附加参数、指定任务的调度器和任务选项等。以下是一些常见的用法示例: (1)基本的后续操作:

cs 复制代码
        Task<int> task = Task.Run(() => {
            // 执行一些操作并返回结果
            return 42;
        });

        task.ContinueWith(t => {
            int result = t.Result;
            Console.WriteLine("Task completed with result: " + result);
        });

(2)指定调度器:

cs 复制代码
        Task<int> task = Task.Run(() => {
            // 执行一些操作并返回结果
            return 42;
        });

        task.ContinueWith(t => {
            int result = t.Result;
            Console.WriteLine("Task completed with result: " + result);
        }, TaskScheduler.FromCurrentSynchronizationContext());

上面后续操作将在当前同步上下文的调度器中执行,通常用于在UI线程中更新用户界面。 (3)多个后续操作:

cs 复制代码
        Task<int> task = Task.Run(() => {
            // 执行一些操作并返回结果
            return 42;
        });

        task.ContinueWith(t => {
            int result = t.Result;
            Console.WriteLine("Task completed with result: " + result);
        });

        task.ContinueWith(t => {
            Console.WriteLine("Task finished.");
        }, TaskContinuationOptions.OnlyOnRanToCompletion);

上面两个后续操作将依次执行。第一个后续操作用于处理任务的结果,第二个后续操作仅在任务成功完成时执行。

注意,ContinueWith方法返回一个新的Task对象,表示后续操作。您可以使用这个返回的Task对象来进行更多的链式操作,例如添加更多的后续任务。

总之,ContinueWith方法是一种强大的工具,可以为任务的完成定义自定义的行为。它使得异步任务的工作流程可以更加灵活和可控。

问:上面(2)中TaskScheduler.FromCurrentSynchronizationContext()是什么意思?

答:TaskScheduler是任务调度器,FromCurrentSynchronizationContext()是当前同步上下文相关.

TaskScheduler.FromCurrentSynchronizationContext() 是一个方法,它可用于创建一个与当前同步上下文相关的任务调度器。为了理解其含义和作用,需要先了解同步上下文和任务调度器的概念。

同步上下文(Synchronization Context)是一个抽象概念,它描述了代码在哪个线程上运行以及如何进行线程间的通信。在UI应用程序中,通常存在一个主要的UI线程,用于处理用户界面的操作。与此相关联的同步上下文负责处理UI事件并确保在UI线程上正确执行相关代码。

任务调度器(Task Scheduler)则用于协调和调度异步任务的执行。它决定了任务在哪个线程上执行,并处理任务的队列、优先级和并发等。

TaskScheduler.FromCurrentSynchronizationContext() 方法的作用是创建一个与当前同步上下文相关联的任务调度器。这使得通过该调度器安排的任务能够在与UI线程关联的上下文中执行,通常用于将操作转移到UI线程上更新用户界面。

例如,当您在后台线程上执行某个任务并获得结果后,如果需要将结果显示在UI界面上,就需要确保UI操作在UI线程上执行。这时,您可以使用 TaskScheduler.FromCurrentSynchronizationContext() 方法来创建一个任务调度器,以便在UI的上下文中执行后续操作。

因为UI线程上才能操作界面控件属性和方法,跨线程是不允许的,用UI相关的线程可以直接操作,这个选项就不需要人为去切换担心跨线程等问题,直接切换到UI相关的上下文中,直接操作。

如果不使用TaskScheduler.FromCurrentSynchronizationContext(),在某些情况下可能会导致后续任务(Continuations)在错误的线程上执行,从而引发异常或不正确的行为。默认情况下,ContinueWith方法使用默认的任务调度器来执行后续任务,该调度器通常是线程池调度器(ThreadPoolScheduler)。

当在UI上进行显示或更新时,通常需要在UI线程上执行相关操作,以避免线程冲突和更新界面的需要。如果您在后续任务中不使用TaskScheduler.FromCurrentSynchronizationContext()或其他适当的任务调度器,后续任务将在默认的线程池线程上执行,而不是在UI线程上执行。

这可能会导致以下问题:

1、跨线程异常:在许多UI框架中,只能在创建它们的线程上访问UI元素。因此,在非UI线程上更新UI元素可能会引发异常,如InvalidOperationException或AccessViolationException。

2、不一致的UI更新:如果后续任务在非UI线程上执行,它们可能会导致UI更新的不一致性。例如,在不同的线程上连续执行UI更新操作可能会导致视觉上的闪烁或不正确的更新顺序。

为了在UI线程上进行显示或更新,您可以使用以下几种方式:

3、使用TaskScheduler.FromCurrentSynchronizationContext():这是最常见和推荐的方式。通过使用TaskScheduler.FromCurrentSynchronizationContext()方法,您可以为ContinueWith方法指定一个与当前同步上下文相关的任务调度器,从而确保后续任务在UI线程上执行。

4、使用UI框架提供的特定方法:许多UI框架(如WPF、Windows Forms和ASP.NET)提供了专门用于在UI线程上执行的方法,如Dispatcher.Invoke(在WPF中)或Control.Invoke(在Windows Forms中)。您可以手动使用这些方法来在UI线程上调度和执行后续操作。

无论使用哪种方法,关键是确保在UI线程上执行UI更新操作,以避免不一致性和跨线程的异常。

需要注意的是,默认的参数为TaskScheduler.Default,它使用线程池调度器来执行后续任务。如果没有明确指定任务调度器,将使用此默认值。这意味着后续任务将在默认的线程池线程上执行,而如果任务涉及UI操作,则可能引发异常或导致不正确的行为。

在对UI进行更新时,确保选择正确的任务调度器是非常重要的,以保持正确的操作顺序和线程上下文。

问:TaskContinuationOptions.OnlyOnRanToCompletion是什么意思?

答:TaskContinuationOptions.OnlyOnRanToCompletion 是一个枚举值,用于指定任务(Task)的延续(continuation)应该仅在原始任务成功完成时执行。

当使用 Task.ContinueWith 方法创建延续任务时,可以通过指定 TaskContinuationOptions.OnlyOnRanToCompletion 选项来控制延续任务的触发时机。

如果将 TaskContinuationOptions.OnlyOnRanToCompletion 选项应用于延续任务,那么延续任务只会在原始任务成功完成(即没有引发异常)时触发执行。如果原始任务失败或被取消,延续任务将被自动跳过,不会执行。

3、ContinueWith(...)执行的顺序与异步线程和调用线程的关系?

答:ContinueWith() 方法是在 Task 对象上调用的,用于创建一个表示在原始任务完成后执行的延续任务。延续任务可以指定在原始任务成功完成、失败或取消时触发执行。

关于 ContinueWith() 方法的执行顺序,有以下几点需要注意:

1、串行执行:延续任务默认是在原始任务完成后立即执行。这意味着在原始任务完成后,延续任务会立即在相同的线程上执行。这是串行执行的情况,延续任务在原始任务完成后立即开始,不需要返回到调用线程。

2、调度器选项:TaskContinuationOptions 枚举提供了几个选项,可以控制延续任务的调度行为。例如,TaskContinuationOptions.RunContinuationsAsynchronously 可以指示延续任务在不同的线程上执行,而不是继续在原始任务的线程上执行。

cs 复制代码
               Task originalTask = Task.Run(() =>;
               {
                   // 原始任务
               });

               Task continuationTask = originalTask.ContinueWith(previousTask =>;
               {
                   // 延续任务
               }, TaskContinuationOptions.RunContinuationsAsynchronously);

通过指定 TaskContinuationOptions.RunContinuationsAsynchronously 选项,延续任务将在不同的线程上执行,而不是在原始任务的线程上执行。这里指明延续任务不在当前异步线程上执行,而是另一个异步线程上执行。

此选项,任务调度器会尽力将延续任务分配给一个新的异步线程,以实现并行执行。这通常涉及使用线程池或其他可用的异步线程资源。

所以,使用 TaskContinuationOptions.RunContinuationsAsynchronously 选项确实意味着延续任务将在一个独立的、新的异步线程上执行。这有助于提高并行性和响应性,允许延续任务独立于原始任务和调用线程执行。

4、我又想回调,又想异步全部完成后才显示最后总时间?

cs 复制代码
        private async void BtnConCallback_Click(object sender, EventArgs e)//回调
        {
            Stopwatch sw = Stopwatch.StartNew();
            TxtInfo.Clear();
            AppendLine("异步回调检索开始...");
            int idx = 0;
            List<Task> ts = new List<Task>();//a
            foreach (var b in Data.Books)
            {
                ts.Add(Task.Run(b.Search).ContinueWith(t => AppendLineThread($"{++idx}.{t.Result}")));//b
            }
            await Task.WhenAll(ts);//c
            sw.Stop();
            AppendLine($"异步检索完成:{Convert.ToSingle(sw.ElapsedMilliseconds) / 1000}秒");
        } 

根据要求,由于Task是异步,不会等待会直接向下执行到总时间,所以必须中异步添加完成之后,总时间之前,即c处位置左右进行等待,这样才会异步结果出来,总时间在最后。

因此在C处用await/async这样即阻塞,又让程序看似是活的。同时whenall()加入异步任务列表ts,表示这些ts完成才向下。

注意:

(1)根据b,Task.Run(b.Search)返回的是一个Task<string>类型,但LIst不能定义为List<Task<string>>。因此此时后面接了ContinuWith(),它的返回结果是Task,因此a处的List应定义为List<Task>。

(2)如果c处修改为await Task.WhenAll();结果是在C处并不会等待,而直接运行到最后一句,然后才是各异步结果出来。原因是Task.WhenAll()没有传递任何任务对象,因此它实际上没有等待任何任务的完成,导致后续代码会立即执行,而不会等待异步任务的结果。所以最后一句"异步检索完成..."会提前显示。

正确的做法是使用await Task.WhenAll(ts);,将包含所有异步任务的列表作为参数传递给Task.WhenAll(),以确保在所有任务完成后再继续执行后续代码。

六、事件

cs 复制代码
        private async void BtnEvent_Click(object sender, EventArgs e)//e事件
        {
            Stopwatch sw = Stopwatch.StartNew();
            TxtInfo.Clear();
            AppendLine("检索开始...");
            List<Task> ts = new List<Task>();
            foreach (var b in Data.Books)
            { ts.Add(Task.Run(b.SearchEvent)); }//d

            await Task.WhenAll(ts);
            sw.Stop();
            AppendLine($"检索完成:{Convert.ToSingle(sw.ElapsedMilliseconds) / 1000}秒");
        }    
        public Form1()
        {
            InitializeComponent();
            Data.Books.ForEach(b => { b.EventCompleted += Book_EventCompleted; });//f
        }        
        private void Book_EventCompleted(object sender, Book.BookEventArgs e)
        {
            TxtInfo.Invoke(new Action(() => AppendLine(e.Result)));
        }
        
        //下面为book类中定义
        public class BookEventArgs : EventArgs
        {
            public BookEventArgs(string name, string result)
            { Name = name; Result = result; }

            public string Name { get; }
            public string Result { get; }
        }

        public event EventHandler<BookEventArgs> EventCompleted;//a    
        public void SearchEvent()//b
        {
            Stopwatch sw = Stopwatch.StartNew();
            Thread.Sleep(Duration * 1000);
            sw.Stop();
            EventCompleted(this, new BookEventArgs(Name, Result(sw.ElapsedMilliseconds)));//c
        }

1、复习事件

发布者(Publisher):Book 类是发布者,它定义了一个名为 EventCompleted 的事件。该事件使用 EventHandler<BookEventArgs> 委托作为事件的类型。

订阅者(Subscriber):Form1 类是订阅者,它订阅了 EventCompleted 事件。具体来说,通过在构造函数中使用 Data.Books.ForEach(b => { b.EventCompleted += Book_EventCompleted; }) 的方式,将 Book_EventCompleted 方法注册为事件处理程序。

订阅(Subscribe):Form1 类的构造函数中的代码 Data.Books.ForEach(b => { b.EventCompleted += Book_EventCompleted; }) 将订阅了 EventCompleted 事件。这意味着当 EventCompleted 事件在 Book 类中被触发时,Book_EventCompleted 方法会被调用。

触发(Trigger):在 SearchEvent 方法中的代码 EventCompleted(this, new BookEventArgs(Name, Result(sw.ElapsedMilliseconds))) 触发了 EventCompleted 事件。这意味着当 SearchEvent 方法被调用时,将会触发该事件,通知所有订阅了该事件的处理程序执行相应的逻辑。

在点击 BtnEvent_Click 事件处理程序时,它会启动一个异步操作,并通过调用 Task.Run(b.SearchEvent) 在线程池中异步执行 SearchEvent 方法。每个 Book 对象都会执行自己的 SearchEvent 方法并触发相应的 EventCompleted 事件。

当 EventCompleted 事件被触发时,订阅了该事件的处理程序 Book_EventCompleted 会被调用。在此处理程序中,使用 TxtInfo.Invoke 方法来确保在 UI 线程上更新界面。这是因为在 WinForms 应用程序中,只能在创建 UI 控件的线程上访问和更新它们。

2、在异步线程中调用事件,执行事件的线程是哪个?

在异步线程中调用事件时,事件的执行方式取决于事件处理程序的实现和事件的关联性。

如果事件处理程序与UI无关,即不涉及UI控件的访问或更新操作,那么事件将在异步线程中直接执行。异步线程不会涉及UI上下文的切换,处理程序将在异步线程完成后立即执行。

然而,如果事件处理程序涉及UI控件的访问或更新操作,就需要考虑UI线程上下文的限制。在这种情况下,当异步线程调用事件时,事件处理程序会要求将UI操作切换到UI线程上执行。

在基于Windows Forms或WPF的应用程序中,可以使用Invoke(WinForms)或Dispatcher.Invoke(WPF)方法将UI操作切换到UI线程上执行。这样做可以确保在UI线程中访问和更新UI控件,从而避免线程安全问题。

一般的流程是,异步线程调用事件后,如果事件处理程序涉及UI操作,它会通过Invoke或Dispatcher.Invoke将相关的UI操作切换到UI线程上执行。一旦UI操作完成,控制权将返回异步线程,继续执行事件后的代码。

注意,为了实现跨线程操作UI,必须确保UI线程正在运行并处理消息循环。如果UI线程被阻塞或不在运行状态,那么异步线程的请求将无法正确处理,可能导致死锁或性能问题。

相关推荐
向宇it6 小时前
【从零开始入门unity游戏开发之——C#篇25】C#面向对象动态多态——virtual、override 和 base 关键字、抽象类和抽象方法
java·开发语言·unity·c#·游戏引擎
向宇it8 小时前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎
阿甘知识库8 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
坐井观老天13 小时前
在C#中使用资源保存图像和文本和其他数据并在运行时加载
开发语言·c#
pchmi15 小时前
C# OpenCV机器视觉:模板匹配
opencv·c#·机器视觉
bufanjun00116 小时前
JUC并发工具---ThreadLocal
java·jvm·面试·并发·并发基础
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭17 小时前
C#都可以找哪些工作?
开发语言·c#
boligongzhu18 小时前
Dalsa线阵CCD相机使用开发手册
c#
向宇it1 天前
【从零开始入门unity游戏开发之——C#篇23】C#面向对象继承——`as`类型转化和`is`类型检查、向上转型和向下转型、里氏替换原则(LSP)
java·开发语言·unity·c#·游戏引擎·里氏替换原则