(11)C#多线程
参考博客:
https://www.cnblogs.com/asdyzh/p/9869956.html
https://www.cnblogs.com/wuyifu/archive/2013/03/22/2974826.html
https://www.cnblogs.com/lvjy-net/p/8484943.html
https://blog.csdn.net/weixin_30604651/article/details/99052659
1、线程与进程相关概念
1.1、线程和进程的基本概念
- 进程:
程序在一个数据集合上的运行的过程,是资源分配的最小单位,进程间相互独立,一个进程运行失败不会影响另外的进程,可以使不同程序彼此分开。 - 线程:
进程的基本执行单元,是CPU的调度最小单元,拥有的资源很少(CPU寄存器、调用栈和线程本地存储器,主要是记录线程状态),共享进程的资源。
一个进程也可以分为多个线程,来同时得完成进程的任务。由于线拥有资源很少,因此并行效率更高。 - 多线程:
在一个进程可以同时运行多个不同的线程,来并行执行执行不同的任务。
利用多线程可以实现并行、异步功能,提高程序运行效率,多线程也不是越多越好,只有多个独立的任务同时运行,才能加快速度。
虽然系统中可能会同时存在非常多的线程,一个核上实际上只能运行一个线程,但是由于CPU的时间片切换功能,可以“同时”处理很多线程;
对于线程本身,同样也存在着和“时间片切换”类似的功能,它可以在线程对应的多个操作中进行切换。

1.2、线程和进程的高阶概念
1)线程池
2)托管线程
- 托管线程池与托管线程
.NET中托管的意思是指:系统帮忙管理,列如不需要主动回收资源,有CLR帮你做了,你只需要建立对象和使用,不需要处置。
ThreadPool为应用程序提供一个受系统管理的辅助线程池,此线程池被称为托管线程池。
在托管线程池中的线程被称为托管线程。 -
何时不使用托管线程
(1)需要前台线程(托管线程都是后台线程)
(2)需要具有特定优先级的线程
(3)...
3)前台线程和后台线程
- 前台线程:只有所有的前台线程都结束,应用程序才能结束。默认情况下创建的线程都是前台线程
- 后台线程:只要所有的前台线程结束,后台线程自动结束(即其不影响进程结束)。通过Thread.IsBackground设置后台线程。必须在调用Start方法之前设置线程的类型,否则一旦线程运行,将无法改变其类型。
class Program { //当调用线程和前台线程运行完时,后台线程还没有运行完;此时不会影响程序的结束,后台线程找不到了。 //如果将backGroundThread也改成前台线程,那么调用线程会堵塞直到前台线程运行完毕 static void Main(string[] args) { //默认创建前台线程 Thread frontThread = new Thread(new ParameterizedThreadStart(RunLoop)); if (frontThread.IsBackground) ; //创建后台线程 Thread backGroundThread = new Thread(new ParameterizedThreadStart(RunLoop)); backGroundThread.IsBackground = true; //启动线程 frontThread.Start(10); backGroundThread.Start(20); } public static void RunLoop(object iMax) { //获取当前线程的名称 var threadID = Thread.CurrentThread.ManagedThreadId; for (int i = 0; i < (int)iMax; i++) { Console.WriteLine("{0}计数:{1}", threadID, i.ToString()); //线程休眠500毫秒 Thread.Sleep(1000); } Console.WriteLine("{0}完成计数", threadID); } }
4)应用程序域
- 应用程序域
.Net中,应用程序有了一个新的边界:应用程序域(以下简称域)。它是一个用于隔离应用程序的虚拟边界。为了禁止不应交互的代码进行交互,这种隔离是必要的。
.Net的应用程序在域层次上进行隔离,一个域中的应用程序不能直接访问另一个域中的代码和数据
这种隔离使得在一个应用程序范围内创建的所有对象都在一个域内创建,确保在同一进程中一个域内运行的代码不会影响其他域内的应用程序,大大提高了运行的安全。 - 域与线程
域和线程之间并不具有一 一对应关系。在任意给定时间,单个域中可以执行不止一个线程,而且特定线程也并不局限在单个域内。也就是说,线程可以跨越域边界,不为每个域创建新线程。
在指定时刻,每一线程都只能在一个域中执行。运行库会跟踪所有域中有哪些线程正在运行。通过调用.Net类库的 Thread.GetDomain 方法,你还可以确定正在执行的线程所在的域。
2、System.Treading.Thread类介绍
Thread类是BCL基础类库中控制线程的基础类,下面将对这个类进行详细介绍。
2.1、构造函数
Thread类中有如下4个构造函数,window中默认线程调用栈大小为1024KB,可以通过构造函数设置。

其中参数ThreadStart和ParameterizedThreadStart就是两种委托类型,如下:
public delegate void ThreadStart(); public delegate void ParameterizedThreadStart(object obj);
示例如下:(匿名方法和lambda表达式只针对ThreadStart参数)
static void main(string[] args) { //通过匿名委托创建 Thread thread1 = new Thread(delegate() { console.writeline("我是通过匿名委托创建的线程"); }); Thread1.start(); //通过lambda表达式创建 Thread thread2 = new Thread(() => console.writeline("我是通过lambda表达式创建的委托")); Thread2.start(); console.readkey(); } //运行结果 我是通过匿名委托创建的线程 我是通过lambda表达式创建的委托
class Program { static void Main(string[] args) { //通过ParameterizedThreadStart创建线程 Thread thread = new Thread(new ParameterizedThreadStart(Thread1)); //给方法传值 thread.Start("这是一个有参数的委托"); //不传值时,默认参数为null //thread.Start(); Console.ReadKey(); } static void Thread1(object obj) { Console.WriteLine(obj); } }
2.2、属性
常用属性如下:

- CurrentThread
静态属性,获取当前环境下的线程 - ManagedThreadId&Name
ManagedThreadId获取当前线程的唯一标识符;
Name获取或设置线程名称,是一个可变值,在默认时候,Name为一个空值 Null,开发人员可以通过程序设置线程的名称,但这只是一个辅助功能。 - Priority(Lowest<BelowNormal<Normal<AboveNormal<Highest)
获取或设置线程优先级,默认为Normal。
当线程之间争夺CPU时间时,CPU按照线程的优先级给予服务。因为高优先级的线程可以完全阻止低优先级的线程执行,除非系统有特殊要求,否则不应该随便设置线程的优先级别。 - IsActive
获取当前线程状态,指示线程是否正在运行中。 - ThreadState(Running、StopRequested、SuspendRequested、Background、Unstarted、Stopped、WaitSleepJoin、Suspended、AbortRequested、Aborted)
获取当前线程更详细的状态:
(1)Aborted:线程已停止;
(2)AbortRequested:线程的Thread.Abort()方法已被调用,但是线程还未停止;
(3)Background:线程在后台执行,与属性Thread.IsBackground有关;
(4)Running:线程正在正常运行;
(5)Stopped:线程已经被停止;
(6)StopRequested:线程正在被要求停止;
(7)Suspended:线程已经被挂起(此状态下,可以通过调用Resume()方法重新运行);
(8)SuspendRequested:线程的Thread.Suspend()方法已被调用,但是未来得及响应;
(9)Unstarted:未调用Thread.Start()开始线程的运行;
(10)WaitSleepJoin:线程因为调用了Sleep()或Join()等方法处于封锁状态; - IsBackground
获取当前线程是否为后台线程。 - IsThreadPoolThread
获取当前线程是否为线程池线程(托管线程) - CurrentContext
静态属性,获取当前线程的当前上下文,线程上下文是指线程的线程所拥有的独立资源。 - ExecutionContext
获取当前线程的所有上下文。
2.3 常用方法
常用方法如下:

2.4 线程同步
线程同步是指:是指在多线程共享某个变量时,在某一时刻只有一个线程可以访问变量,否则会超出错误。
c#为同步访问变量提供了一个非常简单的方式,即使用c#语言的关键字Lock,它可以把一段代码定义为互斥段,互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。
在c#中,关键字Lock定义如下:
//expression代表你希望跟踪的对象: // 如果你想保护一个类的实例,一般地,你可以使用this; // 如果你想保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了 Lock(expression) { //互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。 statement_block }
示例:
class Program { static void Main(string[] args) { BookShop book = new BookShop(); //创建两个线程同时访问Sale方法 Thread t1 = new Thread(new ThreadStart(book.Sale)); Thread t2 = new Thread(new ThreadStart(book.Sale)); //启动线程 t1.Start(); t2.Start(); Console.ReadKey(); } } class BookShop { //剩余图书数量 public int num = 1; public void Sale() { //两个线程同时访问本类实例,因此要保护的是本类的实例,用this就行 lock (this) { int tmp = num; if (tmp > 0)//判断是否有书,如果有就可以卖 { Thread.Sleep(1000); num -= 1; Console.WriteLine("售出一本图书,还剩余{0}本", num); } else { Console.WriteLine("没有了"); } } } } //运行结果 售出一本图书,还剩余0本 没有了
2.5 跨线程访问
先看一个例子,对应如下UI,当点击测试按钮后,新建一个线程去更改UI的内容。
代码如下:
private void btn_Test_Click(object sender, EventArgs e) { Thread thread = new Thread(new ThreadStart(Test)); thread.IsBackground = true; thread.Start(); } private void Test() { for (int i = 0; i < 10000; i++) { this.textBox1.Text = i.ToString(); } }
执行过程中提示如下错误,这是因为,textBox1是由主线程创建的,thread线程是另外创建的一个线程,C#强制要求这些代码必须是线程安全的,即不允许跨线程访问Windows窗体的控件。
可以将设置UI的逻辑放在TextBox的回调函数中,这样就是在主线程中修改UI了。


浙公网安备 33010602011771号