C# Task取消暂停的实现

 更新时间:2024年11月29日 11:20:39   作者:語衣  
本文主要介绍了C# Task取消暂停的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

CancellationTokenSource 和 CancellationToken

CancellationTokenSource

CancellationTokenSource是一个用于创建和控制CancellationToken的类。它是触发取消操作的关键点,并且提供了触发取消操作的方法(如Cancel)和检查是否已请求取消的属性(如IsCancellationRequested)。

主要方法

  • Cancel(): 触发取消操作。
  • Dispose(): 释放CancellationTokenSource占用的资源。

主要属性

  • IsCancellationRequested: 指示是否已请求取消。
  • Token: 获取与此CancellationTokenSource关联的CancellationToken。

CancellationToken

CancellationToken是一个轻量级的对象,用于在线程之间传递取消信号。它本身不执行任何取消操作,而是作为取消请求的标记。当某个操作应该被取消时,与该CancellationToken关联的CancellationTokenSource会发出一个取消信号,然后任何监听这个CancellationToken的代码都可以响应这个取消请求。

主要方法

  • Register(Action callback): 注册一个回调,当取消操作被触发时调用该回调。
  • ThrowIfCancellationRequested(): 如果已请求取消,则抛出OperationCanceledException异常。

主要属性

  • IsCancellationRequested: 指示是否已请求取消。

使用示例

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        var cts = new CancellationTokenSource();
        var token = cts.Token;

        var task = Task.Run(() => DoWork(token), token);

        // 模拟一段时间后取消操作
        Thread.Sleep(2000);
        cts.Cancel();

        try
        {
            await task; // 如果操作被取消,这里会抛出异常
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("Operation was canceled.");
        }
    }

    static void DoWork(CancellationToken token)
    {
        for (int i = 0; i < 10; i++)
        {
            if (token.IsCancellationRequested)
            {
                Console.WriteLine("Cancellation requested.");
                return;
            }
            Thread.Sleep(500); // 模拟工作
            Console.WriteLine($"Working... {i}");
        }
    }
}

当使用 CancellationToken 与 Task.Run 时,有几个关键点需要注意:

  • 传递给 Task.Run 的 CancellationToken:这个 CancellationToken 用于控制 Task.Run 创建的任务的生命周期。如果取消这个令牌(即调用 CancellationTokenSource.Cancel()),那么 Task 将接收到一个取消请求,但这并不意味着任务会立即停止执行。任务中的代码需要显式检查这个取消请求(通常通过调用 cancellationToken.ThrowIfCancellationRequested() 或检查 cancellationToken.IsCancellationRequested 属性)并据此决定是否停止执行。

  • 传递给 DoWork 方法的 CancellationToken:这个 CancellationToken 是作为参数传递给 DoWork 方法的。在 DoWork 方法内部,你可以根据这个令牌的状态来决定是否继续执行。如果检测到取消请求(IsCancellationRequested 为 true),你可以提前退出方法或执行清理操作。但是,仅仅设置这个令牌的取消状态并不会自动停止 DoWork 方法的执行;你需要编写相应的逻辑来处理取消请求。

  • 任务的取消:即使你取消了 CancellationToken,Task 对象本身也不会立即变为“已取消”状态。相反,它会变为“已取消请求”状态,这意味着已经请求取消该任务,但任务可能仍在执行中。只有当任务中的代码显式检查并响应取消请求时,任务才会停止执行并最终变为“已取消”状态。

  • 异常处理:如果在任务中检测到取消请求并调用了 cancellationToken.ThrowIfCancellationRequested(),则会抛出一个 OperationCanceledException 异常。这个异常通常被视为正常的取消流程的一部分,而不是一个需要捕获和处理的错误。

因此,总结来说,当 CancellationToken 被取消时,Task.Run 创建的任务会接收到一个取消请求,但 DoWork 方法本身并不会自动停止执行。你需要在 DoWork 方法内部编写逻辑来响应这个取消请求并据此决定是否停止执行。同样,任务本身也不会立即停止执行;它将继续执行直到遇到检查取消请求的代码,并根据该代码的逻辑来决定是否停止。

CancellationToken取消了啥

在C#中,当DoWork方法通过检查CancellationToken.IsCancellationRequested属性并提前返回时,它只是从该方法中退出了。这并不直接结束或取消底层的Task对象。但是,由于Task.Run创建的Task对象是与DoWork方法的执行相关联的,因此当DoWork方法返回时,该Task对象会将其状态设置为“已完成”(RanToCompletion),而不是“已取消”(Canceled)。

然而,如果你想要在检测到取消请求时使Task对象的状态变为“已取消”,你需要抛出一个OperationCanceledException。这通常通过调用CancellationToken.ThrowIfCancellationRequested()方法来实现,该方法在取消请求已发送时会抛出异常。

以下是修改后的DoWork方法示例,它在检测到取消请求时抛出OperationCanceledException:

static void DoWork(CancellationToken token)
{
    for (int i = 0; i &lt; 10; i++)
    {
        token.ThrowIfCancellationRequested(); // 如果取消请求已发送,则抛出异常
        
        Console.WriteLine($"DoWork: Working {i + 1}...");
        Thread.Sleep(500); // 模拟耗时操作
    }

    Console.WriteLine("DoWork: Work completed."); // 这行代码实际上不会被执行,因为循环会在某个点抛出异常
}

在这个例子中,如果CancellationToken的取消请求被发送(即调用了CancellationTokenSource.Cancel()方法),ThrowIfCancellationRequested()方法将抛出一个OperationCanceledException异常。这个异常会冒泡到调用Task.Run的代码,并最终导致Task对象的状态变为“已取消”。

但是,请注意,即使你抛出了OperationCanceledException,Task对象中的任何本地资源(如文件句柄、数据库连接等)仍然需要由你的代码显式释放。异常只是改变了Task的状态并通知调用者任务已取消,但它不会自动执行任何清理工作。

ManualResetEvent

ManualResetEvent是一个同步基元,它允许线程通过信号进行通信。当事件处于未发出状态时,线程调用WaitOne、WaitAny或WaitAll等方法时会阻塞,直到其他线程通过调用Set方法将事件标记为已发出状态。事件保持已发出状态,直到调用Reset方法将其重置为未发出状态。

主要方法

  • Set(): 将ManualResetEvent设置为已发出状态,允许一个或多个等待的线程继续执行。
  • Reset(): 将ManualResetEvent重置为未发出状态,阻塞等待的线程。
  • WaitOne(): 阻塞当前线程,直到ManualResetEvent被设置为已发出状态。

使用示例

创建了几个Task,它们都等待同一个ManualResetEvent被设置。一旦事件被设置,所有等待的Task都会继续执行。

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static ManualResetEvent resetEvent = new ManualResetEvent(false);

    static async Task Main(string[] args)
    {
        // 创建并启动多个Task
        Task[] tasks = new Task[3];
        for (int i = 0; i < tasks.Length; i++)
        {
            int taskId = i; // 捕获循环变量
            tasks[i] = Task.Run(() => Worker(taskId));
        }

        // 模拟一些工作...
        Console.WriteLine("Main thread is doing some work...");
        await Task.Delay(2000); // 异步等待

        // 设置事件,允许所有等待的Task继续执行
        Console.WriteLine("Setting the event to allow all tasks to continue...");
        resetEvent.Set();

        // 等待所有Task完成
        await Task.WhenAll(tasks);

        Console.WriteLine("All tasks have completed.");
    }

    static void Worker(int taskId)
    {
        Console.WriteLine($"Task {taskId} is waiting...");
        resetEvent.WaitOne(); // 等待事件被设置
        Console.WriteLine($"Task {taskId} continues...");

        // 模拟一些工作...
        Thread.Sleep(1000); // 注意:在async方法中通常使用await Task.Delay()
    }
}

上面的Worker方法实际上是同步的,并且它使用了Thread.Sleep来模拟工作,这通常不是async/await模式中的最佳做法。在async方法中,你应该使用await Task.Delay()来异步等待。但是,由于Worker方法被设计为与ManualResetEvent一起工作,并且是在Task.Run中调用的,所以这里使用Thread.Sleep是可以接受的。

如果你想要一个完全基于async/await的示例,并且不使用ManualResetEvent(因为async/await提供了更自然的异步等待模式),你可以这样做:

using System;
using System.Threading.Tasks;

class Program
{
    static Task completionSignal = new TaskCompletionSource<bool>().Task;

    static async Task Main(string[] args)
    {
        // 创建并启动多个Task
        Task[] tasks = new Task[3];
        for (int i = 0; i < tasks.Length; i++)
        {
            tasks[i] = Task.Run(async () => await Worker(i));
        }

        // 模拟一些工作...
        Console.WriteLine("Main thread is doing some work...");
        await Task.Delay(2000);

        // 设置事件,允许所有等待的Task继续执行
        ((TaskCompletionSource<bool>)completionSignal.Source).SetResult(true);

        // 等待所有Task完成
        await Task.WhenAll(tasks);

        Console.WriteLine("All tasks have completed.");
    }

    static async Task Worker(int taskId)
    {
        Console.WriteLine($"Task {taskId} is waiting...");
        await completionSignal; // 等待事件被设置
        Console.WriteLine($"Task {taskId} continues...");

        // 模拟一些异步工作...
        await Task.Delay(1000);
    }
}

在这个async/await示例中,我们使用TaskCompletionSource<T>来创建一个可以等待的任务,并在适当的时候通过调用SetResult来标记它已完成。这是async/await世界中同步多个异步操作的一种更自然的方式。

对于无限循环的线程,并且需要外部控制暂停/恢复的场景,使用ManualResetEvent 你可以根据需要多次设置和重置它。

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static ManualResetEvent pauseEvent = new ManualResetEvent(true); // 初始化为已设置状态,即线程不暂停

    static void Main(string[] args)
    {
        // 启动工作线程
        Task.Run(WorkerThread);

        // 控制台输入来控制暂停和恢复
        Console.WriteLine("Press 'p' to pause, 'r' to resume, or 'q' to quit.");
        while (true)
        {
            var key = Console.ReadKey(true).KeyChar;
            switch (key)
            {
                case 'p':
                    pauseEvent.Reset(); // 暂停线程
                    Console.WriteLine("Worker thread paused.");
                    break;
                case 'r':
                    pauseEvent.Set(); // 恢复线程
                    Console.WriteLine("Worker thread resumed.");
                    break;
                case 'q':
                    // 这里需要一种方式来通知工作线程优雅地退出,但在这个简单示例中我们直接退出程序
                    Environment.Exit(0);
                    break;
            }
        }
    }

    static void WorkerThread()
    {
        while (true)
        {
            // 等待暂停事件
            pauseEvent.WaitOne();

            // 执行工作...
            Console.WriteLine("Worker thread is working...");

            // 模拟工作耗时
            Thread.Sleep(500);

            // 注意:在真实场景中,你可能不需要在每次循环迭代中都调用 WaitOne,
            // 除非你真的希望每次迭代都检查暂停状态。
            // 在这个示例中,我们只是为了演示而这样做。
        }
    }
}

上面的代码示例有一个问题:它会在每次循环迭代中都检查暂停状态,这可能会导致不必要的性能开销。在实际应用中,你可能希望只在需要时才检查暂停状态,或者将工作分解为更大的块,并在每个块之后检查暂停状态。

AutoResetEvent

AutoResetEvent和ManualResetEvent都用于线程间的同步和通信,但它们之间存在一些关键区别:

  • 重置行为:AutoResetEvent在释放一个等待线程后会自动重置为未设定状态,而ManualResetEvent需要显式调用Reset()方法才能重置为未设定状态。
  • 信号通知:AutoResetEvent每次调用Set()方法时,只允许一个等待线程被唤醒;而ManualResetEvent在调用Set()方法后,会唤醒所有等待的线程。

AutoResetEvent和ManualResetEvent(实际上是同一事物)的主要特点是其自动重置的行为,这使得它在控制线程执行顺序和同步线程操作方面非常有用,其他方法调用都和ManualResetEvent一致。

到此这篇关于C# Task取消暂停的实现的文章就介绍到这了,更多相关C# Task取消暂停内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C#程序(含多个Dll)合并成一个Exe的简单方法

    C#程序(含多个Dll)合并成一个Exe的简单方法

    这篇文章主要为大家详细介绍了C#程序(含多个Dll)合并成一个Exe的简单方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-12-12
  • C#组合模式实例详解

    C#组合模式实例详解

    这篇文章主要介绍了C#组合模式,实例分析了C#实现组合模式的原理与相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-07-07
  • c# 可疑文件扫描代码(找到木马)(简)

    c# 可疑文件扫描代码(找到木马)(简)

    c# 可疑文件扫描代码(找到木马),需要的朋友可以参考下。
    2010-05-05
  • C#利用System.Uri转URL为绝对地址的方法

    C#利用System.Uri转URL为绝对地址的方法

    这篇文章主要介绍了C#利用System.Uri转URL为绝对地址的方法,涉及C#操作URL的技巧,非常具有实用价值,需要的朋友可以参考下
    2015-02-02
  • Unity实现音频播放管理器

    Unity实现音频播放管理器

    这篇文章主要为大家详细介绍了Unity实现音频播放管理器,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-09-09
  • C#泛型设计需要注意的一个小陷阱

    C#泛型设计需要注意的一个小陷阱

    这篇文章主要给大家介绍了关于C#泛型设计需要注意的一个小陷阱,文中通过示例代码以及图文介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-03-03
  • C#调用mmpeg进行各种视频转换的类实例

    C#调用mmpeg进行各种视频转换的类实例

    这篇文章主要介绍了C#调用mmpeg进行各种视频转换的类,实例分析了C#调用mmpeg操作视频文件的技巧,非常具有实用价值,需要的朋友可以参考下
    2015-03-03
  • 判断一个整数是否是2的N次幂实现方法

    判断一个整数是否是2的N次幂实现方法

    下面小编就为大家分享一篇判断一个整数是否是2的N次幂实现方法,实例简洁,具有很好的参考价值。希望对大家有所帮助
    2017-11-11
  • C#使用Task实现异步方法

    C#使用Task实现异步方法

    本文主要介绍了C#使用Task实现异步方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-04-04
  • c#高效比对大量图片的实例代码

    c#高效比对大量图片的实例代码

    以前传统的比较方式是遍历图片中的每一个像素,然后进行比对。这样的比对在少量图片的比对上虽然效率低一点,但是也没有什么不好。但是在大量图片比对的时候,过长的反应时间和对服务器比较高的消耗肯定是不行的,下面介绍下新的方法
    2013-10-10

最新评论