.net 多线程编程

并行和并发

并发:指应用能够交替执行不同的任务,比如单 CPU 核心下执行多线程并非是 同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不 断去切换这两个任务,已达到"同时执行效果",其实并不是的,只是计算机的速度太 快,我们无法察觉到而已。

并行:指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边打电话, 这两件事情可以同时执行

两者区别:一个是交替执行,一个是同时执行。

为什么要使用多线程编程

充分利用 CPU 的资源
从上面的 CPU 的介绍,可以看的出来,现在市面上没有 CPU 的内核不使用多线 程并发机制的,特别是服务器还不止一个 CPU,如果还是使用单线程的技术做思路, 明显就 out 了。因为程序的基本调度单元是线程,并且一个线程也只能在一个 CPU 的一个核的一个线程跑,如果你是个 i3 的 CPU 的话,最差也是双核心 4 线程的运算 能力:如果是一个线程的程序的话,那是要浪费 3/4 的 CPU 性能:如果设计一个多线 程的程序的话,那它就可以同时在多个 CPU 的多个核的多个线程上跑,可以充分地 利用 CPU,减少 CPU 的空闲时间,发挥它的运算能力,提高并发量。

就像我们平时坐地铁一样,很多人坐长线地铁的时候都在认真看书,而不是为 了坐地铁而坐地铁,到家了再去看书,这样你的时间就相当于有了两倍。这就是为 什么有些人时间很充裕,而有些人老是说没时间的一个原因,工作也是这样,有的 时候可以并发地去做几件事情,充分利用我们的时间,CPU 也是一样,也要充分利用。

加快响应用户的时间
比如我们经常用的迅雷下载,都喜欢多开几个线程去下载,谁都不愿意用一个 线程去下载,为什么呢?答案很简单,就是多个线程下载快啊。

我们在做程序开发的时候更应该如此,特别是我们做互联网项目,网页的响应 时间若提升 1s,如果流量大的话,就能增加不少转换量。做过高性能 web 前端调优 的都知道,要将静态资源地址用两三个子域名去加载,为什么?因为每多一个子域 名,浏览器在加载你的页面的时候就会多开几个线程去加载你的页面资源,提升网 站的响应速度。多线程,高并发真的是无处不在。

可以使你的代码模块化,异步化,简单化
例如我们实现电商系统,下订单和给用户发送短信、邮件就可以进行拆分, 将给用户发送短信、邮件这两个步骤独立为单独的模块,并交给其他线程去执行。 这样既增加了异步的操作,提升了系统性能,又使程序模块化,清晰化和简单化。

多线程程序需要注意什么

线程之间的安全性
从前面的章节中我们都知道,在同一个进程里面的多线程是资源共享的,也就 是都可以访问同一个内存地址当中的一个变量。例如:若每个线程中对全局变量、 静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的:若有多 个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

线程之间的死锁
为了解决线程之间的安全性引入了 Java 的锁机制,而一不小心就会产生 Java 线程死锁的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从 而导致所有的工作都无法完成。假设有两个线程,分别代表两个饥饿的人,他们必 须共享刀叉并轮流吃饭。他们都需要获得两个锁:共享刀和共享叉的锁。
假如线程 A 获得了刀,而线程 B 获得了叉。线程 A 就会进入阻塞状态来等待 获得叉,而线程 B 则阻塞来等待线程 A 所拥有的刀。这只是人为设计的例子,但尽 管在运行时很难探测到,这类情况却时常发生

线程太多了会将服务器资源耗尽形成死机宕机
线程数太多有可能造成系统创建大量线程而导致消耗完系统内存以及 CPU 的“过渡切换”,造成系统的死机

今天的主角 Task

Task介绍
Task是在ThreadPool的基础上推出的,我们简单了解下ThreadPool。ThreadPool中有若干数量的线程,如果有任务需要处理时,会从线程池中获取一个空闲的线程来执行任务,任务执行完毕后线程不会销毁,而是被线程池回收以供后续任务使用。当线程池中所有的线程都在忙碌时,又有新任务要处理时,线程池才会新建一个线程来处理该任务,如果线程数量达到设置的最大值,任务会排队,等待其他任务释放线程后再执行。线程池能减少线程的创建,节省开销

Task的创建

			1.new方式实例化一个Task,需要通过Start方法启动
            Task<string> task = new Task<string>(() =>
            {
                return $"hello, task1的ID为{Thread.CurrentThread.ManagedThreadId}";
            });
            task.Start();

            2.Task.Factory.StartNew(Func func)创建和启动一个Task
            Task<string> task2 =Task.Factory.StartNew<string>(() =>
            {
                return $"hello, task2的ID为{ Thread.CurrentThread.ManagedThreadId}";
            });

            3.Task.Run(Func func)将任务放在线程池队列,返回并启动一个Task
            Task<string> task3= Task.Run<string>(() =>
            {
                return $"hello, task3的ID为{ Thread.CurrentThread.ManagedThreadId}";
            });

Task的阻塞方法(Wait/WaitAll/WaitAny)
task.Wait() 表示等待task执行完毕;
Task.WaitAll(Task[] tasks) 表示只有所有的task都执行完成了再解除阻塞;
Task.WaitAny(Task[] tasks) 表示只要有一个task执行完毕就解除阻塞

private static void TaskWait()
        {
            Task task1 = Task.Run(() =>
            {
                Thread.Sleep(100);
                Console.WriteLine($"task1的线程ID为{ Thread.CurrentThread.ManagedThreadId}");
            });
            task1.Wait();
            Console.WriteLine(" TaskWait的主线程!!");
        }

        private static void TaskWaitAll()
        {
            Task task1 = Task.Run(() =>
            {
                Thread.Sleep(100);
                Console.WriteLine($"task1的线程ID为{ Thread.CurrentThread.ManagedThreadId}");
            });
            Task task2 = Task.Run(() =>
            {
                Thread.Sleep(100);
                Console.WriteLine($"task2的线程ID为{ Thread.CurrentThread.ManagedThreadId}");
            });
            Task.WaitAll(task1, task2);
            Console.WriteLine("TaskWaitAll的主线程!!");
        }

        private static void TaskWaitAny()
        {
            Task task1 = Task.Run(() =>
            {
                Thread.Sleep(100);
                Console.WriteLine($"task1的线程ID为{ Thread.CurrentThread.ManagedThreadId}");
            });
            Task task2 = Task.Run(() =>
            {
                Thread.Sleep(10000);
                Console.WriteLine($"task2的线程ID为{ Thread.CurrentThread.ManagedThreadId}");
            });
            Task.WaitAny(task1, task2);
            Console.WriteLine("TaskWaitAny的主线程!!");
        }

执行结果
在这里插入图片描述

Task的延续操作(WhenAny/WhenAll/ContinueWith)
上边的Wait/WaitAny/WaitAll方法返回值为void,这些方法单纯的实现阻塞线程。我们现在想让所有task执行完毕(或者任一task执行完毕)后,开始执行后续操作,怎么实现呢?这时就可以用到WhenAny/WhenAll方法了,这些方法执行完成返回一个task实例。 task.WhenAll(Task[] tasks) 表示所有的task都执行完毕后再去执行后续的操作, task.WhenAny(Task[] tasks) 表示任一task执行完毕后就开始执行后续操作。

       private static void TaskContinueWith()
        {
            Task task1 = Task.Run(() =>
            {
                Console.WriteLine($"task1的线程ID为{ Thread.CurrentThread.ManagedThreadId}");
                Thread.Sleep(1000);
            }).ContinueWith(task=>
            {
                Console.WriteLine($"task1的延续线程ID为{ Thread.CurrentThread.ManagedThreadId}");
            });
            task1.Wait();
            Console.WriteLine("TaskWait的主线程!!");
        }

		private static void TaskWhenAll()
        {
            Task task1 = Task.Run(() =>
            {
                Thread.Sleep(100);
                Console.WriteLine($"task1的线程ID为{ Thread.CurrentThread.ManagedThreadId}");
            });
            Task task2 = Task.Run(() =>
            {
                Thread.Sleep(100);
                Console.WriteLine($"task2的线程ID为{ Thread.CurrentThread.ManagedThreadId}");
            });
            Task.WhenAll(task1, task2).ContinueWith(task=>
            {
                Console.WriteLine($"task1、task2执行完成后的延续线程ID为{ Thread.CurrentThread.ManagedThreadId}");
            });
            Console.WriteLine("TaskWaitAll的主线程!!");
        }
        
		private static void TaskWhenAny()
        {
            Task task1 = Task.Run(() =>
            {
                Thread.Sleep(100);
                Console.WriteLine($"task1的线程ID为{ Thread.CurrentThread.ManagedThreadId}");
            });
            Task task2 = Task.Run(() =>
            {
                Thread.Sleep(10000);
                Console.WriteLine($"task2的线程ID为{ Thread.CurrentThread.ManagedThreadId}");
            });
            Task.WhenAny(task1, task2).ContinueWith(task=> {
                Console.WriteLine($"task1、task2其中一个执行完成后的延续线程ID为{ Thread.CurrentThread.ManagedThreadId}");
            });
            Console.WriteLine("TaskWaitAny的主线程!!");
        }

执行结果
在这里插入图片描述

在这里插入图片描述
通过执行结果,可以发现WhenAll/WhenAny是不会阻塞主线程的。

Task的任务取消(CancellationTokenSource)
Task中有一个专门的类 CancellationTokenSource 来取消任务执行
CancellationTokenSource的功能不仅仅是取消任务执行,我们可以使用 source.CancelAfter(5000)实现5秒后自动取消任务,也可以通过 source.Token.Register(Action action)注册取消任务触发的回调函数,即任务被取消时注册的action会被执行

private static void TaskCancellationTokenSource()
        {
            CancellationTokenSource source = new CancellationTokenSource();
            //注册任务取消的事件
            source.Token.Register(() =>
            {
                Console.WriteLine("任务被取消后执行xx操作!");
            });

            int index = 0;
            //开启一个task执行任务
            Task task1 = new Task(() =>
            {
                while (!source.IsCancellationRequested)
                {
                    Thread.Sleep(1000);
                    Console.WriteLine($"第{++index}次执行,线程运行中...");
                }
            });
            task1.Start();
            //延时取消,效果等同于Thread.Sleep(5000);source.Cancel();
            source.CancelAfter(5000);
            Console.ReadKey();
        }

执行结果
在这里插入图片描述

主角二号 Parallel

该类是System.Threading.Tasks下所提供的一个静态类。
Parallel.Invoke
试图将很多方法并行运行,如果传入的是4个方法,则至少需要4个逻辑内核才能足以让这4个方法并发运行,逻辑内核也称为硬件线程。

需要注意的是:
1.即使拥有4个逻辑内核,也不一定能够保证所需要运行的4个方法能够同时启动运行,如果其中的一个内核处于繁忙状态,那么底层的调度逻辑可能会延迟某些方法的初始化执行。
2.通过Parallel.Invoke编写的并发执行代码一定不能依赖与特定的执行顺序,因为它的并发执行顺序也是不定的。
3.使用Parallel.Invoke方法一定要测量运行结果、实现加速比以及逻辑内核的使用率,这点很重要。
4.使用Parallel.Invoke,在运行并行方法前都会产生一些额外的开销,如分配硬件线程等。


		static void Main(string[] args)
        {
            Console.WriteLine($"主线程ID为{ Thread.CurrentThread.ManagedThreadId}");
            Parallel.Invoke(Method1, Method2, Method3, Method4);
            Console.ReadKey();
        }
		private static void Method1()
        {
            Thread.Sleep(1000);
            Console.WriteLine($"Method1 线程ID为{ Thread.CurrentThread.ManagedThreadId}");
        }

        private static void Method2()
        {
            Thread.Sleep(1000);
            Console.WriteLine($"Method2 线程ID为{ Thread.CurrentThread.ManagedThreadId}");
        }

        private static void Method3()
        {
            Thread.Sleep(1000);
            Console.WriteLine($"Method3 线程ID为{ Thread.CurrentThread.ManagedThreadId}");
        }

        private static void Method4()
        {
            Thread.Sleep(1000);
            Console.WriteLine($"Method4 线程ID为{ Thread.CurrentThread.ManagedThreadId}");
        }

Parallel.ForEach/Parallel.For

Parallel.ForEach提供一个并行处理一组数据的机制,可以利用一个范围的整数作为一组数据,然后通过一个自定义的分区器将这个范围转换为一组数据块,每一块数据都通过循环的方式进行处理,而这些循环式并行执行的。

Break-这个方法告诉并行循环应该在执行了当前迭代后尽快地停止执行。吐过调用Break时正在处理迭代100,那么循环仍然会处理所有小于100的迭代。

Stop-这个方法告诉并行循环应该尽快停止执行,如果调用Stop时迭代100正在被处理,那么循环无法保证处理完所有小于100的迭代

            Parallel.For(0, students.Count, (i, loopState) =>
            {
                if(i< 100)
                {
                    Console.WriteLine(students[i].Name);
                }
                else
                {
                    //loopState.Stop();
                    loopState.Break();
                    return;
                }
            });
            
             Parallel.ForEach(students, (student) =>
             {
                 Console.WriteLine(student.Name);
             });
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值