Asynchronous SHA256 Hashing
Asked Answered
M

4

12

I have the following method:

public static string Sha256Hash(string input) {
    if(String.IsNullOrEmpty(input)) return String.Empty;
    using(HashAlgorithm algorithm = new SHA256CryptoServiceProvider()) {
        byte[] inputBytes = Encoding.UTF8.GetBytes(input);
        byte[] hashBytes = algorithm.ComputeHash(inputBytes);
        return BitConverter.ToString(hashBytes).Replace("-", String.Empty);
    }
}

Is there a way to make it asynchronous? I was hoping to use the async and await keywords, but the HashAlgorithm class does not provide any asynchronous support for this.

Another approach was to encapsulate all the logic in a:

public static async string Sha256Hash(string input) {
     return await Task.Run(() => {
         //Hashing here...
     });
}

But this does not seem clean and I'm not sure if it's a correct (or efficient) way to perform an operation asynchronously.

What can I do to accomplish this?

Macrophage answered 25/11, 2014 at 18:2 Comment(3)
Why are you trying to do this asynchronously?Petrolic
@CoryNelson I don't know honestly. I thought that I'd be optimizing the code by making it run asynchronously or on another thread. But the answers cleared my mind.Macrophage
@CoryNelson: I ran into a situation recently where I needed to do this asynchronously in order to maintain a responsive UI while computing the hash of a large file.Pretend
B
8

The work that you're doing is inherently synchronous CPU bound work. It's not inherently asynchronous as something like network IO is going to be. If you would like to run some synchronous CPU bound work in another thread and asynchronously wait for it to be completed, then Task.Run is indeed the proper tool to accomplish that, assuming the operation is sufficiently long running to need to perform it asynchronously.

That said, there really isn't any reason to expose an asynchronous wrapper over your synchronous method. It generally makes more sense to just expose the method synchronously, and if a particular caller needs it to run asynchronously in another thread, they can use Task.Run to explicitly indicate that need for that particular invocation.

Breeding answered 25/11, 2014 at 18:4 Comment(1)
I'll follow your advice then. And let the caller decide if a wrapper is convenient or notMacrophage
P
16

As stated by the other answerers, hashing is a CPU-bound activity so it doesn't have Async methods you can call. You can, however, make your hashing method async by asynchronously reading the file block by block and then hashing the bytes you read from the file. The hashing will be done synchronously but the read will be asynchronous and consequently your entire method will be async.

Here is sample code for achieving the purpose I just described.

public static async Threading.Tasks.Task<string> GetHashAsync<T>(this Stream stream) 
    where T : HashAlgorithm, new()
{
    StringBuilder sb;

    using (var algo = new T())
    {
        var buffer = new byte[8192];
        int bytesRead;

        // compute the hash on 8KiB blocks
        while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) != 0)
            algo.TransformBlock(buffer, 0, bytesRead, buffer, 0);
        algo.TransformFinalBlock(buffer, 0, bytesRead);

        // build the hash string
        sb = new StringBuilder(algo.HashSize / 4);
        foreach (var b in algo.Hash)
            sb.AppendFormat("{0:x2}", b);
    }

    return sb?.ToString();
}

The function can be invoked as such

using (var stream = System.IO.File.OpenRead(@"C:\path\to\file.txt"))
    string sha256 = await stream.GetHashAsync<SHA256CryptoServiceProvider>();

Of course,you could equally call the method with other hash algorithms such as SHA1CryptoServiceProvider or SHA512CryptoServiceProvider as the generic type parameter.

Likewise with a few modifications, you can also get it to hash a string as is specific to your case.

Pretend answered 16/1, 2018 at 11:11 Comment(5)
thanks ! two little things : no need for null check on sb at the end and better use ConfigureAwait(false) on such code.Hobbledehoy
@Hobbledehoy You're right on both counts. I, however, put the null check in there to satisfy over-zealous code analysers. You can remove it if it suits you. And yes, it's advisable to use ConfigureAwait(false).Pretend
Your code assumes that if Stream.ReadAsync ever returns less than buffer.Length that it's reached the end - but the documentation for Stream.ReadAsync says that only a zero return value indicates reaching EOF.Unstuck
@Dai: Thanks for catching that oversight. It's been corrected now.Pretend
@AlexEssilfie as of .NET5 (Nov 2020) there is a ComputeHashAsync available, but it's not intended to be used in the same context as OP's question (hashing a string) but rather hashing Streams (like FileStream). Thus the async code you show above to compute the hash of a file is no longer necessary under .NET 5+. I posted another answer that addresses this.Involucrum
B
8

The work that you're doing is inherently synchronous CPU bound work. It's not inherently asynchronous as something like network IO is going to be. If you would like to run some synchronous CPU bound work in another thread and asynchronously wait for it to be completed, then Task.Run is indeed the proper tool to accomplish that, assuming the operation is sufficiently long running to need to perform it asynchronously.

That said, there really isn't any reason to expose an asynchronous wrapper over your synchronous method. It generally makes more sense to just expose the method synchronously, and if a particular caller needs it to run asynchronously in another thread, they can use Task.Run to explicitly indicate that need for that particular invocation.

Breeding answered 25/11, 2014 at 18:4 Comment(1)
I'll follow your advice then. And let the caller decide if a wrapper is convenient or notMacrophage
E
1

The overhead of running this asynchronously (using Task.Run) will probably be higher that just running it synchronously.

An asynchronous interface is not available because it is a CPU bound operation. You can make it asynchronous (using Task.Run) as you pointed out, but I would recommend against it.

Eric answered 25/11, 2014 at 18:5 Comment(0)
I
0

As of .NET 5 (available since Nov 2020), there IS a HashAlgorithm.ComputeHashAsync that you can use... However, as other answers above have pointed out, computing a hash is a CPU-bound operation but async tasks are usually intended to solve I/O-bound operations. For anyone starting on their async programming journey, this is a good example of the difference.

The important thing to notice is that ComputeHashAsync doesn't provide a signature that works on byte[]. It only provides a version that operates on a Stream:

byte[] ComputeHash(byte[]);
byte[] ComputeHash(Stream);
byte[] ComputeHash(byte[], int, int);
Task<byte[]> ComputeHashAsync(Stream, CancellationToken);

Why is that?? It's exactly because those methods that accept a byte[] are CPU-bound, not I/O-bound, so there is little reason to provide an async version of those signatures. Thus, even though it compiles and works fine, you would not want to do something like this:

async Task<byte[]> BadComputeHashAsync(string input = "Don't do this")
{
    byte[] hash, inputBytes = Encoding.UTF8.GetBytes(input);
    // Get a MemoryStream so ComputeHashAsync can be used (bad idea!)
    using (MemoryStream ms = new MemoryStream(inputBytes))
        hash = await MD5.Create().ComputeHashAsync(ms);
    return hash;
}

Using a MemoryStream to convert the bytes to a Stream does allow you to use ComputeHashAsync, but it's an abuse of it's purpose. However, you could, for example, use ComputeHashAsync with a FileStream to compute the hash of a file - that would be I/O-bound (reading the file) in addition to CPU-bound (computing the hash). This is a perfectly good usage:

async Task<byte[]> ComputeFileHash(string filename)
{
    byte[] hash;
    using (FileStream fs = File.OpenRead(filename)) 
        hash = await MD5.Create().ComputeHashAsync(fs);
    return hash;
}
Involucrum answered 4/7, 2023 at 4:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.