文章目录
提示:以下是本篇文章正文内容,下面案例可供参考
一、进程、线程、句柄,同步、异步
进程:就是计算机概念,一种虚拟的记录,描述一个应用在计算机上运行的时候,所消耗的各种资源你的集合。例如:CPU,磁盘,网络资源。
线程:某一个程序进程中所执行的具体的某一个动作的最小执行流。
句柄:句柄是对系统资源的抽象引用,程序员通常不需要知道具体的内存地址或资源的物理布局,只需通过句柄来操作这些资源。
同步:主要涉及多个进程、线程或操作的协调,以确保它们以一种有序和一致的方式运行。
异步(Asynchronous):编程是一种编程范式,它允许某些任务在后台运行,而不会阻塞(即停止执行)主程序流程。在异步操作中,程序可以在等待异步任务完成的同时继续执行其他任务,这对于提高应用程序的响应性和性能尤其重要
二、Task
Task和prallel的所操作的线程都是来自线程池。
1.线程开启
开启线程方式:
Task task = new Task(new Action(() =>
{
Console.WriteLine("Hello World!");
}));
task.Start(); //开启线程
Task.Run(() =>
{
Console.WriteLine("Hello World!");
});
Task.Run<int>(() =>
{
return DateTime.Now.Year;
});
TaskFactory factory = Task.Factory;
factory.StartNew(() =>
{
Console.WriteLine("Hello World!");
});
TaskFactory factory = new TaskFactory();
Task<int> tTask = factory.StartNew<int>(() => DateTime.Now.Year);
父子级线程:
一个Task内部,开启了几个线程以后;Task内部的线程可以理解为子线程;
父线程在等待的时候,子线程没有线程等待;子线程可能内容还没有执行万一,父线程就已经结束了;
Task task = new Task(() =>
{
Debug.WriteLine($"task开始了: {Thread.CurrentThread.ManagedThreadId.ToString("00")} ");
Task task1 = new Task(() =>
{
Debug.WriteLine($"task1: {Thread.CurrentThread.ManagedThreadId.ToString("00")} ");
Thread.Sleep(1000);
Debug.WriteLine("我是task1线程");
});
Task task2 = new Task(() =>
{
Debug.WriteLine($"task2: {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
Thread.Sleep(1000);
Debug.WriteLine("我是task2线程");
});
task1.Start();
task2.Start();
Debug.WriteLine($"task: {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
});
task.Start();//线程启动了
task.Wait(); //单个线程的等待;等待task 内部的内容执行完毕,这里会卡顿界面;是主线程等待;
//TaskCreationOptions.PreferFairness 相对来说比较公平执行的:如果是先申请的线程,就会优先执行;
Task task1= new Task(() =>
{
}, TaskCreationOptions.PreferFairness);
Task task2 = new Task(() =>
{
}, TaskCreationOptions.PreferFairness);
2.线程等待
Task task = Task.Run(() =>
{
Debug.WriteLine("Hello World");
});
task.Wait(); //等待这个线程执行完毕
Thread.Sleep(100); //主线程执行到这里会卡顿等待,100ms后继续执行
Task.Delay(300); //不卡顿界面延迟300ms继续执行逻辑
//不卡顿界面:言辞3000ms以后,去执行一段业务逻辑:执行的动作就是ContinueWith内部的委托;
//ContinueWith内部的执行有可能是一个全新的线程去执行,也有可能是主线程去执行;
Task.Delay(3000).ContinueWith(t =>
{
Debug.WriteLine($"Delay || 线程ID: {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
stopwatch.Stop();
Debug.WriteLine($"Task.Delay(3000):{stopwatch.ElapsedMilliseconds}");
});
3.线程等待的多种方式
场景模拟:现在需要多人同时开发一个项目,那么我们就可以开启多线程去完成。
List<Task> taskList = new List<Task>();
TaskFactory factory = new TaskFactory();
taskList.Add(factory.StartNew(() =>Coding("A","负责权限的数据库设计")));
taskList.Add(factory.StartNew(() => Coding("B","负责WeChat接口对接")));
taskList.Add(factory.StartNew(() => Coding("C","负责脚手架框架搭建")));
taskList.Add(factory.StartNew(() => Coding("D","负责ABPVNetxt框架搭建")));
taskList.Add(factory.StartNew(() => Coding("E","负责编写Webapi")));
正常情况下我们我们不需要去考虑完成的前后顺序,那么我要在上面任何以为的工作完成之后,F来搭建环境要如何处理呢?
//方案一:
//Task.WaitAny:等待几个线程中的某一个线程执行结束,主线程会等待,会卡顿界面,一直到某一个线程执行结束后,才往后执行。体验不好 !!!
Task.WaitAny(taskList.ToArray());
Debug.WriteLine($"F准备环境");
//方案二:
//相当于是一个回调;主线程执行的时候,不会阻塞UI界面;当taskList集合中的某一个线程执行结束语了;就触发后面委托中的动作;
factory.ContinueWhenAny(taskList.ToArray(), ts =>
{
Debug.WriteLine($"F准备环境");
});
如果所有人开发完毕后,要正式上线要如何处理呢?
//方案一:
//主线程等待,阻塞UI界面,其中某一个线程执行结束,然后继续往后执行;
Task.WaitAny(taskList.ToArray());
Task.WaitAll(taskList.ToArray());
Debug.WriteLine($"正式上线!!!");
//方案二:
factory.ContinueWhenAll(taskList.ToArray(), ts =>
{
Debug.WriteLine($"正式上线!!!");
});
演变=》如果要知道那个最先开发完成,我们也可以通过状态来判断。
List<Task> taskList = new List<Task>();
TaskFactory factory = new TaskFactory();
taskList.Add(factory.StartNew(obj => Coding("A","负责权限的数据库设计"),"A"));
taskList.Add(factory.StartNew(obj => Coding("B","负责WeChat接口对接"), "B"));
taskList.Add(factory.StartNew(obj => Coding("C","负责脚手架框架搭建"), "C"));
taskList.Add(factory.StartNew(obj => Coding("D","负责ABPVNetxt框架搭建"), "D"));
taskList.Add(factory.StartNew(obj => Coding("E","负责编写Webapi"), "E"));
factory.ContinueWhenAny(taskList.ToArray(), ts =>
{
Debug.WriteLine($"{ts.AsyncState}开发完成,,F准备环境");
});
factory.ContinueWhenAll(taskList.ToArray(), ts =>
{
Debug.WriteLine($"正式上线");
});
Console.Read();
void Coding(string name, string projectName)
{
Debug.WriteLine($"Coding Start || {name} {projectName} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
long lResult = 0;
for (int i = 0; i < 1_000_000_000; i++)
{
lResult += i;
}
Thread.Sleep(3000);
Debug.WriteLine($"Coding End || {name} {projectName} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}");
}
运行结果:
4.线程等待应用场景
ContinueWhenAny:如果需要在一堆任务中,某一种执行结束后,去触发一个动作;
ContinueWhenAll:如果需要回调和需要控制顺序
WaitAny:如果有不确定的数据源,比如可能是缓存,可能是数据库,可能是第三方接口的时候,我们可以使用WaitAny,只要查到任何一种数据源的数据就直接返回
WaitAll:例如有多个模块信息需要同时加载时。
我之前在做首页优化接口时,也是使用多线程加载各个模块数据:
await Task.WhenAll(
Task.Run(async () =>
{
//首页模块A
}),
Task.Run(async () =>
{
//首页模块B
}),
Task.Run(async () =>
{
//首页模块C
})
);
三、Parallel
1.可以传入多个委托
2.多个委托中的内容是会开启线程来执行—执行这里的线程有可能是新的线程,也有可能是主线程参与计算的
3.会阻塞主线程—相当于是主线程等待子线程执行结束
List<Action> acitonList = new List<Action>();
acitonList.Add(() => { });
acitonList.Add(() => { });
acitonList.Add(() => { });
Parallel.Invoke(acitonList.ToArray());//也是可以开启线程,可以一次放入很多个委托去执行;
我们可以看下这个的运行结果:
Task.Run(() =>
{
Parallel.Invoke(
() =>
{
Debug.WriteLine($"线程ID: {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
this.DoSomething("this is Action 01");
},
() =>
{
Debug.WriteLine($"线程ID: {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
this.DoSomething("this is Action 02");
},
() =>
{
Debug.WriteLine($"线程ID: {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
this.DoSomething("this is Action 03");
});
});
如果想要不阻塞主线程,可以在外围包一个Task.Run。Parallel是基于Task一个封装;
那么Parallel有什么作用呢?
如果有一大堆的任务要执行;100个任务—需要多线来执行:100个任务要开启100个线程?
Parallel.For(0,100,index=>
{
//执行任务
})
那么我们Parallel最大的优势就是可以控制线程的数量,不至于出现线程泛滥的情况
ParallelOptions options = new ParallelOptions();
//开启5个线程去去执行100个任务
options.MaxDegreeOfParallelism =5
Parallel.ForEach(intlist, options, s =>
{
//执行任务
});
四、多线程的异步处理
我们在日常代码中常常使用try…catch…finally…去捕捉到异常,那么我们在多线程中要如何处理呢?
1.在多线程中,如果发生异常,使用try-catch包裹,是捕捉不到异常的,异常肯定是被吞掉了
2.多线程中,如果要捕捉异常,需要设置主线程等待子线程执行结束;可以捕捉到异常
3.多线程内部发生异常后,抛出的异常类型是:system.AggregateException(多线程的异常类型)
当多个线程发生异常要如何捕捉到呢?我们可以做个demo进行测试
try
{
List<Task> tasklist = new List<Task>();
for (int i = 0; i < 20; i++)
{
string keywork = $"Click_{i}";
tasklist.Add(Task.Run(() =>
{
Thread.Sleep(new Random().Next(50, 100));
if (keywork == "Click_6")
{
throw new Exception("Click_6");
}
else if (keywork == "Click_9")
{
throw new Exception("Click_9");
}
else if (keywork == "Click_12")
{
throw new Exception("Click_12");
}
}));
}
Task.WaitAll(tasklist.ToArray());
}
catch (AggregateException aex) //具体
{
Debug.WriteLine(aex.Message);
foreach (var exception in aex.InnerExceptions)
{
Console.WriteLine(exception.Message);
}
}
catch (Exception ex) //父类(Exception 一切异常的父类) 抽象
{
Debug.WriteLine(ex.Message);
}
我们可以看到AggregateException 会有异常集合!!!
那么我们模拟一个场景,如果三个线程去执行一个业务逻辑体(查询:线程1,执行修改数据库;线程2,执行修改缓存,线程3,执行一个调用接口去修改另外一个服务器上的数据),如果中间有某一个线程出现异常了,那就代表整个逻辑有问题,那要如何处理?
多线程执行的时候,有时候,必须是多个线程都执行成功,才算成功,只要有一个线程异常了,就表示都异常----既然都是异常了,就应该让线程取消,逻辑取消
五、线程取消
线程取消:线程是无法从外部取消的(除非关闭进程),只能自己取消自己。
如何执行标准的线程取消?
1.实例化一个 CancellationTokenSource
2.包含了一个IsCancellationRequested 属性,属性值默认为false
3.包含了一个Cancel方法。Cancel的作用:方法如果被执行,CancellationTokenSource内部的属性值由false变为true,且只能从false变为true
demo:
try
{
List<Task> tasks = new List<Task>();
CancellationTokenSource cts = new CancellationTokenSource();
for (int i = 0; i < 100; i++) //开启并发线程
{
string keywork = $"task_{i}";
Task.Run(() =>
{
Thread.Sleep(new Random().Next(10, 300)); //随机休眠
if (cts.IsCancellationRequested == false)
{
Debug.WriteLine($" 线程ID: {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} 正常开始了");
}
else
{
Debug.WriteLine($" 线程ID: {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} 没有正常开始了");
}
try
{
if (keywork == "task_9")
{
throw new Exception("task_9");
}
if (keywork == "task_6")
{
throw new Exception("task_6");
}
}
catch (Exception)
{
cts.Cancel(); //取消线程
}
if (cts.IsCancellationRequested == false)
{
Debug.WriteLine($" 线程ID: {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} 正常结束了。。。");
}
else
{
Debug.WriteLine($" 线程ID: {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} 没有正常结束了。。。");
}
});
}
}
catch (AggregateException aex)
{
foreach (var exception in aex.InnerExceptions)
{
Debug.WriteLine(exception.Message);
}
}
你们可以自己拿这个demo执行看结果。。。。
4…当有一个线程发生异常的时候,其他线程就会有三种情况:1.已经结束的线程无法去处理,2.已经正常开始,我可以让你取消,让这线程在结束的时候,抛出异常(取消了);3.还有部分的线程根本都还没有开启:只要有异常的线程,就应该让没有开启的线程不再开启了。
try
{
List<Task> tasks = new List<Task>();
CancellationTokenSource cts = new CancellationTokenSource();
for (int i = 0; i < 100; i++)
{
string keywork = $"Task_{i}";
tasks.Add(Task.Run(() =>
{
Thread.Sleep(new Random().Next(10, 300));
cts.Token.ThrowIfCancellationRequested();
try
{
//为什么要先定义一个字符串,然后在这里判断?
if ($"Task_{i}" == "Task_9")
{
throw new Exception("Task_9");
}
if (keywork == "Task_6")
{
throw new Exception("Task_6");
}
}
catch (Exception)
{
cts.Cancel();
}
cts.Token.ThrowIfCancellationRequested();
}, cts.Token)); //有线程发生异常后,还没有开启的线程就不再开启了
}
Task.WaitAll(tasks.ToArray());
}
catch (AggregateException aex)
{
foreach (var exception in aex.InnerExceptions)
{
Debug.WriteLine(exception.Message);
}
}
输出:
这是表示有剩余未开启的线程已经取消
六、 线程安全
线程安全:一段完整的业务逻辑,单线程执行和多线程执行后的结果如果完全一致,是线程安全的;否则就表示是线程不安全;
这个就是最直接的线程安全问题:
因为我们开启的是多线程,其中就可能在同一个时刻出现抢占资源的情况,所以会出现线程安全问题。
那要如何解决线程安全问题呢?
1.加锁,标准锁,锁的本质是独占引用。可以解决线程安全问题,加锁是反多线程,会影响性能。
private readonly static object obj = new object();
2.使用单线程,把要操作的整块数据做一个切分。例如9w条数据,分成三块,再开启多线程,每个线程去执行3W条数据,等待三个线程执行结束后,在单线程做一个汇总。(这个需要将程序加以设计)
3.使用线程安全对象。
这是系统提供的三种线程安全数据集
BlockingCollection<int> blockinglist = new BlockingCollection<int>();
ConcurrentBag<int> conocurrentbag = new ConcurrentBag<int>();
ConcurrentDictionary<string, int> concurrentDictionary = new ConcurrentDictionary<string, int>();
ConcurrentQueue<int> concurrentQueue = new ConcurrentQueue<int>();
ConcurrentStack<int> concurrentStack = new ConcurrentStack<int>();
七、中间变量
为什么我们每次循环开启多线程,i一直是等于5的呢?
因为Task开启线程的时候是延迟开启的,延迟的时间很短。循环并不会阻塞主线程,当有一个线程在正式的执行业务逻辑的时候,循环已经结束了,所以会取到最后的值!!!
如何解决?
可以另外定义个变量,在每次循环的时候赋值,循环多少次就会有多少个变量(k),每个线程使用的是每一次循环内部的变量(k)
总结
如果还有其他思路请在评论区指出,我们继续完善,,往共勉!!!!!!!