多线程的操作在程序中也是比较常见的, 比如开启一个线程执行一些比较耗时的操作(IO 操作), 而主线程继续执行当前操作, 不会造成主线程阻塞. 线程又分为前台线程和后台线程, 区别是: 整个程序必须要运行完前台线程才会退出, 而后台线程会在程序退出的时候结束掉. Thread 默认创建的是前台线程, 而 ThreadPool 和 Task 默认创建的是后台线程, Thread 可以通过设置 IsBackground 属性将线程设置为后台线程.
- static void Main(string[] args)
- {
- Thread thread = new Thread(new ThreadStart(NoParameterMethod));
- thread.Start();
- Console.WriteLine("程序已经执行完成");
- }
- static void NoParameterMethod()
- {
- Thread.Sleep(1000);
- Console.WriteLine("NoParameterMethod");
- }
前台线程
效果:
- static void Main(string[] args)
- {
- Thread thread = new Thread(new ThreadStart(NoParameterMethod))
- {
- IsBackground = true
- };
- thread.Start();
- Console.WriteLine("程序已经执行完成");
- }
- static void NoParameterMethod()
- {
- Thread.Sleep(1000);
- Console.WriteLine("NoParameterMethod");
- }
后台线程
效果:
下面来说一下几种开启多线程的方法:
1,Thread
1.1 开启一个线程, 执行一个不带参数的方法
- static void Main(string[] args)
- {
- Thread thread = new Thread(new ThreadStart(NoParameterMethod));
- // 注意 Start 开启线程之后, 当前线程不是说一定会立马执行
- // 而是说当前线程已经准备好被 CPU 调用, 至于 CPU 什么时候调用是需要看情况而定
- thread.Start();
- Console.WriteLine("程序已经执行完成");
- }
- static void NoParameterMethod()
- {
- // 使当前线程停止 1s
- Thread.Sleep(1000);
- Console.WriteLine("NoParameterMethod");
- }
1.2 开启一个线程, 执行带参数的方法
- static void Main(string[] args)
- {
- Thread thread = new Thread(new ParameterizedThreadStart(ParameterMethod));
- // 要传入的参数在 Start 的时候传入
- thread.Start("ParameterMethod");
- Console.WriteLine("程序已经执行完成");
- }
- // 参数类型必须为 Object 类型, 方法只能有一个参数
- // 如果想传入多个参数, 可已将参数封装进入一个类中
- static void ParameterMethod(Object x) {
- Thread.Sleep(1000);
- Console.WriteLine(x);
- }
- 2,ThreadPool
使用 ThreadPool 开启一个线程
- // 无参 Thread.CurrentThread.ManagedThreadId 是当前线程的唯一标识符
- ThreadPool.QueueUserWorkItem(new WaitCallback(obj => Console.WriteLine(Thread.CurrentThread.ManagedThreadId)));
- // 有参
- ThreadPool.QueueUserWorkItem(new WaitCallback(obj => Console.WriteLine(Thread.CurrentThread.ManagedThreadId)), "参数");
ThreadPool 是 Thread 的一个升级版, ThreadPool 是从线程池中获取线程, 如果线程池中又空闲的元素, 则直接调用, 如果没有才会创建, 而 Thread 则是会一直创建新的线程, 要知道开启一个线程就算什么事都不做也会消耗大约 1m 的内存, 是非常浪费性能的, 接下来我们写一个例子来看一下二者的区别:
- #region 使用 Thread 开启 100 个线程
- for (int i = 0; i <100; i++)
- {
- (new Thread(new ThreadStart(() => Console.WriteLine(Thread.CurrentThread.ManagedThreadId)))).Start();
- }
- #endregion
运行结果:
我们可以看到每一个主线程表示 id 都是不同的, 也就是说使用 Thread 开启线程每次都是新创建一个
- #region 使用 ThreadPool 开启 100 个线程
- for (int i = 0; i <100; i++)
- {
- ThreadPool.QueueUserWorkItem(new WaitCallback(obj => Console.WriteLine(Thread.CurrentThread.ManagedThreadId)));
- }
- #endregion
运行结果:
相信区别已经很明显了, 这里我再说一下, 线程池中一开始是没有一个线程的, 使用 ThreadPool 开启一个线程之后, 线程执行完毕, 会加入到线程池中, 后续需要再次开启线程的时候查看线程池中有没有空闲的线程, 有则调用, 没有则创建, 如此循环
二者之间还有一个区别, 就是 ThreadPool 可以操控线程的状态, 比如等待线程完成, 或者终止超时子线程操作
取消子线程操作
- CancellationTokenSource cts = new CancellationTokenSource();
- ThreadPool.QueueUserWorkItem(new WaitCallback(CanCancelMethod),cts.Token);
- cts.Cancel();
- Console.ReadKey();
- static void CanCancelMethod(Object obj) {
- CancellationToken ct = (CancellationToken)obj;
- if (ct.IsCancellationRequested) {
- Console.WriteLine("该线程已取消");
- }
- // 就算 ct.IsCancellationRequested 为真, 接下来的代码还是会执行
- // 因为该方法并没有 ruturn
- Thread.Sleep(1000);
- Console.WriteLine($"子线程 {Thread.CurrentThread.ManagedThreadId} 结束");
- }
感觉这个取消子线程的方法和设置一个全局变量, 然后通过判断和更改全局变量的值, 设置线程是否取消的效果一样
ThreadPool 的其他操作感兴趣的可以自己搜索学一下, 因为终止线程什么操作都是比较麻烦的, 关于 ThreadPool 就不再多说了
3,Task
Task 和 ThreadPool 是一样的, 都是从线程池中取空闲的线程
使用 Task 开启一个线程
- // 方法 1 使用 Task 的 Run 方法
- Task.Run(()=> {
- Console.WriteLine($"线程 {Thread.CurrentThread.ManagedThreadId} 已开启");
- });
- // 方法 2 使用 Task 工厂类 TaskFactory 对象的 StartNew 方法
- (new TaskFactory()).StartNew(() =>
- {
- Console.WriteLine($"线程 {Thread.CurrentThread.ManagedThreadId} 已开启");
- });
Run 和 StartNew 方法都是返回一个 Task 类型的对象, 代表当前开启的线程, 如果方法有返回值
- // 如果方法有返回值
- Task<int> t1 = Task.Run<int>(() => {
- return 1;
- });
- // 通过 t1.Result 查看返回的结果
- Console.WriteLine(t1.Result);
取消线程操作的话和 ThreadPool 取消线程操作一样
- //1s 后自动取消线程
- CancellationTokenSource cts = new CancellationTokenSource(1000);
- // 为取消线程注册回调函数
- cts.Token.Register(()=> {
- Console.WriteLine("线程已取消");
- });
- Task.Run(()=> {
- Console.WriteLine("开始执行");
- Thread.Sleep(2000);
- // 判断当前线程是否已被取消
- if (cts.Token.IsCancellationRequested) {
- Console.WriteLine("方法已结束");
- return;
- }
- Console.WriteLine("线程继续执行");
- },cts.Token);
等待所有线程执行完毕
- // 存放所有线程
- List<Task> lst = new List<Task>();
- // 开启 10 个线程
- for (int i = 0;i <10;i++) {
- lst.Add(Task.Run(()=> {
- Thread.Sleep(new Random().Next(1,3000));
- Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}");
- }));
- }
- // 等待所有线程执行完毕
- Task.WaitAll(lst.ToArray());
- Console.WriteLine("所有线程执行完毕");
等待任意一个先线程执行完毕
- // 存放所有线程
- List<Task> lst = new List<Task>();
- // 开启 10 个线程
- for (int i = 0;i <10;i++) {
- lst.Add(Task.Run(()=> {
- Thread.Sleep(new Random().Next(1,3000));
- Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}");
- }));
- }
- // 等待任意线程执行完毕
- Task.WaitAny(lst.ToArray());
- Console.WriteLine("已有现成执行完毕");
对于 Thread,ThreadPool 和 Task, 如果要用多线程的话, 优先使用 Task, 如果版本不支持 Task, 则考虑 ThreadPool
4,Parallel
Parallel 循环开启多线程, 并行任务, 对于多线程开启任务, 开启的顺序都是不确定的
- Parallel.Invoke()
- Action[] action = new Action[] {
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- };
- Parallel.Invoke(action);
相当于
- Action[] action = new Action[] {
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- };
- for (int i = 0; i <action.Length; i++)
- {
- Task.Run(action[i]);
- }
Invoke 时也可以进行一些配置, 例如配置线程池中只能最多保持一个线程
- Action[] action = new Action[] {
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- ()=>Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}"),
- };
- Parallel.Invoke(new ParallelOptions()
- {
- MaxDegreeOfParallelism = 1
- }, action);
运行结果:
- Parallel.For()
- // 将迭代的结果保存起来
- ParallelLoopResult plr = Parallel.For(1, 10, (i) =>
- {
- Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}");
- });
- Console.WriteLine(plr.IsCompleted);
相当于
- for (int i = 1; i <10; i++)
- {
- Task.Run(() =>
- {
- Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}");
- });
- }
相对于循环 Task.Run()更加简洁
Parallel.ForEach()
方法和 foreach 类似, 不过是采用的是异步方式遍历, 要想被 Parallel.ForEach()必须实现 IEnumerable 接口
- Parallel.ForEach<String>(new List<String>() {
- "a","b","c","d","e","f","g","h","i"
- }, (str) =>
- {
- Console.WriteLine(str);
- });
运行结果:
停止循环的方法
- // 将迭代的结果保存起来
- ParallelLoopResult plr = Parallel.For(1, 10, (i,state) =>
- {
- Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId}");
- if (i==4) {
- // 结束
- state.Break();
- }
- });
- Console.WriteLine(plr.IsCompleted);
5,Async,Await
async 和 await 关键字用来实现异步编程, async 用来修饰方法, await 用来调用方法, await 关键字必须出现在有 async 的方法中, await 调用的方法可以不用 async 关键字修饰, 但是返回值类型必须为 Task<T > 类型, 下面来说一下用法:
- static void Main(string[] args)
- {
- Demo1();
- Console.ReadKey();
- }
- static async void Demo1()
- {
- await Demo2();
- }
- static async Task<int> Demo2()
- {
- return 1;
- }
await 开启异步和 Task 开启异步还是有区别的
例如下面两个例子
我们先用 Task 开启异步编程
- static void Main(string[] args)
- {
- Console.WriteLine("主线程开始");
- TaskDemo1();
- Console.WriteLine("主线程结束");
- Console.ReadKey();
- }
- static void TaskDemo1() {
- Console.WriteLine("异步开始");
- Task.Run<int>(() =>
- {
- return TaskDemo2();
- });
- Console.WriteLine("异步结束");
- }
- static int TaskDemo2()
- {
- Console.WriteLine("子线程开始");
- Thread.Sleep(1000);
- Console.WriteLine("子线程结束");
- return 1;
- }
我们这是可以大胆的猜测一下显示的顺寻
大致应该是: 主线程开始 ==》异步开始 ==》(子线程开始 | 异步结束)=》(子线程开始 | 主线程结束)==》(子线程开始)=》子线程结束
运行结果:
果然和我们猜想的差不多, 大致顺序没有变, 接下来我们用 async 和 await 关键字开启异步
- static void Main(string[] args)
- {
- Console.WriteLine("主线程开始");
- AsyncDemo1();
- Console.WriteLine("主线程结束");
- Console.ReadKey();
- }
- static async void AsyncDemo1()
- {
- Console.WriteLine("异步开始");
- await AsyncDemo2();
- Console.WriteLine("异步结束");
- }
- static async Task<int> AsyncDemo2()
- {
- Console.WriteLine("子线程开始");
- // 当前子线程暂停 1s
- await Task.Delay(1000);
- Console.WriteLine("子线程结束");
- return 0;
- }
按理说顺序也会是: 主线程开始 ==》异步开始 ==》(子线程开始 | 异步结束)=》(子线程开始 | 主线程结束)==》(子线程开始)=》子线程结束
但事实是:
Task 和 async&await 关键字的区别就此处
首先说一下梳理一下 Task 的执行过程(画图画的很粗糙, 重点是流程)
然后我们再来看一下 async 和 await 的执行过程
现在问题已经很清晰了, 就是当主线程执行到 await AsyncDemo2()时, 会像是碰到了 return 语句一样, 退出当前方法 (AsyncDemo1), 将当前方法(AsyncDemo1) 的后续执行语句交给子线程来执行, 子线程会在执行完 AsyncDemo2 方法之后, 返回过来执行 AsyncDemo1 方法.
这一点就是 await 与 Task 异步编程的不同点
来源: https://www.cnblogs.com/ckka/p/11336759.html