Here's how I would go about it (mostly from a readability standpoint rather than a performance one - though the use of immutable data may help contribute to well-measurablmeasurable performance and memory usage metrics):
namespace MD5Library
{
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
public interface IFileBlock
{
byte[] Data
{
get;
}
int Length
{
get;
set;
}
}
public sealed class FileBlock : IFileBlock
{
private readonly byte[] data;
public FileBlock(int inBufferSize)
{
this.data = new byte[inBufferSize];
}
public byte[] Data
{
get
{
return this.data;
}
}
public int Length
{
get;
set;
}
}
public class MD5Hasher
{
private readonly int bufferSize = 1024 * 1024 * 16; // 16mb at a time.
public MD5Hasher()
{
}
public MD5Hasher(int bufferSize)
{
this.bufferSize = bufferSize;
}
protected int BufferSize
{
get
{
return this.bufferSize;
}
}
public Task<string> Go(string inFile, CancellationToken ct, IProgress<IProgressReport> prg)
{
// Queue with capacity 3x the buffer.
var fi = new FileInfo(inFile);
var bigBuffer = new BlockingCollection<IFileBlock>(3);
var readFileTask = new Task(() => this.ReadFiles(fi, ct, bigBuffer));
var computeHashTask = new Task<string>(() => this.CalcHash(fi, bigBuffer, ct, prg));
readFileTask.Start();
computeHashTask.Start();
return computeHashTask;
}
private static string HashToString(IEnumerable<byte> inHash)
{
return inHash.Aggregate(string.Empty, (current, b) => current + b.ToString("x2"));
}
private async void ReadFiles(FileInfo fi, CancellationToken ct, BlockingCollection<IFileBlock> bigBuffer)
{
using (var stream = File.OpenRead(fi.FullName))
{
int bytesRead;
do
{
var fileBlock = new FileBlock(this.bufferSize);
fileBlock.Length = await stream.ReadAsync(fileBlock.Data, 0, this.bufferSize);
bytesRead = fileBlock.Length;
// Infinite timeout.
bigBuffer.TryAdd(fileBlock, -1, ct);
}
while (bytesRead != 0);
bigBuffer.CompleteAdding();
}
}
private static string CalcHash(FileInfo fi, BlockingCollection<IFileBlock> bigBuffer, CancellationToken ct, IProgress<IProgressReport> prg)
{
var fileSize = fi.Length;
var bytesProcessed = 0L;
using (HashAlgorithm hashAlgorithm = MD5.Create())
{
foreach (var b in bigBuffer.GetConsumingEnumerable(ct))
{
hashAlgorithm.TransformBlock(b.Data, 0, b.Length, b.Data, 0);
bytesProcessed += b.Length;
// Report progress.
prg.Report(new ProgressReport(bytesProcessed, fileSize));
}
hashAlgorithm.TransformFinalBlock(new byte[0], 0, 0);
return HashToString(hashAlgorithm.Hash);
}
}
}
public interface IProgressReport
{
int PercentDone
{
get;
}
string InfoText
{
get;
}
}
public sealed class ProgressReport : IProgressReport
{
private const string PercentComplete = "% complete.";
private readonly int percentDone;
private readonly string infoText;
public ProgressReport(long progress, long total)
{
this.percentDone = Convert.ToInt32((100 * progress) / total);
this.infoText = this.PercentDone + PercentComplete;
}
public int PercentDone
{
get
{
return this.percentDone;
}
}
public string InfoText
{
get
{
return this.infoText;
}
}
}
// Borrowed from Stephen Cleary: http://nitoprograms.blogspot.co.uk/2012/02/reporting-progress-from-async-tasks.html
public class PropertyProgress<T> : IProgress<T>, INotifyPropertyChanged
{
private readonly SynchronizationContext context;
private T progress;
public PropertyProgress(T initialProgress = default(T))
{
this.context = SynchronizationContext.Current ?? new SynchronizationContext();
this.progress = initialProgress;
}
public event PropertyChangedEventHandler PropertyChanged;
public T Progress
{
get
{
return this.progress;
}
private set
{
this.progress = value;
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs("Progress"));
}
}
}
void IProgress<T>.Report(T value)
{
this.context.Post(_ => { this.Progress = value; }, null);
}
}
}