C# 多线程详细讲解

C#中的多线程(一) CPU(中央处理器)或内核/核心是实际执行程序的硬件单元。许多现代CPU都支持同时多线程(Intel称之为超线程),即使一个CPU能表现为多个「虚拟」CPU。 进程(process)是某个程序当前正在执行的实例。操作系统的一项基本功能就是管理进程。每个进程都包含一个或多个线程。 线程(thread)是操作系统能够进行运算调度的最小单位,也是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流。 单线程程序的进程仅包含一个线程;多线程程序则包含多个。 在多线程程序中运行具有正确行为的代码,就说代码是 阅读详情

C# 多线程详细讲解

一、基本概念

1、进程

首先打开任务管理器,查看当前运行的进程:

从任务管理器里面可以看到当前所有正在运行的进程。那么究竟什么是进程呢?

进程(Process)是Windows系统中的一个基本概念,它包含着一个运行程序所需要的资源。一个正在运行的应用程序在操作系统中被视为一个进程,进程可以包括一个或多个线程。

线程是操作系统分配处理器时间的基本单元,在进程中可以有多个线程同时执行代码。进程之间是相对独立的,一个进程无法访问另一个进程的数据(除非利用分布式计算方式),

一个进程运行的失败也不会影响其他进程的运行,Windows系统就是利用进程把工作划分为多个独立的区域的。进程可以理解为一个程序的基本边界。是应用程序的一个运行例程,

是应用程序的一次动态执行过程。

二、线程

在任务管理器里面查询当前总共运行的线程数:

线程(Thread)是进程中的基本执行单元,是操作系统分配CPU时间的基本单位,一个进程可以包含若干个线程,在进程入口执行的第一个线程被视为这个进程的主线程。

在.NET应用程序中,都是以Main()方法作为入口的,当调用此方法时系统就会自动创建一个主线程。线程主要是由CPU寄存器、调用栈和线程本地存储器(Thread Local Storage,TLS)

组成的。CPU寄存器主要记录当前所执行线程的状态,调用栈主要用于维护线程所调用到的内存与数据,TLS主要用于存放线程的状态信息。

二、多线程

多线程的优点:可以同时完成多个任务;可以使程序的响应速度更快;可以让占用大量处理时间的任务或当前没有进行处理的任务定期将处理时间让给别的任务;可以随时停止任务;

可以设置每个任务的优先级以优化程序性能。那么可能有人会问:为什么可以多线程执行呢?总结起来有下面两方面的原因:

1、CPU运行速度太快,硬件处理速度跟不上,所以操作系统进行分时间片管理。这样,从宏观角度来说是多线程并发的,因为CPU速度太快,察觉不到,看起来是同一时刻执行了不同的操作。

但是从微观角度来讲,同一时刻只能有一个线程在处理。

2、目前电脑都是多核多CPU的,一个CPU在同一时刻只能运行一个线程,但是多个CPU在同一时刻就可以运行多个线程。

然而,多线程虽然有很多优点,但是也必须认识到多线程可能存在影响系统性能的不利方面,才能正确使用线程。不利方面主要有如下几点:

(1)线程也是程序,所以线程需要占用内存,线程越多,占用内存也越多。

(2)多线程需要协调和管理,所以需要占用CPU时间以便跟踪线程。

(3)线程之间对共享资源的访问会相互影响,必须解决争用共享资源的问题。

(4)线程太多会导致控制太复杂,最终可能造成很多程序缺陷。

当启动一个可执行程序时,将创建一个主线程。在默认的情况下,C#程序具有一个线程,此线程执行程序中以Main方法开始和结束的代码,Main()方法直接或间接执行的每一个命令都有

默认线程(主线程)执行,当Main()方法返回时此线程也将终止。一个进程可以创建一个或多个线程以执行与该进程关联的部分程序代码。在C#中,线程是使用Thread类处理的,

该类在System.Threading命名空间中。使用Thread类创建线程时,只需要提供线程入口,线程入口告诉程序让这个线程做什么。通过实例化一个Thread类的对象就可以创建一个线程。

创建新的Thread对象时,将创建新的托管线程。Thread类接收一个ThreadStart委托或ParameterizedThreadStart委托的构造函数,该委托包装了调用Start方法时由新线程调用的方法,示例代码如下:

Thread thread=new Thread(new ThreadStart(method));//创建线程

thread.Start();                                                           //启动线程

上面代码实例化了一个Thread对象,并指明将要调用的方法method(),然后启动线程。ThreadStart委托中作为参数的方法不需要参数,并且没有返回值。ParameterizedThreadStart委托一个对象作为参数,

利用这个参数可以很方便地向线程传递参数,示例代码如下:

Thread thread=new Thread(new ParameterizedThreadStart(method));//创建线程

thread.Start(3);                                                                             //启动线程

创建多线程的步骤:
1、编写线程所要执行的方法
2、实例化Thread类,并传入一个指向线程所要执行方法的委托。(这时线程已经产生,但还没有运行)
3、调用Thread实例的Start方法,标记该线程可以被CPU执行了,但具体执行时间由CPU决定

2.1 System.Threading.Thread类

Thread类是是控制线程的基础类,位于System.Threading命名空间下,具有4个重载的构造函数:

名称说明
Thread(ParameterizedThreadStart)

初始化 Thread 类的新实例,指定允许对象在线程启动时传递给线程的委托。要执行的方法是有参的。

Thread(ParameterizedThreadStart, Int32)初始化 Thread 类的新实例,指定允许对象在线程启动时传递给线程的委托,并指定线程的最大堆栈大小
Thread(ThreadStart)

初始化 Thread 类的新实例。要执行的方法是无参的。

Thread(ThreadStart, Int32)

初始化 Thread 类的新实例,指定线程的最大堆栈大小。

ThreadStart是一个无参的、返回值为void的委托。委托定义如下:

public delegate void ThreadStart()

通过ThreadStart委托创建并运行一个线程:

运行结果

除了可以运行静态的方法,还可以运行实例方法

 1  class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             //创建ThreadTest类的一个实例
 6             ThreadTest test=new ThreadTest();
 7             //调用test实例的MyThread方法
 8             Thread thread = new Thread(new ThreadStart(test.MyThread));
 9             //启动线程
10             thread.Start();
11             Console.ReadKey();
12         }
13     }
14 
15     class ThreadTest
16     {
17         public void MyThread()
18         {
19             Console.WriteLine("这是一个实例方法");
20         }
21     }

运行结果:

如果为了简单,也可以通过匿名委托或Lambda表达式来为Thread的构造方法赋值

 1  static void Main(string[] args)
 2  {
 3        //通过匿名委托创建
 4        Thread thread1 = new Thread(delegate() { Console.WriteLine("我是通过匿名委托创建的线程"); });
 5        thread1.Start();
 6        //通过Lambda表达式创建
 7        Thread thread2 = new Thread(() => Console.WriteLine("我是通过Lambda表达式创建的委托"));
 8        thread2.Start();
 9        Console.ReadKey();
10  }

 运行结果:

ParameterizedThreadStart是一个有参的、返回值为void的委托,定义如下:

public delegate void ParameterizedThreadStart(Object obj)

 1  class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             //通过ParameterizedThreadStart创建线程
 6             Thread thread = new Thread(new ParameterizedThreadStart(Thread1));
 7             //给方法传值
 8             thread.Start("这是一个有参数的委托");
 9             Console.ReadKey();
10         }
11 
12         /// <summary>
13         /// 创建有参的方法
14         /// 注意:方法里面的参数类型必须是Object类型
15         /// </summary>
16         /// <param name="obj"></param>
17         static void Thread1(object obj)
18         {
19             Console.WriteLine(obj);
20         }
21     }

注意:ParameterizedThreadStart委托的参数类型必须是Object的。如果使用的是不带参数的委托,不能使用带参数的Start方法运行线程,否则系统会抛出异常。但使用带参数的委托,可以使用thread.Start()来运行线程,这时所传递的参数值为null。

2.2 线程的常用属性

属性名称说明
CurrentContext获取线程正在其中执行的当前上下文。
CurrentThread获取当前正在运行的线程。
ExecutionContext获取一个 ExecutionContext 对象,该对象包含有关当前线程的各种上下文的信息。
IsAlive获取一个值,该值指示当前线程的执行状态。
IsBackground获取或设置一个值,该值指示某个线程是否为后台线程。
IsThreadPoolThread获取一个值,该值指示线程是否属于托管线程池。
ManagedThreadId获取当前托管线程的唯一标识符。
Name获取或设置线程的名称。
Priority获取或设置一个值,该值指示线程的调度优先级。
ThreadState获取一个值,该值包含当前线程的状态。

2.2.1 线程的标识符

ManagedThreadId是确认线程的唯一标识符,程序在大部分情况下都是通过Thread.ManagedThreadId来辨别线程的。而Name是一个可变值,在默认时候,Name为一个空值 Null,

开发人员可以通过程序设置线程的名称,但这只是一个辅助功能。

 

2.2.2 线程的优先级别

当线程之间争夺CPU时间时,CPU按照线程的优先级给予服务。高优先级的线程可以完全阻止低优先级的线程执行。.NET为线程设置了Priority属性来定义线程执行的优先级别,

里面包含5个选项,其中Normal是默认值。除非系统有特殊要求,否则不应该随便设置线程的优先级别。

成员名称说明
Lowest可以将 Thread 安排在具有任何其他优先级的线程之后。
BelowNormal可以将 Thread 安排在具有 Normal 优先级的线程之后,在具有 Lowest 优先级的线程之前。
Normal默认选择。可以将 Thread 安排在具有 AboveNormal 优先级的线程之后,在具有 BelowNormal 优先级的线程之前。
AboveNormal可以将 Thread 安排在具有 Highest 优先级的线程之后,在具有 Normal 优先级的线程之前。
Highest可以将 Thread 安排在具有任何其他优先级的线程之前。

 

2.2.3 线程的状态

通过ThreadState可以检测线程是处于Unstarted、Sleeping、Running 等等状态,它比 IsAlive 属性能提供更多的特定信息。

前面说过,一个应用程序域中可能包括多个上下文,而通过CurrentContext可以获取线程当前的上下文。

CurrentThread是最常用的一个属性,它是用于获取当前运行的线程。

 

2.2.4 System.Threading.Thread的方法

Thread 中包括了多个方法来控制线程的创建、挂起、停止、销毁,以后来的例子中会经常使用。

方法名称说明
Abort()    终止本线程。
GetDomain()返回当前线程正在其中运行的当前域。
GetDomainId()返回当前线程正在其中运行的当前域Id。
Interrupt()中断处于 WaitSleepJoin 线程状态的线程。
Join()已重载。 阻塞调用线程,直到某个线程终止时为止。
Resume()继续运行已挂起的线程。
Start()  执行本线程。
Suspend()挂起当前线程,如果当前线程已属于挂起状态则此不起作用
Sleep()  把正在运行的线程挂起一段时间。

线程示例

 

 1     static void Main(string[] args)
 2         {
 3             //获取正在运行的线程
 4             Thread thread = Thread.CurrentThread;
 5             //设置线程的名字
 6             thread.Name = "主线程";
 7             //获取当前线程的唯一标识符
 8             int id = thread.ManagedThreadId;
 9             //获取当前线程的状态
10             ThreadState state= thread.ThreadState;
11             //获取当前线程的优先级
12             ThreadPriority priority= thread.Priority;
13             string strMsg = string.Format("Thread ID:{0}\n" + "Thread Name:{1}\n" +
14                 "Thread State:{2}\n" + "Thread Priority:{3}\n", id, thread.Name,
15                 state, priority);
16 
17             Console.WriteLine(strMsg);
18                       
19             Console.ReadKey();
20         }

运行结果:

2.3 前台线程和后台线程

前台线程:只有所有的前台线程都结束,应用程序才能结束。默认情况下创建的线程
              都是前台线程
后台线程:只要所有的前台线程结束,后台线程自动结束。通过Thread.IsBackground设置后台线程。必须在调用Start方法之前设置线程的类型,否则一旦线程运行,将无法改变其类型。

通过BeginXXX方法运行的线程都是后台线程。

 1 class Program
 2     {
 3         static void Main(string[] args)
 4         {                   
 5             //演示前台、后台线程
 6             BackGroundTest background = new BackGroundTest(10);
 7             //创建前台线程
 8             Thread fThread = new Thread(new ThreadStart(background.RunLoop));
 9             //给线程命名
10             fThread.Name = "前台线程";
11             
12 
13             BackGroundTest background1 = new BackGroundTest(20);
14             //创建后台线程
15             Thread bThread = new Thread(new ThreadStart(background1.RunLoop));
16             bThread.Name = "后台线程";
17             //设置为后台线程
18             bThread.IsBackground = true;
19 
20             //启动线程
21             fThread.Start();
22             bThread.Start();
23         }
24     }
25 
26     class BackGroundTest
27     {
28         private int Count;
29         public BackGroundTest(int count)
30         {
31             this.Count = count;
32         }
33         public void RunLoop()
34         {
35             //获取当前线程的名称
36             string threadName = Thread.CurrentThread.Name;
37             for (int i = 0; i < Count; i++)
38             {
39                 Console.WriteLine("{0}计数:{1}",threadName,i.ToString());
40                 //线程休眠500毫秒
41                 Thread.Sleep(1000);
42             }
43             Console.WriteLine("{0}完成计数",threadName);
44             
45         }
46     }

 

 

运行结果:前台线程执行完,后台线程未执行完,程序自动结束。

把bThread.IsBackground = true注释掉,运行结果:主线程执行完毕后(Main函数),程序并未结束,而是要等所有的前台线程结束以后才会结束。

后台线程一般用于处理不重要的事情,应用程序结束时,后台线程是否执行完成对整个应用程序没有影响。如果要执行的事情很重要,需要将线程设置为前台线程。

2.4 线程同步

所谓同步:是指在某一时刻只有一个线程可以访问变量。
如果不能确保对变量的访问是同步的,就会产生错误。
c#为同步访问变量提供了一个非常简单的方式,即使用c#语言的关键字Lock,它可以把一段代码定义为互斥段,互斥段在一个时刻内只允许一个线程进入执行,

而其他线程必须等待。在c#中,关键字Lock定义如下:
Lock(expression)
{
   statement_block
}

expression代表你希望跟踪的对象:
           如果你想保护一个类的实例,一般地,你可以使用this;
           如果你想保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了
而statement_block就算互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。

以书店卖书为例

 1 class Program
 2     {
 3         static void Main(string[] args)
 4         {                   
 5             BookShop book = new BookShop();
 6             //创建两个线程同时访问Sale方法
 7             Thread t1 = new Thread(new ThreadStart(book.Sale));
 8             Thread t2 = new Thread(new ThreadStart(book.Sale));
 9             //启动线程
10             t1.Start();
11             t2.Start();
12             Console.ReadKey();
13         }
14     }
15 
16    
17 
18     class BookShop
19     {
20         //剩余图书数量
21         public int num = 1;
22         public void Sale()
23         {
24             int tmp = num;
25             if (tmp > 0)//判断是否有书,如果有就可以卖
26             {
27                 Thread.Sleep(1000);
28                 num -= 1;
29                 Console.WriteLine("售出一本图书,还剩余{0}本", num);
30             }
31             else
32             {
33                 Console.WriteLine("没有了");
34             }
35         }
36     }

 

运行结果:

从运行结果可以看出,两个线程同步访问共享资源,没有考虑同步的问题,结果不正确。

考虑线程同步,改进后的代码:

 

 1 class Program
 2     {
 3         static void Main(string[] args)
 4         {                   
 5             BookShop book = new BookShop();
 6             //创建两个线程同时访问Sale方法
 7             Thread t1 = new Thread(new ThreadStart(book.Sale));
 8             Thread t2 = new Thread(new ThreadStart(book.Sale));
 9             //启动线程
10             t1.Start();
11             t2.Start();
12             Console.ReadKey();
13         }
14     }
15 
16    
17 
18     class BookShop
19     {
20         //剩余图书数量
21         public int num = 1;
22         public void Sale()
23         {
24             //使用lock关键字解决线程同步问题
25             lock (this)
26             {
27                 int tmp = num;
28                 if (tmp > 0)//判断是否有书,如果有就可以卖
29                 {
30                     Thread.Sleep(1000);
31                     num -= 1;
32                     Console.WriteLine("售出一本图书,还剩余{0}本", num);
33                 }
34                 else
35                 {
36                     Console.WriteLine("没有了");
37                 }
38             }
39         }
40     }

运行结果:

2.5 跨线程访问

点击“测试”,创建一个线程,从0循环到10000给文本框赋值,代码如下:

 1  private void btn_Test_Click(object sender, EventArgs e)
 2         {
 3             //创建一个线程去执行这个方法:创建的线程默认是前台线程
 4             Thread thread = new Thread(new ThreadStart(Test));
 5             //Start方法标记这个线程就绪了,可以随时被执行,具体什么时候执行这个线程,由CPU决定
 6             //将线程设置为后台线程
 7             thread.IsBackground = true;
 8             thread.Start();
 9         }
10 
11         private void Test()
12         {
13             for (int i = 0; i < 10000; i++)
14             {               
15                 this.textBox1.Text = i.ToString();
16             }
17         }

运行结果:

产生错误的原因:textBox1是由主线程创建的,thread线程是另外创建的一个线程,在.NET上执行的是托管代码,C#强制要求这些代码必须是线程安全的,即不允许跨线程访问Windows窗体的控件。

解决方案:

1、在窗体的加载事件中,将C#内置控件(Control)类的CheckForIllegalCrossThreadCalls属性设置为false,屏蔽掉C#编译器对跨线程调用的检查。

 private void Form1_Load(object sender, EventArgs e)
 {
        //取消跨线程的访问
        Control.CheckForIllegalCrossThreadCalls = false;
 }

使用上述的方法虽然可以保证程序正常运行并实现应用的功能,但是在实际的软件开发中,做如此设置是不安全的(不符合.NET的安全规范),在产品软件的开发中,此类情况是不允许的。

如果要在遵守.NET安全标准的前提下,实现从一个线程成功地访问另一个线程创建的空间,要使用C#的方法回调机制。

2、使用回调函数

回调实现的一般过程:

 C#的方法回调机制,也是建立在委托基础上的,下面给出它的典型实现过程。

(1)、定义、声明回调。

1 //定义回调
2 private delegate void DoSomeCallBack(Type para);
3 //声明回调
4 DoSomeCallBack doSomaCallBack;

可以看出,这里定义声明的“回调”(doSomaCallBack)其实就是一个委托。

(2)、初始化回调方法。

doSomeCallBack=new DoSomeCallBack(DoSomeMethod);

所谓“初始化回调方法”实际上就是实例化刚刚定义了的委托,这里作为参数的DoSomeMethod称为“回调方法”,它封装了对另一个线程中目标对象(窗体控件或其他类)的操作代码。

(3)、触发对象动作

Opt  obj.Invoke(doSomeCallBack,arg);

其中Opt obj为目标操作对象,在此假设它是某控件,故调用其Invoke方法。Invoke方法签名为:

object  Control.Invoke(Delegate  method,params  object[] args);

它的第一个参数为委托类型,可见“触发对象动作”的本质,就是把委托doSomeCallBack作为参数传递给控件的Invoke方法,这与委托的使用方式是一模一样的。

最终作用于对象Opt obj的代码是置于回调方法体DoSomeMethod()中的,如下所示:

private void DoSomeMethod(type para)

{

     //方法体

    Opt obj.someMethod(para);

}

如果不用回调,而是直接在程序中使用“Opt obj.someMethod(para);”,则当对象Opt obj不在本线程(跨线程访问)时就会发生上面所示的错误。

从以上回调实现的一般过程可知:C#的回调机制,实质上是委托的一种应用。在C#网络编程中,回调的应用是非常普遍的,有了方法回调,就可以在.NET上写出线程安全的代码了。

使用方法回调,实现给文本框赋值:

 1 namespace MultiThreadDemo
 2 {
 3     public partial class Form1 : Form
 4     {
 5         public Form1()
 6         {
 7             InitializeComponent();
 8         }
 9 
10         //定义回调
11         private delegate void setTextValueCallBack(int value);
12         //声明回调
13         private setTextValueCallBack setCallBack;
14 
15         private void btn_Test_Click(object sender, EventArgs e)
16         {
17             //实例化回调
18             setCallBack = new setTextValueCallBack(SetValue);
19             //创建一个线程去执行这个方法:创建的线程默认是前台线程
20             Thread thread = new Thread(new ThreadStart(Test));
21             //Start方法标记这个线程就绪了,可以随时被执行,具体什么时候执行这个线程,由CPU决定
22             //将线程设置为后台线程
23             thread.IsBackground = true;
24             thread.Start();
25         }
26 
27         private void Test()
28         {
29             for (int i = 0; i < 10000; i++)
30             {               
31                 //使用回调
32                 textBox1.Invoke(setCallBack, i);
33             }
34         }
35 
36         /// <summary>
37         /// 定义回调使用的方法
38         /// </summary>
39         /// <param name="value"></param>
40         private void SetValue(int value)
41         {
42             this.textBox1.Text = value.ToString();
43         }
44     }
45 }

 2.6 终止线程

若想终止正在运行的线程,可以使用Abort()方法。

三、同步和异步

同步和异步是对方法执行顺序的描述。

同步:等待上一行完成计算之后,才会进入下一行。

例如:请同事吃饭,同事说很忙,然后就等着同事忙完,然后一起去吃饭。

异步:不会等待方法的完成,会直接进入下一行,是非阻塞的。

例如:请同事吃饭,同事说很忙,那同事先忙,自己去吃饭,同事忙完了他自己去吃饭。

下面通过一个例子讲解同步和异步的区别

1、新建一个winform程序,上面有两个按钮,一个同步方法、一个异步方法,在属性里面把输出类型改成控制台应用程序,这样可以看到输出结果,代码如下:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.ComponentModel;
 4 using System.Data;
 5 using System.Drawing;
 6 using System.Linq;
 7 using System.Text;
 8 using System.Threading;
 9 using System.Threading.Tasks;
10 using System.Windows.Forms;
11 
12 namespace MyAsyncThreadDemo
13 {
14     public partial class Form1 : Form
15     {
16         public Form1()
17         {
18             InitializeComponent();
19         }
20 
21         /// <summary>
22         /// 异步方法
23         /// </summary>
24         /// <param name="sender"></param>
25         /// <param name="e"></param>
26         private void btnAsync_Click(object sender, EventArgs e)
27         {
28             Console.WriteLine($"***************btnAsync_Click Start {Thread.CurrentThread.ManagedThreadId}");
29             Action<string> action = this.DoSomethingLong;
30             // 调用委托(同步调用)
31             action.Invoke("btnAsync_Click_1");
32             // 异步调用委托
33             action.BeginInvoke("btnAsync_Click_2",null,null);
34             Console.WriteLine($"***************btnAsync_Click End    {Thread.CurrentThread.ManagedThreadId}");
35         }
36 
37         /// <summary>
38         /// 同步方法
39         /// </summary>
40         /// <param name="sender"></param>
41         /// <param name="e"></param>
42         private void btnSync_Click(object sender, EventArgs e)
43         {
44             Console.WriteLine($"****************btnSync_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
45             int j = 3;
46             int k = 5;
47             int m = j + k;
48             for (int i = 0; i < 5; i++)
49             {
50                 string name = string.Format($"btnSync_Click_{i}");
51                 this.DoSomethingLong(name);
52             }
53         }
54 
55 
56         private void DoSomethingLong(string name)
57         {
58             Console.WriteLine($"****************DoSomethingLong {name} Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
59             long lResult = 0;
60             for (int i = 0; i < 1000000000; i++)
61             {
62                 lResult += i;
63             }
64             Console.WriteLine($"****************DoSomethingLong {name}   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***************");
65         }
66     }
67 }

 2、启动程序,点击同步,结果如下:

从上面的截图中能够很清晰的看出:同步方法是等待上一行代码执行完毕之后才会执行下一行代码。

点击异步,结果如下:

从上面的截图中看出:当执行到action.BeginInvoke("btnAsync_Click_2",null,null);这句代码的时候,程序并没有等待这段代码执行完就执行了下面的End,没有阻塞程序的执行。

在刚才的测试中,如果点击同步,这时winform界面不能拖到,界面卡住了,是因为主线程(即UI线程)在忙于计算。

点击异步的时候,界面不会卡住,这是因为主线程已经结束,计算任务交给子线程去做。

在仔细检查上面两个截图,可以看出异步的执行速度比同步执行速度要快。同步方法执行完将近16秒,异步方法执行完将近6秒。

在看下面的一个例子,修改异步的方法,也和同步方法一样执行循环,修改后的代码如下:

 1 private void btnAsync_Click(object sender, EventArgs e)
 2 {
 3       Console.WriteLine($"***************btnAsync_Click Start 
         {Thread.CurrentThread.ManagedThreadId}");
 4       //Action<string> action = this.DoSomethingLong;
 5       //// 调用委托(同步调用)
 6       //action.Invoke("btnAsync_Click_1");
 7       //// 异步调用委托
 8       //action.BeginInvoke("btnAsync_Click_2",null,null);
 9       Action<string> action = this.DoSomethingLong;
10       for (int i = 0; i < 5; i++)
11       {
12            //Thread.Sleep(5);
13            string name = string.Format($"btnAsync_Click_{i}");
14            action.BeginInvoke(name, null, null);
15       }
16       Console.WriteLine($"***************btnAsync_Click End        
        {Thread.CurrentThread.ManagedThreadId}");
17 }

 结果如下:

从截图中能够看出:同步方法执行是有序的,异步方法执行是无序的。异步方法无序包括启动无序和结束无序。启动无序是因为同一时刻向操作系统申请线程,操作系统收到申请以后,

返回执行的顺序是无序的,所以启动是无序的。结束无序是因为虽然线程执行的是同样的操作,但是每个线程的耗时是不同的,所以结束的时候不一定是先启动的线程就先结束。

从上面同步方法中可以清晰的看出:btnSync_Click_0执行时间耗时不到3秒,而btnSync_Click_1执行时间耗时超过了3秒。可以想象体育比赛中的跑步,每位运动员听到发令枪起跑的顺序不同,

每位运动员花费的时间不同,最终到达终点的顺序也不同。

总结一下同步方法和异步方法的区别:

1、同步方法由于主线程忙于计算,所以会卡住界面。

      异步方法由于主线程执行完了,其他计算任务交给子线程去执行,所以不会卡住界面,用户体验性好。

2、同步方法由于只有一个线程在计算,所以执行速度慢。

      异步方法由多个线程并发运算,所以执行速度快,但并不是线性增长的(资源可能不够)。多线程也不是越多越好,只有多个独立的任务同时运行,才能加快速度。

3、同步方法是有序的。

      异步多线程是无序的:启动无序,执行时间不确定,所以结束也是无序的。一定不要通过等待几毫秒的形式来控制线程启动/执行时间/结束。

四、回调

先来看看异步多线程无序的例子:

在界面上新增一个按钮,实现代码如下:

1 private void btnAsyncAdvanced_Click(object sender, EventArgs e)
2 {
3    Console.WriteLine($"****************btnAsyncAdvanced_Click Start 
     {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM- 
     dd HH:mm:ss.fff")}***************");
4    Action<string> action = this.DoSomethingLong;
5    action.BeginInvoke("btnAsyncAdvanced_Click", null, null);
6     // 需求:异步多线程执行完之后再打印出下面这句
7    Console.WriteLine($"到这里计算已经完成了。 
     {Thread.CurrentThread.ManagedThreadId.ToString("00")}。");
8    Console.WriteLine($"****************btnAsyncAdvanced_Click End 
     {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM- 
     dd HH:mm:ss.fff")}***************");
9 }

运行结果:

从上面的截图中看出,最终的效果并不是我们想要的效果,而且打印输出的还是主线程。

既然异步多线程是无序的,那我们有没有什么办法可以解决无序的问题呢?办法当然是有的,那就是使用回调,.NET框架已经帮我们实现了回调:

BeginInvoke的第二个参数就是一个回调,那么AsyncCallback究竟是什么呢?F12查看AsyncCallback的定义:

发现AsyncCallback就是一个委托,参数类型是IAsyncResult,明白了AsyncCallback是什么以后,将上面的代码进行如下的改造:

 1 private void btnAsyncAdvanced_Click(object sender, EventArgs e)
 2 {       
 3     Console.WriteLine($"****************btnAsyncAdvanced_Click Start 
       {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy- 
       MM-dd HH:mm:ss.fff")}***************");
 4     Action<string> action = this.DoSomethingLong;
 5     // 定义一个回调
 6     AsyncCallback callback = p => 
 7     {
 8        Console.WriteLine($"到这里计算已经完成了。 
       {Thread.CurrentThread.ManagedThreadId.ToString("00")}。");
 9     };
10     // 回调作为参数
11     action.BeginInvoke("btnAsyncAdvanced_Click", callback, null);          
12     Console.WriteLine($"****************btnAsyncAdvanced_Click End 
       {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy- 
       MM-dd HH:mm:ss.fff")}***************");
13  }

运行结果:

上面的截图中可以看出,这就是我们想要的效果,而且打印是子线程输出的,但是程序究竟是怎么实现的呢?我们可以进行如下的猜想:

程序执行到BeginInvoke的时候,会申请一个基于线程池的线程,这个线程会完成委托的执行(在这里就是执行DoSomethingLong()方法),在委托执行完以后,

这个线程又会去执行callback回调的委托,执行callback委托需要一个IAsyncResult类型的参数,这个IAsyncResult类型的参数是如何来的呢?鼠标右键放到BeginInvoke上面,查看返回值:

发现BeginInvoke的返回值就是IAsyncResult类型的。那么这个返回值是不是就是callback委托的参数呢?将代码进行如下的修改:

 1 private void btnAsyncAdvanced_Click(object sender, EventArgs e)
 2 {
 3     // 需求:异步多线程执行完之后再打印出下面这句
 4     Console.WriteLine($"****************btnAsyncAdvanced_Click Start 
       {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy- 
       MM-dd HH:mm:ss.fff")}***************");
 5     Action<string> action = this.DoSomethingLong;
 6     // 无序的
 7     //action.BeginInvoke("btnAsyncAdvanced_Click", null, null);
 8 
 9     IAsyncResult asyncResult = null;
10     // 定义一个回调
11     AsyncCallback callback = p =>
12     {
13        // 比较两个变量是否是同一个
14         Console.WriteLine(object.ReferenceEquals(p,asyncResult));
15         Console.WriteLine($"到这里计算已经完成了。 
           {Thread.CurrentThread.ManagedThreadId.ToString("00")}。");
16             };
17        // 回调作为参数
18         asyncResult= action.BeginInvoke("btnAsyncAdvanced_Click", callback, null);           
19        Console.WriteLine($"****************btnAsyncAdvanced_Click End 
          {Thread.CurrentThread.ManagedThreadId.ToString("00")} 
          {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
20 }

 结果:

这里可以看出BeginInvoke的返回值就是callback委托的参数。

现在我们可以使用回调解决异步多线程无序的问题了。

2、获取委托异步调用的返回值

使用EndInvoke可以获取委托异步调用的返回值,请看下面的例子:

 1 private void btnAsyncReturnVlaue_Click(object sender, EventArgs e)
 2 {
 3        // 定义一个无参数、int类型返回值的委托
 4        Func<int> func = () =>
 5        {
 6              Thread.Sleep(2000);
 7              return DateTime.Now.Day;
 8        };
 9        // 输出委托同步调用的返回值
10        Console.WriteLine($"func.Invoke()={func.Invoke()}");
11        // 委托的异步调用
12        IAsyncResult asyncResult = func.BeginInvoke(p => 
13        {
14             Console.WriteLine(p.AsyncState);
15        },"异步调用返回值");
16        // 输出委托异步调用的返回值
17        Console.WriteLine($"func.EndInvoke(asyncResult)={func.EndInvoke(asyncResult)}");
18 }

 结果:

 

转发:https://www.cnblogs.com/w6w6/p/10648921.html

c#多线程 总结一下同步方法和异步方法的区别:1、同步方法由于主线程忙于计算,所以会卡住界面。异步方法由于主线程执行完了,其他计算任务交给子线程去执行,所以不会卡住界面,用户体验性好。2、同步方法由于只有一个线程在计算,所以执行速度慢。异步方法由多个线程并发运算,所以执行速度快,但并不是线性增长的(资源可能不够)。多线程也不是越多越好,只有多个独立的任务同时运行,才能加快速度。3、同步方法是有序的。异步多线程是无序的:启动无序,执行时间不确定,所以结束也是无序的。 阅读详情

相关推荐

C# 多线程魔法:5种方法让你的应用程序“分身有术”,你选对了吗?

首先,我们需要了解一些基本概念。在计算机科学中,进程(Process)是操作系统中的一个执行单元,它包含了程序运行所需的所有资源。而线程(Thread),则是进程中更小的执行单位,它可以独立于其他线程运行,并且共享进程内的资源。通过创建多个线程,我们可以在同一时间内完成更多的工作,这就是所谓的“分身有术”。以上就是 C# 中实现多线程的五种常见方法。每种方法都有其特点和适用场景,选择哪一种取决于你的具体需求和技术背景。对于初学者来说,建议先掌握Thread和Task,因为它们是最常用且最容易理解的方式;

java专栏 1435

C#高级--多线程详解

C#高级–多线程详解 零、文章目录 一、什么是多线程 1、进程 当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。 而一个进程又是由多个线程所组成的。 2、线程 线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。 3、句柄 句柄是Windows系统中对象或实例的标识,这些对象包括模块、应用程序实例、窗口、控制、位图、GDI对象、资源、文件等。 4、多线程 (1)多线程概念 多线程是指程序中

李宥的博客 5万+

构建C#高效多线程网络扫描器

本文还有配套的精品资源,点击获取 简介:多线程编程是提升程序效率的关键技术,特别是在并发任务处理中。C#通过System.Threading命名空间中的类提供多线程支持,用于创建如网络端口扫描器等高效工具。本课程详细讨论多线程概念、C#中的实现及端口扫描,包括线程创建、同步、线程池、端口扫描方法、异常处理、性能优化以及CPU占用监控。学生将学习如何实现一个利用多线程技术进行...

weixin_29859471的博客 729

C#基础-多线程

闲得蛋疼,整理一下C#线程的基础概念和常规用法。

SchuylerEX的博客 1万+

【XR技术介绍】深度解析|AI眼镜核心技术架构:从硬件层到端云AI全链路拆解

本文将逐层拆解AI眼镜完整技术架构,梳理硬件、感知、光学、端侧AI、软件云端五大核心层级,剖析核心数据流与行业技术瓶颈,同时附上标准化技术架构图,适合技术学习、方案汇报、行业科普使用。

houdou112358的博客 435

新手小白 必须精进的 “ FLASH分区管理Code and Data ” !!!最强文章解析!!!

FLASH“代码区”和“数据区”不是每个项目都必须显式管理,但只要你的产品功能越来越丰富,对可靠性和扩展性要求提高,就一定要学会科学划分和使用。这也是嵌入式工程师进阶路上的必修课!

qq_75157958的博客 933

c# .NET core多线程的详细讲解

.NET Core 多线程编程摘要: 本文系统介绍了.NET Core多线程技术,涵盖基础概念(线程、并发/并行)、核心API及适用场景。重点讲解了三种实现方式:1) Thread类提供底层线程控制,适合精细化管理;2) ThreadPool优化线程复用,适合短期任务;3) Task异步模型(推荐)简化编程,支持返回值。文章通过代码示例演示各API用法,对比分析优缺点,指导开发者根据CPU/I/O密集型等场景选择合适方案,提升程序性能与资源利用率。

lwpoor123的博客 1380

nrf52840的uart0与uart1双串口使用

nrf52840的uart0与uart1双串口使用 项目原使用nrf52832,后面由于项目需要用到双串口,改用nrf52840,官方的例程没看到使用uart1的,找遍全网都没找到uart1的使用资料,有些不负责任的网文居然说40也只有一个uart,吓得俺赶紧又查看了一次datesheet,O得K、木鸭梨 SDK代码uart驱动中,搜索到uart1的驱动部分,说明官方驱动有uart1支持的 自己动手,丰衣足食吧,下面以nRF5_SDK_17.0.0_9d13099版本,在examples\ble_per

chhen168的博客 5625

C#多线程详细讲解2

C#多线程学习(四) 多线程的自动管理(线程池) 在多线程的程序中,经常会出现两种情况: 一种情况:应用程序中,线程把大部分的时间花费在等待状态,等待某个事件发生,然后才能给予响应 这一般使用ThreadPool(线程池)来解决; 另一种情况:线程平时都处于休眠状态,只是周期性地被唤醒 这一般使用Timer(定时器)来解决; ThreadPool类提供一个由系统维护的线程池(可以

hemeinvyiqiluoben的专栏 542

Labview邪修01:贪吃蛇小游戏

摘要:博主使用LabVIEW 2020(32位)复刻了经典贪吃蛇游戏。程序具备随机生成蛇(初始长度3)和食物、三档速度调节、键盘方向控制等功能,碰撞边界或自身即游戏结束。实现过程包括:1)界面设计,使用二维颜色盒数组构建游戏区,添加速度枚举控件和功能按钮;2)程序架构,采用模块化设计包含Main.vi、Start.vi等6个核心VI,分别处理初始化、位置计算、碰撞检测等逻辑;3)事件驱动机制,通过开始/结束事件、按键响应和超时循环实现游戏流程控制。运行时可实时显示分数,完整还原了贪吃蛇的核心玩法。

weixin_38637405的博客 970

点云从入门到精通技术详解100篇-基于 CBCT 与口内扫描数据的牙齿点云配准

口扫生成的三维牙模型只包含牙冠结构,容易受到唾液、食物残留等杂质的干扰。像精度,但其扫描成本较高,在公立医院的普及率较低。此外,口扫探入式的体。完整、精确的牙齿信息,有助于医生更好地进行数字化辅助诊疗,提高手术的预。可以表示上颌骨、下颌骨、牙冠以及牙根的位置和形状,但其扫描分辨率较低,精配准前实现粗略的预配准来确定点云间的初始位置,为精配准提供良好的配准。该算法通过计算点对间的距离确定源点云中的某一点在目标点云中的对应。算两点之间的欧氏距离,运算量较大,而且由于算法只考虑点集间的距离而忽略。

getusushu的博客 1640

C#多线程详细讲解1

C#多线程学习(一) 多线程的相关概念 什么是进程? 当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。而一个进程又是由多个线程所组成的。 什么是线程? 线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。 什么是多线程? 多线程是指程序中包含多个执行流,即在一个

hemeinvyiqiluoben的专栏 582

让openclaw接入web端的deepseek或着qwen,使用无限token

🪟 Windows: 提供原生的 PowerShell 一键安装支持!配置过程和官方版本是相同的,唯一的区别就是再选则model时要选择带Browser字样的deepseek或者qwen。安装之前需要先清理官方版本的安装痕迹,否则openclaw版本跳转不到这个中文版本。因为此版本与官方版本存在代差,在配置飞书时可能会出现无法安装飞书的情况。剩下的配置保持和官方版本一致操作即可。选中之后需要进行一次网页端的登录。登录后会出现可选择的model。安装完成后会跳到这个中文界面。涉及到接入飞书机器人的操作。

z1872385的博客 302

详细讲解C#的多线程能力 转载

  线程是允许进行并行计算的一个抽象概念:在另一个线程完成计算任务的同时,一个线程可以对图像进行更新,二个线程可以同时处理同一个进程发出的二个网络请求。我们在这篇文章中将重点讨论Java和C#在线程方面的不同之处,并将一些Java中线程的常用模式转换为C#。        从概念上讲,线程提供了一种在一个软件中并行执行代码的方式━━每个线程都“同时”在一个共享的内存空间中执行指令,(当然是在一个处...

weixin_34113237的博客 67

电源完整性分析:PDN阻抗与去耦电容设计——目标阻抗计算与电容选型深度实战

摘要:随着嵌入式处理器主频突破 GHz、多核架构普及,电源完整性(Power Integrity, PI)已成为高速 PCB 设计中不可忽视的关键环节。本文从 PDN(电源分配网络)阻抗模型出发,系统讲解目标阻抗计算方法、去耦电容选型策略、反谐振抑制技术,并结合 STM32F4 核心板的完整设计实例,提供一套可落地的电源完整性工程实践方案。电源完整性设计是一项系统工程,需要从目标阻抗计算电容选型反谐振抑制PCB 布局目标阻抗是设计的起点,由电源电压、纹波要求和瞬态电流决定去耦电容。

从事移动开发八年,csdn博客专家,阿里云特邀专家,华为云云享专家,高级网络信息安全工程师,大数据工程师(高级),python技术开发(高级),HarmonyOS 应用开发者高级认证,现在某学校任教。 1万+

Vue3 - 详细实现虚拟列表前端虚拟滚动列表解决方案,vue3长列表优化之虚拟列表,解决列表动态高度不固定高度及图片视频图文异步请求加载问题,虚拟列表DOM大量数据同时加载渲染卡顿太慢及下滑列表闪烁

vue3,vue3虚拟列表,用vue3实现一个虚拟列表,长列表高度不一致如何实现虚拟滚动加载列表,虚拟列表有图片视频,图文列表,上万条数据同时渲染解决,长列表dom渲染太卡,列表滚动条加载超多数据,瀑布流,在Vue3中如何实现列表的虚拟滚动,虚拟列表滑动白屏闪烁闪一下,虚拟列表每个项的高度不确定,可动态改变高度,vue3虚拟滚动列表,页面数据太多加载很慢怎么办,vue3实现不定高的虚拟列表,定高和不定高,虚拟列表非固定高度,vue3长列表优化之虚拟列表实现,vue3虚拟列表高度不固定,用vue3实现一个虚拟

🏆 前端领域优质创作者 1万+

C#中多线程Task详解

频繁的创建和消耗比较好资源;提供操作线程的API不是马上响应(线程是操作系统统一管理,收到指令之后,具体还得操作系统真实处理,而操作系统收到指令之后并非马上执行相关指令);

m0_48667560的博客 1万+

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

一文带你搞懂C#多线程的5种写法

一文带你搞懂C#多线程的5种写法 1.简介 超长警告! 在学习本篇文章前你需要学习的相关知识: 线程基本知识 此篇文章简单总结了C#中主要的多线程实现方法,包括: Thread 线程 ThreadPool 线程池 Parallel Task 任务 BackgroundWorker组件 2. Thread类 2.1 概述 使用Thread类通过ThreadStart(无参数)或ParameterizedThreadStart(一个输入参数)类型的委托创建一个Thread对象,开启一个新线程,执行该委托

买烤麸烤饼儿的博客 6万+

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

C#多线程详解

上下文切换(Context Switching)是多线程环境中的一种操作,指的是在一个 CPU 核心上切换正在执行的线程,从当前线程的执行上下文(包括寄存器状态、程序计数器等)切换到另一个线程的执行上下文, 线程的切换需要额外的开销,因此在某些情况下,过多的线程可能会导致性能下降。因此,在设计多线程应用程序时,需要考虑如何减少上下文切换的发生,以提高程序的执行效率。是用于实现线程同步的一种机制,用于保护共享资源,避免竞态条件。上下文切换开销指的是从一个线程切换到另一个线程的过程中所涉及的时间和资源开销。

世界既不黑也不白,而是一道精致的灰。 4455

C# 多线程与单线程

本文通过烹饪场景对比了C#单线程与多线程的实现差异。单线程按顺序执行任务(总耗时8秒),导致界面卡顿;多线程通过Thread类或Task类并行处理(总耗时5秒),提升效率且保持界面响应。文章详细介绍了三种创建线程的方法:定义方法名、使用委托和Lambda表达式,并对比了它们的优缺点。关键提示:多线程需注意线程安全,合理设置后台线程(IsBackground=true),避免程序无法正常退出。虽然多线程能显著提升性能,但需权衡资源消耗与线程同步问题。

LZQqqqqo的博客 1602

C#多线程编程实例 线程与窗体交互

C#多线程编程实例 线程与窗体交互代码: public partial class Form1 : Form { //声明线程数组 Thread[] workThreads = new Thread[10]; public Form1() { InitializeComponent();

无知人生,记录点滴 3万+

C#基础之多线程讲解

进程(Process)是Windows系统中的一个基本概念,它包含着一个运行程序所需要的资源。一个正在运行的应用程序在操作系统中被视为一个进程,进程可以包括一个或多个线程。线程是操作系统分配处理器时间的基本单元,在进程中可以有多个线程同时执行代码。进程之间是相对独立的,一个进程无法访问另一个进程的数据(除非利用分布式计算方式),一个进程运行的失败也不会影响其他进程的运行,Windows系统就是利用进程把工作划分为多个独立的区域的。进程可以理解为一个程序的基本边界。

上善若泪 3331

介绍C#多线程编程的文章

挺不错的,讲解比较浅显而且详细。现在正在根据它学习多线程编程中……

869

C#多线程

线程在C#中并不难实现。它有一个命名空间:System.Threading,提供了多线程的支持。要开启一个新线程,需要以下的初始化:ThreadStart startDownload = new ThreadStart( DownLoad ); //线程起始设置:即每个线程都执行DownLoad(),注意:DownLoad()必须为不带有参数的方法Thread downloadThre

Asp.net2.0开发专栏 1716

C#【高级篇】 C# 多线程

线程 被定义为程序的执行路径。每个线程都定义了一个独特的控制流。如果您的应用程序涉及到复杂的和耗时的操作,那么设置不同的线程执行路径往往是有益的,每个线程执行特定的工作。 线程是轻量级进程。一个使用线程的常见实例是现代操作系统中并行编程的实现。使用线程节省了 CPU 周期的浪费,同时提高了应用程序的效率。 到目前为止我们编写的程序是一个单线程作为应用程序的运行实例的单一的过程运行的。但是,这样子应用程序同时只能执行一个任务。为了同时执行多个任务,它可以被划分为更小的线程。 ...

星Yeah 的博客 8577
上一篇: HObject 转HRegion 、HXLDCont
下一篇: C#中TabControl相关用法
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值