并行编程和任务(二)

前言

  上一篇我们主要介绍了并行编程相关的知识,这一节我们继续介绍关于任务相关的知识。为了更好的控制并行操作,我们可以使用System.Threading.Tasks中的Task类。我们首先来了解是什么是任务——任务表示将要完成的一个或某个工作单元,这个工作单元可以在单独线程中运行,也可以使用同步方式启动运行(需要等待主线程调用)。为什么使用任务呢?——任务不仅可以获得一个抽象层(将要完成的工作单元)、还可以对底层的线程运行进行更好更多的控制(任务的运行)。

使用线程池的任务

  我们讲到使用任务可以更好更多的控制底层的线程。就涉及到——线程池,线程池提供的是一个后台线程的池。线程池独自管理线程、根据需求增加或减少线程数。使用完成的线程返回至线程池中。我们下面就看看创建任务:

我们看下创建任务的几种方式:

1、使用实例化的TaskFactory类,然后使用其StartNew方法启动任务。

2、使用Task静态的Factory以来访问TaskFactory,然后调用StartNew方法启动任务。与第一种相似,但是对工厂的创建的控制就没那么全面。

3、使用Task的构造函数,实例化Task对象来指定创建任务,然后通过Start()方法进行启动任务。

4、使用Task.Run方法来立即启动任务。

  我们看下以上方法创建的任务有何区别和相同吧,看代码:

        private static object _lock = new object();     public static void TaskMethond(object item)
        {
            lock (_lock)
            {
                Console.WriteLine(item?.ToString());
                Console.WriteLine($"任务Id:{Task.CurrentId?.ToString() ?? "没有任务运行"}\t 线程Id:{Thread.CurrentThread.ManagedThreadId}");
                Console.WriteLine($"是否是线程池中的线程:{Thread.CurrentThread.IsThreadPoolThread}");
                Console.WriteLine($"是否是后台线程:{Thread.CurrentThread.IsBackground}");
                Console.WriteLine();
            }
        }

        #region 任务创建
        public static void TaskCreateRun()
        {
            var taskFactory = new TaskFactory();
            var task1 = taskFactory.StartNew(TaskMethond, "使用实例化TaskFactory");
            var task2 = Task.Factory.StartNew(TaskMethond, "使用Task静态调用Factory");
            var task3 = new Task(TaskMethond, "使用Task构造函数实例化");
            task3.Start();
            var task4 = Task.Run(() => TaskMethond("使用Task.Run"));

        }
        #endregion

  我们看代码运行的结果,发现不管使用的那种方法创建任务,都是使用过的线程池中的线程。

使用单独线程的任务

  任务当然也不一定就是使用线程池中的线程运行的,也是可以使用其他线程的。如果任务的将长时间运行的话,我们尽可能的考虑使用单独线程运行(TaskCreationOptions.LongRunning),这个情况下线程就不由线程池管理。我们看下不使用线程池中线程运行,使用单独线程运行的任务。

        #region 单独线程运行任务
        public static void OnlyThreadRun()
        {
            var task1 = new Task(TaskMethond, "单独线程运行任务", TaskCreationOptions.LongRunning);
            task1.Start();
        }
        #endregion

  我们看其运行结果,依旧是后台线程,但是不是使用的线程池中的线程。

使用同步任务

同时任务也可以同步运行,以相同的线程作为调用线程,下面我们看下使用Task类中的RunSynchronoushly方法实现同步运行。

        #region 同步任务

        public static void TaskRunSynchronoushly()
        {
            TaskMethond("主线程调用");
            var task1 = new Task(TaskMethond, "任务同步调用");
            task1.RunSynchronously();
        }
        #endregion

我们看运行结果,发现首先调用TaskMethond方法时候没有任务并且使用的线程1,再我们创建Task实例运行TaskMethond方法的时候,任务id是1,但是线程我们依然使用的是主线程1。

连续任务

在任务中,我们可以指定在某个任务完成后,应该马上开始另外一个任务。好比一个任务完成之后应该继续其处理。但是失败后我们应该进行一些处理工作。

我们可以使用ContinueWith()方法来定义使用连续任务,表示某任务之后应该开始其他任务,我们也可以指定任务成功后开始某个任务或者失败后开启某个任务(TaskContinuationOptions)。

        #region 连续任务
        public static void TaskOne()
        {
            Console.WriteLine($"任务{Task.CurrentId},方法名:{System.Reflection.MethodBase.GetCurrentMethod().Name }启动");
            Task.Delay(1000).Wait();
            Console.WriteLine($"任务{Task.CurrentId},方法名:{System.Reflection.MethodBase.GetCurrentMethod().Name }结束");
        }
        public static void TaskTwo(Task task)
        {
            Console.WriteLine($"任务{task.Id}以及结束了");
            Console.WriteLine($"现在开始的 任务是任务{Task.CurrentId},方法名称:{System.Reflection.MethodBase.GetCurrentMethod().Name  }  ");
            Console.WriteLine($"任务处理");
            Task.Delay(1000).Wait();
        }
        public static void TaskThree(Task task)
        {
            Console.WriteLine($"任务{task.Id}以及结束了");
            Console.WriteLine($"现在开始的 任务是任务{Task.CurrentId}.方法名称:{System.Reflection.MethodBase.GetCurrentMethod().Name } ");
            Console.WriteLine($"任务处理");
            Task.Delay(1000).Wait();
        }
        public static void ContinueTask()
        {
            Task task1 = new Task(TaskOne);
            Task task2 = task1.ContinueWith(TaskTwo, TaskContinuationOptions.OnlyOnRanToCompletion);//已完成情况下继续任务
            Task task3 = task1.ContinueWith(TaskThree, TaskContinuationOptions.OnlyOnFaulted);//出现未处理异常情况下继续任务
            task1.Start();
        }
        #endregion

我们看代码中写的是先开始运行TaskOne(),然后当任务完成后运行TaskTwo(Task task) ,如果任务失败的话机会运行TaskThree(Task task)。我们看运行结果中是运行了TaskOne()然后成功后运行了TaskTwo(Task task),避开了TaskThree(Task task)的运行,所以我们是可以通过ContinueWith来进行连续任务和TaskContinuationOptions进行控制任务运行的。

任务层次—父子层次结构

这里我们利用任务的连续性,我就就可以实现在一个任务结束后立即开启另一个任务,任务也可以构成一个层次结构。就比如一个任务中启动了一个任务,这样的情况就形成了父子层次的结构。下面我们看的案例就是这么一个案例。

        #region 任务的层次结构——父子层次结构
        public static void ChildTask()
        {
            Console.WriteLine("当前运行的子任务,开启");
            Task.Delay(5000).Wait();
            Console.WriteLine("子任务运行结束");
        }

        public static void ParentTask()
        {
            Console.WriteLine("父级任务开启");
            var child = new Task(ChildTask);
            child.Start();
            Task.Delay(1000).Wait();
            Console.WriteLine("父级任务结束");
        }

        public static void ParentAndChildTask()
        {
            var parent = new Task(ParentTask);
            parent.Start();
            Task.Delay(2000).Wait();
            Console.WriteLine($"父级任务的状态 :{parent.Status}");
            Task.Delay(4000).Wait();
            Console.WriteLine($"父级任务的状态 :{parent.Status}");
        }
        #endregion

等待任务

  在前面问介绍的.Net异步编程中我们讲到了WhenAll,用于处理多个异步方法。在这里我们继续扩展点,WhenAll()和WaitAll(),都是等待传递给他们的任务完成。但是WaitAll()方法阻塞调用任务,知道所有任务完成为止,而WhenAll()返回了一个任务,从而可以使用async关键在等待结果。不会阻塞任务。与之相对应的也还有WaitAny()和WhenAn()。等待任务还有我们一直都用到了的Task.Delay()方法,指定这个方法放回的任务前要等待的毫秒数。

  下面我们看这个ValueTask等待类型(结构),相对于Task类来说,ValueTask没有堆中对象的开销。在一般情况下,Task类型的开销可以被忽略掉,但是在一些特殊情况下,例如方法被调用千次万次来看。这种情况ValueTask就变得很适用了。我们看下面这个案例,使用ValueTask时,在五秒内的情况下直接从它的构造函数返回值。如果时间不在五秒内的话就使用真正获取数据的方法。然后我们与使用Task的方法进行对比。这里我们采取十万条数据的测试对比。

        #region 等待任务
        private static DateTime _time;
        private static List<string> _data;
        public static async ValueTask<List<string>> GetStringDicAsync()
        {
            if (_time >= DateTime.Now.AddSeconds(-5))
            {
                return await new ValueTask<List<string>>(_data);
            }

            else
            {
                (_data, _time) = await GetData();
                return _data;
            }
        }
        public static Task<(List<string> data, DateTime time)> GetData() =>
            Task.FromResult(
                (Enumerable.Range(0, 10).Select(x => $"itemString{x}").ToList(), DateTime.Now));

        public static async Task<List<string>> GetStringList()
        {
            (_data, _time) = await GetData();
            return _data;
        }
        #endregion
        static async Task  Main(string[] args)
        {
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            Console.WriteLine("ValueTask开始");
            for (int i = 0; i < 100000; i++)
            {
               var itemList= await GetStringDicAsync();
            }
            Console.WriteLine("ValueTask结束");
            Console.WriteLine($"ValueTask耗时:{stopwatch.ElapsedMilliseconds}");

            Console.WriteLine();
            Console.WriteLine();

            stopwatch.Restart();
            Console.WriteLine("Task开始");
            for (int i = 0; i < 100000; i++)
            {
                var itemList = await GetStringList();
            }
            Console.WriteLine("Task结束");
            Console.WriteLine($"Task耗时:{stopwatch.ElapsedMilliseconds}");
            Console.ReadLine();
        }

  我们看其运行结果,使用Task和ValueTask的运行结果耗时相差是巨大的。所以在一些特殊情况下使用ValueTask或许会更加的适用。

总结

  今天我们介绍了关于任务相关的一些知识概念。我们结合上一篇文章我们来梳理一些任务、线程、多线程、异步、同步、并发、并行任务之间的联系与关系吧。

  首先我们看我们这章节学习的任务、任务是一个将要完成的工作单元,那么由谁完成呢?由线程来运行这个任务。那么关于多线程呢?多线程应该可以说是一个设计概念,用来实现线程切换的。多线程就可以运行多个任务,但是在并发中。在同一时间内只能有一个程序运行。只不过线程间切换速度极快,让它看起来似乎是在同一时间运行了多个程序。其实在微观上讲,并发在任意时间点只有一个程序在运行,只不过是线程切换速度快。那么怎么达到切换速度极快呢?这就需要异步了。在线程运行的时候不需要等到它完成结果再去继续其他的线程任务。也就是可等待的。实现A运行起来不等待其结果,然后切换到B继续运行。这样切换速度就极快了。我们再仔细看多线程切换线程似乎成了实现异步的一种方法手段了。有异步就有同步,同步来说就不需要使用到多线程了,没必要。反正等到上一个任务运行完成。就继续使用上一个线程继续运行。这里都是讲的并发中的情况。那么并行呢?并行可以说不管在微观还是宏观上都是可以实现一个时间运行多个程序的。并发是多个程序运行在一个处理机上,但是并行任务是运行在多个处理机上。例如实现四个任务并行,那么我们至少需要四个逻辑处理内核的配合才能到达。

项目源码地址



  世界上那些最容易的事情中,拖延时间最不费力。坚韧是成功的一大要素,只要在门上敲得够久够大声,终会把人唤醒的。

     欢迎大家扫描下方二维码,和我一起学习更多的知识??

原文地址:https://www.cnblogs.com/hulizhong/p/11812428.html

时间: 2024-11-06 18:09:14

并行编程和任务(二)的相关文章

OpenCL学习笔记(二):并行编程概念理解

欢迎转载,转载请注明:本文出自Bin的专栏blog.csdn.net/xbinworld. 技术交流QQ群:433250724,欢迎对算法.技术.应用感兴趣的同学加入. 并行编程的需求是显而易见的,其最大的难题是找到算法的并行功能,同时必须处理数据的共享和同步.但是,因为每一个算法都是不一样的,很难有通用的并行功能--粒度都有可能是不一样的.OpenCL提供了很多并行的抽象模型,因此算法开发人员可以在不同粒度上开发并行的算法,以及数据的共享和同步. 一般来说,并行编程有两种大类型--分散收集(s

并行编程入门

目录 1. 并行编程简介 2. MapReduce 2.1 MapReduce简介 2.2 MapReduce框架 2.3 Hadoop介绍 2.4 Hadoop基本类 2.5 Hadoop编程实例 1.并行编程简介 1.1.并行编程作用,用途 商业用途,科学计算,大数据分析 1.2.并行编程兴起原因 目前的串行编程的局限性 使用的流水线等隐式并行模式的局限性 硬件的发展 1.3.并行算法设计原则步骤 a.分析问题 b.分解问题 其中分解方法有: 数据分解 递归分解 探测性分解 推测性分解 混合

五 浅谈CPU 并行编程和 GPU 并行编程的区别

前言 CPU 的并行编程技术,也是高性能计算中的热点,也是今后要努力学习的方向.那么它和 GPU 并行编程有何区别呢? 本文将做出详细的对比,分析各自的特点,为将来深入学习 CPU 并行编程技术打下铺垫. 区别一:缓存管理方式的不同 GPU:缓存对程序员不透明,程序员可根据实际情况操纵大部分缓存 (也有一部分缓存是由硬件自行管理). CPU:缓存对程序员透明.应用程序员无法通过编程手段操纵缓存. 区别二:指令模型的不同 GPU:采用 SIMT - 单指令多线程模型,一条指令配备一组硬件,对应32

Net并行编程高级教程--Parallel

Net并行编程高级教程--Parallel 一直觉得自己对并发了解不够深入,特别是看了<代码整洁之道>觉得自己有必要好好学学并发编程,因为性能也是衡量代码整洁的一大标准.而且在<失控>这本书中也多次提到并发,不管是计算机还是生物都并发处理着各种事物.人真是奇怪,当你关注一个事情的时候,你会发现周围的事物中就常出现那个事情.所以好奇心驱使下学习并发.便有了此文. 一.理解硬件线程和软件线程 多核处理器带有一个以上的物理内核--物理内核是真正的独立处理单元,多个物理内核使得多条指令能够

C#编程总结(二)多线程基础

C#编程总结(二)多线程基础 无论您是为具有单个处理器的计算机还是为具有多个处理器的计算机进行开发,您都希望应用程序为用户提供最好的响应性能,即使应用程序当前正在完成其他工作.要使应用程序能够快速响应用户操作,同时在用户事件之间或者甚至在用户事件期间利用处理器,最强大的方式之一是使用多线程技术. 多线程:线程是程序中一个单一的顺序控制流程.在单个程序中同时运行多个线程完成不同的工作,称为多线程.如果某个线程进行一次长延迟操作, 处理器就切换到另一个线程执行.这样,多个线程的并行(并发)执行隐藏了

.Net中的并行编程-6.常用优化策略

            本文是.Net中的并行编程第六篇,今天就介绍一些我在实际项目中的一些常用优化策略.      一.避免线程之间共享数据 避免线程之间共享数据主要是因为锁的问题,无论什么粒度的锁,最好的线程之间同步方式就是不加锁,这个地方主要措施就是找出数据之间的哪个地方需要共享数据和不需要共享数据的地方,再设计上避免多线程之间共享数据. 在以前做过的某项目,开始时设计的方案: 开始设计时所有的数据都放入到了公共队列,然后队列通知多个线程去处理数据,队列采用互斥锁保证线程同步,造成的结果就

.Net中的并行编程-3.ConcurrentQueue实现与分析

在上文<.Net中的并行编程-2.ConcurrentQueue的实现与分析> 中解释了无锁的相关概念,无独有偶BCL提供的ConcurrentQueue也是基于原子操作实现, 由于ConcurrentQueue的代码较多所以本文主要分析几个常用操作: 入队(EnQueue) .出队(TryDequeue) .是否为空(IsEmpty).获取队列内元素数量(Count). 一.ConcurrentQueue内部结构: 1.实现原理 众所周知,在普通的非线程安全队列有两种实现方式: 1.使用数组

.Net中的并行编程-5.流水线模型实战

自己在Excel整理了很多想写的话题,但苦于最近比较忙(其实这是借口).... 上篇文章<.Net中的并行编程-4.实现高性能异步队列>介绍了异步队列的实现,本篇文章介绍我实际工作者遇到了处理多线程问题及基于异步队列底层数据结构的解决方案. 需求如下:1.提供数据服写入务供上层应用调用,数据写入服务处理的吞吐量要达到60w/s每秒,也就是用户每秒发送60w的数据然后通过数据写入服务写到数据库中(数据库为公司自主研发的实时数据库). 2.尽量简化上层应用调用服务的复杂度. 一.分析性能瓶颈: 1

【读书笔记】.Net并行编程高级教程--Parallel

一直觉得自己对并发了解不够深入,特别是看了<代码整洁之道>觉得自己有必要好好学学并发编程,因为性能也是衡量代码整洁的一大标准.而且在<失控>这本书中也多次提到并发,不管是计算机还是生物都并发处理着各种事物.人真是奇怪,当你关注一个事情的时候,你会发现周围的事物中就常出现那个事情.所以好奇心驱使下学习并发.便有了此文. 一.理解硬件线程和软件线程 多核处理器带有一个以上的物理内核--物理内核是真正的独立处理单元,多个物理内核使得多条指令能够同时并行运行.硬件线程也称为逻辑内核,一个物