避免这些C#错误,构建高性能应用

作者:微信公众号:【架构师老卢】
4-27 8:3
34

概述:你有没有觉得你的 .NET 应用程序正在以蜗牛的速度移动?如果你的第一反应是“是”,别担心,你并不孤单。您可能会遭受一些伪装成看似合乎逻辑的方法的不良做法的后果。以下是一些可能导致应用程序性能不佳的常见错误。但是,如果我们不知道如何修复或避免错误,那么知道错误有什么意义呢?这就是为什么我在这里为您提供一些提示。持续访问相同数据在 C# 应用程序中,多次查询数据库是很常见的,不仅要检索完成用例所需的操作信息,还要获取不经常更改的信息。我们将讨论后一种情况。假设您有一个存储凭据的表,用于向第三方服务进行身份验证。通常,在使用该服务之前,您需要进行身份验证。在应用程序中,您可能有一个例程来从数据库中

你有没有觉得你的 .NET 应用程序正在以蜗牛的速度移动?如果你的第一反应是“是”,别担心,你并不孤单。您可能会遭受一些伪装成看似合乎逻辑的方法的不良做法的后果。

以下是一些可能导致应用程序性能不佳的常见错误。但是,如果我们不知道如何修复或避免错误,那么知道错误有什么意义呢?这就是为什么我在这里为您提供一些提示。

持续访问相同数据

在 C# 应用程序中,多次查询数据库是很常见的,不仅要检索完成用例所需的操作信息,还要获取不经常更改的信息。我们将讨论后一种情况。

假设您有一个存储凭据的表,用于向第三方服务进行身份验证。通常,在使用该服务之前,您需要进行身份验证。在应用程序中,您可能有一个例程来从数据库中的表中检索这些凭据。现在,如果例程不经常执行或每天只执行几次,这似乎不是问题。但是,在需要更频繁地使用第三方服务的较大用例中,随着时间的推移,这可能会成为应用程序中的性能问题。考虑到凭据是不经常更新的信息,可以选择使用应用程序的内存中缓存。

在最新版本的 .NET 中,这可以非常简单且快速实现。请考虑以下方案。

public async Task<List<AccountSenders>> GetAccountSenders()  
{  
  return await _context.AccountSenders.ToListAsync();  
}

在上面的代码中,我们从数据库中的表中检索凭据列表。此方法本身似乎不会造成性能问题。但是,如果我们认为我们在应用程序中多次使用第三方服务,则此方法的使用会成倍增加,从而导致不必要的数据库查询。这是因为凭据不会更改或不会像我们进行身份验证那样频繁更改。让我们考虑使用 .NET 提供的内存中缓存的以下方法。

public async Task<List<AccountSenders>?> GetAccountSenders(IMemoryCache _cache)  
{  
    string cacheKey = "AccountSenders";  
  
    if (!_cache.TryGetValue(cacheKey, out List<AccountSenders>? accountSenders))  
    {  
        accountSenders = await _context.AccountSenders.ToListAsync();  
  
        var cacheEntryOptions = new MemoryCacheEntryOptions()  
            .SetSlidingExpiration(TimeSpan.FromMinutes(5));  
  
         _cache.Set(cacheKey, accountSenders, cacheEntryOptions);  
    }  
    return accountSenders;  
}

您可能认为更多的代码并不一定能转化为更好的性能,但让我们来看看幕后......

这就是缓存发挥作用的地方。这是一种将数据存储在内存中的方法,因此您不必每次都获取数据。尽管设置它最初可能看起来更具挑战性,但相信我,最终是值得的。

回到前面的代码,在查询数据库之前,我们将检查该信息是否已存储在缓存中。同时,该方法还考虑了缓存过期时间,这是一个关键属性。继续例程,我们注意到,当根据提供的名称在缓存中找不到信息时,我们可以继续查询数据库。之后,我们可以将该信息存储在缓存中,以便下次可以重复验证并从缓存而不是数据库中检索信息。

另外,请注意,在配置缓存时,我们设置了过期时间,这一点至关重要,因为缓存必须具有过期时间。一旦这段时间过去了,我们就可以再次查询数据库,因为毕竟,我们总是希望获得更新的信息。

所以你有它,这是一种简单、直接的方法,可以极大地帮助你处理那些经常执行的查询。此外,这是开始在 .NET 应用程序中实现缓存的实用方法。

Bad HttpClient 实例 — 性能的无声敌人

在考虑通过 HTTP 协议与外部服务通信的解决方案时,几乎可以肯定的是,我们将使用 HttpClient 来实现它。但是,创建一个简单的 HttpClient 实例的背后是什么,它可能会成为我们应用程序性能的无声敌人?好吧,几乎所有东西!每次创建 HttpClient 实例时,都会打开新的 TCP 连接,从而消耗资源并可能造成瓶颈,尤其是在高负载下。

public async Task<string> GetAsync(string url)  
{  
  using (var client = new HttpClient())  
  {  
    var response = await client.GetAsync(url);  
    return await response.Content.ReadAsStringAsync();  
  }  
}

在上面的代码中,似乎没有任何问题,只是 HttpClient 发出请求的简单实例。虽然它的用法看起来很简单,但实例化不正确或使用不充分可能是应用程序中无数性能问题的根本原因。

建议使用 HttpClientFactory 创建 HttpClient 的单个共享实例。

public async Task<string> GetAsync(string url)  
{  
  using (var client = _httpClientFactory.CreateClient())  
  {  
    var response = await client.GetAsync(url);  
    return await response.Content.ReadAsStringAsync();  
  }  
}

使用 HttpClientFactory,您可以创建由框架本身正确配置和管理的 HTTP 客户端。HttpClientFactory 创建的这些客户端存储在池中,因此不会在每次需要新客户端时创建新客户端。此外,当客户端不再使用时,它会返回到池中并清理以供重用。

总之,错误地实例化 HttpClient 或在没有正确配置的情况下使用它可能会对应用程序的性能产生负面影响。利用 HttpClientFactory 和正确的配置可以显着提高 HTTP 请求的速度和效率。

连接字符串

与使用 StringBuilder 相比,在 .NET 中连接字符串可能会影响性能,因为字符串在内存中的处理方式。

使用运算符或方法连接字符串时,每次发生串联操作时,.NET 都会创建一个新的字符串对象。这意味着内存分配经常发生,尤其是在循环或大规模字符串操作中执行多个串联的情况下。每个新的字符串对象都需要内存分配和解除分配,这可能会导致内存碎片和垃圾回收开销增加。+String.Concat()

另一方面,StringBuilder 提供了一种更有效的方法来操作字符串,尤其是在连接多个字符串或执行重复字符串操作时。StringBuilder 在内部使用可调整大小的缓冲区来存储字符串数据,从而最大限度地减少内存分配并减少与创建新字符串对象相关的开销。

下面是一个示例来说明差异:

// Using string concatenation  
string result = "";  
for (int i = 0; i < 10000; i++) {  
    result += i.ToString();  // Each concatenation creates a new string object  
}  
  
// Using StringBuilder  
StringBuilder sb = new StringBuilder();  
for (int i = 0; i < 10000; i++) {  
    sb.Append(i.ToString());  // StringBuilder appends data to its internal buffer  
}  
string result = sb.ToString();  // Convert StringBuilder to string when needed

在上面的示例中,在循环中重复使用字符串连接 () 会为每个连接操作创建一个新的字符串对象,从而导致性能不佳并增加内存使用量。但是,使用 StringBuilder 的方法可以有效地将数据追加到其内部缓冲区,从而避免不必要的内存分配并提高性能。+=Append()

总之,虽然字符串连接看起来很方便,尤其是对于小规模操作,但将 StringBuilder 用于较大的字符串操作或性能对于优化 .NET 中的内存使用和应用程序性能至关重要的方案至关重要。

异常和 NULL

由于异常处理和 null 检查涉及的开销,引发过多的异常和不正确的返回 null 用法可能会影响 .NET 应用程序的性能。

当频繁抛出异常时,尤其是在代码的性能关键部分,可能会导致性能显著下降。异常处理涉及捕获堆栈跟踪、取消展开调用堆栈以及其他消耗 CPU 周期和内存的操作。

例:

try {  
    // Code that might throw exceptions  
} catch (Exception ex) {  
    // Exception handling logic  
}

在上面的示例中,如果 try 块中的代码经常引发异常,则处理这些异常的性能开销可能会变得明显,从而影响应用程序的整体性能。

返回 null 的错误用法

在没有正确 null 检查的情况下访问返回的值时,返回 null 而不是适当地处理 null 值可能会导致 null 引用异常 (NRE)。这可能会在应用程序中引入 bug 和意外行为,并且需要在整个代码库中进行额外的 null 检查,这可能会影响性能。

例:

public string GetCustomerName(int customerId) {  
    // Some logic to fetch customer name  
    if (customerExists) {  
        return customerName;  
    } else {  
        return null; // Incorrect usage if caller does not handle null properly  
    }  
}
  1. 在上面的示例中,如果调用方不处理 null 返回值的可能性,则可能会导致 null 引用异常,从而导致运行时错误和潜在的应用程序崩溃。GetCustomerName

要缓解这些性能问题,请执行以下操作:

  • 对特殊情况使用例外,而不是对常规控制流使用例外。
  • 尽量减少在代码的性能关键部分中使用异常。
  • 使用返回代码或其他机制(而不是异常)处理预期的错误条件。
  • 确保返回 null 的方法清楚地记录了 null 返回值的可能性,并且调用方会适当地处理它们。

通过遵循这些最佳做法,可以提高 .NET 应用程序的性能和可靠性。

在 .NET 应用程序开发领域,忽略性能注意事项的情况并不少见,假设微小的低效率不会对应用程序的功能产生重大影响。但是,这种自满情绪可能会导致次优做法的逐渐积累,随着时间的推移,这些做法会共同侵蚀应用程序的性能。虽然最初优先考虑开发的其他方面似乎无关紧要,但随着应用程序的扩展或使用量的增加,忽视性能考虑可能会导致严重后果。

尽管开发中的权宜之计很诱人,但必须认识到,每个糟糕的性能诱导实践实例都会导致应用程序性能的整体下降。无论是低效的数据库查询、过多的异常引发,还是不正确的字符串操作,这些看似微小的优化失误都可能加剧并表现为明显的性能瓶颈。作为开发人员,培养一种性能意识文化至关重要,在这种文化中,优化的考虑因素被集成到开发生命周期的每个阶段。

归根结底,.NET 应用程序的长期成功和可行性取决于开发人员从一开始就优先考虑性能注意事项的集体努力。通过培养一种主动的性能优化方法并遵循最佳实践,开发人员可以降低应用程序性能下降的风险。通过齐心协力解决性能瓶颈并坚持优化标准,开发人员可以确保其 .NET 应用程序在面对不断变化的需求和使用模式时提供最佳性能、可伸缩性和用户体验。

相关留言评论
昵称:
邮箱:
阅读排行