专题01 异步 多线程

1. Thread类

1.1 使用Thread创建线程

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()

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线程,直到该线程完成
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()设定一个超时时间和一个取消令牌来提前结束等待。
/* 带有时间限制的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();
}
}
/* 带有取消令牌的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定义,后台调度时会为这一任务新开一个线程。
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终止异步下载过程。

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取消下载任务。

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 【示例】从多个网页上下载内容

title: 示例来源

C\# Async/Await: 让你的程序变身时间管理大师
https://www.bilibili.com/video/av846932409/

不过做了改进,不要界面,只用控制台程序。

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

{
"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

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

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

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类,该类的功能有:

  1. 读取配置文件
  2. 计算Hash(这里用SHA512)
  3. 动态并行度调整(如果计算的快就适量增加并行度,如果计算的慢就适量减少并行度)
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。

首先是主窗口设计:

<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>

主窗口对应的代码:

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

<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对应的代码如下:

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中的内容一致:

{
"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
}
}

至此,这一项目就完成了。

dotnet学习笔记-专题01-异步与多线程-01的更多相关文章

  1. [置顶] iOS学习笔记47——图片异步加载之EGOImageLoading

    上次在<iOS学习笔记46——图片异步加载之SDWebImage>中介绍过一个开源的图片异步加载库,今天来介绍另外一个功能类似的EGOImageLoading,看名字知道,之前的一篇学习笔 ...

  2. Java学习笔记(八)——java多线程

    [前面的话] 实际项目在用spring框架结合dubbo框架做一个系统,虽然也负责了一块内容,但是自己的能力还是不足,所以还需要好好学习一下基础知识,然后做一些笔记.希望做完了这个项目可以写一些dub ...

  3. STM32学习笔记(五) USART异步串行口输入输出(轮询模式)

    学习是一个简单的过程,只要有善于发掘的眼睛,总能学到新知识,然而如何坚持不懈的学习却很困难,对我亦如此,生活中有太多的诱惑,最后只想说一句勿忘初心.闲话不多扯,本篇讲诉的是异步串行口的输入输出,串口在 ...

  4. JavaScript 学习笔记之线程异步模型

    核心的javascript程序语言并没有包含任何的线程机制,客户端javascript程序也没有任何关于线程的定义,事件驱动模式下的javascript语言并不能实现同时执行,即不能同时执行两个及以上 ...

  5. Java多线程学习笔记——从Java JVM对多线程数据同步的一些理解

       我们知道在多线程编程中,我们很大的一部分内容是为了解决线程间的资源同步问题和线程间共同协作解决问题.线程间的同步,通俗我们理解为僧多粥少,在粥有限情况下,我们怎么去防止大家有秩序的喝到粥,不至于 ...

  6. JAVA并发设计模式学习笔记(一)—— JAVA多线程编程

    这个专题主要讨论并发编程的问题,所有的讨论都是基于JAVA语言的(因其独特的内存模型以及原生对多线程的支持能力),不过本文传达的是一种分析的思路,任何有经验的朋友都能很轻松地将其扩展到任何一门语言. ...

  7. python学习笔记-(十三)线程&多线程

    为了方便大家理解下面的知识,可以先看一篇文章:http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html 线程 1.什么是线程? ...

  8. 孙鑫MFC学习笔记16:异步套接字

    16 1.事件对象 2.CreateEvent创建事件对象 3.SetEvent设置事件对象为通知状态 4.ResetEvent设置事件对象为非通知状态 5.InitializeCriticalSec ...

  9. JMeter 学习笔记从不懂慢慢提升(01)

    开源已经成为一个趋势,虽然说做测试是一个低端的行业,但是我们也应该在这个低端的行业慢慢提升自己,让自己到达理想的高度. 以前说如果你会使用loadrunner可能别人就会觉得你有一定的水平那么就会拿高 ...

  10. Python学习笔记16:标准库多线程(threading包裹)

    Python主要是通过标准库threading包来实现多线程. 今天,互联网时代,所有的server您将收到大量请求. server要利用多线程的方式的优势来处理这些请求,为了改善网络port读写效率 ...

随机推荐

  1. 【YashanDB知识库】v$instance视图中实例角色含义不明确

    [标题]v$instance视图中实例角色含义不明确 [问题分类]文档问题 [关键词]YashanDB, v$instance, 实例角色 [问题描述]v$instance视图中实例角色(如MASTE ...

  2. ansible rpm包下载

    Ansible2.9.18版本下载链接:https://pan.baidu.com/s/1dKlwtLWSOKoMkanW900n9Q 提取码:ansi 将软件上传至系统并解压安装: # tar -z ...

  3. LeetCode 二叉树的最近公共祖先

    一.二叉搜索树的最近公共祖先 利用二叉搜索树的性质,祖先的两个孩子,左孩子的小于根节点的值,右孩子大于根节点的值. 如果根节点的值,同时大于p的值和q的值,那么在左子树找根节点: 如果根节点的值,同时 ...

  4. 科技助力上亿用户隐私安全保护,合合信息两款产品再获CCIA PIA星级标识

    随着互联网技术的飞速发展,个人信息的收集.存储.使用和传输变得日益频繁,其泄露和滥用的风险也随之增加,个人信息保护已成为社会共同关注的热点议题.近期,"中国网络安全产业联盟(CCIA)数据安 ...

  5. JavaScript – 冷知识 (新手)

    当 charAt 遇上 Emoji 参考: stackoverflow – How to get first character of string? 我们经常会用 charAt(0) 来获取 fir ...

  6. Facebook – Facebook Page Embed

    前言 在网站嵌套 Facebook 专页是很好的推广方式哦. 虽然网站还是需要做 Blog, 但是通常被订阅的都是 Facebook 专页而不是网站 Blog. 开通账号 它的 setup 很简单, ...

  7. QT框架中的缓存:为什么有QHash和QMap,还设计了QCache和QContiguousCache?

    简介 本文介绍了QT框架中可用于缓存的几个数据类型各自的特点:通过本文读者可以了解到为什么有QHash和QMap,还设计了QCache和QContiguousCache? 目录 QHash和QMap有 ...

  8. Android Qcom USB Driver学习(十一)

    基于TI的Firmware Update固件升级的流程分析usb appliction layers的数据 USB Protocol Package ①/② map to check password ...

  9. 使用VNC连接ubuntu16.4错误Authentication Failure问题

    解决办法:是因为vnc用一套自己的密码系统,不要去输入ssh登录时的密码,所以只需要进入远程服务器中,设置一哈vnc的密码即可! 在终端输入命令:vncpasswd 到此可以试试远程

  10. Java实用小工具系列1---使用StringUtils分割字符串

    经常有这种情况,需要将逗号分割的字符串,比如:aaa, bbb ,ccc,但往往是人工输入的,难免会有多空格逗号情况,比如:aaa, bbb , ccc, ,,这种情况使用split会解析出不正常的结 ...