C#多线程(15):任务基础③

简介: 任务基础一共三篇,本篇是第三篇,之后开始学习异步编程、并发、异步I/O的知识。本篇会继续讲述 Task 的一些 API 和常用的操作。

TaskAwaiter


先说一下 TaskAwaiterTaskAwaiter 表示等待异步任务完成的对象并为结果提供参数。


Task 有个 GetAwaiter() 方法,会返回TaskAwaiterTaskAwaiter<TResult>TaskAwaiter 类型在 System.Runtime.CompilerServices 命名空间中定义。

TaskAwaiter 类型的属性和方法如下:


属性:

属性 说明
IsCompleted 获取一个值,该值指示异步任务是否已完成。


方法:


方法 说明
GetResult() 结束异步任务完成的等待。
OnCompleted(Action) 将操作设置为当 TaskAwaiter 对象停止等待异步任务完成时执行。
UnsafeOnCompleted(Action) 计划与此 awaiter 相关异步任务的延续操作。


使用示例如下:

static void Main()
        {
            Task<int> task = new Task<int>(()=>
            {
                Console.WriteLine("我是前驱任务");
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return 666;
            });
            TaskAwaiter<int> awaiter = task.GetAwaiter();
            awaiter.OnCompleted(()=>
            {
                Console.WriteLine("前驱任务完成时,我就会继续执行");
            });
            task.Start();
            Console.ReadKey();
        }


另外,我们前面提到过,任务发生未经处理的异常,任务被终止,也算完成任务。


延续的另一种方法


上一节我们介绍了 .ContinueWith() 方法来实现延续,这里我们介绍另一个延续方法 .ConfigureAwait()


.ConfigureAwait() 如果要尝试将延续任务封送回原始上下文,则为 true;否则为 false


我来解释一下, .ContinueWith() 延续的任务,当前驱任务完成后,延续任务会继续在此线程上继续执行。这种方式是同步的,前者和后者连续在一个线程上运行。


.ConfigureAwait(false) 方法可以实现异步,前驱方法完成后,可以不理会后续任务,而且后续任务可以在任意一个线程上运行。这个特性在 UI 界面程序上特别有用。

可以参考:https://medium.com/bynder-tech/c-why-you-should-use-configureawait-false-in-your-library-code-d7837dce3d7f


其使用方法如下:

static void Main()
        {
            Task<int> task = new Task<int>(()=>
            {
                Console.WriteLine("我是前驱任务");
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return 666;
            });
            ConfiguredTaskAwaitable<int>.ConfiguredTaskAwaiter awaiter = task.ConfigureAwait(false).GetAwaiter();
            awaiter.OnCompleted(()=>
            {
                Console.WriteLine("前驱任务完成时,我就会继续执行");
            });
            task.Start();
            Console.ReadKey();
        }


ConfiguredTaskAwaitable<int>.ConfiguredTaskAwaiter 拥有跟 TaskAwaiter 一样的属性和方法。


.ContinueWith().ConfigureAwait(false) 还有一个区别就是 前者可以延续多个任务和延续任务的任务(多层)。后者只能延续一层任务(一层可以有多个任务)。


另一种创建任务的方法


前面提到提到过,创建任务的三种方法:new Task()Task.Run()Task.Factory.SatrtNew(),现在来学习第四种方法:TaskCompletionSource<TResult> 类型。

我们来看看 TaskCompletionSource<TResulr> 类型的属性和方法:


属性:

属性 说明
Task 获取由此 Task 创建的 TaskCompletionSource。


方法:


方法 说明
SetCanceled() 将基础 Task 转换为 Canceled 状态。
SetException(Exception) 将基础 Task 转换为 Faulted 状态,并将其绑定到一个指定异常上。
SetException(IEnumerable) 将基础 Task 转换为 Faulted 状态,并对其绑定一些异常对象。
SetResult(TResult) 将基础 Task 转换为 RanToCompletion 状态。
TrySetCanceled() 尝试将基础 Task 转换为 Canceled 状态。
TrySetCanceled(CancellationToken) 尝试将基础 Task 转换为 Canceled 状态并启用要存储在取消的任务中的取消标记。
TrySetException(Exception) 尝试将基础 Task 转换为 Faulted 状态,并将其绑定到一个指定异常上。
TrySetException(IEnumerable) 尝试将基础 Task 转换为 Faulted 状态,并对其绑定一些异常对象。
TrySetResult(TResult) 尝试将基础 Task 转换为 RanToCompletion 状态。


TaskCompletionSource<TResulr> 类可以对任务的生命周期做控制。

首先要通过 .Task 属性,获得一个 TaskTask<TResult>


TaskCompletionSource<int> task = new TaskCompletionSource<int>();
            Task<int> myTask = task.Task; //  Task myTask = task.Task;


然后通过 task.xxx() 方法来控制 myTask 的生命周期,但是呢,myTask 本身是没有任务内容的。


使用示例如下:

static void Main()
        {
            TaskCompletionSource<int> task = new TaskCompletionSource<int>();
            Task<int> myTask = task.Task;       // task 控制 myTask
            // 新开一个任务做实验
            Task mainTask = new Task(() =>
            {
                Console.WriteLine("我可以控制 myTask 任务");
                Console.WriteLine("按下任意键,我让 myTask 任务立即完成");
                Console.ReadKey();
                task.SetResult(666);
            });
            mainTask.Start();
            Console.WriteLine("开始等待 myTask 返回结果");
            Console.WriteLine(myTask.Result);
            Console.WriteLine("结束");
            Console.ReadKey();
        }


其它例如 SetException(Exception) 等方法,可以自行探索,这里就不再赘述。

参考资料:https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/

这篇文章讲得不错,而且有图:https://gigi.nullneuron.net/gigilabs/taskcompletionsource-by-example/


实现一个支持同步和异步任务的类型


这部分内容对 TaskCompletionSource<TResult> 继续进行讲解。

这里我们来设计一个类似 Task 类型的类,支持同步和异步任务。

  • 用户可以使用 GetResult() 同步获取结果;
  • 用户可以使用 RunAsync() 执行任务,使用 .Result 属性异步获取结果;


其实现如下:

/// <summary>
/// 实现同步任务和异步任务的类型
/// </summary>
/// <typeparam name="TResult"></typeparam>
public class MyTaskClass<TResult>
{
    private readonly TaskCompletionSource<TResult> source = new TaskCompletionSource<TResult>();
    private Task<TResult> task;
    // 保存用户需要执行的任务
    private Func<TResult> _func;
    // 是否已经执行完成,同步或异步执行都行
    private bool isCompleted = false;
    // 任务执行结果
    private TResult _result;
    /// <summary>
    /// 获取执行结果
    /// </summary>
    public TResult Result
    {
        get
        {
            if (isCompleted)
                return _result;
            else return task.Result;
        }
    }
    public MyTaskClass(Func<TResult> func)
    {
        _func = func;
        task = source.Task;
    }
    /// <summary>
    /// 同步方法获取结果
    /// </summary>
    /// <returns></returns>
    public TResult GetResult()
    {
        _result = _func.Invoke();
        isCompleted = true;
        return _result;
    }
    /// <summary>
    /// 异步执行任务
    /// </summary>
    public void RunAsync()
    {
        Task.Factory.StartNew(() =>
        {
            source.SetResult(_func.Invoke());
            isCompleted = true;
        });
    }
}


我们在 Main 方法中,创建任务示例:

class Program
    {
        static void Main()
        {
            // 实例化任务类
            MyTaskClass<string> myTask1 = new MyTaskClass<string>(() =>
            {
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return "www.whuanle.cn";
            });
            // 直接同步获取结果
            Console.WriteLine(myTask1.GetResult());
            // 实例化任务类
            MyTaskClass<string> myTask2 = new MyTaskClass<string>(() =>
            {
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return "www.whuanle.cn";
            });
            // 异步获取结果
            myTask2.RunAsync();
            Console.WriteLine(myTask2.Result);
            Console.ReadKey();
        }
    }


Task.FromCanceled()


微软文档解释:创建 Task,它因指定的取消标记进行的取消操作而完成。


这里笔者抄来了一个示例

var token = new CancellationToken(true);
Task task = Task.FromCanceled(token);
Task<int> genericTask = Task.FromCanceled<int>(token);


网上很多这样的示例,但是,这个东西到底用来干嘛的?new 就行了?


带着疑问我们来探究一下,来个示例:

public static Task Test()
        {
            CancellationTokenSource source = new CancellationTokenSource();
            source.Cancel();
            return Task.FromCanceled<object>(source.Token);
        }
        static void Main()
        {
            var t = Test(); // 在此设置断点,监控变量
            Console.WriteLine(t.IsCanceled);
         }


Task.FromCanceled() 可以构造一个被取消的任务。我找了很久,没有找到很好的示例,如果一个任务在开始前就被取消,那么使用 Task.FromCanceled() 是很不错的。


这里有很多示例可以参考:https://www.csharpcodi.com/csharp-examples/System.Threading.Tasks.Task.FromCanceled(System.Threading.CancellationToken)/


如何在内部取消任务


之前我们讨论过,使用 CancellationToken 取消令牌传递参数,使任务取消。但是都是从外部传递的,这里来实现无需 CancellationToken 就能取消任务。


我们可以使用 CancellationTokenThrowIfCancellationRequested() 方法抛出 System.OperationCanceledException 异常,然后终止任务,任务会变成取消状态,不过任务需要先传入一个令牌。


这里笔者来设计一个难一点的东西,一个可以按顺序执行多个任务的类。


示例如下:

/// <summary>
    /// 能够完成多个任务的异步类型
    /// </summary>
    public class MyTaskClass
    {
        private List<Action> _actions = new List<Action>();
        private CancellationTokenSource _source = new CancellationTokenSource();
        private CancellationTokenSource _sourceBak = new CancellationTokenSource();
        private Task _task;
        /// <summary>
        ///  添加一个任务
        /// </summary>
        /// <param name="action"></param>
        public void AddTask(Action action)
        {
            _actions.Add(action);
        }
        /// <summary>
        /// 开始执行任务
        /// </summary>
        /// <returns></returns>
        public Task StartAsync()
        {
            // _ = new Task() 对本示例无效
            _task = Task.Factory.StartNew(() =>
             {
                 for (int i = 0; i < _actions.Count; i++)
                 {
                     int tmp = i;
                     Console.WriteLine($"第 {tmp} 个任务");
                     if (_source.Token.IsCancellationRequested)
                     {
                         Console.ForegroundColor = ConsoleColor.Red;
                         Console.WriteLine("任务已经被取消");
                         Console.ForegroundColor = ConsoleColor.White;
                         _sourceBak.Cancel();
                         _sourceBak.Token.ThrowIfCancellationRequested();
                     }
                     _actions[tmp].Invoke();
                 }
             },_sourceBak.Token);
            return _task;
        }
        /// <summary>
        /// 取消任务
        /// </summary>
        /// <returns></returns>
        public Task Cancel()
        {
            _source.Cancel();
            // 这里可以省去
            _task = Task.FromCanceled<object>(_source.Token);
            return _task;
        }
    }


Main 方法中:

static void Main()
        {
            // 实例化任务类
            MyTaskClass myTask = new MyTaskClass();
            for (int i = 0; i < 10; i++)
            {
                int tmp = i;
                myTask.AddTask(() =>
                {
                    Console.WriteLine("     任务 1 Start");
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                    Console.WriteLine("     任务 1 End");
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                });
            }
            // 相当于 Task.WhenAll()
            Task task = myTask.StartAsync();
            Thread.Sleep(TimeSpan.FromSeconds(1));
            Console.WriteLine($"任务是否被取消:{task.IsCanceled}");
            // 取消任务
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine("按下任意键可以取消任务");
            Console.ForegroundColor = ConsoleColor.White;
            Console.ReadKey();
            var t = myTask.Cancel();    // 取消任务
            Thread.Sleep(TimeSpan.FromSeconds(2));
            Console.WriteLine($"任务是否被取消:【{task.IsCanceled}】");
            Console.ReadKey();
        }


你可以在任一阶段取消任务。


Yield 关键字



迭代器关键字,使得数据不需要一次性返回,可以在需要的时候一条条迭代,这个也相当于异步。


迭代器方法运行到 yield return 语句时,会返回一个 expression,并保留当前在代码中的位置。 下次调用迭代器函数时,将从该位置重新开始执行。

可以使用 yield break 语句来终止迭代。


官方文档:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/yield

网上的示例大多数都是 foreach 的,有些同学不理解这个到底是啥意思。笔者这里简单说明一下。


我们也可以这样写一个示例:

这里已经没有 foreach 了。

private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
        private static IEnumerable<int> ForAsync()
        {
            int i = 0;
            while (i < list.Length)
            {
                i++;
                yield return list[i];
            }
        }


但是,同学又问,这个 return 返回的对象 要实现这个 IEnumerable<T> 才行嘛?那些文档说到什么迭代器接口什么的,又是什么东西呢?


我们可以先来改一下示例:

private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
        private static IEnumerable<int> ForAsync()
        {
            int i = 0;
            while (i < list.Length)
            {
                int num = list[i];
                i++;
                yield return num;
            }
        }


你在 Main 方法中调用,看看是不是正常运行?

static void Main()
        {
            foreach (var item in ForAsync())
            {
                Console.WriteLine(item);
            }
            Console.ReadKey();
        }


这样说明了,yield return 返回的对象,并不需要实现 IEnumerable<int> 方法。

其实 yield 是语法糖关键字,你只要在循环中调用它就行了。


static void Main()
        {
            foreach (var item in ForAsync())
            {
                Console.WriteLine(item);
            }
            Console.ReadKey();
        }
        private static IEnumerable<int> ForAsync()
        {
            int i = 0;
            while (i < 100)
            {
                i++;
                yield return i;
            }
        }
    }


它会自动生成 IEnumerable<T> ,而不需要你先实现 IEnumerable<T>


补充知识点


  • 线程同步有多种方法:临界区(Critical Section)、互斥量(Mutex)、信号量(Semaphores)、事件(Event)、任务(Task);
  • Task.Run()Task.Factory.StartNew() 封装了 Task;
  • Task.Run()Task.Factory.StartNew() 的简化形式;
  • 有些地方 net Task() 是无效的;但是 Task.Run()Task.Factory.StartNew() 可以;


本篇是任务基础的终结篇,至此 C# 多线程系列,一共完成了 15 篇,后面会继续深入多线程和任务的更多使用方法和场景。

相关文章
|
3月前
|
数据采集 XML JavaScript
C# 中 ScrapySharp 的多线程下载策略
C# 中 ScrapySharp 的多线程下载策略
|
16天前
|
C# UED SEO
C# 异步方法async / await任务超时处理
通过使用 `Task.WhenAny`和 `Task.Delay`方法,您可以在C#中有效地实现异步任务的超时处理机制。这种方法允许您在指定时间内等待任务完成,并在任务超时时采取适当的措施,如抛出异常或执行备用操作。希望本文提供的详细解释和代码示例能帮助您在实际项目中更好地处理异步任务超时问题,提升应用程序的可靠性和用户体验。
44 3
|
1月前
|
缓存 负载均衡 Java
c++写高性能的任务流线程池(万字详解!)
本文介绍了一种高性能的任务流线程池设计,涵盖多种优化机制。首先介绍了Work Steal机制,通过任务偷窃提高资源利用率。接着讨论了优先级任务,使不同优先级的任务得到合理调度。然后提出了缓存机制,通过环形缓存队列提升程序负载能力。Local Thread机制则通过预先创建线程减少创建和销毁线程的开销。Lock Free机制进一步减少了锁的竞争。容量动态调整机制根据任务负载动态调整线程数量。批量处理机制提高了任务处理效率。此外,还介绍了负载均衡、避免等待、预测优化、减少复制等策略。最后,任务组的设计便于管理和复用多任务。整体设计旨在提升线程池的性能和稳定性。
79 5
|
3月前
|
前端开发 JavaScript 大数据
React与Web Workers:开启前端多线程时代的钥匙——深入探索计算密集型任务的优化策略与最佳实践
【8月更文挑战第31天】随着Web应用复杂性的提升,单线程JavaScript已难以胜任高计算量任务。Web Workers通过多线程编程解决了这一问题,使耗时任务独立运行而不阻塞主线程。结合React的组件化与虚拟DOM优势,可将大数据处理等任务交由Web Workers完成,确保UI流畅。最佳实践包括定义清晰接口、加强错误处理及合理评估任务特性。这一结合不仅提升了用户体验,更为前端开发带来多线程时代的全新可能。
81 1
|
3月前
|
存储 监控 Java
|
2月前
|
安全 数据库连接 API
C#一分钟浅谈:多线程编程入门
在现代软件开发中,多线程编程对于提升程序响应性和执行效率至关重要。本文从基础概念入手,详细探讨了C#中的多线程技术,包括线程创建、管理及常见问题的解决策略,如线程安全、死锁和资源泄露等,并通过具体示例帮助读者理解和应用这些技巧,适合初学者快速掌握C#多线程编程。
82 0
|
3月前
|
安全 C# 开发者
【C# 多线程编程陷阱揭秘】:小心!那些让你的程序瞬间崩溃的多线程数据同步异常问题,看完这篇你就能轻松应对!
【8月更文挑战第18天】多线程编程对现代软件开发至关重要,特别是在追求高性能和响应性方面。然而,它也带来了数据同步异常等挑战。本文通过一个简单的计数器示例展示了当多个线程无序地访问共享资源时可能出现的问题,并介绍了如何使用 `lock` 语句来确保线程安全。此外,还提到了其他同步工具如 `Monitor` 和 `Semaphore`,帮助开发者实现更高效的数据同步策略,以达到既保证数据一致性又维持良好性能的目标。
50 0
|
4月前
|
Java Linux
Java演进问题之1:1线程模型对于I/O密集型任务如何解决
Java演进问题之1:1线程模型对于I/O密集型任务如何解决
|
3月前
|
Cloud Native Java 调度
项目环境测试问题之线程同步器会造成执行完任务的worker等待的情况如何解决
项目环境测试问题之线程同步器会造成执行完任务的worker等待的情况如何解决