一、进程与线程
1、定义
- 进程:是系统进行资源分配和调度的一个独立单位,记录当前程序在运行的时候对各种资源的消耗,是一个虚拟概念
- 线程:线程是进程的实例,计算机在执行某个动作,一个最小的执行流,也是虚拟概念
- 句柄:句柄其实就是一个数字,对应计算机程序中的最小单位,如:当前程序运行Id。
2、关系
一个进程包括多个线程
二、C# 中的多线程
Thread是对计算机资源操作的一种封装类
1、同步方法
本质
发起调用后,代码一行一行按照顺序执行,符合正常的开发思维,而且线程id是唯一不变的,说明所有的操作都是在同一个线程中执行的
特点
1. 卡顿界面:UI线程(主线程)处理所有的动作和计算,在执行过程中主线程一直被占用,不能再进行其他动作,直到主线程闲置,非常影响用户体验;
2. 运行速度慢,消耗资源少;
3. 同步方法是有序执行的。
(1)创建一个windows窗体应用,新建一个按钮,按钮事件执行以下代码
/// <summary>
/// 同步方法
/// 特点:发起调用,代码一行一行按照顺序执行,符合正常的开发思维,而且线程id是唯一不变的
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SyncBtn_Click(object sender, EventArgs e)
{
//Thread.CurrentThread.ManagedThreadId 当前线程id
Console.WriteLine($"==================SyncBtn_Click start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
for (int i = 0; i < 5; i++)
{
string name = $"SyncBtn_Click_{i}";
this.DoSomething(name);
}
Console.WriteLine($"==================SyncBtn_Click end {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
}
(2)再添加一个模拟资源消耗的方法
#region private methods
/// <summary>
/// 模拟比较消耗资源的方法
/// </summary>
/// <param name="name"></param>
private void DoSomething(string name)
{
Console.WriteLine($"==================DoSomething start {name} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
long result = 0;
//循环一百万次
for (int i = 0; i < 1000_000; i++)
{
result += i;
}
//Thread.Sleep(2000);
Console.WriteLine($"==================DoSomething end {name} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")} {result}========================");
}
#endregion
(3)运行
2、异步方法
本质
发起调用之后,主线程不等待完成,直接进进行下一步操作,发起新的线程来执行指定动作(每个线程的id都不同,与主线程id也不同)
特点
1. 不卡顿界面:UI线程(主线程)闲置,计算任务交给子线程执行,改善了用户体验,应用场景如发送短信邮件等;
2. 运行速度高:实现多线程并发,以资源换性能;
3. 异步方法无序执行:启动无顺序,线程资源是向操作系统申请的,由操作系统的调度策略决定,所以线程启动顺序是随机的,导致执行也是无序的,因此CPU分片执行结束也是无序的。
注:
CPU分片:假设CPU 1秒内能进行10亿次计算,把1秒内的计算能力再次切分,由操作系统去调度执行不同的计算
宏观上:相当于多个任务并发执行
微观上:一个物理CPU同一时刻,只能完成一个任务
(1)新建一个按钮,按钮事件执行以下代码
/// <summary>
/// 异步方法
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void AsyncBtn_Click(object sender, EventArgs e)
{
Console.WriteLine($"==================AsyncBtn_Click start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
Action<string> action = this.DoSomething;
action.Invoke("AsyncBtn_Click");
for (int i = 0; i < 5; i++)
{
string name = $"AsyncBtn_Click{i}";
//分配一个新的线程去执行委托(目前dotnet core中不支持该方法)
action.BeginInvoke(name, null, null);
}
Console.WriteLine($"==================AsyncBtn_Click end {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
}
但是这里有一个问题,就是截至2020年
9月14日dotnet core 还不支持BeginInvoke方法,所以需要创建一个.net framework版本的窗体应用来模拟,代码是一样的。
(2)运行
(3)分析
之所以会出现DoSomething 方法中的start打印完成后不是接着打印end,是因为,在这5个线程中调用DoSomething都是同时进行的,任何一个线程都不会等待另外一个线程执行。
(4)运行速度对比
同步方法耗时28毫秒,异步方法耗时3毫秒,性能大约是同步方法的9倍,但是实际不能达到9倍,因为线程调度也要消耗资源(但是Thread.Sleep只是等待,不消耗资源)
三、多线程进阶
1、控制线程执行顺序
通过异步回调委托,就实现了控制线程的顺序
思考:如果想多线程调用DoSomething完成之后,打印一句话(执行某个动作),该怎么做?
方式一:可以实现吗?
/// <summary>
/// 异步方法进阶
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void AdvancedAsync_Click(object sender, EventArgs e)
{
Console.WriteLine($"==================AdvancedAsync_Click start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
Action<string> action = this.DoSomething;
//AsyncCallback callback = ar =>
//{
// Console.WriteLine("AdvancedAsync_Click执行成功!");
//};
action.BeginInvoke("AdvancedAsync_Click", null, null);
Console.WriteLine("AdvancedAsync_Click执行成功!");
Console.WriteLine($"==================AdvancedAsync_Click end {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
}
结果:很明显不符合需求
方式二:利用异步回调委托AsyncCallback
,如果给异步回调委托中传递了参数,可以通过回调委托对象IAsyncResult
的AsyncState
属性获得
/// <summary>
/// 异步方法进阶
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void AdvancedAsync_Click(object sender, EventArgs e)
{
Console.WriteLine($"==================AdvancedAsync_Click start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
Action<string> action = this.DoSomething;
//异步回调
AsyncCallback callback = ar =>
{
Console.WriteLine(ar.AsyncState);
Console.WriteLine("AdvancedAsync_Click执行成功!");
};
//第二个参数是异步回调委托,第三个参数是给异步回调委托中传递参数
action.BeginInvoke("AdvancedAsync_Click", callback, "异步回调参数,可以是任意类型,通过AsyncState属性获得");
//Console.WriteLine("AdvancedAsync_Click执行成功!");
Console.WriteLine($"==================AdvancedAsync_Click end {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
}
结果:符合需求
2、获取线程返回值
(1)获取返回状态
会阻塞主线程,这里有一个小坑,如果子线程执行完成,就不会再执行while条件判断,也就是说,在子线程执行完成前可以获得返回状态,但是执行完成那一刻是获取不到的,除非再次点击按钮获取状态
/// <summary>
/// 获取异步执行状态
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Process_Click(object sender, EventArgs e)
{
Console.WriteLine($"==================Process_Click start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
Func<int> func = () =>
{
return DateTime.Now.Year;
};
//异步回调
AsyncCallback callback = ar =>
{
Console.WriteLine(ar.AsyncState);
};
//获取异步执行状态(对前面委托的动作执行后的描述)
IAsyncResult asyncResult = func.BeginInvoke(callback, "异步回调参数");
int i = 1;
while (!asyncResult.IsCompleted)
{
if (i <= 9)
{
Console.WriteLine($"已完成{i * 10}%。。。。");
}
else
{
Console.WriteLine($"已完成100%。。。。");
}
i++;
}
Console.WriteLine($"==================Process_Click end {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
}
执行结果
(2)获取返回值
- 方式一:在回调函数中获取
//异步回调
AsyncCallback callback = ar =>
{
int result = func.EndInvoke(ar);
Console.WriteLine(result);
};
- 方式二
//获取返回结果,会阻塞主线程
int result = func.EndInvoke(asyncResult);
Console.WriteLine(result);
上面两种方式可以达到相同的效果,因为异步回调函数是在异步执行完成后执行,且EndInvoke只能被调用一次,所以上述两种方式不能同时出现。
(3)其他
//等待异步委托执行完毕,会阻塞主线程
asyncResult.AsyncWaitHandle.WaitOne();
//一直等待任务完成,会阻塞主线程
asyncResult.AsyncWaitHandle.WaitOne(-1);
//限时等待;这里例子最多等待2s,会阻塞主线程
asyncResult.AsyncWaitHandle.WaitOne(2000);
四、本章代码
dotnet core版本窗体
dotnet Framework版本窗体
注:
运行窗体怎么将结果输出到命令行窗口呢?如下将项目输出类型设置为控制台应用程序即可