C# 多线程
同步和异步的差别
同步单线程方法卡界面——主(UI)线程忙于计算,所以不能响应
异步多线程方法不卡界面——计算任务交予子线程,主线程已经闲置,可以响应别的操作
cs:按钮后不能卡死——上传文件不卡死
bs:用户注册发邮件/发短信
同步单线程方法慢——只有一个线程计算
异步多线程方法快——多个线程并发计算
多线程就是用资源换性能,但不是线性增长
多线程的协调管理额外成本——项目经理
资源也有上限——5辆车只有3条道
线程并不是不是越多越好
无序性——不可预测性
启动无序:几乎是同一时间向操作系统请求线程,也是个需要CPU处理的请求。因为线程是操作系统资源,CLR 只能去申请,具体什么顺序无法掌控。
执行时间不确定:同一线程同一个任务耗时也可能不同,这更操作系统的调度策略有关,CPU分片(计算能力太强,1s拆成1000份,宏观上就变成了并发的)。所以任务执行过程就得看运气了——线程优先级可影响操作系统的调度
结束无序:以上可得
因为多线程具备不可预测性,很多时候你的你的想法并不一定能够贯彻实施,大多数情况可能是可行的,但总有一定的概率出问题(随着时间的推移,任何小概率事件都会成为必然事件)
电商下订单:增加订单表-日志-发邮件-生成支付-物流通知。多线程,有顺序要求,等待500ms执行下一个动作,顺序测试1000次都没问题,上线也没问题。。。但一个月就会发现总有那么几次顺序错了,而且这是无法重现的。随着用户增加,数据量累计,数据库压力变大,服务器硬件资源不够,会影响执行效率,各种情况都有,所以就错了,而且很难跟进。
使用多线程时,不要通过延时等方式掌控顺序,也不要试图“多模式“掌控顺序。
委托 的异步回调控制顺序
AsyncCallback
,回调委托
action.BeginInvoke(”xxx“,AsyncCallback实例,AsyncCallback实例.AsyncState 参数(需要传递的信息参数));
IsCompleted
判断线程是否完成
信号量:
IAsyncResult
可以接受action.BeginInvoke()
,调用asyncResult.AsyncWaitHandle.WaitOne() ;
// 阻塞当前线程,直到收到信号量,从asyncResult
发出。WaitOne
可以设置等待时间。//做超时控制
EndInvoke:
Func<string> func = ()=>DateTime.Now.ToString();
IAsyncResult asyncResult = func.BeginInvoke(null,null); //异步调用结果,描述异步操作的
string sResult = func.EndInvoke(asyncResult);
//string sResult=func.EndInvoke(func.BeginInvoke(null,null));
Thread 和Task的历程
class Program
{
static void Main(string[] args)
{
#region .NetFramework 1.0 1.1
// //.NetFramework 1.0 1.1
// ThreadStart threadStart = () =>
//{
// Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} Start");
// Thread.Sleep(2000);
// Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} End");
//};
//Thread thread = new Thread(threadStart);
//thread.Start();
////thread.Join();
////thread.Suspend();
////thread.Resume();
////thread.IsBackground=true;
////thread.Abort();
////Thread.ResetAbort();
////Thread 的 API 特别丰富,玩的很花,但很难玩好
////——线程是操作系统管理的,所以响应并不是那么灵敏
////Thread 启动线程是没有控制的,可能导致死机
#endregion
#region .NetFeamework 2.0(新的CLR) ThreadPool
// 池化资源管理涉及思想:线程是一种资源
// ,之前每次要调用线程,就去申请一个线程,使用完之后,释放掉;
// 池化就是做一个容器,容器提前申请那个线程,程序需要使用线程,
// 直接找容器获取,用完后再放回容器(控制状态),避免频繁的申请
// 和销毁;容器自己会根据闲置的数量去申请和释放;
//1 线程复用 2 可以限制最大线程数量(get;set;)
//API又太少了,线程等待顺序控制特别弱,MRE,影响了实战。
WaitCallback waitCallback=r=>
{
Console.WriteLine($"ThreadPool {Thread.CurrentThread.ManagedThreadId} Start");
Thread.Sleep(2000);
Console.WriteLine($"ThreadPool {Thread.CurrentThread.ManagedThreadId} End");
};
ThreadPool.QueueUserWorkItem(waitCallback);
#endregion
#region .NetFramework 3.0 Task被称之为多线程的最佳实践!
// 1 Task线程全部是线程池线程 2 提供了丰富的API,非常适合开发实践
Action action=()=>
{
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} Start");
Thread.Sleep(2000);
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} End");
};
Task task = new Task(action);
task.Start();
#endregion
Console.ReadLine();
}
}
Parallel
{
// Parallel 可以启动多线程,主线程也参与计算,节约一个线程
// 可通过给ParallelOptions轻松控制最大并发数量
// Parallel.f
Parallel.Invoke(
() =>
{
Console.WriteLine($"Paralle11 {Thread.CurrentThread.ManagedThreadId} Start1");
Thread.Sleep(2000);
Console.WriteLine($"Paralle11 {Thread.CurrentThread.ManagedThreadId} End1");
}, () =>
{
Console.WriteLine($"Paralle12 {Thread.CurrentThread.ManagedThreadId} Start2");
Thread.Sleep(2000);
Console.WriteLine($"Paralle12 {Thread.CurrentThread.ManagedThreadId} End2");
}, () =>
{
Console.WriteLine($"Paralle13 {Thread.CurrentThread.ManagedThreadId} Start3");
Thread.Sleep(2000);
Console.WriteLine($"Paralle13 {Thread.CurrentThread.ManagedThreadId} End3");
}, () =>
{
Console.WriteLine($"Paralle14 {Thread.CurrentThread.ManagedThreadId} Start4");
Thread.Sleep(2000);
Console.WriteLine($"Paralle14 {Thread.CurrentThread.ManagedThreadId} End4");
}
);
}
Task
使用多线程提高效率,建立在任务是彼此之间互不干扰的基础上。
Task.Run( action )
: 创建一个线程并执行
Task.WaitAny( xxx.ToArray() )
:阻塞当前线程,直到任一任务结束——主线程被阻塞,会卡界面
Task.WaitAll( xxx.ToArray() )
:阻塞当前线程,直到所有任务结束——主线程被阻塞
尽量不要线程套线程,容易翻车,全部由子线程完成的话,不能直接操作界面
TaskFactory Task工厂
taskFactory.ContinueWhenAll( Task[] task, Action<Task[]> continuationAction )
:创建一个延续任务,该任务在一组指定的任务完成后开始
taskFactory.ContinueWhenAny(Task[] task, Action<Task[]> continuationAction)
continue 的后续线程,可能是新线程,可能是刚完成任务的线程,还可能是同一个线程,就不可能是主线程
线程是不可预测的,所以几个动作先后都有可能
上面四个方法已经可以掌控90%的多线程顺序,灵活运用是重点
线程安全
// 这里面的i全部都是5是因为多线程在调用i的时候,i已经循环自增到5了
// k是因为每一次都是全新的k,作用域不一样了
// outofrange 不注意就是常发生的问题
for (int i = 0; i < 5; i++)
{
int k = i;
Task.Run(() =>
{
Console.WriteLine($" {i} {k} Task {Thread.CurrentThread.ManagedThreadId} Start");
Thread.Sleep(2000);
Console.WriteLine($" {i} {k} Task {Thread.CurrentThread.ManagedThreadId} End");
});
}
多线程安全问题:一段代码,单线程执行和多线程执行结果不一致,就表明有线程安全问题
//多线程去访问一个集合一般是没问题的 线程安全问题都是出在修改一个对象上
List<int> intList = new List<int>();
for (int i = 0; i < 10000; i++)
{
// 多线程之后,结果就变成小于10000——数据丢失了
Task.Run(() => // 覆盖了
{
intList.Add(i);
});
}
Thread.Sleep(5000);
Console.WriteLine(intList.Count);
#region 解决线程安全问题
List<int> intList = new List<int>();
for (int i = 0; i < 10000; i++)
{
Task.Run(() =>
{
lock (LOCK)
{
intList.Add(i);
}
});
}
// 加lock 就能解决线程安全问题——就是单线程化
// ——lock就是保证方法块任意时刻只有一个线程能进去,其他线程就排队
// ——单线程化
// lock原理——语法糖——等价于Monitor
// ——锁定一个内存引用地址——所以不能是值类型——null编译不报错,但也不行
Thread.Sleep(5000);
Console.WriteLine(intList.Count);
#endregion
lock语句是可以在同一线程中递归调用的。VS中运行了很多次发现它并不会发生死锁。在《CLR via C#》第二版(中文版,清华大学出版社出版)的第530页中第7行找到了这样的描述:“同样需要引起注意的是线程可以递归拥有同步块”。即同一线程可以递归调用lock语句。 此处参考:点这里
async/await
await/async:是个新语法,出现C#5.0 .NetFramework在4.5及以上(CLR4.0)
是一个语法糖,不是一个全新的异步多线程使用方式
(语法糖就是编译器提供的新功能)
本身并不会产生新的多线程,但是依托于Task而存在,
所以程序执行时,也是有多线程的
async可以随便添加,可以不用await
await只能出现在task前面,但是方法必须声明async,不能单独出现
await/async 之后,原本没有返回值的,可以返回Task
原本返回X类型的,可以返回Task<X>
一般来说,尽量不要再返回void,因为不能再await
可以认为,加了await 就等于将 await 后面的代码,包装成一个回调——其实回调的线程具有多种可能性,回调回来执行的线程不确定
可以用同步编码的形式去写异步
底层是用递归实现的
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库