浅谈.NET下的多线程和并行计算(七)基于多线程的基本组件

简介: 在多线程应用中我们有一些很常见的需求,比如定时去做计划任务,或者是在执行一个长时间的任务,在执行这个任务的过程中能有进度显示(能想到要实现这个需求需要新开一个线程,避免阻塞UI的更新)。对于这些应用.NET提供了现成的组件。

在多线程应用中我们有一些很常见的需求,比如定时去做计划任务,或者是在执行一个长时间的任务,在执行这个任务的过程中能有进度显示(能想到要实现这个需求需要新开一个线程,避免阻塞UI的更新)。对于这些应用.NET提供了现成的组件。

首先来看一下System.Threading的Timer组件,它提供了定时执行某个任务的方法:

ThreadPool.SetMinThreads(2, 2);
ThreadPool.SetMaxThreads(4, 4);

Timer timer = new Timer((state) =>
{
    int a, b;
    ThreadPool.GetAvailableThreads(out a, out b);
    Console.WriteLine(string.Format("({0}/{1}) #{2} : {3}", a, b, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("mm:ss")));
}, null, 2000, 1000);

Console.WriteLine(DateTime.Now.ToString("mm:ss"));
Thread.Sleep(5000);
Console.WriteLine("Change()");
timer.Change(3000, 500);
Thread.Sleep(5000);
Console.WriteLine("Dispose()");
timer.Dispose();
Thread.Sleep(5000);
Console.WriteLine(DateTime.Now.ToString("mm:ss"));

这段代码的运行结果如下:

image

我们可以看到:

1) Timer构造方法中第一个参数就是要定时执行的方法,这个方法接受一个状态参数,第二个参数是状态参数,第三个参数是首次调用回调方法前的延迟毫秒,第四个参数就是执行方法的间隔毫秒

2) 从结果中我们可以看到,第一次回调方法在2秒后执行,然后每一秒执行一次,之后我们调用了Change()方法把延迟时间设置为3秒,把间隔设置为500毫秒,看到Timer在完成了上次回调之后3秒后执行了新的回调,之后间隔500毫秒执行一次。

3) 最后,我们执行了Dispose()方法,在结束最后一次回调之后Timer就再也没有调用回调方法。

4) 在回调方法中我们输出了线程池的可用线程,可以看到Timer基于线程池,也就是Timer基于后台线程。

.NET中还提供了System.Timers.Timer,它封装并增强了System.Threading.Timer:

System.Timers.Timer timer2 = new System.Timers.Timer();
timer2.Elapsed += new System.Timers.ElapsedEventHandler(timer2_Elapsed);
timer2.Interval = 1000;
Console.WriteLine("Start()");
timer2.Start();
Console.WriteLine(DateTime.Now.ToString("mm:ss"));
Thread.Sleep(5000);
Console.WriteLine("Stop()");
timer2.Stop();
Thread.Sleep(5000);
Console.WriteLine("Change Interval and Start()");
timer2.Interval = 500;
timer2.Start();
Thread.Sleep(5000);
Console.WriteLine("Dispose()");
timer2.Dispose();
Thread.Sleep(5000);
Console.WriteLine(DateTime.Now.ToString("mm:ss"));
 
static void timer2_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
    int a, b;
    ThreadPool.GetAvailableThreads(out a, out b);
    Console.WriteLine(string.Format("({0}/{1}) #{2} : {3}", a, b, Thread.CurrentThread.ManagedThreadId, e.SignalTime.ToString("mm:ss")));
}

(假设我们还是设置线程池最小2个线程最大4个线程)

这段代码的结果如下:

image

从运行结果中我们可以看到:

1) 由于System.Timers.Timer封装了System.Threading.Timer,所以还是基于线程池

2) 默认Timer是停止的,启动后需要等待一个Interval再执行回调方法

最后,再来看看BackgroundWorker,它提供了对于前面说的执行一个任务,在UI上更新进度这种应用的封装,首先定义一个static的BackgroundWorker:

static BackgroundWorker bw = new BackgroundWorker();

然后写如下测试代码:

bw.WorkerReportsProgress = true;
bw.WorkerSupportsCancellation = true;
bw.ProgressChanged += new ProgressChangedEventHandler(bw_ProgressChanged);
bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
bw.DoWork += new DoWorkEventHandler(bw_DoWork);
bw.Disposed += new EventHandler(bw_Disposed);
AutoResetEvent are = new AutoResetEvent(false);
Console.WriteLine(DateTime.Now.ToString("mm:ss"));
bw.RunWorkerAsync(are);
Thread.Sleep(2000);
are.Set();
Thread.Sleep(2000);
Console.WriteLine("CancelAsync()");
bw.CancelAsync();
while (bw.IsBusy)
    Thread.Sleep(10);
bw.Dispose();

这段代码中我们:

1) 设置BackgroundWorker可以汇报进度(通过ProgressChanged事件)

2) 设置BackgroundWorker支持任务取消

3) 定义了进度更新的处理事件:

static void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    Console.WriteLine(string.Format("{0} : {1}% completed", DateTime.Now.ToString("mm:ss"), e.ProgressPercentage));
}

4) 定义了任务完成的处理事件:

static void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    if (e.Cancelled)
        Console.WriteLine("Cancelled");
    else if (e.Error != null)
        Console.WriteLine(e.Error.ToString());
    else
        Console.WriteLine(e.Result);
    Console.WriteLine(DateTime.Now.ToString("mm:ss"));
}

5) 定义了任务的主体方法:

static void bw_DoWork(object sender, DoWorkEventArgs e)
{
    (e.Argument as AutoResetEvent).WaitOne();

    for (int i = 0; i <= 100; i += 10)
    {
        //if (bw.CancellationPending)
        //{
        //    Console.WriteLine("Cancelling...");
        //    for (int j = 0; j <= 100; j += 20)
        //    {
        //        bw.ReportProgress(j);
        //        Thread.Sleep(500);
        //    }
        //    e.Cancel = true;
        //    return;
        //}
        bw.ReportProgress(i);
        Thread.Sleep(500);
    }
    e.Result = 100;
}

6) 定义了Dispose BackgroundWorker后的事件:

static void bw_Disposed(object sender, EventArgs e)
{
    Console.WriteLine("disposed");
}

7) 使用信号量作为事件的状态参数让任务延迟2秒执行

8) 主线程通过IsBusy判断任务是否在执行,轮询等待

9) 最后Dispose这个组件

程序执行结果如下:

image

可以看到:

1) 任务延迟2秒执行,任务分为10个阶段执行,每执行一个阶段汇报一下进度。每个阶段需要500毫秒

2) 任务执行完成之后可以设置Result属性的值,这个值在bw_RunWorkerCompleted中可以获取到

我们还原注释的那些代码来看看取消任务的情况:

image

我们看到任务在2秒后取消,要注意这种异步取消任务的方式,我们调用了CancelAsync()其实不能从实质上取消任务的执行,要真正取消任务需要在任务的主体方法中不断检测CancellationPending属性,如果为true表示用户希望取消任务,然后去执行一些取消任务的行为,在完成后设置Cancel属性为true,并结束任务主体,这么做的目的是因为对于长时间的任务取消回滚的过程可能也是长时间的,我们同样可以在主体方法中对取消的行为进行进度汇报。您可以自己做相关实验,可以发现在控制台程序中,BackgroundWorker的DoWork可ProgressReport基于两个独立的线程,这两个线程都是基于线程池的,在Winform中 BackgroundWorker的DoWork基于线程池中的独立线程而ProgressReport执行于UI线程。

作者: lovecindywang
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
相关文章
|
5月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
235 0
|
5月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
6月前
|
Java 数据挖掘 调度
Java 多线程创建零基础入门新手指南:从零开始全面学习多线程创建方法
本文从零基础角度出发,深入浅出地讲解Java多线程的创建方式。内容涵盖继承`Thread`类、实现`Runnable`接口、使用`Callable`和`Future`接口以及线程池的创建与管理等核心知识点。通过代码示例与应用场景分析,帮助读者理解每种方式的特点及适用场景,理论结合实践,轻松掌握Java多线程编程 essentials。
417 5
|
10月前
|
Python
python3多线程中使用线程睡眠
本文详细介绍了Python3多线程编程中使用线程睡眠的基本方法和应用场景。通过 `time.sleep()`函数,可以使线程暂停执行一段指定的时间,从而控制线程的执行节奏。通过实际示例演示了如何在多线程中使用线程睡眠来实现计数器和下载器功能。希望本文能帮助您更好地理解和应用Python多线程编程,提高程序的并发能力和执行效率。
389 20
|
10月前
|
安全 Java C#
Unity多线程使用(线程池)
在C#中使用线程池需引用`System.Threading`。创建单个线程时,务必在Unity程序停止前关闭线程(如使用`Thread.Abort()`),否则可能导致崩溃。示例代码展示了如何创建和管理线程,确保在线程中执行任务并在主线程中处理结果。完整代码包括线程池队列、主线程检查及线程安全的操作队列管理,确保多线程操作的稳定性和安全性。
|
12月前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
222 1
|
12月前
|
Java API 调度
【JavaEE】——多线程(join阻塞,计算,引用,状态)
【JavaEE】——多线程,join,sleep引起的线程阻塞,多线程提升计算效率,如何获取线程的引用和状态
|
12月前
|
监控 前端开发 API
一款基于 .NET MVC 框架开发、功能全面的MES系统
一款基于 .NET MVC 框架开发、功能全面的MES系统
365 5
|
开发框架 前端开发 .NET
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
452 0
|
开发框架 前端开发 JavaScript
ASP.NET MVC 教程
ASP.NET 是一个使用 HTML、CSS、JavaScript 和服务器脚本创建网页和网站的开发框架。
239 7