c# 中的多线程和异步

时间:2023-04-13 08:18:56

前言:

1、异步和多线程有区别吗?

答案:多线程可以说是实现异步的一种方法方法,两者的共同目的:使主线程保持对用户操作的实时响应,如点击、拖拽、输入字符等。使主程序看起来实时都保持着等待用户响应的状态,而后台却有若干件事情在自己干。

2、异步和多线程的两大类

按消耗资源所在地可分为两类:硬件异步类和CPU异步类。

硬件异步类大概有以下这几类。

应用程序范围

支持包含异步方法的 API

Web 访问

HttpClientSyndicationClient

处理文件

StorageFileStreamWriterStreamReaderXmlReader

使用图像处理

MediaCaptureBitmapEncoderBitmapDecoder

WCF 编程

同步和异步操作

与套接字处理

Socket

硬件异步的特点:将需要在后台执行的操作甩给底层硬件去执行,不占用线程和CPU资源。

c# 中的多线程和异步

CPU常用的异步方式、方法

1、独立的线程—ThreadStart

一般情况下,要为不会阻止其他线程的相对较短的任务处理多个线程并且不需要对这些任务执行任何特定调度时,使用 ThreadPool 类是一种最简单的方式。 但是,有多个理由创建您自己的线程:

  • 如果您需要使一个任务具有特定的优先级。

  • 如果您具有可能会长时间运行(并因此阻止其他任务)的任务。

  • 如果您需要将线程放置到单线程单元中(所有 ThreadPool 线程均处于多线程单元中)。

  • 如果您需要与该线程关联的稳定标识。 例如,您应使用一个专用线程来中止该线程,将其挂起或按名称发现它。

  • 如果您需要运行与用户界面交互的后台线程,.NET Framework 2.0 版提供了 BackgroundWorker 组件,该组件可以使用事件与用户界面线程的跨线程封送进行通信。

2、ThreadPool—ThreadPool.QueueUserWorkItem(M())

3、任务,Task系列--普通任务、关联的任务(Task<T>.ContinueWith(…))、父子任务、任务工厂(TaskTactory<TResult>)

4、Parallel静态类--- System.Threading.Tasks.Parallel.For(…) System.Threading.Tasks.Parallel.ForEach(…) Parallel.Invoke(() => Sort());

5、PLINQ

6、定时器

System.Threading.Timer 内部是采用线程池来实现的,在内部,当前线程池会为所有的Timer对象只开辟一个线程去执行注册到timer上的方法。这意味着即使申明再多的Timer并不能保证他们是并行执行的。

System.Windows.Forms.Timer 计数器触发时,windows价格一个计时消息(WM_TIMER)注入调用线程的消息队列,Timer对象消耗的资源全部由调研线程内部完成,计时器的方法不会由多个线程并发执行。

System.Windows.Threading.DispatcherTimer  System.Windows.Forms.Timer在WPF当作的使用

System.Timers.Timer   微软早期的timer控件 建议不要使用

3、区分多线程构成的间隔式死循环和Timer构成的定时触发死循环

我们在使用多线程和Timer往往是希望构建一个不影响UI响应的死循环。注意Timer的特点是定期就会触发下一次方法的执行,不管上一次执行是否执行完毕。而采用一个多线程代码形式构造一个死循环+Thread.Sleep(int)的方式的意思是:在每次执行完毕后间隔固定的时间再执行下一次循环。

4、多线程、异步编程面临的最大问题:状态、结果跟踪(即数据同步问题)

一、实现一个可取消的多线程操作

1.1采用ThreadPool.QueueUserWorkItem+CancellationTokenSource实现

 private void CancelllationToken_Click_1(object sender, RoutedEventArgs e)
 {
     ///定义一个为可取消资源标志
     CancellationTokenSource cts = new CancellationTokenSource();
     ///定义第二个为可取消资源标志
     CancellationTokenSource cts1 = new CancellationTokenSource();

     ///实现一个可取消操作的回调函数,
     ThreadPool.QueueUserWorkItem(o => Count(cts.Token, ));
     ///为可取消资源标志注册取消后的回调函数(无参,无返回值,匿名委托)
     cts.Token.Register(() => Console.WriteLine("Canceled 1"));
     ///为可取消资源标志注册取消后的回调函数(有参,无返回值,显式申明委托)
     cts.Token.Register(o => TestCancellationMethead(, ), true);

     cts1.Token.Register(() => Console.WriteLine("Canceled 2"));
     ///连接两个可取消资源标志
     var LinkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cts1.Token);
     ///给连接后的可取消资源标志集注册回调函数(匿名委托),集合中任意一个可取消资源标志取消,都将触发该回调函数
     LinkedCts.Token.Register(() => Console.WriteLine("linkedCts canceled"));
     Thread.Sleep();
     cts.Cancel();
 }

 void TestCancellationMethead(int x, int y)
 {
     Console.WriteLine(x + y);

 }
 /// <summary>
 /// 一个可取消操作的回调函数,函数的关键是传入一个CancellationToken对象
 /// </summary>
 /// <param name="token"></param>
 /// <param name="countTo"></param>
 void Count(CancellationToken token, Int32 countTo)
 {
     Console.WriteLine("一个可取消的操作开始执行");
     ; count < countTo; count++)
     {
         if (token.IsCancellationRequested)
         {
             Console.WriteLine("一个可取消的操作被取消" + count.ToString());
             break;
         }
         Console.WriteLine(count.ToString());
     }
 }

本实现方案的关键点在于39行token.IsCancellationRequested作为一个标志位对操作(具体到本例代码为循环)进行了终止break。

其实可以把CancellationTokenSource 理解为一个全局变量,只是实现这个全局的方法比较特殊,是通过将CancellationTokenSource 自己本身作为参数传递给可取消操作的委托函数(Count)来实现的,具体代码为9行对应34行。

其实这样的功能完全可以采用传统的、非常简洁的写法,无需申明CancellationTokenSource 这样的大对象,只需额外申明一个bool类型的全局变量即可,如果无需作出<对取消后进行状态跟踪>等复杂操作,采用传统写法才是获取最佳性能的首选。

同等效果的代码如下:

 bool IsCancellationRequested = false;
 private void CancelllationToken_DIY_Click_1(object sender, RoutedEventArgs e)
 {
     ThreadPool.QueueUserWorkItem(o => Count());
     Thread.Sleep();
     IsCancellationRequested = true;
 }
 void Count(Int32 countTo)
 {
     Console.WriteLine("一个可取消的操作开始执行");
     ; count < countTo; count++)
     {
         if (IsCancellationRequested)
         {
             Console.WriteLine("一个可取消的操作被取消" + count.ToString());
             break;
         }
         Console.WriteLine(count.ToString());
     }
 }

1.2采用Task来实现


 /// <summary>
 /// 定义一个多参数的函数,供o =>来调用
 /// </summary>
 /// <param name="countTo"></param>
 /// <param name="y"></param>
 void TeskMetheadMoreInPar(int countTo, int y)
 {
     ; count < countTo; count++)
     {
         if (IsCancellationRequested)
         {
             Console.WriteLine("一个可取消的操作被取消" + count.ToString());
             break;
         }
         Console.WriteLine((y * count).ToString());
     }
 }
 int SumTest(int x, int y)
 {
     x = y = x * y;
     return x * y;
 }
 int TeskMetheadCancellation(int countTo, int y, CancellationToken ct)
 {
     ;
     ; count < countTo; count++)
     {
         ct.ThrowIfCancellationRequested();
         Console.WriteLine((y * count).ToString());
         z += count;
     }
     return z;
 }
 int TeskMetheadCancellation(int countTo, int y)
 {
     ;
     ; count < countTo; count++)
     {

         Console.WriteLine((y * count).ToString());
         z += count;
     }
     return z;
 }

 private void NormalTesk_Click(object sender, RoutedEventArgs e)
 {

     #region 一个普通任务
     Task<, ));
     t.Start();
     MessageBox.Show(t.Result.ToString());
     #endregion

     #region 使用全局变量终止普通任务
     , ), null).Start();
     Thread.Sleep();
     IsCancellationRequested = true;
     #endregion

     #region 支持终止、取消的任务
     CancellationTokenSource cts = new CancellationTokenSource();
     Task<, , cts.Token), cts.Token);
     cts.Token.Register(() =>
     {
         try
         {
             MessageBox.Show(t1.Result.ToString());
         }
         catch
         {
             MessageBox.Show("调用一个终止操作的结果,会诱发一个异常!");
         }
     });
     t1.Start();
     Thread.Sleep();
     cts.Cancel();
     #endregion

     #region 两个衔接的任务
     Task<, ));
     t2.Start();
     Task t3 = t2.ContinueWith(task => MessageBox.Show(t2.Result.ToString()), TaskContinuationOptions.OnlyOnRanToCompletion);
     #endregion

 }

 

二、针对I/O底层硬件的异步