专题01 异步 多线程
1. Thread类
1.1 使用Thread创建线程
CSharp
namespace ConsoleApp1;
internal class Program
{
private static void Main(string[] Args)
{
var t = new Thread(WirteY);
t.Name = "Y thread ...";
t.Start();
for (int i = 0; i < 1000; i++)
{
Console.Write("x");
}
}
private static void WirteY()
{
for (int i = 0; i < 1000; i++)
{
Console.Write("y");
}
}
}
- 线程的一些属性
- 线程一旦开始执行,IsAlive就是true,线程结束变为false
- 线程结束的条件是:线程构造函数传入的委托结束了执行
- 线程一旦结束,就无法再重启
- 每个线程都有个Name属性,通常用于调试
- 线程Name只能设置一次,以后更改会抛出异常
- 静态的thread.CurrentThread属性,会返回当前执行的线程
1.2 Thread.join()和Thread.Sleep()
CSharp
namespace ConsoleApp1;
internal class Program
{
private static void Main()
{
Thread.CurrentThread.Name = "Main Thread";
var thread1 = new Thread(ThreadProc1)
{
Name = "Thread1"
};
var thread2 = new Thread(ThreadProc2)
{
Name = "Thread2"
};
thread1.Start();
thread2.Start();
// 等待thread2运行结束
thread2.Join();
var currentTime = DateTime.Now;
Console.WriteLine($"\nCurrent thread " +
$"{Thread.CurrentThread.Name}: {currentTime.Hour}:{currentTime.Minute}:{currentTime.Second}");
}
private static void ThreadProc1()
{
Thread.Sleep(5000);
var currentTime = DateTime.Now;
Console.WriteLine($"\nCurrent thread " +
$"{Thread.CurrentThread.Name}: {currentTime.Hour}:{currentTime.Minute}:{currentTime.Second}");
}
private static void ThreadProc2()
{
Thread.Sleep(3000);
var currentTime = DateTime.Now;
Console.WriteLine($"\nCurrent thread " +
$"{Thread.CurrentThread.Name}: {currentTime.Hour}:{currentTime.Minute}:{currentTime.Second}");
}
}
1.3 Thread存在的问题
- 虽然开始线程的时候可以方便的传入数据,但当Join的时候很难从线程获得返回值
- 可能需要设置共享字段
- 如果操作抛出异常,捕获和传播该异常都很麻烦
- 无法告诉子线程在结束时在当前线程做另外的工作,而不是结束子线程
- 这要求必须进行Join操作,这会在进程中阻塞主线程
- 很难使用较小的并发(concurrent)来组建大型的并发
- 导致了对手动同步的更大依赖以及随之而来的问题
2. Task类
2.1 Task类的优势
- Task类是对Thread的高级抽象,能够解决Thread存在的问题
- Task代表了一个并发操作(concurrent)
- 这个并发操作可能由Thread支持,也可以不由Thread支持
- Task是可组合的
- 可以使用Continuation把任务串联起来,同时使用线程池减少了启动延迟
- TaskCompletionSource类使得Tasks可以利用回调方法,在等待I/O绑定操作时完全避免使用线程
2.2 建立一个Task
- 开始一个Task最简单的办法是使用Task.Run这个静态方法
- 传入一个Action委托即可
- Task默认使用线程池,也就是后台线程
- 当主线程结束时,创建的所有Tasks都会结束
- Task.Run返回一个Task对象,可以用来监视其过程
- 在Task.Run之后无需调用Start的原因在于,使用该方法创建的是hot task,热任务可以立刻执行。当然,也可以通过Task的构造函数 创建"冷"任务,但很少这样做。
- 可以使用Task类的Status属性来跟踪task的执行状态。
- 可以使用Task.Wait()阻塞对应task线程,直到该线程完成
CSharp
namespace ConsoleApp1;
internal class Program
{
/*该函数没有任何输出的原因在于:Task默认使用后台线程,
*当主线程Main()结束时所有task都会结束,然而此时task1还未执行完成 */
private static void Function1()
{
var task1 = Task.Run(() =>
{
Thread.Sleep(3000);
Console.WriteLine("Function1");
});
// 输出当前task的执行状态
Console.WriteLine($"task1执行完成? {task1.IsCompleted}");
}
private static void Function2()
{
var task2 = Task.Run(() =>
{
Thread.Sleep(1000);
Console.WriteLine("Function2");
});
Console.WriteLine($"task2执行完成? {task2.IsCompleted}");
// 等待task2执行完成
task2.Wait();
Console.WriteLine($"task2执行完成? {task2.IsCompleted}");
}
private static void Main()
{
Function1();
Function2();
}
}
- 也可以使用Task.Wait()设定一个超时时间和一个取消令牌来提前结束等待。
CSharp
/* 带有时间限制的task.Wait() */
/* 结束等待的途径:1.任务超时;2.任务完成 */
namespace ConsoleApp1;
internal class Program
{
private static void TimeLimitExample()
{
// 在0到100之间生成500万个随机数,并计算其值
var t = Task.Run(() =>
{
var rnd = new Random();
long sum = 0;
int n = 500 * (int)Math.Pow(10, 4);
for (int ctr = 0; ctr < n; ctr++)
{
int number = rnd.Next(0, 101);
sum += number;
}
Console.WriteLine("Total: {0:N0}", sum);
Console.WriteLine("Mean: {0:N2}", sum / n);
Console.WriteLine("N: {0:N0}", n);
});
// 设置超时时间为150ms
var ts = TimeSpan.FromMilliseconds(150);
if (!t.Wait(ts))
{
Console.WriteLine("The timeout interval elapsed.");
}
}
private static void TimeoutExample()
{
// 在0到100之间生成500万个随机数,并计算其值
var t = Task.Run(() =>
{
var rnd = new Random();
long sum = 0;
int n = 500 * (int)Math.Pow(10, 4);
for (int ctr = 0; ctr < n; ctr++)
{
int number = rnd.Next(0, 101);
sum += number;
}
// 人为增加延时200ms
Thread.Sleep(200);
Console.WriteLine("Total: {0:N0}", sum);
Console.WriteLine("Mean: {0:N2}", sum / n);
Console.WriteLine("N: {0:N0}", n);
});
// 设置超时时间为150ms
var ts = TimeSpan.FromMilliseconds(150);
if (!t.Wait(ts))
{
Console.WriteLine("The timeout interval elapsed.");
}
}
private static void Main()
{
TimeLimitExample();
Console.WriteLine(Environment.NewLine);
TimeoutExample();
}
}
CSharp
/* 带有取消令牌的task.Wait() */
/* 结束等待的途径:1.通过取消令牌取消等待;2.任务完成 */
/* 取消令牌需要ts.Cancel();和Task.Wait(ts.Token); */
/* 其中触发ts.Cancel()的条件要在Task.Run(func)的func方法中指定 */
namespace ConsoleApp1;
internal class Program
{
private static void Main()
{
var ts = new CancellationTokenSource();
var t = Task.Run(() =>
{
Console.WriteLine("Calling Cancel...");
// 传达取消请求
ts.Cancel();
Task.Delay(5000).Wait();
Console.WriteLine("Task ended delay...");
});
try
{
Console.WriteLine("About to wait for the task to complete...");
// 使用ts.Token获取与此CancellationToken关联的CancellationTokenSource
t.Wait(ts.Token);
}
catch (OperationCanceledException e)
{
Console.WriteLine("{0}: The wait has been canceled. Task status: {1:G}",
e.GetType().Name, t.Status);
Thread.Sleep(6000);
Console.WriteLine("After sleeping, the task status: {0:G}", t.Status);
}
// 释放CancellationTokenSource类的当前实例所使用的所有资源
ts.Dispose();
}
}
- 针对长时间运行的任务或者阻塞操作,可以不采用线程池。可以使用Task.Factory.StartNew和TaskCreationOptions.LongRunning定义,后台调度时会为这一任务新开一个线程。
CSharp
namespace ConsoleApp1;
internal class Program
{
private static void Main()
{
var task = Task.Factory.StartNew(() =>
{
Thread.Sleep(1000);
Console.WriteLine("Run");
}, TaskCreationOptions.LongRunning);
task.Wait();
}
}
- 如果同时运行多个long-running tasks,那么性能将会受到很大影响
- 如果任务是IO-Bound(受限于数据吞吐量),可以使用由TaskCompletionSource和异步函数组成的回调代替线程实现并发。
- 如果任务是CPU-Bound(受限于CPU运算能力),使用 生产者/消费者 队列对任务的并发进行限制,防止把其他的线程或进程"饿死"。
- 注:绝大多数任务都是IO-Bound的,CPU的数据处理能力远强于数据传输能力。
2.3 CancellationToken的使用
2.3.1 示例1
以异步方式下载一个网站内容100次,设置超时时间1.5s,如果超时则使用CancellationToken终止异步下载过程。
CSharp
namespace ConsoleApp1ForTest;
internal static class Program
{
private static async Task Main()
{
var cancellationTokenSource = new CancellationTokenSource();
// 在多长时间后取消
cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(1.5));
// 从cancellationTokenSource创建令牌
var cancellationToken = cancellationTokenSource.Token;
const string url = "https://www.youzack.com";
await Download(url, 100, cancellationToken);
}
private static async Task Download(string url, int n, CancellationToken token)
{
using var httpclient = new HttpClient
{
Timeout = TimeSpan.FromSeconds(1)
};
for (var i = 0; i < n; i++)
{
try
{
var html = await httpclient.GetStringAsync(url, token);
Console.WriteLine($"{DateTime.Now}:{html}");
}
catch (TaskCanceledException exception)
{
Console.WriteLine(exception.Message);
Console.WriteLine("任务被取消");
}
// 如果没有CancellationToken继续下次循环,跳过Console.WriteLine
// 如果有CancellationToken则终止循环
if (!token.IsCancellationRequested) continue;
Console.WriteLine("CancellationToken触发,跳过剩余循环");
break;
}
}
}
2.3.2 示例2
以异步方式下载一个网站内容10000次,在此过程中用户可以手动设置Cancellation Token取消下载任务。
CSharp
namespace ConsoleApp1ForTest;
internal static class Program
{
private static void Main()
{
var cancellationTokenSource = new CancellationTokenSource();
const string url = "https://www.youzack.com";
var cancellationToken = cancellationTokenSource.Token;
_ = Download(url, 10000, cancellationToken);
while (!cancellationToken.IsCancellationRequested)
{
Console.WriteLine("按Q键取消任务");
var cancelSignal = Console.ReadLine();
if (cancelSignal == "Q") cancellationTokenSource.Cancel();
else Console.WriteLine("指令输入错误,请重试");
}
}
private static async Task Download(string url, int n, CancellationToken token)
{
using var httpclient = new HttpClient
{
Timeout = TimeSpan.FromSeconds(1)
};
for (var i = 0; i < n; i++)
{
try
{
_ = await httpclient.GetStringAsync(url, token);
Console.WriteLine($"{DateTime.Now}:{i}/{n}");
}
catch (TaskCanceledException exception)
{
Console.WriteLine(exception.Message);
Console.WriteLine("任务被取消");
}
// 如果没有CancellationToken继续下次循环,跳过Console.WriteLine
// 如果有CancellationToken则终止循环
if (!token.IsCancellationRequested) continue;
Console.WriteLine("CancellationToken触发,跳过剩余循环");
break;
}
}
}
3. 示例
3.1 【示例】从多个网页上下载内容
ad-info
title: 示例来源
C\# Async/Await: 让你的程序变身时间管理大师
https://www.bilibili.com/video/av846932409/
不过做了改进,不要界面,只用控制台程序。
CSharp
using System.Diagnostics;
namespace ConsoleApp1;
internal static class Program
{
private static void Main(string[] args)
{
var time1 = Downloader.DownloadWebsitesSync();
var time2 = Downloader.DownloadWebsitesAsync();
Console.WriteLine($"同步方法下载用时:{time1}, 异步方法下载用时:{time2}");
}
}
internal static class Downloader
{
private readonly static List<string> WebAddresses = new List<string>()
{
"https://docs.microsoft.com",
"https://docs.microsoft.com/aspnet/core",
"https://docs.microsoft.com/azure",
"https://docs.microsoft.com/azure/devops",
"https://docs.microsoft.com/dotnet",
"https://docs.microsoft.com/dynamics365",
"https://docs.microsoft.com/education",
"https://docs.microsoft.com/enterprise-mobility-security",
"https://docs.microsoft.com/gaming",
"https://docs.microsoft.com/graph",
"https://docs.microsoft.com/microsoft-365",
"https://docs.microsoft.com/office",
"https://docs.microsoft.com/powershell",
"https://docs.microsoft.com/sql",
"https://docs.microsoft.com/surface",
"https://docs.microsoft.com/system-center",
"https://docs.microsoft.com/visualstudio",
"https://docs.microsoft.com/windows",
"https://docs.microsoft.com/xamarin"
};
private readonly static HttpClient HttpClient = new HttpClient()
{
Timeout = TimeSpan.FromSeconds(5)
};
public static string DownloadWebsitesSync()
{
var stopwatch = Stopwatch.StartNew();
var linqQuery = from url in WebAddresses
let response = HttpClient.GetByteArrayAsync(url).Result
select new
{
Url = url,
ResponseLength = response.Length
};
foreach (var element in linqQuery)
{
var outputSting =
$"Finish downloading data from {element.Url}. Total bytes returned {element.ResponseLength}.";
Console.WriteLine(outputSting);
}
var totalTime = $"{stopwatch.Elapsed:g}";
Console.WriteLine($"Total bytes downloaded: {totalTime}");
stopwatch.Stop();
return totalTime;
}
public static string DownloadWebsitesAsync()
{
var stopwatch = Stopwatch.StartNew();
var downloadWebsiteTasks =
WebAddresses.Select(site => Task.Run(() => new
{
Url = site,
ResponseLength = HttpClient.GetByteArrayAsync(site).Result.Length
})).ToList();
var results = Task.WhenAll(downloadWebsiteTasks).Result;
foreach (var element in results)
{
var outputSting =
$"Finish downloading data from {element.Url}. Total bytes returned {element.ResponseLength}.";
Console.WriteLine(outputSting);
}
var totalTime = $"{stopwatch.Elapsed:g}";
Console.WriteLine($"Total bytes downloaded: {totalTime}");
stopwatch.Stop();
return totalTime;
}
}
测试结果如下:
Finish downloading data from https://docs.microsoft.com. Total bytes returned 42574.
Finish downloading data from https://docs.microsoft.com/aspnet/core. Total bytes returned 86207.
Finish downloading data from https://docs.microsoft.com/azure. Total bytes returned 395639.
Finish downloading data from https://docs.microsoft.com/azure/devops. Total bytes returned 84141.
Finish downloading data from https://docs.microsoft.com/dotnet. Total bytes returned 89027.
Finish downloading data from https://docs.microsoft.com/dynamics365. Total bytes returned 59208.
Finish downloading data from https://docs.microsoft.com/education. Total bytes returned 38835.
Finish downloading data from https://docs.microsoft.com/enterprise-mobility-security. Total bytes returned 31118.
Finish downloading data from https://docs.microsoft.com/gaming. Total bytes returned 66954.
Finish downloading data from https://docs.microsoft.com/graph. Total bytes returned 51945.
Finish downloading data from https://docs.microsoft.com/microsoft-365. Total bytes returned 66549.
Finish downloading data from https://docs.microsoft.com/office. Total bytes returned 29537.
Finish downloading data from https://docs.microsoft.com/powershell. Total bytes returned 58100.
Finish downloading data from https://docs.microsoft.com/sql. Total bytes returned 60741.
Finish downloading data from https://docs.microsoft.com/surface. Total bytes returned 46145.
Finish downloading data from https://docs.microsoft.com/system-center. Total bytes returned 49409.
Finish downloading data from https://docs.microsoft.com/visualstudio. Total bytes returned 34358.
Finish downloading data from https://docs.microsoft.com/windows. Total bytes returned 29840.
Finish downloading data from https://docs.microsoft.com/xamarin. Total bytes returned 58138.
Total bytes downloaded: 0:00:11.323132
Finish downloading data from https://docs.microsoft.com. Total bytes returned 42574.
Finish downloading data from https://docs.microsoft.com/aspnet/core. Total bytes returned 86207.
Finish downloading data from https://docs.microsoft.com/azure. Total bytes returned 395639.
Finish downloading data from https://docs.microsoft.com/azure/devops. Total bytes returned 84141.
Finish downloading data from https://docs.microsoft.com/dotnet. Total bytes returned 89027.
Finish downloading data from https://docs.microsoft.com/dynamics365. Total bytes returned 59208.
Finish downloading data from https://docs.microsoft.com/education. Total bytes returned 38835.
Finish downloading data from https://docs.microsoft.com/enterprise-mobility-security. Total bytes returned 31118.
Finish downloading data from https://docs.microsoft.com/gaming. Total bytes returned 66954.
Finish downloading data from https://docs.microsoft.com/graph. Total bytes returned 51945.
Finish downloading data from https://docs.microsoft.com/microsoft-365. Total bytes returned 66549.
Finish downloading data from https://docs.microsoft.com/office. Total bytes returned 29537.
Finish downloading data from https://docs.microsoft.com/powershell. Total bytes returned 58100.
Finish downloading data from https://docs.microsoft.com/sql. Total bytes returned 60741.
Finish downloading data from https://docs.microsoft.com/surface. Total bytes returned 46145.
Finish downloading data from https://docs.microsoft.com/system-center. Total bytes returned 49409.
Finish downloading data from https://docs.microsoft.com/visualstudio. Total bytes returned 34358.
Finish downloading data from https://docs.microsoft.com/windows. Total bytes returned 29840.
Finish downloading data from https://docs.microsoft.com/xamarin. Total bytes returned 58138.
Total bytes downloaded: 0:00:01.8900202
同步方法下载用时:0:00:11.323132, 异步方法下载用时:0:00:01.8900202
3.2 【示例】批量计算文件hash值
首先先编写配置文件,命名为configuration.json
。
json
{
"DirectoryPath": "D:\\pictures",
"HashResultOutputConfig": {
"ConnectionString": "Data Source=sql.nas.home;Database=LearnAspDotnet_CalculateHash;User ID=root;Password=Aa123456+;pooling=true;port=3306;CharSet=utf8mb4",
"ExcelFilePath": "D:\\hashResult.xlsx",
"OutputToMySql": false,
"OutputToExcel": true
},
"ParallelHashCalculateConfig": {
"InitialDegreeOfParallelism": 4,
"MaxDegreeOfParallelism": 32,
"MinDegreeOfParallelism": 1,
"AdjustmentInterval_Seconds": 5,
"TargetBatchSize_MByte": 100,
"FileCountPreAdjust": 5,
"AdjustByFileSize": true
}
}
为了方便调用,将配置文件和配置类绑定。现在新建配置类Configuration
。
CSharp
namespace CalculateHash.Core;
public class Configuration
{
public string DirectoryPath { get; set; } = string.Empty;
public HashResultOutputConfig HashResultOutputConfig { get; set; } = new();
public ParallelHashCalculateConfig ParallelHashCalculateConfig { get; set; } = new();
}
public class HashResultOutputConfig
{
public string ConnectionString { get; set; } = string.Empty;
public string ExcelFilePath { get; set; } = string.Empty;
public bool OutputToMySql { get; set; } = false;
public bool OutputToExcel { get; set; } = false;
}
public class ParallelHashCalculateConfig
{
// 初始并行度
public int InitialDegreeOfParallelism { get; set; }
// 最大并行度
public int MaxDegreeOfParallelism { get; set; }
// 最小并行度
public int MinDegreeOfParallelism { get; set; }
// 调整时间间隔
public int AdjustmentInterval_Seconds { get; set; }
// 每次并行时计算时调整的总文件大小
public int TargetBatchSize_MByte { get; set; }
// 是否按照总文件大小调整(false:不动态调整)
public bool AdjustByFileSize { get; set; } = false;
}
现在还需要一个静态工具类完成配置文件和配置类的绑定并承担配置的读写工作,该工具类为ConfigurationJsonHelper
。
CSharp
using System.Data;
using Newtonsoft.Json;
namespace CalculateHash.Core;
public static class ConfigurationJsonHelper
{
public static string GetFullPathOfJsonConfiguration()
{
var fileInfo = new FileInfo("./configuration.json");
return fileInfo.FullName;
}
public static Configuration GetConfiguration()
{
try
{
using var fileReader = new StreamReader("./configuration.json");
var jsonContent = fileReader.ReadToEndAsync().GetAwaiter().GetResult();
var configuration = JsonConvert.DeserializeObject<Configuration>(jsonContent);
if (configuration is null) throw new NoNullAllowedException("无法反序列化");
return configuration;
}
catch (Exception e) when (e is FileNotFoundException or IOException or DirectoryNotFoundException)
{
Console.WriteLine("路径出错: {0}", e.Message);
throw;
}
catch (NoNullAllowedException e)
{
Console.WriteLine("反序列化出错:{0}", e.Message);
throw;
}
}
public static Configuration GetConfiguration(string configurationPath)
{
try
{
using var fileReader = new StreamReader(configurationPath);
var jsonContent = fileReader.ReadToEndAsync().GetAwaiter().GetResult();
var configuration = JsonConvert.DeserializeObject<Configuration>(jsonContent);
if (configuration is null) throw new NoNullAllowedException("无法反序列化");
return configuration;
}
catch (Exception e) when (e is FileNotFoundException or IOException or DirectoryNotFoundException)
{
Console.WriteLine("路径出错: {0}", e.Message);
throw;
}
catch (NoNullAllowedException e)
{
Console.WriteLine("反序列化出错:{0}", e.Message);
throw;
}
}
}
还需要一个工具类负责将Hash结果保存起来,该工具类为SaveTo
。
CSharp
using MySql.Data.MySqlClient;
using OfficeOpenXml;
namespace CalculateHash.Core;
public class SaveTo(Configuration configuration)
{
private readonly static object ExcelLock = new object();
private readonly static object DatabaseLock = new object();
public void SaveHashToDatabase(HashResultOutputDto hashResult)
{
lock (DatabaseLock)
{
using var connection = new MySqlConnection(configuration.HashResultOutputConfig.ConnectionString);
connection.Open();
// 是否有相同文件记录,如果有相同文件就不插入
const string sqlCommand =
"INSERT INTO FileHashTable (fileCounter, fileName, sha512, fileNameWithFullPath) " +
"select @fileCounter,@fileName, @sha512, @fileNameWithFullPath " +
// 只有文件名+文件路径、sha512值完全一致时才视为重复插入,只有文件名+文件路径相同或者只有sha512值相同,均视为不同文件,允许插入
"where exists(select fileNameWithFullPath from FileHashTable where fileNameWithFullPath=@fileNameWithFullPath)=0" +
"or exist(select sha512 from FileHashTable where sha512=@sha512)=0";
using var command = new MySqlCommand(sqlCommand, connection);
command.Parameters.AddWithValue("@fileCounter", hashResult.FileCounter);
command.Parameters.AddWithValue("@fileName", hashResult.FileName);
command.Parameters.AddWithValue("@sha512", hashResult.HashResult_Sha512);
command.Parameters.AddWithValue("@fileNameWithFullPath", hashResult.FileFullName);
command.ExecuteNonQuery();
}
}
public void SaveHashToExcel(HashResultOutputDto hashResult)
{
lock (ExcelLock)
{
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
using var excelPackage = new ExcelPackage(new FileInfo(configuration.HashResultOutputConfig.ExcelFilePath));
var worksheet = excelPackage.Workbook.Worksheets.FirstOrDefault() ??
excelPackage.Workbook.Worksheets.Add("File Hash Table");
var lastRow = worksheet.Dimension?.Rows ?? 0;
worksheet.Cells[lastRow + 1, 1].Value = hashResult.FileCounter;
worksheet.Cells[lastRow + 1, 2].Value = hashResult.FileName;
worksheet.Cells[lastRow + 1, 3].Value = hashResult.HashResult_Sha512;
worksheet.Cells[lastRow + 1, 4].Value = hashResult.FileFullName;
excelPackage.SaveAsync().GetAwaiter().GetResult();
}
}
}
现在可以开始文件Hash值的计算工作了,新建FilesHashCalculator
类,该类的功能有:
- 读取配置文件
- 计算Hash(这里用SHA512)
- 动态并行度调整(如果计算的快就适量增加并行度,如果计算的慢就适量减少并行度)
CSharp
using System.Security.Cryptography;
using System.Text;
namespace CalculateHash.Core;
public class FilesHashCalculator
{
private readonly Configuration _configuration;
private readonly SaveTo _save;
private long _fileCounter;
private readonly ManualResetEventSlim _pauseEvent;
public FilesHashCalculator(ManualResetEventSlim pauseEvent)
{
_configuration = ConfigurationJsonHelper.GetConfiguration();
_save = new SaveTo(_configuration);
_fileCounter = 0;
_pauseEvent = pauseEvent;
}
public void DynamicParallelHashCalculation(Action<long, string, string, long>? updateProgress,
CancellationToken cancellationToken)
{
var directoryPath = _configuration.DirectoryPath;
var initialDegreeOfParallelism = _configuration.ParallelHashCalculateConfig.InitialDegreeOfParallelism;
var maxDegreeOfParallelism = _configuration.ParallelHashCalculateConfig.MaxDegreeOfParallelism;
var minDegreeOfParallelism = _configuration.ParallelHashCalculateConfig.MinDegreeOfParallelism;
// 时间间隔:毫秒
long adjustmentInterval = _configuration.ParallelHashCalculateConfig.AdjustmentInterval_Seconds * 1000;
// 每次调整的大小:字节
long targetBatchSize = 1024 * 1024 * _configuration.ParallelHashCalculateConfig.TargetBatchSize_MByte;
try
{
var files = Directory.GetFiles(directoryPath)
.Select(filePath => new FileInfo(filePath))
.OrderByDescending(file => file.Length) // 按文件大小降序排列
.ToList();
var totalFilesCount = files.Count;
var currentDegreeOfParallelism = initialDegreeOfParallelism;
var batches = CreateBatches(files, targetBatchSize);
foreach (var batch in batches)
{
var parallelOptions = new ParallelOptions
{
MaxDegreeOfParallelism = currentDegreeOfParallelism,
CancellationToken = cancellationToken
};
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
Parallel.ForEach(batch, parallelOptions, fileInfo =>
{
_pauseEvent.Wait(cancellationToken);
var hash = ComputeHash(fileInfo.FullName);
var fileNumber = Interlocked.Increment(ref _fileCounter);
var hashResult = new HashResultOutputDto()
{
FileCounter = fileNumber,
FileName = fileInfo.Name,
FileFullName = fileInfo.FullName,
HashResult_Sha512 = hash
};
if (_configuration.HashResultOutputConfig.OutputToMySql) _save.SaveHashToDatabase(hashResult);
if (_configuration.HashResultOutputConfig.OutputToExcel) _save.SaveHashToExcel(hashResult);
#if DEBUG
Console.WriteLine($"{hashResult.FileCounter}-{hashResult.FileName}-{hashResult.HashResult_Sha512}");
#endif
updateProgress?.Invoke(hashResult.FileCounter, hashResult.FileName, hashResult.HashResult_Sha512,
totalFilesCount);
});
stopwatch.Stop();
Console.WriteLine(
$"Processed {batch.Count} files in {stopwatch.ElapsedMilliseconds} ms with degree of parallelism {currentDegreeOfParallelism}");
// 是否动态调整并行度
if (!_configuration.ParallelHashCalculateConfig.AdjustByFileSize) continue;
// 动态调整并行度
if (stopwatch.ElapsedMilliseconds < adjustmentInterval &&
currentDegreeOfParallelism < maxDegreeOfParallelism)
{
currentDegreeOfParallelism++;
}
else if (stopwatch.ElapsedMilliseconds > adjustmentInterval &&
currentDegreeOfParallelism > minDegreeOfParallelism)
{
currentDegreeOfParallelism--;
}
}
}
catch (AggregateException ae)
{
foreach (var ex in ae.InnerExceptions) Console.WriteLine($"An error occurred: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
}
}
private static List<List<FileInfo>> CreateBatches(List<FileInfo> files, long targetBatchSize)
{
var batches = new List<List<FileInfo>>();
var currentBatch = new List<FileInfo>();
long currentBatchSize = 0;
foreach (var file in files)
{
if (currentBatchSize + file.Length > targetBatchSize && currentBatch.Count > 0)
{
batches.Add(currentBatch);
currentBatch = [];
currentBatchSize = 0;
}
currentBatch.Add(file);
currentBatchSize += file.Length;
}
if (currentBatch.Count > 0) batches.Add(currentBatch);
return batches;
}
private static string ComputeHash(string fileInfoFullName)
{
using var stream = File.OpenRead(fileInfoFullName);
using var sha512 = SHA512.Create();
var hashBytes = sha512.ComputeHash(stream);
var hashString = new StringBuilder();
foreach (var b in hashBytes) hashString.Append(b.ToString("x2"));
return hashString.ToString();
}
}
下面添加图形化界面,图形化界面使用WPF。
首先是主窗口设计:
xml
<Window x:Class="CalculateHash.GUI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:CalculateHash.GUI"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<Button x:Name="BtnSettings" Content="设置" HorizontalAlignment="Left" VerticalAlignment="Top" Width="100"
Margin="120,10,0,0" Click="BtnSettings_Click" />
<Button x:Name="BtnStart" Content="开始" HorizontalAlignment="Left" VerticalAlignment="Top" Width="100"
Margin="230,10,0,0" Click="BtnStart_Click" />
<Button x:Name="BtnPause" Content="暂停" HorizontalAlignment="Left" VerticalAlignment="Top" Width="100"
Margin="340,10,0,0" Click="BtnPause_Click" />
<Button x:Name="BtnStop" Content="停止" HorizontalAlignment="Left" VerticalAlignment="Top" Width="100"
Margin="450,10,0,0" Click="BtnStop_Click" />
<Button x:Name="BtnClear" Content="清除" HorizontalAlignment="Left" VerticalAlignment="Top" Width="100"
Margin="560,10,0,0" Click="BtnClear_Click" />
<ProgressBar x:Name="ProgressBar" HorizontalAlignment="Left" VerticalAlignment="Top" Width="760" Height="30"
Margin="10,50,0,0" />
<DataGrid x:Name="DataGridResults" HorizontalAlignment="Left" VerticalAlignment="Top" Height="250" Width="760"
Margin="10,90,0,0" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="文件编号" Binding="{Binding FileNumber}" Width="*" />
<DataGridTextColumn Header="文件名" Binding="{Binding FileName}" Width="*" />
<DataGridTextColumn Header="哈希值" Binding="{Binding HashResult}" Width="*" />
</DataGrid.Columns>
</DataGrid>
<TextBlock x:Name="TxtTimeElapsed" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10,350,0,0"
Width="200" Text="已用时间: 0s" />
<TextBlock x:Name="TxtTimeRemaining" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="220,350,0,0"
Width="200" Text="预计剩余时间: 0s" />
</Grid>
</Window>
主窗口对应的代码:
CSharp
using System.Collections.ObjectModel;
using System.Windows;
using CalculateHash.Core;
namespace CalculateHash.GUI;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private CancellationTokenSource _cancellationTokenSource;
private readonly ManualResetEventSlim _pauseEvent;
private bool _isPaused;
private readonly ObservableCollection<DataGridResult> _hashResults;
private DateTime _startTime;
public MainWindow()
{
InitializeComponent();
_cancellationTokenSource = new CancellationTokenSource();
_pauseEvent = new ManualResetEventSlim(initialState: true);
_hashResults = new ObservableCollection<DataGridResult>();
_isPaused = false;
DataGridResults.ItemsSource = _hashResults;
_startTime = DateTime.Now;
BtnPause.IsEnabled = false;
BtnClear.IsEnabled = false;
BtnStop.IsEnabled = false;
BtnStart.IsEnabled = false;
}
private void BtnSettings_Click(object sender, RoutedEventArgs e)
{
BtnStart.IsEnabled = true;
var settingsWindow = new SettingsWindow(ConfigurationJsonHelper.GetConfiguration());
settingsWindow.ShowDialog();
}
private void BtnStart_Click(object sender, RoutedEventArgs e)
{
_startTime = DateTime.Now;
_cancellationTokenSource = new CancellationTokenSource();
var filesHashCalculator = new FilesHashCalculator(_pauseEvent);
try
{
BtnStart.IsEnabled = false;
BtnPause.IsEnabled = true;
BtnStop.IsEnabled = true;
// 确保信号状态为:"有信号"
_pauseEvent.Set();
Task.Run(() =>
{
filesHashCalculator.DynamicParallelHashCalculation(UpdateProgress,
_cancellationTokenSource.Token);
});
}
catch (OperationCanceledException)
{
MessageBox.Show("计算已停止");
}
}
// 回调方法:每当一个文件的Hash计算完成就会执行该方法更新UI
private void UpdateProgress(long fileNumber, string fileName, string hashResult, long totalFilesCount)
{
// Dispatcher.Invoke:从其他线程调用UI线程上的元素
// 该方法是回调方法,不是在UI线程上执行的,如果想和UI线程交互,必须使用Dispatcher.Invoke才可以
Dispatcher.Invoke(() =>
{
_hashResults.Add(new DataGridResult()
{
FileName = fileName,
FileNumber = fileNumber,
HashResult = hashResult
});
ProgressBar.Value = _hashResults.Count / (double)totalFilesCount * 100;
TxtTimeElapsed.Text = $"已用时间: {(DateTime.Now - _startTime).TotalSeconds}s";
TxtTimeRemaining.Text =
$"预计剩余时间: {(DateTime.Now - _startTime).TotalSeconds / _hashResults.Count * (totalFilesCount - _hashResults.Count)}s";
});
}
private void BtnPause_Click(object sender, RoutedEventArgs e)
{
if (!_isPaused)
{
// 将事件设置为无信号状态,暂停操作
_pauseEvent.Reset();
_isPaused = true;
MessageBox.Show("已暂停");
}
else
{
// 将事件设置为有信号状态,恢复操作
_pauseEvent.Set();
_isPaused = false;
MessageBox.Show("已恢复");
}
}
private void BtnStop_Click(object sender, RoutedEventArgs e)
{
_cancellationTokenSource.Cancel();
BtnPause.IsEnabled = false;
BtnClear.IsEnabled = true;
_isPaused = false;
MessageBox.Show("已停止");
}
private void BtnClear_Click(object sender, RoutedEventArgs e)
{
BtnStart.IsEnabled = true;
_hashResults.Clear();
}
}
public struct DataGridResult
{
public long FileNumber { get; set; }
public string FileName { get; set; }
public string HashResult { get; set; }
}
主窗口中调用了SettingsWindow,但现在SettingsWindow这个视图还未创建,现在创建SettingsWindow.xaml
xml
<Window x:Class="CalculateHash.GUI.SettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:CalculateHash.GUI"
mc:Ignorable="d"
Title="SettingsWindow" Height="210" Width="650">
<Grid>
<TextBox x:Name="TxtDirectoryPath" HorizontalAlignment="Left" VerticalAlignment="Top" Width="300"
Margin="10,10,0,0" Text="Directory Path" />
<TextBox x:Name="TxtConnectionString" HorizontalAlignment="Left" VerticalAlignment="Top" Width="300"
Margin="10,40,0,0" Text="Connection String" />
<TextBox x:Name="TxtInitialDegree" HorizontalAlignment="Left" VerticalAlignment="Top" Width="300"
Margin="10,70,0,0" Text="Initial Degree" />
<TextBox x:Name="TxtMaxDegree" HorizontalAlignment="Left" VerticalAlignment="Top" Width="300"
Margin="10,100,0,0" Text="Max Degree" />
<TextBox x:Name="TxtAdjustmentInterval" HorizontalAlignment="Left" VerticalAlignment="Top" Width="300"
Margin="320,10,0,0" Text="Adjustment Interval" />
<TextBox x:Name="TxtExcelFilePath" HorizontalAlignment="Left" VerticalAlignment="Top" Width="300"
Margin="320,40,0,0" Text="Excel File Path" />
<TextBox x:Name="TxtTargetBatchSize" HorizontalAlignment="Left" VerticalAlignment="Top" Width="300"
Margin="320,70,0,0" Text="Target BatchSize" />
<TextBox x:Name="TxtMinDegree" HorizontalAlignment="Left" VerticalAlignment="Top" Width="300"
Margin="320,100,0,0" Text="Min Degree" />
<CheckBox x:Name="ChkOutputToMySql" Content="Output to MySQL" HorizontalAlignment="Left"
VerticalAlignment="Top" Margin="10,130,0,0" />
<CheckBox x:Name="ChkOutputToExcel" Content="Output to Excel" HorizontalAlignment="Left"
VerticalAlignment="Top" Margin="170,130,0,0" />
<Button x:Name="BtnSaveSettings" Content="Save" HorizontalAlignment="Left" VerticalAlignment="Top" Width="80"
Margin="330,125,0,0" Click="BtnSaveSettings_Click" />
<Button x:Name="BtnDefaultSettings" Content="Default" HorizontalAlignment="Left" VerticalAlignment="Top"
Width="80"
Margin="420,125,0,0" Click="BtnDefaultSettings_Click" />
<Button x:Name="BtnClearSettings" Content="Clear" HorizontalAlignment="Left" VerticalAlignment="Top"
Width="80" Margin="510,125,0,0" Click="BtnClearSettings_Click" />
<Button x:Name="BtnSelectFolder" Content="选择文件夹" HorizontalAlignment="Left" VerticalAlignment="Top" Width="100"
Margin="250,170,0,0" Click="BtnSelectFolder_Click" />
</Grid>
</Window>
SettingWindow.xaml对应的代码如下:
CSharp
using System.IO;
using System.Windows;
using CalculateHash.Core;
using Microsoft.Win32;
using Newtonsoft.Json;
namespace CalculateHash.GUI;
public partial class SettingsWindow : Window
{
private readonly Configuration _configuration;
public SettingsWindow(Configuration configuration)
{
InitializeComponent();
_configuration = configuration;
}
private void BtnSaveSettings_Click(object sender, RoutedEventArgs e)
{
try
{
_configuration.DirectoryPath = TxtDirectoryPath.Text;
_configuration.HashResultOutputConfig.ConnectionString = TxtConnectionString.Text;
_configuration.HashResultOutputConfig.ExcelFilePath = TxtExcelFilePath.Text;
_configuration.HashResultOutputConfig.OutputToMySql = ChkOutputToMySql.IsChecked == true;
_configuration.HashResultOutputConfig.OutputToExcel = ChkOutputToExcel.IsChecked == true;
_configuration.ParallelHashCalculateConfig.InitialDegreeOfParallelism = int.Parse(TxtInitialDegree.Text);
_configuration.ParallelHashCalculateConfig.MaxDegreeOfParallelism = int.Parse(TxtMaxDegree.Text);
_configuration.ParallelHashCalculateConfig.MinDegreeOfParallelism = int.Parse(TxtMinDegree.Text);
_configuration.ParallelHashCalculateConfig.AdjustmentInterval_Seconds =
int.Parse(TxtAdjustmentInterval.Text);
_configuration.ParallelHashCalculateConfig.TargetBatchSize_MByte = int.Parse(TxtTargetBatchSize.Text);
var jsonContent = JsonConvert.SerializeObject(_configuration);
File.WriteAllText(ConfigurationJsonHelper.GetFullPathOfJsonConfiguration(), jsonContent);
MessageBox.Show("Settings saved successfully.");
Close();
}
catch (Exception ex)
{
MessageBox.Show($"Error saving settings: {ex.Message}");
}
}
private void BtnDefaultSettings_Click(object sender, RoutedEventArgs e)
{
var defaultConfiguration = ConfigurationJsonHelper.GetConfiguration("./configuration_default.json");
TxtDirectoryPath.Text = defaultConfiguration.DirectoryPath;
TxtConnectionString.Text = defaultConfiguration.HashResultOutputConfig.ConnectionString;
TxtExcelFilePath.Text = defaultConfiguration.HashResultOutputConfig.ExcelFilePath;
ChkOutputToMySql.IsChecked = defaultConfiguration.HashResultOutputConfig.OutputToMySql;
ChkOutputToExcel.IsChecked = defaultConfiguration.HashResultOutputConfig.OutputToExcel;
TxtInitialDegree.Text = defaultConfiguration.ParallelHashCalculateConfig.InitialDegreeOfParallelism.ToString();
TxtMaxDegree.Text = defaultConfiguration.ParallelHashCalculateConfig.MaxDegreeOfParallelism.ToString();
TxtMinDegree.Text = defaultConfiguration.ParallelHashCalculateConfig.MinDegreeOfParallelism.ToString();
TxtAdjustmentInterval.Text =
defaultConfiguration.ParallelHashCalculateConfig.AdjustmentInterval_Seconds.ToString();
TxtTargetBatchSize.Text = defaultConfiguration.ParallelHashCalculateConfig.TargetBatchSize_MByte.ToString();
}
private void BtnClearSettings_Click(object sender, RoutedEventArgs e)
{
TxtDirectoryPath.Text = "";
TxtConnectionString.Text = "";
TxtExcelFilePath.Text = "";
ChkOutputToMySql.IsChecked = false;
ChkOutputToExcel.IsChecked = false;
TxtInitialDegree.Text = "";
TxtMaxDegree.Text = "";
TxtMinDegree.Text = "";
TxtAdjustmentInterval.Text = "";
TxtTargetBatchSize.Text = "";
}
private void BtnSelectFolder_Click(object sender, RoutedEventArgs e)
{
var dialog = new OpenFileDialog
{
CheckFileExists = false,
CheckPathExists = true,
ValidateNames = false,
FileName = "Folder Selection."
};
if (dialog.ShowDialog() != true) return;
var directoryPath = Path.GetDirectoryName(dialog.FileName);
TxtDirectoryPath.Text = directoryPath ?? "NULL";
}
}
由于用到了默认设置这一选项,所以还要再编写一个configuration_default.json,这个configuration_default.json和configuration.json中的内容一致:
json
{
"DirectoryPath": "D:\\pictures",
"HashResultOutputConfig": {
"ConnectionString": "Data Source=sql.nas.home;Database=LearnAspDotnet_CalculateHash;User ID=root;Password=Aa123456+;pooling=true;port=3306;CharSet=utf8mb4",
"ExcelFilePath": "D:\\hashResult.xlsx",
"OutputToMySql": false,
"OutputToExcel": true
},
"ParallelHashCalculateConfig": {
"InitialDegreeOfParallelism": 4,
"MaxDegreeOfParallelism": 32,
"MinDegreeOfParallelism": 1,
"AdjustmentInterval_Seconds": 5,
"TargetBatchSize_MByte": 100,
"FileCountPreAdjust": 5,
"AdjustByFileSize": true
}
}
至此,这一项目就完成了。