文章目录
- 一. 目标
- 二. 技能介绍
- ① 进程和线程
- ② 为什么需要多线程
- ③ C#实现多线程的方式
- ④ 线程的操作(创建_终止_挂起_恢复)
一. 目标
- 进程和线程基本概念
- 为什么需要多线程?
- C#实现多线程的方式?
- 线程Thread的创建,终止,挂起和恢复?
二. 技能介绍
① 进程和线程
- 什么是进程(Process)
- 一个正在运行的程序实例就是一个进程,拥有独立的内存空间和资源.
- 每个进程都在自己的内存空间内运行,相互之间不直接共享内存,进程间通信一般需要一些机制,比如进程间通信IPC.
- 每一个进程都有自己的一个主线程,而这个主线程是程序的入口点,它可以创建其他线程来执行不同的任务
- 什么是线程(Thread)
- 线程是进程内的一个执行单元,也是操作系统可执行的最小单元.
- 一个进程中的多个线程共享进程的资源,它们之间可以共享数据.
- 线程在程序中是可以并行执行的.
- 多进程
多进程指的是多个独立运行的进程,每个进程都有自己的内存控件,独立执行任务,相互之间不会收到影响.
多进程可以提高系统的并行性和稳定性,但是进程间的通信开销比较大
- 多线程
多线程是指在同一个进程内同时执行多个线程.线程之间可以更方便地共享数据和通信,适用于需要高度协作和共享资源的任务.多线程可以提高程序的响应速度和资源利用率
② 为什么需要多线程
在软件开发中,我们可能会遇到下面这些需求:
- 1. 图像用户界面GUI应用程序
主线程
UI线程
需要保持响应性,所以在执行耗时操作的的时候,要创建新的线程去操作,如果用主线程去执行耗时任务,界面将会出现卡顿,就影响了用户使用体验.
- 2. 网络编程
在网络编程中,常常需要同时处理多个网络请求或者连接.使用多线程可以让程序更高效处理这些请求,避免阻塞主线程
- 3. 并行计算
对于需要大量计算的任务,如数据处理,图像处理等,通过使用多线程可以充分利用多核处理器,加快任务完成速度
总结
- 提高效率
- 提高响应速度
- 充分利用多喝处理器
③ C#实现多线程的方式
- 使用Thread类
语法
Thread thread = new Thread(()={});
例子
#region 1. 使用Thread创建线程
Thread thread = new Thread(() =>
{
Console.WriteLine("我是线程1,我采用的是Thread(lambda=>{}) 匿名表达式的方式");
});
thread.Start();
// thread线程启动之后,会继续往下执行,不会阻塞线程
#endregion
- 使用Task类
语法
Task task = Task.Run(()=> {})
例子
Task task = Task.Run(() =>
{
Console.WriteLine("我是线程2,我采用的是Task(lambda=>{}) 匿名表达式的方式");
});
// Task.Wait()方法用于等待任务的完成,在调用该方法之后,当前现场会被阻塞,直到任务执行完成为止
task.Wait();
- 使用ThreadPool类
语法
ThreadPool.QueueUserWorkItem((state)=>{})
例子
ThreadPool.QueueUserWorkItem((state) =>
{
Console.WriteLine("我是线程3,我采用的是ThreadPool方式");
});
// 这种方式创建的线程是线程池创建线程,是不会阻塞主线程的,主线程会继续往下执行.
- 使用Async/Await异步编程
例子
#region 4. 使用Async/Await
await Task.Run(() =>
{
Console.WriteLine("我是线程4,我采用的额Async/Await的方式");
});
#endregion
- 使用Parallel.For方法
用于并行执行一个for循环,可以在多个线程中同时处理循环中的元素,可以在单独的线程上执行for循环中的数据和后面的计算表达式
例子:
Parallel.For(0, 10, i =>
{
Console.WriteLine($"当前数据i = {i},线程Id = {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000);
});
- 使用Parallel.ForEach方法
Parallel.ForEach方法用于遍历一个集合,在多个线程中同时处理集合中的元素
例子
var numbers = Enumerable.Range(0, 10);
Parallel.ForEach(numbers, num =>
{
Console.WriteLine($"处理的数据 = {num},处理线程 = {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(200);
});
- Parallel.Invoke 方法
Parallel.Invoke
方法用于并行执行多个操作,可以在多个线程中同时执行这些操作.什么意思呢,就是可以在Parallel.Invoke
方法中传递多个方法(或者是匿名方法)作为参数,然后去并行执行这些方法
#region 7. Parallel.Invoke
Parallel.Invoke(
() => { Console.WriteLine($"函数1,线程Id = {Thread.CurrentThread.ManagedThreadId}"); },
() => { Console.WriteLine($"函数2,线程Id = {Thread.CurrentThread.ManagedThreadId}"); },
() => { Console.WriteLine($"函数3,线程Id = {Thread.CurrentThread.ManagedThreadId}"); },
() => { Console.WriteLine($"函数4,线程Id = {Thread.CurrentThread.ManagedThreadId}"); },
() => { Console.WriteLine($"函数5,线程Id = {Thread.CurrentThread.ManagedThreadId}"); },
() => { Console.WriteLine($"函数6,线程Id = {Thread.CurrentThread.ManagedThreadId}"); }
);
#endregion
- PLINQ的AsParallel方法
AsParallel
方法用于将LINQ
查询转换为并行查询,实现并行处理查询结果
例子:
#region 8. AsParallel 方法用于将LINQ查询转换为并行查询,实现并行处理查询结果
var numbers02 = Enumerable.Range(10, 20);
var result = numbers.AsParallel().Where(num =>
{
Console.WriteLine($"数据num: {num},所在线程ID: {Thread.CurrentThread.ManagedThreadId}");
return num % 2 == 0;
}).ToList();
#endregion
- PLINQ的AsSequential 和 AsOrdered 方法
AsSequential
方法用于将并行查询转换为顺序查询,以保留查询结果的顺序性.AsOrdered
方法用不指定查询结果的顺序行,确保结果按照源数据的顺序返回.
在一开始接触这两个方法的时候,我是迷惑的,为什么一会顺序,一会又并行,他们之间到底有什么区别呢?AsSequential()
它的意思就是将后续的操作采用顺序处理,而不是继续并行执行,什么意思呢,就是比如有一个
操作要使用AsParallel()
进行并行计算,但是后续的操作又要使用顺序执行,这个时候就要使用AsSequential()
了.
而AsOrdered()
保证并行处理的结果按照输入数据的顺序排列,并不影响操作的并行执行.而AsSequential()
将后续的操作转换为按顺序执行,但是不影响之前的并行处理,仅影响后续操作的执行顺序.
var nubers03 = Enumerable.Range(0, 10);
var result02 = nubers03.AsParallel().AsSequential().Where(num =>
{
Console.WriteLine($"数据num03: {num},所在线程ID: {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000);
return num % 2 == 0;
}).ToList();
上面这个例子在运行的时候,就是说按照顺序1秒打印一条日志,所以可以看出来在使用AsSequential()的时候是按照顺序执行的
#region 10. AsOrdered 和 AsSequential
var numbers04 = Enumerable.Range(0, 10);
var queryOrdered = numbers04.AsParallel()
.AsOrdered()
.Select(num => num * num)
.Where(num =>
{
Thread.Sleep(1000);
Console.WriteLine($"数据num04: {num},所在线程ID: {Thread.CurrentThread.ManagedThreadId}");
return num % 2 == 0;
}).ToList();
Console.WriteLine($"QueryOrderd: {string.Join(',', queryOrdered)}");
#endregion
然后AsOrdered()
的执行结果可以看出来,它其实也是并行执行的,并且不能保证哪个数据先执行,只是它的结果是按照输入数据的顺序来进行生成的.
④ 线程的操作(创建_终止_挂起_恢复)
- 线程的创建
1. 无参创建Thread,通过构造方法(委托)
2. 有参创建Thread,通过构造方法(有参委托)
Thread thread01 = new Thread(DoThread01);
Thread thread02 = new Thread(DoThread02);
thread01.Start();
// 有参线程传递参数的方式
thread02.Start("Hello World!");
void DoThread01()
{
Console.WriteLine("我是无参线程1,我正在运行!");
}
void DoThread02(object? obj)
{
Console.WriteLine($"我是有参数的线程2,我的参数是{obj ?? "Null"},我正在运行.");
}
- 线程等待阻塞
方法Join()
Join()
方法的意思就是创建Join
的线程会阻塞创建线程的执行,直到Join
线程执行完毕,创建线程才会继续往下执行.
假如主线程创建了线程A
,然后A.Join()
,意思就是A会阻塞主线程的执行,主线程会等待A
线程执行完毕之后才会继续往下执行.
如果没有A.join()
主线程会继续往下执行,不会阻塞
Thread thread = new Thread(() =>
{
for (int i = 0; i < 20; i++)
{
Console.WriteLine($"线程Id: {Thread.CurrentThread.ManagedThreadId},执行For循环的 第 {i + 1} 次");
Thread.Sleep(100);
}
});
thread.Start();
thread.Join();
Console.WriteLine("主线程结束!");
- 线程终止
Interrupt()终止线程
Interrupt()
方法用于中断线程的阻塞状态,引发ThreadInterruptedException
异常,需要去捕获这个异常Interrrupt()
方法需要终止的线程有类似IO
或者是Sleep
这种阻塞操作才可以,如果没有,比如写一个While(True)
死循环,然后里面都是计算,这样Interrupt()
被调用的时候,线程是没有事件去响应的,所以对中断的线程是有要求的
Thread thread = new Thread(() =>
{
int startIndex = 1;
try
{
while (true)
{
Console.WriteLine($"线程: {Thread.CurrentThread.ManagedThreadId} 正在运行,第 {startIndex++} 次.. ");
Thread.Sleep(200);
}
}
catch (ThreadInterruptedException)
{
Console.WriteLine($"线程: {Thread.CurrentThread.ManagedThreadId} 被终止");
}
});
thread.Start();
Thread.Sleep(1000);
thread.Interrupt();
Console.WriteLine("主线程结束.");
现在加入我们将线程里面的Thread.Sleep(200)去除掉,那么会发现我们根本就无法终止这个线程(这里也是可以终止线程的,因为打印也是IO操作,所以我们把打印也去除掉,就来个运算将startIndex++)
Thread thread = new Thread(() =>
{
int startIndex = 1;
try
{
while (true)
{
startIndex++;
}
}
catch (ThreadInterruptedException)
{
Console.WriteLine($"线程: {Thread.CurrentThread.ManagedThreadId} 被终止");
}
});
thread.Start();
Thread.Sleep(1000);
thread.Interrupt();
Console.WriteLine("主线程结束.");
About()终止线程
为什么不推荐使用About()来终止线程?
- 使用
About()
来终止线程可能导致一些严重的问题,可能导致线程处于不确定状态.- 在新版本的
.NET
版本中,About()
方法别调用的时候可能会引发异常
- 线程的挂起和恢复
之前的
.NET
中,使用Suspent
和Resume
方法用于挂起和恢复线程,但是这两个方法已经被标记为已过时,主要原因就是这些方法可能会导致线程死锁,死活锁等问题,推荐使用Monitor类的Wait
和Pulse
方法实现线程的挂起和恢复功能.Wait
用于将当前线程挂起,
而Pulse
方法用于唤醒被挂起的线程.这种方式更加的安全,避免线程死锁问题.
bool IsPause = false;
object lockObj = new object();
Thread thread = new Thread(() =>
{
lock (lockObj)
{
int startIndex = 1;
while (true)
{
if (IsPause)
{
Monitor.Wait(lockObj, 2000);
}
Console.WriteLine($"线程 {Thread.CurrentThread.ManagedThreadId}正在运行,index = {startIndex++}");
Thread.Sleep(100);
}
}
});
thread.Start();
Thread.Sleep(1000);
IsPause = true;
Thread.Sleep(2000);
IsPause = false;
lock (lockObj)
{
Monitor.Pulse(lockObj);
}
注意一点就是在使用Monitor.Wait()方法和Monitor.Pulse()方法的时候要在lock语句块中,来保证线程同步和保证对象的状态的一致性