服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - C# - C#多线程系列之任务基础(一)

C#多线程系列之任务基础(一)

2022-12-29 13:49痴者工良 C#

本文详细讲解了C#多线程的任务基础,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

多线程编程

多线程编程模式

.NET 中,有三种异步编程模式,分别是基于任务的异步模式(TAP)、基于事件的异步模式(EAP)、异步编程模式(APM)。

  • 基于任务的异步模式 (TAP) :.NET 推荐使用的异步编程方法,该模式使用单一方法表示异步操作的开始和完成。包括我们常用的 async 、await 关键字,属于该模式的支持。
  • 基于事件的异步模式 (EAP) :是提供异步行为的基于事件的旧模型。《C#多线程(12):线程池》中提到过此模式,.NET Core 已经不支持。
  • 异步编程模型 (APM) 模式:也称为 IAsyncResult 模式,,这是使用 IAsyncResult 接口提供异步行为的旧模型。.NET Core 也不支持,请参考 《C#多线程(12):线程池》

前面,我们学习了三部分的内容:

  • 线程基础:如何创建线程、获取线程信息以及等待线程完成任务;
  • 线程同步:探究各种方式实现进程和线程同步,以及线程等待;
  • 线程池:线程池的优点和使用方法,基于任务的操作;

这篇开始探究任务和异步,而任务和异步是十分复杂的,内容错综复杂,笔者可能讲不好。。。

探究优点

我们现在来探究一下多线程编程的复杂性。

  • 传递数据和返回结果

传递数据倒是没啥问题,只是难以获取到线程的返回值,处理线程的异常也需要技巧。

  • 监控线程的状态

新建新的线程后,如果需要确定新线程在何时完成,需要自旋或阻塞等方式等待。

  • 线程安全

设计时要考虑如果避免死锁、合理使用各种同步锁,要考虑原子操作,同步信号的处理需要技巧。

  • 性能

玩多线程,最大需求就是提升性能,但是多线程中有很多坑,使用不当反而影响性能。

[以上总结可参考《C# 7.0本质论》19.3节,《C# 7.0核心技术指南》14.3 节]

我们通过使用线程池,可以解决上面的部分问题,但是还有更加好的选择,就是 Task(任务)。另外 Task 也是异步编程的基础类型,后面很多内容要围绕 Task 展开。

原理的东西,还是多参考微软官方文档和书籍,笔者讲得不一定准确,而且不会深入说明这些。

任务操作

任务(Task)实在太多 API 了,也有各种骚操作,要讲清楚实在不容易,我们要慢慢来,一点点进步,一点点深入,多写代码测试。

下面与笔者一起,一步步熟悉、摸索 Task 的 API。

两种创建任务的方式

通过其构造函数创建一个任务,其构造函数定义为:

?
1
public Task (Action action);

其示例如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Program
{
    static void Main()
    {
        // 定义两个任务
        Task task1 = new Task(()=>
        {
            Console.WriteLine("① 开始执行");
            Thread.Sleep(TimeSpan.FromSeconds(1));
 
            Console.WriteLine("① 执行中");
            Thread.Sleep(TimeSpan.FromSeconds(1));
 
            Console.WriteLine("① 执行即将结束");
        });
 
        Task task2 = new Task(MyTask);
        // 开始任务
        task1.Start();
        task2.Start();
 
        Console.ReadKey();
    }
 
    private static void MyTask()
    {
        Console.WriteLine("② 开始执行");
        Thread.Sleep(TimeSpan.FromSeconds(1));
 
        Console.WriteLine("② 执行中");
        Thread.Sleep(TimeSpan.FromSeconds(1));
 
        Console.WriteLine("② 执行即将结束");
    }
}

.Start() 方法用于启动一个任务。微软文档解释:启动 Task,并将它安排到当前的 TaskScheduler 中执行。

TaskScheduler 这个东西,我们后面讲,别急。

另一种方式则使用 Task.Factory,此属性用于创建和配置 Task 和 Task<TResult> 实例的工厂方法。

使用https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.taskfactory.startnew?view=netcore-3.1#--可以添加任务。

当需要对长时间运行、计算限制的任务(计算密集型)进行精细控制时才使用 StartNew() 方法;
官方推荐使用 Task.Run 方法启动计算限制任务。 
Task.Factory.StartNew() 可以实现比 Task.Run() 更细粒度的控制。

Task.Factory.StartNew() 的重载方法是真的多,你可以参考: https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.taskfactory.startnew?view=netcore-3.1#--

这里我们使用两个重载方法编写示例:

?
1
public Task StartNew(Action action);
?
1
public Task StartNew(Action action, TaskCreationOptions creationOptions);

代码示例如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Program
{
    static void Main()
    {
        // 重载方法 1
        Task.Factory.StartNew(() =>
        {
            Console.WriteLine("① 开始执行");
            Thread.Sleep(TimeSpan.FromSeconds(1));
 
            Console.WriteLine("① 执行中");
            Thread.Sleep(TimeSpan.FromSeconds(1));
 
            Console.WriteLine("① 执行即将结束");
        });
 
        // 重载方法 1
        Task.Factory.StartNew(MyTask);
 
        // 重载方法 2
        Task.Factory.StartNew(() =>
        {
            Console.WriteLine("① 开始执行");
            Thread.Sleep(TimeSpan.FromSeconds(1));
 
            Console.WriteLine("① 执行中");
            Thread.Sleep(TimeSpan.FromSeconds(1));
 
            Console.WriteLine("① 执行即将结束");
        },TaskCreationOptions.LongRunning);
 
        Console.ReadKey();
    }
 
    // public delegate void TimerCallback(object? state);
    private static void MyTask()
    {
        Console.WriteLine("② 开始执行");
        Thread.Sleep(TimeSpan.FromSeconds(1));
 
        Console.WriteLine("② 执行中");
        Thread.Sleep(TimeSpan.FromSeconds(1));
 
        Console.WriteLine("② 执行即将结束");
    }
}

通过 Task.Factory.StartNew() 方法添加的任务,会进入线程池任务队列然后自动执行,不需要手动启动。

TaskCreationOptions.LongRunning 是控制任务创建特性的枚举,后面讲。

Task.Run() 创建任务

Task.Run() 创建任务,跟 Task.Factory.StartNew() 差不多,当然 Task.Run() 还有很多重载方法和骚操作,我们后面再来学。

Task.Run() 创建任务示例代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void Main()
{
    Task.Run(() =>
    {
        Console.WriteLine("① 开始执行");
        Thread.Sleep(TimeSpan.FromSeconds(1));
 
        Console.WriteLine("① 执行中");
        Thread.Sleep(TimeSpan.FromSeconds(1));
 
        Console.WriteLine("① 执行即将结束");
    });
    Console.ReadKey();
}

取消任务

取消任务,《C#多线程(12):线程池》 中说过一次,不过控制太自由,全靠任务本身自觉判断是否取消。

这里我们通过 Task 来实现任务的取消,其取消是实时的、自动的,并且不需要手工控制。

其构造函数如下:

?
1
public Task StartNew(Action action, CancellationToken cancellationToken);

代码示例如下:

按下回车键的时候记得切换字母模式。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Program
{
    static void Main()
    {
        Console.WriteLine("任务开始启动,按下任意键,取消执行任务");
        CancellationTokenSource cts = new CancellationTokenSource();
        Task.Factory.StartNew(MyTask, cts.Token);
 
        Console.ReadKey();
 
        cts.Cancel();       // 取消任务
        Console.ReadKey();
    }
 
    // public delegate void TimerCallback(object? state);
    private static void MyTask()
    {
        Console.WriteLine(" 开始执行");
        int i = 0;
        while (true)
        {
            Console.WriteLine($" 第{i}次任务");
            Thread.Sleep(TimeSpan.FromSeconds(1));
 
            Console.WriteLine("     执行中");
            Thread.Sleep(TimeSpan.FromSeconds(1));
 
            Console.WriteLine("     执行结束");
            i++;
        }
    }
}

父子任务

前面创建任务的时候,我们碰到了 TaskCreationOptions.LongRunning 这个枚举类型,这个枚举用于控制任务的创建以及设定任务的行为。

其枚举如下:

枚举 说明
AttachedToParent 4 指定将任务附加到任务层次结构中的某个父级。
DenyChildAttach 8 指定任何尝试作为附加的子任务执行的子任务都无法附加到父任务,会改成作为分离的子任务执行。
HideScheduler 16 防止环境计划程序被视为已创建任务的当前计划程序。
LongRunning 2 指定任务将是长时间运行的、粗粒度的操作,涉及比细化的系统更少、更大的组件。
None 0 指定应使用默认行为。
PreferFairness 1 提示 TaskScheduler 以一种尽可能公平的方式安排任务。
RunContinuationsAsynchronously 64 强制异步执行添加到当前任务的延续任务。

这个枚举在 TaskFactory 和 TaskFactory<TResult> 、Task 和 Task<TResult> 、

StartNew()FromAsync() 、TaskCompletionSource<TResult> 等地方可以使用到。

子任务使用了 TaskCreationOptions.AttachedToParent ,并不是指父任务要等待子任务完成后,父任务才能继续完往下执行;而是指父任务如果先执行完毕,那么必须等待子任务完成后,父任务才算完成。

这里来探究 TaskCreationOptions.AttachedToParent的使用。代码示例如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 父子任务
Task task = new Task(() =>
{
    // TaskCreationOptions.AttachedToParent
    // 将此任务附加到父任务中
    // 父任务需要等待所有子任务完成后,才能算完成
    Task task1 = new Task(() =>
    {
        Thread.Sleep(TimeSpan.FromSeconds(1));
        for (int i = 0; i < 5; i++)
        {
            Console.WriteLine("     内层任务1");
            Thread.Sleep(TimeSpan.FromSeconds(0.5));
        }
    }, TaskCreationOptions.AttachedToParent);
    task1.Start();
 
    Console.WriteLine("最外层任务");
    Thread.Sleep(TimeSpan.FromSeconds(1));
});
 
task.Start();
task.Wait();
 
Console.ReadKey();

而 TaskCreationOptions.DenyChildAttach 则不允许其它任务附加到外层任务中。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static void Main()
{
    // 不允许出现父子任务
    Task task = new Task(() =>
    {
        Task task1 = new Task(() =>
        {
            Thread.Sleep(TimeSpan.FromSeconds(1));
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine("  内层任务1");
                Thread.Sleep(TimeSpan.FromSeconds(0.5));
            }
        }, TaskCreationOptions.AttachedToParent);
        task1.Start();
 
        Console.WriteLine("最外层任务");
        Thread.Sleep(TimeSpan.FromSeconds(1));
    }, TaskCreationOptions.DenyChildAttach); // 不收儿子
 
    task.Start();
    task.Wait();
 
    Console.ReadKey();
}

然后,这里也学习了一个新的 Task 方法:Wait() 等待 Task 完成执行过程。Wait() 也可以设置超时时间。

如果父任务是通过调用 Task.Run 方法而创建的,则可以隐式阻止子任务附加到其中。

关于附加的子任务,请参考:https://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/attached-and-detached-child-tasks?view=netcore-3.1

任务返回结果以及异步获取返回结果

要获取任务返回结果,要使用泛型类或方法创建任务,例如 Task<Tresult>Task.Factory.StartNew<TResult>()Task.Run<TResult>

通过 其泛型的 的 Result 属性,可以获得返回结果。

异步获取任务执行结果:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Program
{
    static void Main()
    {
        // *******************************
        Task<int> task = new Task<int>(() =>
        {
            return 666;
        });
        // 执行
        task.Start();
        // 获取结果,属于异步
        int number = task.Result;
 
        // *******************************
        task = Task.Factory.StartNew<int>(() =>
        {
            return 666;
        });
 
        // 也可以异步获取结果
        number = task.Result;
 
        // *******************************
        task = Task.Run<int>(() =>
          {
              return 666;
          });
 
        // 也可以异步获取结果
        number = task.Result;
        Console.ReadKey();
    }
}

如果要同步的话,可以改成:

?
1
2
3
4
int number = Task.Factory.StartNew<int>(() =>
{
    return 666;
}).Result;

捕获任务异常

进行中的任务发生了异常,不会直接抛出来阻止主线程执行,当获取任务处理结果或者等待任务完成时,异常会重新抛出。

示例如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void Main()
{
    // *******************************
    Task<int> task = new Task<int>(() =>
    {
        throw new Exception("反正就想弹出一个异常");
    });
    // 执行
    task.Start();
    Console.WriteLine("任务中的异常不会直接传播到主线程");
    Thread.Sleep(TimeSpan.FromSeconds(1));
 
    // 当任务发生异常,获取结果时会弹出
    int number = task.Result;
 
    // task.Wait(); 等待任务时,如果发生异常,也会弹出
 
    Console.ReadKey();
}

乱抛出异常不是很好的行为噢~可以改成如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void Main()
{
    Task<Program> task = new Task<Program>(() =>
    {
        try
        {
            throw new Exception("反正就想弹出一个异常");
            return new Program();
        }
        catch
        {
            return null;
        }
    });
    task.Start();
 
    var result = task.Result;
    if (result is null)
        Console.WriteLine("任务执行失败");
    else Console.WriteLine("任务执行成功");
 
    Console.ReadKey();
}

全局捕获任务异常

TaskScheduler.UnobservedTaskException 是一个事件,其委托定义如下:

?
1
public delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);

下面是一个示例:

请发布程序后,打开目录执行程序。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Program
{
    static void Main()
    {
        TaskScheduler.UnobservedTaskException += MyTaskException;
 
        Task.Factory.StartNew(() =>
         {
             throw new ArgumentNullException();
         });
        Thread.Sleep(100);
        GC.Collect();
        GC.WaitForPendingFinalizers();
 
        Console.WriteLine("Done");
        Console.ReadKey();
    }
    public static void MyTaskException(object sender, UnobservedTaskExceptionEventArgs eventArgs)
    {
        // eventArgs.SetObserved();
        ((AggregateException)eventArgs.Exception).Handle(ex =>
        {
            Console.WriteLine("Exception type: {0}", ex.GetType());
            return true;
        });
    }
}

TaskScheduler.UnobservedTaskException 到底怎么用,笔者不太清楚。而且效果难以观察。

请参考:

https://stackoverflow.com/search?q=TaskScheduler.UnobservedTaskException

到此这篇关于C#多线程系列之任务基础(一)的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

原文链接:https://www.cnblogs.com/whuanle/p/12792639.html

延伸 · 阅读

精彩推荐
  • C#C#调用python脚本的方法步骤(2种)

    C#调用python脚本的方法步骤(2种)

    这篇文章主要介绍了C#调用python脚本的方法步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面...

    Zoe_yan3392022-08-15
  • C#关于C#操作文件路径(Directory)的常用静态方法详解

    关于C#操作文件路径(Directory)的常用静态方法详解

    这篇文章主要给大家介绍了关于C#操作文件路径(Directory)的常用静态方法,Directory类位于System.IO 命名空间,Directory类提供了在目录和子目录中进行创建移动和列...

    小马的棺材板7632022-11-29
  • C#c# 如何使用结构体实现共用体

    c# 如何使用结构体实现共用体

    这篇文章主要介绍了c# 如何使用结构体实现共用体,帮助大家更好的理解和学习使用c#,感兴趣的朋友可以了解下...

    精致码农6272022-11-10
  • C#C#实现简易猜数字游戏

    C#实现简易猜数字游戏

    这篇文章主要为大家详细介绍了C#实现简易猜数字游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    飘散的枫叶9402022-02-22
  • C#C#类中方法的执行顺序是什么

    C#类中方法的执行顺序是什么

    有些中级开发小伙伴还是搞不太明白在继承父类以及不同场景实例化的情况下,父类和子类的各种方法的执行顺序到底是什么,本文就来介绍一下...

    犁痕7912022-11-20
  • C#Unity3D实现人物移动示例

    Unity3D实现人物移动示例

    这篇文章主要为大家详细介绍了Unity3D实现人物移动示例,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    文艺V11912022-08-17
  • C#深入理解C# 7.0中的Tuple特性

    深入理解C# 7.0中的Tuple特性

    这篇文章主要介绍了C#7中Tuple特性的相关资料,文中通过示例代码介绍的非常详细,相信对大家具有一定的参考价值,需要的朋友可以们下面来一起学习学...

    Tony9452021-12-30
  • C#C#异步下载文件

    C#异步下载文件

    这篇文章主要介绍了C#异步下载文件的相关资料,需要的朋友可以参考下...

    guwei40378352021-11-08