异步SHA256哈希处理

12

我有以下方法:

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

有没有一种方法可以使它异步?我希望使用 asyncawait 关键字,但是 HashAlgorithm 类没有为此提供任何异步支持。

另一种方法是封装所有逻辑在一个:

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

但是这种方法似乎不够简洁,而且我不确定它是否是一种正确(或高效)的异步操作方式。

有什么办法可以实现这个目标吗?


2
你为什么要尝试异步执行这个操作? - Cory Nelson
@CoryNelson 我说实话不知道。我以为通过使代码异步或在另一个线程上运行来优化代码。但答案澄清了我的想法。 - Matias Cicero
@CoryNelson:最近我遇到了这样一种情况,需要异步执行以在计算大文件哈希值时保持响应式用户界面。 - Alex Essilfie
4个回答

16
正如其他回答者所说,哈希是一种CPU密集型活动,因此它没有可以调用的异步方法。但是,您可以通过异步按块读取文件,然后对从文件中读取的字节进行哈希来使您的哈希方法异步化。哈希将同步完成,但读取将是异步的,因此整个方法将是异步的。
以下是实现上述目的的示例代码。
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();
}

该函数可按以下方式调用:
using (var stream = System.IO.File.OpenRead(@"C:\path\to\file.txt"))
    string sha256 = await stream.GetHashAsync<SHA256CryptoServiceProvider>();

当然,你也可以使用其他哈希算法调用该方法,例如将SHA1CryptoServiceProviderSHA512CryptoServiceProvider作为通用类型参数。

同样地,只需进行一些修改,您也可以使其根据您的情况对字符串进行哈希处理。


谢谢!两个小事情:最后不需要对sb进行null检查,最好在这样的代码上使用ConfigureAwait(false)。 - Guillaume
@Guillaume 你说的都对。不过,我在那里加了 null 检查是为了满足过于热衷于代码分析的需求。如果你觉得不需要,可以将其删除。另外,建议使用 ConfigureAwait(false) - Alex Essilfie
1
你的代码假设如果Stream.ReadAsync返回的字节数小于buffer.Length,那么就已经到达了结尾 - 但是Stream.ReadAsync的文档说只有返回值为零才表示到达了EOF。 - Dai
1
@Dai:感谢你发现了那个疏忽。现在已经进行了更正。 - Alex Essilfie
@AlexEssilfie 截至.NET5(2020年11月),有一个ComputeHashAsync可用,但它不打算在同样的上下文中使用OP的问题(哈希字符串),而是对流进行散列(如FileStream)。因此,在.NET 5+下,你展示的异步代码计算文件哈希已不再必要。我发布了另一个回答来解决这个问题。 - Michael Bray

8
你正在进行的工作本质上是同步CPU绑定的工作。它不像网络IO那样本质上是异步的。如果您想在另一个线程中运行一些同步CPU绑定的工作并异步等待其完成,那么Task.Run确实是实现此目的的适当工具,假设该操作足够长时间才需要异步执行。 话虽如此,实际上没有理由在您的同步方法上公开异步包装器。通常更有意义的做法是只公开同步方法,如果特定的调用者需要在另一个线程中异步运行,则可以使用Task.Run明确指示该调用的需求。

2
那我会听从你的建议。让调用者决定是否使用包装器更方便。 - Matias Cicero

1
此处使用异步方式(使用Task.Run)的开销可能会比同步方式更大。由于这是一个CPU密集型操作,因此无法提供异步接口。您可以通过使用Task.Run使其变为异步操作,但我不建议这样做。

0
根据.NET 5(自2020年11月起可用)的最新版本,确实有一个可以使用的HashAlgorithm.ComputeHashAsync。然而,正如上面其他答案指出的那样,计算哈希是一个CPU密集型操作,而async任务通常用于解决I/O密集型操作。对于初学异步编程的人来说,这是一个很好的例子。
需要注意的重要事项是,ComputeHashAsync没有提供适用于byte[]的签名,它只提供了一个在Stream上操作的版本。
byte[] ComputeHash(byte[]);
byte[] ComputeHash(Stream);
byte[] ComputeHash(byte[], int, int);
Task<byte[]> ComputeHashAsync(Stream, CancellationToken);

为什么会这样呢?正是因为那些接受byte[]的方法是CPU密集型而不是I/O密集型,所以几乎没有理由提供这些签名的异步版本。因此,即使它可以编译并正常工作,你也不会想要做类似这样的事情:
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;
}

使用MemoryStream将字节转换为流确实允许您使用ComputeHashAsync,但这是对其目的的滥用。然而,例如,您可以使用FileStreamComputeHashAsync计算文件的哈希值-这将是I/O限制(读取文件)和CPU限制(计算哈希)的结合。这是一个完全合理的用法:
async Task<byte[]> ComputeFileHash(string filename)
{
    byte[] hash;
    using (FileStream fs = File.OpenRead(filename)) 
        hash = await MD5.Create().ComputeHashAsync(fs);
    return hash;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接