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-measurabl 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 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); } } }