目录
即使你不熟悉C#,也可能至少遇到过Task、Thread或BackgroundWorker中的一个。如果再花一点时间,您可能已经在旅途中看到了这三者。它们都是在C#中运行并发代码的方法,每种方法都有自己的优点和缺点。在本文中,我们将探讨每个如何在高层次上运作。值得注意的是,在大多数现代.NET应用程序和库中,你会看到内容会聚合到Task。
方法
我继续创建了一个测试应用程序,您可以在此处找到该应用程序。因为这是在源代码管理中,所以它可能/很可能与我们在本文中看到的内容有所不同,所以我只是想为作为读者的您提供免责声明。
该应用程序允许我们选择要运行的不同示例。我将首先粘贴下面的代码,以便您可以看到工作原理。
using System.Globalization;
internal sealed class Program
{
private static readonly IReadOnlyDictionary<int, IExample> _examples =
new Dictionary<int, IExample>()
{
[1] = new NonBackgroundThreadExample(),
[2] = new BackgroundThreadExample(),
[3] = new BackgroundWorkerExample(),
[4] = new SimultaneousExample(),
};
private static void Main(string[] args)
{
Console.WriteLine("Enter the number for one of the following examples to run:");
foreach (var entry in _examples)
{
Console.WriteLine("----");
var restoreColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine($"Choice: {entry.Key}");
Console.ForegroundColor = ConsoleColor.Magenta;
Console.WriteLine($"Name: {entry.Value.Name}");
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Description: {entry.Value.Description}");
Console.ForegroundColor = restoreColor;
}
Console.WriteLine("----");
IExample example;
while (true)
{
var input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
{
Console.WriteLine("Would you like to exit? Y/N");
input = Console.ReadLine();
if ("y".Equals(input, StringComparison.OrdinalIgnoreCase))
{
return;
}
Console.WriteLine("Please make another selection.");
continue;
}
if (!int.TryParse(input, NumberStyles.Integer,
CultureInfo.InvariantCulture, out var exampleId) ||
!_examples.TryGetValue(exampleId, out example))
{
Console.WriteLine("Invalid input. Please make another selection.");
continue;
}
break;
}
Console.WriteLine($"Starting example '{example.Name}'...");
Console.WriteLine("-- Before entering example method");
example.ExecuteExample();
Console.WriteLine("-- After leaving example method");
}
}
线程
线程是C#中最基本的并发执行形式。它们由操作系统创建和管理,可用于与执行主线程并行运行代码。线程的概念是一般编程的并发性时最基本的构建块之一。但是,它也是一个类的名称,我们可以在C#中直接使用它来运行并发代码。
线程允许您传入要执行的方法。它们也可以标记为后台或不是后台,当应用程序尝试退出时,后台线程将被终止。相反,非后台线程将尝试使应用程序保持活动状态,直到线程退出。
下面是创建和启动新线程的示例:
Thread newThread = new Thread(new ThreadStart(MyMethod));
newThread.Start();
使用Thread的一个主要优点是它们的开销很低,因为它们由操作系统直接管理。但是,它们可能比其他并发选项更难使用,因为它们没有对取消、进度报告或异常处理的内置支持。在C#中,我们已经可以访问该Thread对象很长时间了,因此在此基础上构建其他构造是有意义的,以便我们添加额外的生活质量增强功能。
让我们看看第一个线程示例:
public void ExecuteExample()
{
void DoWork(string label)
{
while (true)
{
Task.Delay(1000).Wait();
Console.WriteLine($"Waiting in '{label}'...");
}
};
var thread = new Thread(new ThreadStart(() => DoWork("thread")));
thread.Start();
Console.WriteLine("Press enter to exit!");
Console.ReadLine();
}
在我们的示例应用程序的上下文中,我们将能够在Thread运行时看到方法打印到控制台。但是,当用户按Enter时,示例方法将退出,然后程序也将尝试退出。因为Thread没有标记为后台,所以实际上会阻止应用程序自然终止!试试看。
我们可以直接将其与第二个示例进行比较,第二个示例有一个区别:Thread标记为后台运行。当您尝试此示例时,您会注意到正在运行的线程不会阻止应用程序退出!
Background Workers
BackgroundWorker是C#中更高级别的并发执行选项。它是包含在System.ComponentModel命名空间中的组件,通常您可以在GUI应用程序中看到它。例如,经典的WinForms应用程序将利用这些。
让我们看一下 Background Worker的示例:
public void ExecuteExample()
{
void DoWork(string label)
{
while (true)
{
Task.Delay(1000).Wait();
Console.WriteLine($"Waiting in '{label}'...");
}
};
var backgroundWorker = new BackgroundWorker();
// NOTE: RunWorkerCompleted may not have a chance to run
// before the application exits
backgroundWorker.RunWorkerCompleted +=
(s, e) => Console.WriteLine("Background worker finished.");
backgroundWorker.DoWork += (s, e) => DoWork("background worker");
backgroundWorker.RunWorkerAsync();
Console.WriteLine("Press enter to exit!");
Console.ReadLine();
}
使用BackgroundWorker的一个主要优点是它具有对取消、进度报告和异常处理的内置支持。它的设置略有不同,因为事件处理程序已注册到BackgroundWorker。您还可以将完成处理程序和其他处理程序注册到对象。就像标记为后台运行的Thread一样,BackgroundWorker不会阻止应用程序退出。
任务
任务是C# 4.0中作为任务并行库(TPL)的一部分引入的新式并发执行选项。它们类似于线程,但由.NET运行时而不是操作系统管理。下面是一个示例,说明了在后台运行Task是多么简单:
Task.Run(() => SomeMethod());
使用Task的主要优点是它们内置了对取消、进度报告和异常处理的支持,类似于BackgroundWorker。此外,它们比Thread更易于使用,因为它们由.NET运行时管理。Task还支持 async/await 模式,该模式允许您编写外观和行为类似于同步代码的异步代码。
在我们的最后一个示例中,我们可以看到Task正在设置并沿着Thread和BackgroundWorker运行。这里没有async/await修饰,因为我想尽可能直接地进行比较。
public void ExecuteExample()
{
var cancellationTokenSource = new CancellationTokenSource();
void DoWork(string label)
{
while (!cancellationTokenSource.IsCancellationRequested)
{
Task.Delay(1000).Wait();
Console.WriteLine($"Waiting in '{label}'...");
}
};
var thread = new Thread(new ThreadStart(() => DoWork("thread")));
thread.Start();
var backgroundWorker = new BackgroundWorker();
// NOTE: RunWorkerCompleted may not have a chance to run
// before the application exits
backgroundWorker.RunWorkerCompleted +=
(s, e) => Console.WriteLine("Background worker finished.");
backgroundWorker.DoWork += (s, e) => DoWork("background worker");
backgroundWorker.RunWorkerAsync();
var task = Task.Run(() => DoWork("task"));
Console.WriteLine("Press enter to exit!");
Console.ReadLine();
cancellationTokenSource.Cancel();
}
在上面的代码中,我们可以看到CancellationToken的使用。这允许我们在循环尝试下一次迭代时中断循环的执行。但是,Task很像BackgroundWorker和标记为后台运行的Thread,因为启动Task不会阻止应用程序退出。
摘要——任务FTW?
多年来,在C#中提供并发的选项一直在发展。Task似乎是现在实现并发模式的首选。但是,如果您需要最大的性能和控制,Thread可能是另一种选择。如果您正在寻找运行一些工作并对进度报告提供简单的支持,BackgroundWorker则可能是一个有用的选择。在这些选项中,如果您需要一个支持async/await模式的现代且易于使用的选项,Task可能是最佳选择。
所有这些都是您可以使用的工具,希望您对它们各自的功能有更多的了解。
本文最初发表于 Tasks, BackgroundWorkers, and Threads - Simple Comparisons
https://www.codeproject.com/Articles/5352757/Tasks-BackgroundWorkers-and-Threads-Simple-Compari