xiyuan技术圈

溪源的技术博客,技术的不断进取之路


  • Home

  • 技术

  • 随笔

  • 读书

  • 管理

  • 归档

使用自定义DelegatingHandler编写更整洁的Typed HttpClient

Posted on 2020-04-30 | Edited on 2021-04-25 | In 技术

使用自定义DelegatingHandler编写更整洁的Typed HttpClient

简介

我写了很多HttpClient,包括类型化的客户端。自从我发现Refit以来,我只使用了那一个,所以我只编写了很少的代码!但是我想到了你!你们中的某些人不一定会使用Refit,因此,我将为您提供一些技巧,以使用HttpClient消息处理程序(尤其是DelegatingHandlers)编写具有最大可重用性的类型化HttpClient。

编写类型化的HttpClient来转发JWT并记录错误

这是要清理的键入的HttpClient:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
using DemoRefit.Models;
using DemoRefit.Repositories;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace DemoRefit.HttpClients
{
public class CountryRepositoryClient : ICountryRepositoryClient
{
private readonly HttpClient _client;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<CountryRepositoryClient> _logger;

public CountryRepositoryClient(HttpClient client, ILogger<CountryRepositoryClient> logger, IHttpContextAccessor httpContextAccessor)
{
_client = client;
_logger = logger;
_httpContextAccessor = httpContextAccessor;
}

public async Task<IEnumerable<Country>> GetAsync()
{
try
{
string accessToken = await _httpContextAccessor.HttpContext.GetTokenAsync("access_token");
if (string.IsNullOrEmpty(accessToken))
{
throw new Exception("Access token is missing");
}
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", accessToken);

var headers = _httpContextAccessor.HttpContext.Request.Headers;
if (headers.ContainsKey("X-Correlation-ID") && !string.IsNullOrEmpty(headers["X-Correlation-ID"]))
{
_client.DefaultRequestHeaders.Add("X-Correlation-ID", headers["X-Correlation-ID"].ToString());
}

using (HttpResponseMessage response = await _client.GetAsync("/api/democrud"))
{
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsAsync<IEnumerable<Country>>();
}
}
catch (Exception e)
{
_logger.LogError(e, "Failed to run http query");
return null;
}
}
}
}

这里有许多事情需要清理,因为它们在您将在同一应用程序中编写的每个客户端中可能都是多余的:

  • 从HttpContext读取访问令牌
  • 令牌为空时,管理访问令牌
  • 将访问令牌附加到HttpClient进行委派
  • 从HttpContext读取CorrelationId
  • 将CorrelationId附加到HttpClient进行委托
  • 使用EnsureSuccessStatusCode()验证Http查询是否成功

编写自定义的DelegatingHandler来处理冗余代码

这是DelegatingHandler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;

namespace DemoRefit.Handlers
{
public class MyDelegatingHandler : DelegatingHandler
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<MyDelegatingHandler> _logger;

public MyDelegatingHandler(IHttpContextAccessor httpContextAccessor, ILogger<MyDelegatingHandler> logger)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpResponseMessage httpResponseMessage;
try
{
string accessToken = await _httpContextAccessor.HttpContext.GetTokenAsync("access_token");
if (string.IsNullOrEmpty(accessToken))
{
throw new Exception($"Access token is missing for the request {request.RequestUri}");
}
request.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);

var headers = _httpContextAccessor.HttpContext.Request.Headers;
if (headers.ContainsKey("X-Correlation-ID") && !string.IsNullOrEmpty(headers["X-Correlation-ID"]))
{
request.Headers.Add("X-Correlation-ID", headers["X-Correlation-ID"].ToString());
}

httpResponseMessage = await base.SendAsync(request, cancellationToken);
httpResponseMessage.EnsureSuccessStatusCode();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to run http query {RequestUri}", request.RequestUri);
throw;
}
return httpResponseMessage;
}
}
}

如您所见,现在它封装了用于同一应用程序中每个HttpClient的冗余逻辑 。

现在,清理后的HttpClient如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using DemoRefit.Models;
using DemoRefit.Repositories;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;

namespace DemoRefit.HttpClients
{
public class CountryRepositoryClientV2 : ICountryRepositoryClient
{
private readonly HttpClient _client;
private readonly ILogger<CountryRepositoryClient> _logger;

public CountryRepositoryClientV2(HttpClient client, ILogger<CountryRepositoryClient> logger)
{
_client = client;
_logger = logger;
}

public async Task<IEnumerable<Country>> GetAsync()
{
using (HttpResponseMessage response = await _client.GetAsync("/api/democrud"))
{
try
{
return await response.Content.ReadAsAsync<IEnumerable<Country>>();
}
catch (Exception e)
{
_logger.LogError(e, "Failed to read content");
return null;
}
}
}
}
}

好多了不是吗?🙂

最后,让我们将DelegatingHandler附加到Startup.cs中的HttpClient:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
using DemoRefit.Handlers;
using DemoRefit.HttpClients;
using DemoRefit.Repositories;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Refit;
using System;

namespace DemoRefit
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();

services.AddControllers();

services.AddHttpClient<ICountryRepositoryClient, CountryRepositoryClientV2>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri(Configuration.GetSection("Apis:CountryApi:Url").Value))
.AddHttpMessageHandler<MyDelegatingHandler>();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}

使用Refit

如果您正在使用Refit,则绝对可以重用该DelegatingHandler!

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
using DemoRefit.Handlers;
using DemoRefit.HttpClients;
using DemoRefit.Repositories;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Refit;
using System;

namespace DemoRefit
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();

services.AddControllers();

services.AddRefitClient<ICountryRepositoryClient>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri(Configuration.GetSection("Apis:CountryApi:Url").Value));
.AddHttpMessageHandler<MyDelegatingHandler>();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
轮子介绍:

Refit是一个深受Square的 Retrofit 库启发的库,目前在github上共有star 4000枚,通过这个框架,可以把你的REST API变成了一个活的接口:

1
2
3
4
5
public interface IGitHubApi
{
[Get("/users/{user}")]
Task<User> GetUser(string user);
}

RestService类生成一个IGitHubApi的实现,它使用HttpClient进行调用:

1
2
3
var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com");

var octocat = await gitHubApi.GetUser("octocat");

查看更多: https://reactiveui.github.io/refit/

在C#中使用RESTful API的几种好方法

Posted on 2020-04-28 | Edited on 2021-04-25 | In 技术

在C#中使用RESTful API的几种好方法

Vladimir Pecanac

通过Web开发的路径,您发现自己迟早需要处理外部API(应用程序编程接口)。在本文中,我的目标是列出在C#项目中使用RESTful API的方法的最全面列表,并通过一些简单示例向您展示如何做到这一点。

阅读该文章后,您将更深入地了解可以使用哪些选项,以及下次需要使用RESTful API时如何选择正确的选项。

什么是RESTful API?

因此,在开始之前,您可能想知道API代表什么,以及RESTful的全部含义是什么?

简而言之,API是软件应用程序之间的层。您可以将请求发送到API,并从中获得响应。API隐藏了软件应用程序具体实现的所有细节,并公开了您用于与该应用程序通信的接口。

整个互联网是由API组成的大型蜘蛛网。我们使用API在应用程序之间通信和关联信息。我们有一个API,可以处理几乎所有内容。您每天使用的大多数服务都有自己的API(GoogleMaps,Facebook,Twitter,Instagram,天气门户…)

RESTful部分意味着API是根据REST(表示状态传输)的原理和规则来实现的,REST是网络的基础架构原理。RESTful API在大多数情况下会返回纯文本,JSON或XML响应。更详细地解释REST不在本文的讨论范围之内,但是您可以在我们的文章REST API最佳实践中阅读有关REST的更多信息。

如何使用RESTful API

好吧,让我们进入整个故事中最重要的部分。

有几种方法可以在C#中使用RESTful API:

  • HttpWebRequest/Response Class
  • WebClient Class
  • HttpClient Class
  • RestSharp NuGet Package
  • ServiceStack Http Utils
  • Flurl
  • DalSoft.RestClient

这些中的每一个都有优点和缺点,因此让我们仔细研究它们,看看它们提供了什么。

例如,我们将通过GitHub API收集有关RestSharp回购版本及其发布日期的信息。此信息是公开可用的,您可以在此处查看原始JSON响应的外观: RestSharp版本

我们将利用Json.NET库的帮助来反序列化获得的响应。同样,对于某些示例,我们将使用库的内置反序列化机制。选择哪种方式取决于您,因为没有正确的方法。(您可以在源代码中看到这两种机制的实现)。

我期望通过接下来的几个示例得到一个反序列化JArray(为简单起见),其中包含RestSharp发布信息。之后,我们可以遍历它以获得以下结果。

img

HttpWebRequest / Response类

这是WebRequest 类的特定于HTTP的实现,该实现最初用于处理HTTP请求,但已过时并由WebClient该类代替 。

该HttpWebRequest 提供细粒度控制的要求制定过程的每一个环节。您可以想象,这可能是一把双刃剑,您很容易浪费大量时间来微调您的请求。另一方面,这可能正是您针对特定案例所需要的。

HttpWebRequest 类不会阻止用户界面,也就是说,我相信您会同意这一点,这一点非常重要。

HttpWebResponse 类为传入的响应提供了一个容器。

这是有关如何使用这些类使用API的简单示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class HttpWebRequestHandler : IRequestHandler
{
public string GetReleases(string url)
{
var request = (HttpWebRequest)WebRequest.Create(url);

request.Method = "GET";
request.UserAgent = RequestConstants.UserAgentValue;
request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;

var content = string.Empty;

using (var response = (HttpWebResponse)request.GetResponse())
{
using (var stream = response.GetResponseStream())
{
using (var sr = new StreamReader(stream))
{
content = sr.ReadToEnd();
}
}
}

return content;
}
}

尽管是一个简单的示例,但是当您需要处理更复杂的方案(例如,发布表单信息,授权等)时,它会变得更加复杂。

WebClient类别

这个类对HttpWebRequest的包装。它通过HttpWebRequest从开发人员中提取的细节来简化流程。该代码更容易编写,并且您通过这种方式犯错误的可能性较小。如果您想编写更少的代码,而不用担心所有细节,并且执行速度是不重要的,请考虑使用WebClientclass。

这个示例应该使您大致了解WebClient与HttpWebRequest/ HttpWebResponse方法相比使用起来要容易得多。

1
2
3
4
5
6
7
8
9
public string GetReleases(string url)
{
var client = new WebClient();
client.Headers.Add(RequestConstants.UserAgent, RequestConstants.UserAgentValue);

var response = client.DownloadString(url);

return response;
}

容易得多,对吗?

除了其他DownloadString方法,WebClient类还提供了许多其他有用的方法,使我们的生活更轻松。我们可以轻松地使用它来操作字符串,文件或字节数组,并且价格比HttpWebRequest/ HttpWebResponse方法要慢几毫秒。

无论是HttpWebRequest/ HttpWebResponse和WebClient类在旧版本的.NET可供选择。如果您对其他产品感兴趣,请务必查看MSDNWebClient。

HttpClient类

HttpClient 是“新人”,它提供了旧库所缺乏的一些现代.NET功能。例如,您可以使用的单个实例发送多个请求HttpClient,它不绑定到特定的HTTP服务器或主机,而是使用async / await机制。

您可以在此视频中找到使用HttpClient的五个很好的理由:

  • 强类型标题。
  • 共享缓存,cookie和凭据
  • 访问cookie和共享cookie
  • 控制缓存和共享缓存。
  • 将您的代码模块注入ASP.NET管道。清洁和模块化的代码。

HttpClient在我们的示例中,这是实际的:

1
2
3
4
5
6
7
8
9
10
11
public string GetReleases(string url)
{
using (var httpClient = new HttpClient())
{
httpClient.DefaultRequestHeaders.Add(RequestConstants.UserAgent, RequestConstants.UserAgentValue);

var response = httpClient.GetStringAsync(new Uri(url)).Result;

return response;
}
}

为了简单起见,我同步实现了它。每个HttpClient方法都应异步使用,应该以这种方式使用。

另外,我还要提到一件事。是否HttpClient应该包装在using块中还是在应用程序级别上进行静态讨论。尽管它实现了IDisposable,但似乎通过将它包装在using块中,会使应用程序出现故障并获得SocketException。而在ANKIT博客中,提供了基于很多有利于静态初始化的的HttpClient性能测试结果是。请务必阅读这些博客文章,因为它们可以帮助您更了解该HttpClient 库的正确用法。(原文编写时间比较旧,在新版的.NET Core3.1中,相关问题已经解决)

并且不要忘记,由于是新的,HttpClient是.NET 4.5以上版本才有,因此在某些旧项目中使用它可能会遇到麻烦。

RestSharp

RestSharp是标准.NET库的OpenSource替代品,也是目前最酷的.NET库之一。它以NuGet软件包的形式提供,出于某些原因,您应该考虑尝试一下。

就像HttpClientRestSharp 一样,它是一个现代而全面的库,易于使用且令人愉悦,同时仍支持旧版本的.NET Framework。它具有内置的身份验证和序列化/反序列化机制,但允许您使用自定义机制覆盖它们。它可跨平台使用,并支持OAuth1,OAuth2,基本,NTLM和基于参数的身份验证。您可以选择同步或异步工作。该库还有很多其他功能,而这些只是它提供的众多好处中的一部分。有关RestSharp的用法和功能的详细信息,您可以访问GitHub上的RestSharp 页面。

现在,让我们尝试使用RestSharp get获取RestSharp版本的列表。

1
2
3
4
5
6
7
8
public string GetReleases(string url)
{
var client = new RestClient(url);

var response = client.Execute(new RestRequest());

return response.Content;
}

很简单。RestSharp非常灵活,拥有使用RESTful API时几乎可以实现所有功能所需的所有工具。

在此示例中要注意的一件事是,由于示例的一致性,我没有使用RestSharp的反序列化机制,这有点浪费,但是我鼓励您使用它,因为它确实非常容易和方便。因此,您可以轻松地制作一个这样的容器:

1
2
3
4
5
6
7
public class GitHubRelease
{
[JsonProperty(PropertyName = "name")]
public string Name { get; set; }
[JsonProperty(PropertyName = "published_at")]
public string PublishedAt { get; set; }
}

然后使用该Execute方法直接反序列化对该容器的响应。您可以仅添加所需的属性,并使用属性JsonProperty将它们映射到C#属性(很好的触摸)。由于我们在响应中获得了发布列表,因此我们将List 用作包含类型。

1
2
3
4
5
6
7
8
public List<GitHubRelease> GetDeserializedReleases(string url)
{
var client = new RestClient(url);

var response = client.Execute<List<GitHubRelease>>(new RestRequest());

return response.Data;
}

一种非常直接而优雅的方式来获取我们的数据。

RestSharp不仅具有发送GET请求的功能,还可以自己探索并观察它的酷炫之处。

在RestSharp案例中要补充的最后一点是,其存储库需要维护者。如果您想了解更多有关这个很棒的库的信息,我敦促您前往RestSharp存储库,帮助该项目继续发展并变得更好。

ServiceStack Http实用程序

另一个库,但与RestSharp不同,ServiceStack似乎得到了适当维护,并与现代API趋势保持同步。ServiceStack功能列表令人印象深刻,并且肯定具有各种应用程序。

在这里对我们最有用的是演示如何使用外部RESTful API。ServiceStack具有一种专门的方式来处理称为Http Utils的第三方HTTP API 。

让我们看看如何首先使用Json.NET解析器来获取RestSharp版本是如何使用ServiceStack Http Utils。

1
2
3
4
5
6
7
8
9
public string GetReleases(string url)
{
var response = url.GetJsonFromUrl(webReq =>
{
webReq.UserAgent = RequestConstants.UserAgentValue;
});

return response;
}

您还可以选择将其留给ServiceStack解析器。我们可以重用本文前面定义的Release类 。

1
2
3
4
5
6
7
8
9
public List<GitHubRelease> GetDeserializedReleases(string url)
{
var releases = url.GetJsonFromUrl(webReq =>
{
webReq.UserAgent = RequestConstants.UserAgentValue;
}).FromJson<List<GitHubRelease>>();

return releases;
}

如您所见,无论哪种方式都可以正常工作,并且您可以选择是获取字符串响应还是立即反序列化它。

尽管ServiceStack是我们偶然发现的最后一个库,但令我感到惊讶的是,它使用起来如此容易,而且我认为它将来可能成为我处理API和服务的首选工具。

Flurl

评论库中许多人要求的图书馆之一,并在Internet上受到许多人的喜爱,但仍吸引着人们。

Flurl代表Fluent Url Builder,这是库构建其查询的方式。对于不熟悉flurl的做事方式的人来说,flurl只是意味着库的构建方式是将方法链接在一起以实现更高的可读性,类似于人类语言。

为了使事情更容易理解,让我们举一些例子(这个例子来自官方文档):

1
2
3
4
5
6
7
8
9
10
11
// Flurl will use 1 HttpClient instance per host
var person = await "https://api.com"
.AppendPathSegment("person")
.SetQueryParams(new { a = 1, b = 2 })
.WithOAuthBearerToken("my_oauth_token")
.PostJsonAsync(new
{
first_name = "Claire",
last_name = "Underwood"
})
.ReceiveJson<Person>();

您可以看到方法如何链接在一起以完成“句子”。

在后台,Flurl使用HttpClient或通过自己的语法糖增强HttpClient库。因此,这意味着Flurl是一个异步库,因此请牢记这一点。

与其他高级库一样,我们可以通过两种不同的方式来做到这一点:

1
2
3
4
5
6
7
8
9
public string GetReleases(string url)
{
var result = url
.WithHeader(RequestConstants.UserAgent, RequestConstants.UserAgentValue)
.GetJsonAsync<List<GitHubRelease>>()
.Result;

return JsonConvert.SerializeObject(result);
}

这种方式相当糟糕,因为我们只是序列化结果,以便稍后对其进行反序列化。如果您使用的是Flurl之类的库,则不应以这种方式进行操作。

更好的做事方式是:

1
2
3
4
5
6
7
8
9
public List<GitHubRelease> GetDeserializedReleases(string url)
{
var result = url
.WithHeader(RequestConstants.UserAgent, RequestConstants.UserAgentValue)
.GetJsonAsync<List<GitHubRelease>>()
.Result;

return result;
}

随着.Result我们强迫代码的同步行为。使用Flurl的实际和预期方式如下所示:

1
2
3
4
5
6
7
8
public async Task<List<GitHubRelease>> GetDeserializedReleases(string url)
{
var result = await url
.WithHeader(RequestConstants.UserAgent, RequestConstants.UserAgentValue)
.GetJsonAsync<List<GitHubRelease>>();

return result;
}

这展示了Flurl库的全部潜力。

如果您想了解更多有关如何在不同的现实生活场景使用Flurl,看看我们的 消费GitHub的API(REST)随着Flurl 文章

总而言之,它就像广告一样:易于使用,现代,可读性和可测试性。您对这个库还有什么期望?要开源?签出: Flurl存储库,如果您愿意,可以贡献自己的力量!

DalSoft.RestClient

现在,此列表与该列表中的任何内容都有些不同。但这一点有所不同。

让我们看看如何使用DalSoft.RestClient来使用GitHub API,然后谈论我们已完成的工作。

首先,您可以通过输入以下内容,通过NuGet软件包管理器下载DalSoft.RestClient: Install-Package DalSoft.RestClient

或通过.NET Core CLI: dotnet add package DalSoft.RestClient

两种方法都可以。

拥有图书馆后,我们可以执行以下操作:

1
2
3
4
5
6
7
8
9
public string GetReleases(string url)
{
dynamic client = new RestClient(RequestConstants.BaseUrl,
new Headers { { RequestConstants.UserAgent, RequestConstants.UserAgentValue } });

var response = client.repos.restsharp.restsharp.releases.Get().Result.ToString();

return response;
}

或最好使用DalSoft.RestClient在充分利用其功能的同时立即反序列化响应:

1
2
3
4
5
6
7
8
9
public async Task<List<GitHubRelease>> GetDeserializedReleases(string url)
{
dynamic client = new RestClient(RequestConstants.BaseUrl,
new Headers { { RequestConstants.UserAgent, RequestConstants.UserAgentValue } });

var response = await client.repos.restsharp.restsharp.releases.Get();

return response;
}

因此,让我们稍微讨论一下这些例子。

乍一看,它似乎并不比我们使用的其他一些现代库简单得多。

但这归结为形成请求的方式,那就是利用RestClient的动态特性。例如,我们的BaseUrl是https://api.github.com ,我们需要进入https://api.github.com/repos/restsharp/restsharp/releases。我们可以通过动态创建客户端,然后通过链接Url的“部分”来形成Url来做到这一点:

1
await client.repos.restsharp.restsharp.releases.Get();

形成请求的一种非常独特的方法。还有一个非常灵活的!

因此,一旦我们设置了基本的网址,就可以轻松地使用不同的端点。

还值得一提的是,我们得到的JSON响应会自动进行类型转换。如您在第二个示例中看到的那样,我们方法的返回值是Task>. So,该库足够聪明,可以将响应转换为我们的类型(依赖于Json.NET)。这使我们的生活更加轻松。

除了易于理解和使用之外,DalSoft.RestClient还具有现代库应具备的所有功能。它是可配置的,异步的,可扩展的,可测试的,并且支持多个平台。

我们仅演示了DalSoft.RestClient功能的一小部分。如果您对使用DalSoft.RestClient感兴趣,请转至我们的文章,以学习如何在不同情况下使用它,或参阅 GitHub官方仓库和文档。

其他选择

对于您的特定问题,还有许多其他选项可用。您可以使用任何这些库来使用特定的RESTful API。例如,octokit.net专门 用于GitHub API,Facebook SDK 用于使用Facebook API,并且还有许多其他功能可用于任何用途。

虽然这些库是专门为这些API而设计的,并且可能擅长于它们的用途,但它们的用途是有限的,因为您经常需要在应用程序中连接多个API。这可能会导致每个实现都有不同的实现方式,以及更多的依赖关系,这可能导致重复并且容易出错。库越具体,其灵活性就越差。

GitHub上的源代码

GitHub上的源代码

结论

因此,总而言之,我们已经讨论了可用于使用RESTful API的不同工具。我们已经提到了一些.NET库,可以这样做HttpWebRequest,WebClient和HttpClient,以及一些惊人的第三方工具,如RestSharp和ServiceStack。您还对这些工具进行了简短的介绍,并给出了一些非常简单的示例来向您展示如何开始使用它们。

我认为您现在至少有95%准备使用一些REST。继续展开翅膀,探索并找到更多有趣且有趣的方式来使用和连接不同的API。

您应该知道的5种避免C#.NET中事件造成的内存泄漏的技术

Posted on 2020-04-22 | Edited on 2021-04-25 | In 技术

您应该知道的5种避免C#.NET中事件造成的内存泄漏的技术

C#(通常是.NET)中的事件注册是内存泄漏的最常见原因。至少从我的经验来看。实际上,我从事件中看到了太多的内存泄漏,因此 在代码中看到 + =将立即使我感到怀疑。

尽管事件很常见,但它们也很危险。如果您不知道要查找的内容,则事件很容易导致内存泄漏。在本文中,我将解释此问题的根本原因,并提供几种最佳实践技术来解决该问题。最后,我将向您展示一个简单的技巧,以找出您是否确实存在内存泄漏。

了解内存泄漏

在垃圾收集环境中,术语“内存泄漏”有点反直觉。当有一个垃圾收集器负责收集所有内容时,我的内存如何泄漏?

答案是,在存在垃圾收集器(GC)的情况下,内存泄漏表示有些对象仍在引用中,但实际上未被使用。由于已引用它们,因此GC将不会收集它们,并且它们将永久保存,占用内存。

让我们来看一个例子:

1
2
3
4
5
public class WiFiManager
{
public event EventHandler <WifiEventArgs> WiFiSignalChanged;
// ...
}
1
2
3
4
5
6
7
8
9
10
11
public class MyClass
{
public MyClass(WiFiManager wiFiManager)
{
wiFiManager.WiFiSignalChanged += OnWiFiChanged;
}

private void OnWiFiChanged(object sender, WifiEventArgs e)
{
// do something
}
1
2
3
4
5
6
7
public void SomeOperation(WiFiManager wiFiManager)
{
var myClass = new MyClass(wiFiManager);
myClass.DoSomething();

//... myClass is not used again
}

在此示例中,我们假设WiFiManager 在程序的整个生命周期中都处于活动状态。执行SomeOperation之后,将创建MyClass的实例,并且不再使用它。程序员可能会认为GC将收集它,但事实并非如此。所述WiFiManager保持在其事件MyClass的参考 WiFiSignalChanged和它引起了内存泄漏。GC将永远不会收集MyClass。

1.确保退订

显而易见的解决方案(尽管并非总是最简单的)是记住从事件中注销事件处理程序。一种方法是实现IDisposable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyClass : IDisposable
{
private readonly WiFiManager _wiFiManager;

public MyClass(WiFiManager wiFiManager)
{
_wiFiManager = wiFiManager;
_wiFiManager.WiFiSignalChanged += OnWiFiChanged;
}

public void Dispose()
{
_wiFiManager.WiFiSignalChanged -= OnWiFiChanged;
}

private void OnWiFiChanged(object sender, WifiEventArgs e)
{
// do something
}

当然,您必须确保调用Dispose。如果您有WPF控件,一个简单的解决方案是退订Unloaded事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public partial class MyUserControl : UserControl
{
public MyUserControl(WiFiManager wiFiManager)
{
InitializeComponent();
this.Loaded += (sender, args) => wiFiManager.WiFiSignalChanged += OnWiFiChanged;
this.Unloaded += (sender, args) => wiFiManager.WiFiSignalChanged -= OnWiFiChanged;
}

private void OnWiFiChanged(object sender, WifiEventArgs e)
{
// do something
}
}

优点**:简单易读的代码。

缺点:您很容易忘记取消订阅,或者在所有情况下都不会取消订阅,这将导致内存泄漏。

注意:并非所有事件注册都会导致内存泄漏。注册到将要过期的事件时,不会发生内存泄漏。例如,在WPF UserControl中,您可以注册到Button的Click事件。这很好,并且不需要注销,因为用户控件是唯一引用该Button的控件。如果没有一个人引用用户控件,那么也将没有一个人引用按钮,并且GC将同时收集两者。

2.让处理程序退订

在某些情况下,您可能希望事件处理程序仅发生一次。在这种情况下,您将希望代码自己退订。当事件处理程序是命名方法时,它很容易:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyClass
{
private readonly WiFiManager _wiFiManager;

public MyClass(WiFiManager wiFiManager)
{
_wiFiManager = wiFiManager;
_wiFiManager.WiFiSignalChanged += OnWiFiChanged;
}

private void OnWiFiChanged(object sender, WifiEventArgs e)
{
// do something
_wiFiManager.WiFiSignalChanged -= OnWiFiChanged;
}
}

但是,有时您希望事件处理程序是lambda表达式。在这种情况下,以下是一种使自己退订的有用技术:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

public class MyClass
{
public MyClass(WiFiManager wiFiManager)
{
var someObject = GetSomeObject();
EventHandler<WifiEventArgs> handler = null;
handler = (sender, args) =>
{
Console.WriteLine(someObject);
wiFiManager.WiFiSignalChanged -= handler;
};
wiFiManager.WiFiSignalChanged += handler;
}
}

在上面的示例中,lambda表达式非常有用,因为您可以捕获局部变量someObject,而使用处理程序方法则无法做到这一点。

优点:简单,易读,只要您确定事件至少会触发一次,就不会发生内存泄漏。

缺点:仅在需要处理一次事件的特殊情况下可用。

3.将弱事件与事件聚合器一起使用

在.NET中引用对象时,您基本上会告诉GC该对象正在使用中,因此请不要收集它。有一种引用对象的方法,而无需实际说“我正在使用它”。这种参考称为 弱参考。您是说“我不需要它,但是如果它仍然存在,那么我会使用它”。在其他换句话说,如果某个对象仅被弱引用引用,则GC会收集该对象并释放该内存。这是使用.NET的WeakReference 类实现的。

我们可以通过多种方式使用它来防止内存泄漏。一种流行的设计模式是使用事件聚合器。这个概念是,任何人都可以订阅 T类型的事件,任何人都可以发布 T类型的事件。因此,当一个类发布事件时,将调用所有订阅的事件处理程序。事件聚合器使用WeakReference引用所有内容。所以即使有物体提斯 订阅事件,仍然可以对其进行垃圾回收。

这是一个使用Prism 流行的事件聚合器(通过NuGet Prism.Core提供)的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class WiFiManager
{
private readonly IEventAggregator _eventAggregator;

public WiFiManager(IEventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
}

public void PublishEvent()
{
_eventAggregator.GetEvent<WiFiEvent>().Publish(new WifiEventArgs());
}
1
2
3
4
5
6
7
8
9
10
11
12
public class MyClass
{
public MyClass(IEventAggregator eventAggregator)
{
eventAggregator.GetEvent<WiFiEvent>().Subscribe(OnWiFiChanged);

}

private void OnWiFiChanged(WifiEventArgs args)
{
// do something
}
1
2
3
4
public class WiFiEvent : PubSubEvent<WifiEventArgs>
{
// ...
}

优点: 防止内存泄漏,相对易于使用。

缺点:

充当所有事件的全局容器。任何人都可以订阅任何人。这使得系统在过度使用时难以理解。没有分离的关注点。

4.对常规事件使用弱事件处理程序

借助一些代码技巧,可以将弱引用与常规事件一起使用。这可以通过几种不同的方式来实现。这是使用Paul Stovell的WeakEventHandler的示例:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyClass
{
public MyClass(WiFiManager wiFiManager)
{
wiFiManager.WiFiSignalChanged += new WeakEventHandler<WifiEventArgs>(OnWiFiChanged).Handler;
}

private void OnWiFiChanged(object sender, WifiEventArgs e)
{
// do something
}
}
1
2
3
4
5

public class WiFiManager
{
public event EventHandler<WifiEventArgs> WiFiSignalChanged;
// ...
1
2
3
4
5
6
7
public void SomeOperation(WiFiManager wiFiManager)
{
var myClass = new MyClass(wiFiManager);
myClass.DoSomething();

//... myClass is not used again
}

我真的很喜欢这种方法,因为在我们的案例中,发布者WiFiManager保留了标准的C#事件。这只是这种模式的一种实现,但是实际上有很多方法可以解决。Daniel Grunwald写了一篇有关不同实现及其差异的文章。

优点:利用标准事件。简单。没有内存泄漏。关注点分离(与事件聚合器不同)。

缺点:此模式的不同实现有一些细微之处和不同问题。该示例中的实现实际上创建了一个 注册的包装对象,该 包装对象从未被GC收集。其他实现可以解决此问题,但还有其他问题,例如其他样板代码。在Daniel的文章中了解有关此内容的更多信息 。

WeakReference解决方案存在的问题

使用WeakReference意味着GC将能够在可能的情况下收集订阅类。但是,GC不会立即收集未引用的对象。就开发商而言,它是随机的。因此,对于弱事件,您可能会在当时不应该存在的对象中调用事件处理程序。

事件处理程序可能会执行无害的操作,例如更新内部状态。或者,它可能会更改程序状态,直到GC决定随机收集某个时间为止。这种行为确实很危险。在“弱事件模式是危险的”中对此进行附加阅读 。

5.在没有内存探查器的情况下检测内存泄漏

此技术是为了测试现有的内存泄漏,而不是编码模式以首先避免它们。

假设您怀疑某个类存在内存泄漏。如果您有创建一个实例然后希望GC收集它的情况,则可以轻松地确定是否将收集您的实例或是否存在内存泄漏。按着这些次序:

1.将终结器添加到您的可疑类中,并在其中放置一个断点:

img

  1. 在场景开始时添加以下要调用的魔术3行:
1
2
3
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

这将迫使GC到目前为止收集所有未引用的实例(不在生产环境中使用),因此它们不会干扰我们的调试。

3.添加相同的3条魔术代码行,以 在方案之后运行。请记住,该方案是创建并收集可疑对象的方案。

4.运行有问题的方案。

在第1步中,我告诉您在类的终结器中放置一个断点。在第一个垃圾回收完成之后,您实际上应该注意该断点。否则,您可能会被废弃旧实例感到困惑。需要注意的重要时刻是 您的方案之后调试器是否在Finalizer中停止 。

它还有助于在类的构造函数中放置一个断点。这样,您可以计算创建次数和完成次数。如果触发了终结器中的断点,则GC会收集您的实例,一切正常。如果没有,则可能发生内存泄漏。

这是我调试的一种方案,该方案使用了上一种技术中的WeakEventHandler,并且没有内存泄漏:

这是我使用常规事件注册的另一种情况,它确实存在内存泄漏:

摘要

总是让我感到惊讶的是,C#看起来像是一种易于学习的语言,并且提供了一个提供训练平台的环境。但实际上,还远远没有做到。诸如使用事件之类的简单事情,可以由未经培训的手轻松地将您的应用程序变成一堆内存泄漏。

至于在代码中使用的正确模式,我认为本文的结论应该是,在所有情况下都没有正确答案。提供的所有技术,以及他们, 视情况而定是可行的解决方案。

原来这是一个相对较大的职位,但在此问题上,我仍然处于较高水平。这恰恰证明了在这些问题上存在多少深度,以及软件开发如何永无止境。

有关内存泄漏的更多信息,请查看我的文章查找,修复和避免C#.NET:8最佳实践中的内存泄漏。从我自己的经验和其他高级.NET开发人员那里获得的大量信息都为我提供了建议。它包括有关内存分析器,非托管代码的内存泄漏,监控内存等信息。

我希望您在评论部分中留下一些反馈。并确保订阅博客并收到新帖子通知。

分析EFCore中的内存泄漏

Posted on 2020-04-22 | Edited on 2021-04-25 | In 技术

分析EFCore中的内存泄漏

消防漏水入水坑

术语“内存泄漏”和“ .NET应用程序”不是经常一起使用。但是,我们最近在一个.NET Core Web应用程序中出现了一系列内存不足异常。事实证明,此问题是由Entity Framework Core中的行为更改引起的,尽管最终的解决方案非常简单,但实现该目标的过程既充满挑战又有趣。

该系统本身托管在Azure中,由Angular SPA前端和后端的.NET Core API组成,使用Entity Framework Core与Azure SQL数据库进行通信。作为专门从事.NET开发的软件咨询公司,我们之前已经编写了许多类似的应用程序。因此,内存不足崩溃是无法预料的,因此我们立即知道这是需要认真对待的事情。使用Azure门户中的指标,我们可以看到内存使用率稳步上升,然后突然下降:此下降是应用程序崩溃。

修复之前

修复之前

因此,我们花了一些时间进行调查并逐步进行更改,以解决看似经典的内存泄漏问题。.NET泄漏的常见原因是未正确处理某些问题,在我们的案例中很可能是EF Core数据库上下文。因此,我们遍历了源代码,以寻找可能无法处理上下文的潜在原因。这变成了空白。

我们将Entity Framework Core升级到了最新版本,因为最近的更新包括各种内存泄漏的修复程序和总体效率的提高。

我们还在使用的Application Insights版本中发现了可能的内存泄漏(请参阅https://github.com/microsoft/ApplicationInsights-dotnet/issues/594),因此我们也对该软件包进行了升级。

这些都不能解决问题,因此我们解剖了从Azure应用服务中获取的内存转储(请参阅https://blogs.msdn.microsoft.com/jpsanders/2017/02/02/how-to-get-a-full-memory-dump-in-azure-app-services/)。

我们注意到,绝大多数托管内存最终都由MemoryCache类使用。进一步深入研究表明,大多数缓存数据都是原始SQL查询的形式。我们看到大量的根本上是同一查询的事件被多次缓存,并且参数本身被硬编码在查询中而不是被参数化。

例如,与其像这样缓存查询:

1
2
3
SELECT TOP (1) UserId, FirstName, LastName, EmailAddress
FROM Users
WHERE UserId = @param_1

我们发现这样的多个查询:

1
2
3
SELECT TOP (1) UserId, FirstName, LastName, EmailAddress
FROM Users
WHERE UserId = 5

因此,我们进行了一些搜索,寻找可能与之相关的EF核心问题,并遇到了这个问题:https : //github.com/aspnet/EntityFrameworkCore/issues/10535。

关于这个问题的主题指出了这个问题:我们正在建立一个动态表达式树,并使用它 Expressions.Expression.Constant 来为where子句提供参数。使用常量表达式意味着Entity Framework Core不会参数化SQL查询,并且是Entity Framework 6的行为更改。

我们到处都使用这个表达式树,通过它的ID来获取某些东西,这就是为什么它是一个很大的问题。

因此,这就是我们所做的更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Before
var param = Expressions.Expression.Parameter(typeof(T));
Expression = Expressions.Expression.Lambda<Func<T, bool>>(
Expressions.Expression.Call(
Expressions.Expression.Constant(valuesToFilter),
"Contains",
Type.EmptyTypes,
Expressions.Expression.Property(param, propertyName)),
param);
// After
var param = Expressions.Expression.Parameter(typeof(T));
// This is what we added
Expression<Func<List<int>>> valuesToFilterLambda = () => valuesToFilter;
Expression = Expressions.Expression.Lambda<Func<T, bool>>(
Expressions.Expression.Call(
valuesToFilterLambda.Body,
"Contains",
Type.EmptyTypes,
Expressions.Expression.Property(param, propertyName)),
param);

使用lambda表达式获取表达式主体会使Entity Framework Core对SQL查询进行参数化,因此仅缓存它的一个实例。

这是包括修订版本在内的一段时间内的内存使用情况。该版本以红色标记,您可以看到差异很大。稳定的内存使用量从未超过200MB,而不断攀升至超过1GB,然后发生崩溃。

修复后

修复后

最初进行调查时,真正的解决方案不是我们要注意的事情,而是通过检查内存转储并遵循证据我们最终到达那里。

从此调查中可以汲取的教训是:

  • 内存转储不会说谎-如果内存泄漏,请先查看证据。
  • 微软已经开放了EF Core的源代码,所有问题在那里所有人都可以看到,对有需求的开发者来说非常方便。
  • 简单的代码更改(在这种情况下为一行)可能会产生巨大的影响。

那个年过半百的奋斗者~

Posted on 2020-04-17 | Edited on 2021-04-25 | In 随笔

一

我曾经提到过最终改行从事美缝行业的老w,他靠自己的“不够努力”,最终离开了行业。

但是,这个世界其实有点讽刺。

在沉迷于安逸小日子的老w每天朝九晚五,只想拿钱,不想干活的那段日子,隔壁的总经理办公室却经常通宵达旦、灯火通明,那位五十五岁的老板Y总,正在为了自己的梦想努力奋斗着。

二

在加入公司之前,我曾经见过一次Y总,那是在一个茶室,跟Y总有过一番简单的沟通后,我打算去公司看看,以便了解公司实际的产品情况。

于是受到技术团队负责人的邀请,我选择了一个阳光明媚的下午,坐上了被当地人称为“最长公交线路”的127路公交车,在那条的弯弯曲曲的公交线路上,折腾了大概一个多小时,终于才来到了目的地,位于当地东北角的某商务写字楼。并再次见到了Y总。

虽然是第二次见到Y总,但是第一次其实只是简单的沟通,并没有仔细打量这位领导。这一次,算是对Y总有了更加深刻的印象。

那是一位两鬓开始逐渐开始冒出白发的中年男子,身体精瘦、精神饱满、看起来充满了力量,他操着一口相对于当地人来说非常纯粹的普通话,给我留下了非常深刻的印象。当听他提起他自己已经55岁时,我的内心泛起了波澜。

这是一位和我的父亲一般年龄的中年人啊~在这个年龄,他居然选择了创业,着实让我大吃一惊。

当然,他显然非常的专业,在简单概要的介绍了公司的创业方向、拥有的背景和资源之后,让我深刻的体会到,他一定是想干一番大事业。

于是,我毫不犹豫的加入了公司,并期待在这里开启职场的新征程。

三

创业公司的发展,总是跌宕起伏,看似波澜不惊,其实暗藏杀机。尤其是选择合适的人才,更是难点中的难点。谁都想选择最优秀的人才,但是在优秀的专业人才和优秀的跨职能型人才间,其实非常难以选择。

还好,本人算是一个勉强称职的跨职能型开发者,在我们部门的经理离职之后,毅然扛起了部门的重担,为公司勉强完成了一个非常不错的项目,使得公司能够获得短暂的喘息之机。

但是老w所在的项目,却面临了巨大的问题。

首先是优秀人才的缺失,毕竟能够深刻领悟如何基于物联网技术构建平台的应用开发者,在当时非常的稀缺,更何况公司所能付出的资源(要钱没钱,要股份没股份)其实非常有限,也显然很难招到合适的人才,而这仅仅只是我所看到的web开发方面,还有更严重的方面是物联网基础技术方面。

说来也搞笑,没有物联网基础技术,又如何做物联网产品呢?其实倒也不完全没有积累,这位老板和物联网部门的负责人曾经参与创办了另外一家非常优秀的物联网公司H公司,他们花了十年时间让这家公司从无到有,到做到国际一流。后来H公司业绩到了瓶颈,他们想为公司开辟新的业务方向,才创建了这家新的物联网公司。但是虽然同是物联网创业方向,但选择的技术路线和实现模式却不尽相同,而在新的技术路线上公司的积累非常浅。而在最关键的时候,拥有核心开发能力的一位嵌入式系统开发者,居然只打了个招呼,连交接和培训新人都没有认真开展就离开了公司,使得公司技术层面面临巨大的断层。

为了完成这个项目,Y总只好自己迎难而上。那段时间他不得不捡起曾经荒废多年的嵌入式系统开发技术,天天加班到深夜。每天早上又最早来到公司,恨不能尽早完成目标。

他就不怕猝死么?显然,他是怕的。但为了公司的生存,其实他别无选择。

四

我曾经冒昧的问过他选择创业的原因,他只是轻轻一笑,还不是为了实现自己创业的梦想?

显然他不愿意过多的描述。但是联想到Y总的职业生涯,我大概能猜到一部分原因。

Y总虽然是北方人,但是在这片热土已经呆了三十几年。八十年代在第一大学就读的Y总,年轻时学习成绩特别好,不仅保送本校研究生,还直博,方向是某个热门的领域。当博士毕业后,也许他也曾想去沿海地区发展,但是他最终留下来主导该校的某领域的学术研究。

又过了几年,市场经济放开的九十年代,他也有许多选择的机会。在该领域浸淫十几年的他,一定收到了许多沿海企业或外资企业的聘书,但是他并没有做出这样的选择。

又过了十年,四十岁,他已经决定放开手脚出去干一场时,又是家庭压力最大的时候。也许去沿海城市会让他家庭和事业难以兼顾,他最终还是没有迈出哪一步。

一晃五十岁,他从学校退休。子女也出过留学,得到了顶级互联网公司的offer,基本上算是没什么压力了。

也许,从二十几岁到五十几岁,他错过了太多的机会。

他也曾经偶尔提到那些跟他一起读书的同学,或者研究所的同事,在离开象牙塔后,有的加入了互联网公司花了十年时间获得了财富自由,有的甚至创办了挺不错的公司。

Y总虽然也曾参与了一家公司的创办过程,却并没有从零开始创办一家属于自己的公司,而且他自认为这家公司虽然业务还算稳定,但在技术上,不能算卓越,只能算优秀。由于没有赶上风口,所以做得非常费力;而且行业领域非常狭小,很难获得更大的发展。

他显然想挑战自己。

五

然而,创业难,难于上青天。

我也最终选择了离开这家公司。依稀记得Y总说过的话:“人生短短80年,其中从二十几岁毕业到六十多岁退休,期间有四十年时间。如果抱着把行业当做一辈子的心态,就该前二十年学经验,后二十年才能有经验可以用。”

当然,没有任何一个人敢说自己的技能能够通吃一辈子,也不可能每个人都会在一个公司、一份工作上干一辈子,变化才是人生的常态。适应变化和主动学习,正是人的基本能力。尤其是程序员,有许多程序员能够花十年赚到二十年才能赚到的钱,但是之后呢?年纪轻轻三十岁就养老吗?

学习这条路,其实根本没有终点。

那个程序员,为什么选择改行_

Posted on 2020-04-17 | Edited on 2021-04-25 | In 随笔

一

有一天,一位同事跟我说:老w已经改行做美缝去了,你怎么看?

我想了想,说:他大概终于做出了眼下最符合他的选择。

二

老w是我曾经一位同事。

还记得2014年面试的时候第一次遇到他,当时的他精力旺盛,充满干劲。大概是因为他上一段职场中获得了他认为非常充足的收获,所以找工作的时候,心态也非常积极乐观,这也让面试的过程很轻松愉悦,技术问题一问一个准。

这应该是一个能够为公司创造价值的优秀开发者吧!当时,我这样想,于是毫不犹豫的告知领导,让领导把老w留下来。

被公司招进来之后,他也表现出他的足够专业,使得他能够在这段工作中平滑发展,获得让大家满意的评价。

如果没有其他意外,大概他将从这里起步,在星城长沙好好发展,直到有一天不再适合IT为止。(而那一天,也许得42岁之后吧)。

后来,我从这家公司离开若干年后,与他在一家初创公司相聚。

三

老w来到这里有不少原因。首先原公司项目回款陷入僵持状态,管理层和董事会出现了重大分歧,董事会已经无意于维持公司的进一步发展,进而导致了严重的经济问题,那几个月员工的工资和公积金已经无法按时间缴纳。而老w本身也自我感觉技术到了瓶颈,打算换一个岗位来提升自己,但是原公司的发展困境显然无法给他创造适当的机会。

于是当有人挖他时,他顺势就把工作辞了,来到了这家新成立的公司。

老板很慷慨,看到他是一位经验丰富的开发者,并没有给他安排试用期,而是入职就直接成为正式员工。加入公司的前几个月有点像蜜月期,他和部门经理之间经常对技术进行探讨。由于部门经理主要从事嵌入式系统开发,对互联网技术几乎不懂,老w则也算是从业老兵,虽然基础不扎实,但还能勉强应付部门经理的问题,所以双方的沟通比较融洽。

他们部门的产品也主要是偏物联网的智慧监测管理平台,需要运用嵌入式技术开发设备上的组件,并通过Web平台来展示数据的状态信息。而老W之前并不了解物联网相关领域,但在Web开发领域还是有一点点积累,能够勉强把自己手头上的任务完成妥当。

然而,在初创公司做产品并非只是干好那一亩三分地就够了。他和大部分拥有一定经验的所谓高级开发者一样,总是觉得干好技术就是自己的本份,对业务知识不太在乎。而物联网行业需要太多的行业基础,如果不能深入行业,几乎很难做出成熟的产品。

随着项目的逐渐深入,也完美暴露出老w的技术短板。他已经工作了六七年,但是平时主要负责增删改查,对前端页面和框架底层几乎很少涉及,他本身缺乏主动学习的积极性和创造性,对新技术和前端技术缺乏兴趣和敏感度,甚至连搜索查找问题的能力也很欠缺,这使得他得花许多精力来学习框架知识,并间接导致项目速度进展缓慢。

原本计划三个月做一个小产品,但由于种种原因,以及后来的迭代,硬生生五六个月才完成。在产品终于做完开始正式运行之后,由于后台代码存在的缺陷比较多,部门经理对他大为失望。于是被调到其他部门当研发工程师,但在新的工作岗位上,他很快就暴露出自己无法胜任相关岗位的能力,又回到了原部门。

原部门已经没有他的工作安排了,从此他陷入了长达一个多月的清闲期。这段时间,他也没去找工作,每天朝九晚五,上上网,看看电影,由于他本身就对技术兴趣不足,自然而然也不会踏踏实实补足短板,于是安安稳稳的在公司混了好几个月。

2018年过完年,就从公司离开。之后听说他前前后后找了好几轮工作,但都没能好好的干下去。听说他曾经一度打算选择成为独立开发者,却接不到什么好项目,有的项目完全是费力不讨好,付出了许多努力,却交货时被客户打回。

所以最终选择改行也是万般无奈。

四

老w的职业发展历程总是令我扼腕叹息。虽然程序员转行很正常,但他年轻时也算是精力充沛,对技术充满激情,却为何刚过而立之年就不得不离开行业?要想维持职场长久的生命力,究竟该做哪些努力?这个问题想必已经深深的困扰着许多人。

在我们的身边,有许多这样或那样的人选择IT行业,选择成为程序员。也许大家选择成为程序员的理由不尽相同,但是大家的学习曲线或许却大体类似。这种学习曲线,大概有点像“达克效应”曲线一般魔幻真实。

图片

(达克效应)

用来描述一种认知偏差。它表明,能力差的人总是有一种虚幻的自我优越感,他们总是错误的以为自己比真实的自己更优越。

在这个效应中,将一个人求知的阶段划分为四个阶段:

1、不知道自己不知道。

这一阶段是我们刚刚加入职场的时候,由于眼界和见识的限制,我们将在较短的时间内获得完成短期工作所需的部分知识。这些知识使得我们在短期内自我膨胀,然后心态发生改变,渐渐的成为“愚昧山峰”上的一块顽固之石。过早的登上愚昧之山,对每个开发者来说都不是一件好事情,这意味着你或者你们公司所在行业的天花板太低、或者你的见识太低,使得你过于轻易的就掌握了驾驭当前应用场景的知识,如果不做出改变,将为未来埋下祸根。

2、知道自己不知道。

新技术的发展是如此的突飞猛进,当你还在睡安稳觉时,或许一不小心就被淘汰了。许多“顽固之石”对于新技术的出现,总是抱有成见,甚至会习惯于用自己的固有思想来思考问题。于是在市场面前,一旦遇到一波有一波的挫折,并陷入绝望之谷。

3、知道自己知道

绝望之谷,或使人逃离,或使人成长。

前面故事提到的老w,就是逃离的典型。由于其固有习惯和见识,让他遇到新技术、新应用场景带来的挑战时,总是选择像海龟一样,把自己深藏在一个“壳”中,并且甚至逃避问题,最终只能在一波有一波的挫折面前,选择离开行业。

还有一些人,他们会以过去的挫折为跳板,然后不断的学习,进入“开悟之谷”。这个阶段才是智慧形成的阶段,这意味着你过去的从业经验和知识将成为你成长的宝贵财富。

4、不知道自己知道。

如果始终保持积极乐观和空杯的心态,你掌握的知识也将越来越多,你所散发出的知识的馥郁,也将促使你能够成为身边人学习的榜样,并将促使你成为真正的“大师”。

五

在我们的身边,被类似“达克效应”困扰的现象其实无处不在。拿笔者为例,曾经有一段时间,我经常写博客,还以为自己的博客写得挺有文采的。后来读了许多书,发现自己简直就是可以称为“无知”,于是花了更多的时间来提高自己。

作为开发者也许都将如此,你所知道的越多,其实意味着不知道的越多。每一次你以为大彻大悟,以为道理不过如此,但是往往随着你学习的进一步深入,只会使你更加清楚自己的愚昧。

一次又一次探索中,不断的发现自己的无知,看似在浪费时间,其实是在不断的扎根。人生的每一次成长,从不是一蹴而就,而是像攀登高峰一般,一步一步脚印,每一步都得踏踏实实。

你今天的积累,既是你过去的沉淀,更是适应未来变化的踏脚石。

长沙IT技术圈的百万大佬,何处寻觅?

Posted on 2020-04-10 | Edited on 2021-04-25 | In 随笔

引子

不知不觉,IT技术圈开始流传起“百万年薪”的故事,有人问我,长沙有百万大佬么?其实我也不知道。

一 背景

长沙自古以来就是文风鼎盛之地,在今天也同样如此。

目前长沙有211、985、一本、二本等本科院校数十所,大专以上的院校上百所.每年从长沙毕业,怀揣梦想选择去北上广深杭奋斗的互联网从业人员不下数万人,其中从事技术岗位的(例如开发、测试、运维等)或许是占比最大的部分。

从某种意义上来说,湖南技术人可能真的撑起了全国互联网的半边天。(当然,这样的说法没有统计数据为准,笔者说了不算)。

不过,一直以来,由于沿海地区落户政策的严格、购房和居家生活的成本高,使得许多离开湖南的开发者越来越倾向于选择回到长沙寻找适合自己的工作。

他们都能在长沙找到合适的工作么?他们能成为“百万大佬”么?

二 几个故事

1

老Q是我的同学,大学毕业后,我回了家乡长沙,他则选择去深圳发展。在毕业前的一次吃饭过程中,他说打算先去深圳看看,过几年在深圳混不下去了,再回长沙。

2015年上半年,他的同学邀请他创业,他果断的把在酷派的工作辞了,回到了长沙。但经过半年的折腾,他的创业梦想最终还是“黄”了,又临近年光,他决定留在长沙找工作。

由于他在酷派当时主要从事手机基站测试的相关领域工作,而在长沙实际上几乎没有对应的工作,除了软件测试,就是纯粹的硬件测试。而由于他属于转行的性质,最终工资..更是相当微薄。最终他还是离开了长沙,回到了深圳。

一晃又是五年,当我在跟他交流是否有兴趣回长沙时,他一脸苦涩的说,就算想回,估计也不会在从事IT相关的行业了。

“当时以为混不下去就回长沙,其实长沙反而比北上广深更难混下去。”

2

老L是2014年前后回的长沙。

老L之前在广州,那是一家还不错的上市公司,他在这家公司也算是中层管理者,但考虑到在广州买房不太现实,而且长期离乡背井的工作,也让他对家乡产生了深深的眷恋之情,再加上他的对象也希望他回长沙发展,最终他抛弃了那份月收入破2w的工作,回到了长沙。

回到长沙找工作之前,他找同学打听了长沙当时的开发者工资水平,他的同学告诉他,像这样七八年经验的.NET开发者,大概顶尖水平应该是万把块钱。

老L是一位务实的开发者,他想到自己在广州那家公司,实际上每天投入到软件开发工作中的时间,其实只有不到3个小时,要与专业从事技术开发、天天浸淫其道的高手相比,还存在巨大的差距。

既然顶尖水平才一万,那自己显然只能顶别人的六成,开六千肯定没问题。

结果面试时,公司给他开了8k,顿时他就很欣喜。

虽然他后来又离开了这家公司,但他还是给予了这家公司很高的评价:这充分证明,其实长沙的公司非常识货。

3

老C也是差不多同期回的长沙,当时他已经是一家大型互联网公司的中层管理者。

(这家公司在他离开之后,迎来了一波飞速发展,引入了好几十个华为毕业的员工后,使得公司的整体技术水平和能力都有了巨大的飞跃,在2017年前后成功上市,之前跟他同期加入公司的同事、以及他在公司时招聘的人才,几乎都成为公司的核心骨干或甚至是事业部、分公司总经理。)

老C回到长沙的原因,是因为他存够了能够在深圳付首付款的钱,他的家人却硬是说服他拿这个钱在长沙买房。

当然,同样的钱,在深圳只能买偏僻城乡结合部的小两居,而回长沙则可以买中心地带,地铁口,公园口的120平三室一厅。

他拿着这个钱之后,回来看了一圈房子,发现居然可以选择的余地这么多,而且还这么舒服的,当时就决定回长沙定居,他很快就把深圳的工作辞了,然后回到了长沙找工作。

当他回到长沙找工作之后,得益于一个机会,有幸找到了一个非常有钱的老板,这位老板拉了一个规模还算大的团队,也非常重视像他这样优秀的管理者,以大概15k或更高的薪酬聘用了他。这在当时的长沙已经算非常不错的薪资,使得他能够维持相对高的生活条件。

但公司的资金很快就烧完了,公司解散后,他被迫回到职场求职。

他的心理预算是15k,但在长沙能够开得起15k的公司实在太少,而且这些公司对技术要求非常高,由于他平时工作中对技术的深度钻研有限,最终都被拒之门外。

还好得益于他在HIS领域扎实的行业经验+原公司不错的背景,使得他能够在一家公立医院找到还算可以的工作。

4

当然,并非所有的开发者回到长沙都会面临工资砍一截、甚至减半的情况,我的身边也不乏一些开发者,从北上广深杭回到长沙之后,还能找到与原来公司工资差不多的工作、甚至还有的能找到超过在深圳工资的工作。

那究竟是什么原因决定了长沙开发者的薪资水平,难道真的是长沙的互联网水平发展太低了么?

三 长沙的开发者工资有多高?

1

在长沙有哪些公司的工资最高?

首先还是得排除BAT公司和大型互联网公司也开始在长沙成立相应的分支机构或分公司、子公司。这些公司在长沙也好、深圳也好,其实工资是按照职级来,与城市关系不大。我的一位堂叔在恒大集团(虽然远离IT圈子),但他的工资还不错,据说在长沙一年的收入可以买一套房。。所以。。你懂的,如果你想回长沙,最好的办法就是选择加入BAT,然后公派回老家发展,大概这才是真正的“衣锦还乡”。

其次首屈一指的大概是芒果TV。据悉..芒果TV的前台,每个月虽然工资不高,才8k,但每年能拿到手的年终奖也超过了16万。。而普通开发者,一般每年能够拿到手的收入是20w以上。

稍微牛逼一点的开发者,其实工资并不亚于北上广深的同类型公司。

关键是芒果TV朝九晚五,不用加班,工资还挺轻松,而且公司还挺不错,说出去挺有自豪感。

我一位大学同学表示,他老公曾经在爱奇艺担任运维工程师,回长沙之后去了芒果TV,工资和爱奇艺齐平,工作压力少了一半。

图片

2

其次,大概这张图上的公司,工资都还可以。。。但,离北上广深的同等排名的公司相比,应该还是差距比较大。

此处还需点出几家公司,建议大家多投简历,例如我所了解的御泥坊、问卷星、兴盛优选、蜜獾信息等公司,他们对人才还是挺重视的,工资也还挺高的,值得大家关注关注。。

当然,与北上广深的大公司比起来,嗯,还是别比了。

另外,内推可能比社招工资高,如果你是大佬,想走社招加入这几家公司,估计会被压一些价。。。

3

那长沙的开发者普遍工资大概是什么水平?或许不同的语言体系有一定的不同,例如后端,一位顶尖的后端工程师,大概是30-50k。

当然,目前我还没接触到突破50k的开发者。我接触到突破30k的开发者。。对不起,他们已经不是开发者了,基本上都是公司管理层、甚至在不少公司,甚至是高管级别。

而资深开发者,基本上是在20-40k左右;高级开发者,应该是12-25k左右,中级开发者,大概是8-15k左右,初级开发者,大概是4-8k左右。在这一点上,Java和.NET或其他语言,其实区别不大。

4

当然,我的圈层束缚了我的想象力,显然还有更高收入的群体。。在我了解的圈层。

例如芒果TV。。一个高级运维就能突破30k,而其他公司,不管你k8s\openstack玩得多

溜,20k已经是天花板了。

圈层之外。。大概年薪50万到80万,其实也是长沙一些大型互联网公司技术高管的天花板。其实有的大公司高管,也没能突破40k,不过既然已经成为高管了,估计他们已经不靠工资,更多的是靠公司的业绩提成或股份分红吧。

5

对不起,所有初级、中级、高级、资深开发者,并不是按年资来衡量的,而是看真实实力来衡量。你不能说你工作十年,就一定是高级开发者。例如,如果你做后端开发,连gc、领域驱动设计、分布式缓存、NoSQL都没听过,可能你确实不太适合寻找跨行业的职位。。

然而,事实上大多数开发者都是这么自以为的。我也面过一些工作十年的开发者,他们在特定的业务领域,或许是业务大佬,但一旦离开对应的领域求职,几乎找不到合适的工作。例如,我曾经面过一些做建筑信息化的开发者,工作也有十年多,但一旦问到涉及并发、缓存、gc等问题,他们基本上都没听过。与这类似的还有从事制造业信息化的开发者,可能连.NET技术中的一些新特性,例如async/await都可能没听过。就像搞java的,连稍微新一点的语法都没用过,又该如何被称为“高级Java工程师”。

作为开发者,懂基础概念和术语还是很重要的,不过,长沙的技术圈子,似乎还没形成这样的氛围。面试时,有时不能问技术问题。

当然,退一万不讲,公司不怎么地,居然还好意思问技术问题,不就是“拥有造核弹的心,却只有拧螺丝钉的命么?”。。。

四 我技术牛逼,为何就不能拿高工资?

1

我已经见过不下十位优秀开发者,他们的岗位基本上都是技术经理、技术总监、架构师,而待遇要求基本上都是18k以上。结果他们的求职期无一例外,都达到了一个月以上,最终不得不一点点把自己的心理预期进一步降低,进一步降低,然后在15k左右徘徊。当然,这些都是.NET开发者,如果是Java开发者,可能多那么几千块钱。。(不过Java的竞争更激烈)

这是由于他们能力不行造成的么?绝非如此,我相信,他们在没回长沙之前,公司也是好平台、他们能够成为公司的核心骨干,也证明他们非常优秀。

2

但是。。长沙确实是互联网的荒漠,能够给开发者带来更加丰厚收入、自豪感的公司,实在是太少太少。前面提到的那些技术管理层都是非常优秀的跨职能人才,这让他们能够在技术岗位快速进步的同时,还能很快的成为公司的管理者,给公司的发展带来很大的帮助。只是从他们离开公司开始,就无法以对应的title寻找工作,只能寻找技术领域的高级开发者。

而市场上管理岗位显然少于技术岗位,且不说竞争是否激烈,更何况现在的技术岗位对于技能的要求本身就已经逐渐提高了。当你踏上管理者的岗位开始,一天又能维持几个小时的编码时间呢?你真的会持续刷新自己的技术,保持技术的持久战斗力么?

或许。其实你内心,其实已经不再重视“编码”这个硬技能了吧。

另外,依据“彼得原理”,大部分公司其实找的是当前岗位的胜任者,而不是超出对应岗位的“进阶者”。如果你的公司天花板非常低,那你又如何能在那些有高天花板的公司找到高职级的工作。即便有,或许也很难胜任吧。

4

长沙的软件公司或开发者技术不行么?这也是个悖论。文无第一,武无第二。工资不行不代表技术不行。 许多长沙的公司虽然公司效益远远比不上BAT大公司,一年净收入突破几千万就算是个不错的公司了,但这不能说明公司技术不行。恰好相反,一波一波回长沙的开发者,总会有人一点点把长沙的技术水平逐渐提高。

技术水平的高低没有评判标准,许多大佬技术牛逼,但脱离了公司的平台效应和团队战斗力,单兵作战能力可能并不强。

相比之下,许多长沙的开发者平时下班之后就是学习技术,交流技术,一天八小时撸码,还有四小时学习,这样的开发者技术还能差到哪里去。

例如前面提到的蜜獾信息,就形成了这样一群人。而且公司待遇不错,技术氛围也很不错,公司管理层也重视技术和企业文化氛围,早就把“不加班,不写过时的文档,每两周发布一个版本的敏捷发布”玩的挺顺利的,非常适合大家关注。

5

当然,限于体量原因,许多技术含量较高的场景,长沙还真没有。我一位朋友吐槽,长沙的运维人员,还停留在最多百台服务器运维的能力,几乎相当于他2015年的水平。现在在长沙,能够玩得起自动化测试的公司,都还算不错的公司,所以如果你是拥有自动化测试经验的测试工程师,其实也还是有很多机会。

但那些大公司才有的细分岗位,例如之前说的基站测试,还有咨询师,技术写作专家,配置管理工程师,甚至是python大数据分析师,长沙大概还几乎没有。长沙市场上python相关的职位还非常少,go语言的也并不多。。。

6

长沙的公司为什么不愿意花高工资聘请大牛呢?其实这也是一些回长沙的求职者向我吐槽的。也许他们的言下之意是:为啥不聘请“像自己这样的大牛”呢?

好吧,其实长沙的IT圈也舍得花力气聘请大牛,像已经成为社区电商行业的领跑者的兴盛优选,公司本身已经有不少大牛,而且也愿意用月薪35到60以上的标准聘请阿里巴巴p7以上的开发者,考虑到长沙的物价水平,其实已经挺不错了。

如果你是真大牛。。我可以帮你推荐一番。

总之,长沙的IT公司并非不舍得花高工资聘请大佬,而是因为。。。还没遇到真正牛逼的大佬。

五 高工资是怎么来的?

1

谈起高工资,我们得想想北上广深那么多百万年薪的程序员,他们的高工资是怎么来的?

在互联网飞速发展的今天,靠近资本的北上广深杭已经成为风口浪尖,许多与互联网概念相关的企业都跟着业绩腾飞,员工的薪资也跟着水涨船高,此处就不需要举例子了,毕竟BAT那么多家公司的优秀开发者们,用他们的身价证明,自己的一份努力,完全可以代表中国互联网开发者的顶尖收入水平。

但。。在这些行业巨头之外,还有许多开发者,其实依然处于温饱线的边缘,或稍微比温饱线好那么一点,财务自由?35岁退休?年入百万?大部分人别想了。是由于选择大于努力造成的?还是由于平时温水煮青蛙,不够努力造成的?其实基本上都不是的。事实上,许多开发者,例如一些外包或制造业外包的开发者,他们的条件远比BAT公司更艰苦,每周的工作时长更高,但拿的收入水平,却仅仅只是BAT开发者的零头,甚至不如。

这大概有点像拿非洲人民和美国资本家对比。用经济学术语来说。。就是“剪刀差”(我打算称为工资剪刀差)。

图片

互联网企业员工的高收入,其实来源于互联网企业本身对于资本的凝聚力和投资者对于未来的期望所带来的溢价。而有许多互联网公司其实本身已经不需要靠利润来发工资,仅仅依靠“市梦率”,就能维持公司的飞速发展,让员工获得足够的福利。

2

而长沙的互联网公司并不多,大部分都是所谓“行业互联网”,虽然沾上了“互联网”的名,却没有互联网的命。

“行业互联网”企业由于发展较为缓慢、或者已经较为稳定,事实上已经不太可能从投资者市场获得更高,由于只能靠利润来给员工发工资。

所以,员工的工资包含了公司获得的投资,公司的利润,公司的品牌溢价、个人的品牌溢价和个人的核心竞争力,个人的机遇。如果你核心竞争力强,让那些优秀的公司能够更早的认识你,自然而然也很容易成为职场上的香饽饽。

3

如果品牌竞争力一般,该如何寻找合适自己的工作呢?

图片

认清形势,放弃幻想。回归现实,别妄图拿那么高的工资。找一个自己感兴趣的行业、公司,踏踏实实干下去,通过自己的努力,为公司创造更大的价值,自然而然,你就获得了更好的发展。

当你回到长沙,就别在幻想通过上班来获得“暴富”了。长沙这样的土壤,其实更像普通人凭借自己双手改变家庭命运的跳板。

例如,校管家,就是这样的公司,老板们勤勤恳恳、踏踏实实,靠自己的努力,创造了一家优秀的公司,并成功的获得了投资者们的不断关注。在长沙,这样的公司不下数十家。

结语

与其他城市相比,长沙的IT业态或许更趋于“务实”“勤勉”“实干”“坚持”。

长沙不是互联网人淘金的热土,也不是一夜暴富者的摇篮。

在沿海地区互联网的热闹喧嚣之外,长沙其实就是一个这样静静发展、一声不响就创造出不错佳绩的“小而美”的现代化城市。

在长沙,也许你见不到太多“英雄”,却充满了各种各样、努力付出、细心耕耘、用三年、五年或更长的时间来用心助力公司成长的“普通人”。

显然,这个世界,既要“冒险家”“野心家”“成功学”,同样也需要“普通人”。

在.NET应用程序中分析CPU使用率过高的问题

Posted on 2020-04-09 | Edited on 2021-04-25 | In 技术

作者:胡安·帕勃罗·希达,JUAN PABLO SCIDA是一位软件架构师,在软件开发方面拥有10多年的经验。他是经过认证的.NET和Java开发人员。在过去的几年中,他还热衷于使用Node.js,MongoDB和Erlang。

原文来自:https://www.toptal.com/dot-net/hunting-high-cpu-usage-in-dot-net

软件开发可能是一个非常复杂的过程。作为开发人员,我们需要考虑很多不同的变量。有些不在我们的控制之下,有些在实际代码执行时对我们来说是未知的,有些则由我们直接控制。 .NET开发人员也毫不例外。

考虑到这样的现实情况,当我们在受控环境中工作时,事情通常会按计划进行。假设就是我们的开发机器或我们可以完全访问的集成环境。我们可以使用工具来分析影响我们的代码和软件的不同变量。我们也不必处理服务器的繁重负载,也不必处理并发用户尝试同时执行相同操作的情况。

在可描述和安全的情况下,我们的代码通常可以正常工作,但是在生产环境下,如果处于过度负载或其他一些外部因素的影响,可能会发生意外问题。生产环境的软件性能很难分析。在大多数情况下,我们必须在理论上处理潜在的问题:我们知道可能会发生问题,但无法测试。这就是为什么我们需要以我们所用语言的最佳实践和文档为基础进行开发,并避免常见错误。

如前所述,当软件上线时,可能会出错,并且代码可能会以我们未计划的方式开始执行。当我们不得不处理问题而又无法调试或确定发生了什么情况时,我们可能会遇到这种情况。在这种情况下我们该怎么办?

图片

如果某个进程长时间使用超过90%的CPU,则我们会遇到麻烦

在本文中,我们将分析基于Windows的服务器上. net web应用程序的高CPU使用率的实际案例场景、涉及到的识别问题的过程,以及更重要的问题,为什么会出现这个问题以及我们如何解决它。

CPU使用率和内存消耗是广泛讨论的主题。通常,很难确定某个特定进程应使用的资源(CPU,RAM,I / O)的正确数量以及持续的时间段。尽管可以肯定的是-如果某个进程长时间使用了超过90%的CPU,那么我们将特别麻烦,因为在这种情况下服务器将无法处理任何其他请求。

这是否意味着流程本身存在问题?不必要。该过程可能需要更多的处理能力,或者正在处理大量数据。首先,我们唯一能做的就是尝试确定发生这种情况的原因。

所有操作系统都有几种不同的工具来监视服务器中发生的事情。Windows服务器专门具有任务管理器Performance Monitor,在本例中,我们使用了New Relic Servers,它是监视服务器的绝佳工具。

最初症状和问题分析

部署应用程序后,在头两周的时间里,我们开始看到服务器的CPU使用率达到峰值,这使服务器无响应。为了使其再次可用,我们必须重新启动它,并且该事件在该时间段内发生了3次。如前所述,我们使用New Relic Servers作为服务器监视器,它表明w3wp.exe在服务器崩溃时,该进程占用了94%的CPU。

Internet信息服务(IIS)工作进程是Windows进程(w3wp.exe),它运行Web应用程序,并负责处理发送到特定应用程序池的Web服务器的请求。IIS服务器可能有多个应用程序池(和几个不同的w3wp.exe进程),这些池可能会产生问题。根据该进程具有的用户(这在New Relic报告中显示),我们确定问题出在我们的.NET C#Web表单旧版应用程序。

.NET Framework与Windows调试工具紧密集成在一起,因此,我们要做的第一件事是查看事件查看器和应用程序日志文件,以查找有关正在发生的事情的有用信息。无论我们是否在事件查看器中记录了一些异常,它们都没有提供足够的数据来进行分析。这就是为什么我们决定更进一步并收集更多数据的原因,因此当事件再次发生时,我们将做好准备。

数据采集

收集用户模式进程转储的最简单方法是使用Debug Diagnostic Tools v2.0或仅使用DebugDiag。DebugDiag具有一组用于收集数据(DebugDiag集合)和分析数据(DebugDiag分析)的工具。

因此,让我们开始定义使用调试诊断工具收集数据的规则:

  1. 打开DebugDiag集合,然后选择Performance。图片
  2. 选择Performance Counters并单击Next。
  3. 点击Add Perf Triggers。
  4. 展开Processor(不是Process)对象,然后选择% Processor Time。请注意,如果您使用的是Windows Server 2008 R2,并且具有64个以上的处理器,请选择该Processor Information对象而不是该Processor对象。
  5. 在实例列表中,选择_Total。
  6. 单击Add,然后单击确定OK。
  7. 选择新添加的触发器,然后单击确定Edit Thresholds。图片
  8. Above在下拉菜单中选择。
  9. 将阈值更改为80。
  10. 输入20秒数。您可以根据需要调整该值,但请注意不要指定小数秒,以防止错误触发。图片
  11. 点击OK。
  12. 点击Next。
  13. 点击Add Dump Target。
  14. Web Application Pool从下拉菜单中选择。
  15. 从应用程序池列表中选择您的应用程序池。
  16. 点击OK。
  17. 点击Next。
  18. Next再点击一次。
  19. 如果需要,请输入规则名称,并记下转储的保存位置。您可以根据需要更改此位置。
  20. 点击Next。
  21. 选择Activate the Rule Now并单击Finish。

描述的规则将创建一组小型转储文件,这些文件的大小将非常小。最终转储将是具有完整内存的转储,并且该转储会更大。现在,我们只需要等待高CPU事件再次发生即可。

将转储文件保存在所选文件夹中后,我们将使用DebugDiag Analysis工具来分析收集的数据:

  1. 选择性能分析器。图片
  2. 添加转储文件。图片
  3. 开始分析。

DebugDiag将花费几分钟(或数分钟)来解析转储并提供分析。完成分析后,您将看到一个网页,其中包含摘要以及有关线程的大量信息,类似于以下内容:

图片

正如您在摘要中看到的那样,有一条警告说:“在一个或多个线程上检测到转储文件之间的CPU使用率过高。” 如果单击建议,我们将开始了解应用程序存在问题的地方。我们的示例报告如下所示:

图片

正如我们在报告中看到的那样,有一个关于CPU使用率的模式。所有CPU使用率高的线程都与同一类相关。在跳到代码之前,让我们看一下第一个。

图片

这是我们遇到的第一个线程的细节。对我们来说有趣的部分是:

图片

在这里,我们有一个代码调用,GameHub.OnDisconnected()该代码触发了有问题的操作,但是在此调用之前,我们有两个Dictionary调用,它们可以使您对发生的事情有所了解。让我们看一下.NET代码,看看该方法在做什么:

public override Task OnDisconnected() {

try

{

    var userId = GetUserId();

    string connId;

    if (onlineSessions.TryGetValue(userId, out connId))

        onlineSessions.Remove(userId);

}

catch (Exception)

{

    // ignored

}

return base.OnDisconnected();

}

我们显然在这里有问题。报告的调用堆栈说问题出在字典上,在这段代码中我们正在访问字典,特别是引起问题的那一行是:

if (onlineSessions.TryGetValue(userId, out connId))

这是字典声明:

static Dictionary<int, string> onlineSessions = new Dictionary<int, string>();

.NET代码有什么问题?

具有面向对象编程经验的每个人都知道静态变量将由此类的所有实例共享。让我们更深入地了解.NET世界中静态的含义。

根据.NET C#规范:

使用static修饰符声明一个静态成员,该成员属于类型本身而不是特定对象。

这就是.NET C#语言规范关于静态类和成员的说明:

与所有类类型一样,当加载引用该类的程序时,.NET Framework公共语言运行库(CLR)将加载静态类的类型信息。程序无法确切指定何时加载类。但是,可以保证在程序中首次引用该类之前,将其加载并初始化其字段并调用其静态构造函数。静态构造函数仅被调用一次,并且静态类在程序所在的应用程序域的生存期内保留在内存中。
非静态类可以包含静态方法,字段,属性或事件。即使没有创建该类的实例,该静态成员也可以在该类上调用。始终通过类名称而不是实例名称访问静态成员。无论创建多少个类实例,静态成员只有一个副本。静态方法和属性无法访问其包含类型的非静态字段和事件,并且除非在方法参数中显式传递了实例变量,否则它们无法访问任何对象的实例变量。

这意味着静态成员属于类型本身,而不是对象。它们也由CLR加载到应用程序域中,因此静态成员属于承载应用程序的进程,而不是特定线程。

鉴于Web环境是多线程环境,因为每个请求都是由w3wp.exe进程产生的新线程;考虑到静态成员是该过程的一部分,我们可能会遇到以下情况:几个不同的线程尝试访问静态(由多个线程共享的)变量的数据,这最终可能会导致多线程问题。

线程安全性下的Dictionary 文档声明以下内容:

Dictionary<TKey, TValue>只要不修改集合,A 就可以同时支持多个阅读器。即使这样,通过集合进行枚举本质上也不是线程安全的过程。在极少的枚举与写访问竞争的情况下,必须在整个枚举期间锁定集合。要允许多个线程访问该集合进行读写,您必须实现自己的同步。

此声明解释了为什么我们可能会遇到此问题。根据转储信息,问题出在字典的FindEntry方法上:

图片

如果查看字典的FindEntry 实现,我们可以看到该方法遍历内部结构(存储桶)以查找值。

因此,以下.NET代码枚举了集合,这不是线程安全的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public override Task OnDisconnected() {
try
{
var userId = GetUserId();
string connId;
if (onlineSessions.TryGetValue(userId, out connId))
onlineSessions.Remove(userId);
}
catch (Exception)
{
// ignored
}
return base.OnDisconnected();
}

结论

正如我们在转储中看到的那样,有多个线程试图同时迭代和修改共享资源(静态字典),最终导致迭代进入无限循环,从而导致线程消耗超过90%的CPU。 。

有几种可能的解决方案。我们首先实现的方法是锁定和同步对字典的访问,但会损失性能。那时服务器每天都崩溃,因此我们需要尽快解决此问题。即使这不是最佳解决方案,它也解决了该问题。

解决这个问题的下一步是分析代码并找到最优解决方案。重构代码是一个选项:新的ConcurrentDictionary类可以解决这个问题,因为它只锁定在一个桶级别,这将提高整体性能。尽管这是一大步,还需要进一步的分析。

.NET中的内存管理

Posted on 2020-04-06 | Edited on 2021-04-25 | In 技术

.NET中的内存管理

*资源分配
*

Microsoft .NET公共语言运行时要求从托管堆分配所有资源。当应用程序不再需要对象时,它们将自动释放。

初始化进程后,运行时将保留地址空间的连续区域,该区域最初没有为其分配存储空间。该地址空间区域是托管堆。堆还维护一个指针。该指针指示下一个对象将在堆中分配的位置。最初,将指针设置为保留地址空间区域的基地址。

应用程序使用new运算符创建一个对象。该运算符首先确保新对象所需的字节适合保留区域(必要时进行存储)。如果对象合适,则指针指向堆中的对象,调用该对象的构造函数,并且new运算符返回该对象的地址。

Memory3.gif

上图显示了一个由三个对象组成的托管堆:A,B和C。要分配的下一个对象将放置在NextObjPtr指向的位置(紧随对象C之后)。

当应用程序调用new运算符创建对象时,该区域中可能没有足够的地址空间分配给该对象。堆通过将新对象的大小添加到NextObjPtr来检测到这一点。如果NextObjPtr超出地址空间区域的末尾,则堆已满,必须执行收集。

实际上,当第0代完全填满时发生收集。简而言之,生成是由垃圾收集器实现以提高性能的一种机制。这个想法是,新创建的对象是年轻一代的一部分,而在应用程序生命周期的早期创建的对象是老一代的对象。将对象分成几代可以使垃圾收集器收集特定的世代,而不是收集托管堆中的所有对象。

垃圾收集算法

垃圾收集器检查以查看堆中是否有不再由应用程序使用的对象。如果存在此类对象,则可以回收这些对象使用的内存。(如果没有更多的内存可用于堆,则new运算符将引发OutOfMemoryException。)

每个应用程序都有一组根。根标识存储位置,这些存储位置引用托管堆上的对象或设置为null的对象。例如,应用程序中的所有全局和静态对象指针都被视为应用程序根目录的一部分。另外,线程堆栈上的任何局部变量/参数对象指针都被视为应用程序根目录的一部分。最后,任何包含指向托管堆中对象的指针的CPU寄存器也被视为应用程序根目录的一部分。活动根的列表由即时(JIT)编译器和公共语言运行时维护,并且可以由垃圾收集器的算法访问。

当垃圾收集器开始运行时,它假定堆中的所有对象都是垃圾。换句话说,它假定应用程序的任何根都没有引用堆中的任何对象。现在,垃圾收集器开始遍历根目录,并为从根目录可访问的所有对象建立图形。例如,垃圾收集器可以定位一个指向堆中对象的全局变量。

下图显示了具有几个已分配对象的堆,其中应用程序的根直接引用对象A,C,D和F。所有这些对象都成为图形的一部分。在添加对象D时,收集器会注意到该对象引用了对象H,并且对象H也已添加到图中。收集器将继续递归遍历所有可到达的对象。

图的这一部分完成后,垃圾收集器将检查下一个根并再次遍历对象。当垃圾收集器从一个对象移动到另一个对象时,如果它试图将一个对象添加到先前添加的图形中,则垃圾收集器可以停止沿该路径移动。这有两个目的。首先,它不会多次遍历一组对象,因此可以显着提高性能。其次,如果您有任何循环链接的对象列表,它可以防止无限循环。

一旦检查完所有的根,垃圾收集器的图形就会包含从应用程序的根以某种方式可以访问的所有对象的集合。应用程序无法访问该图中未包含的任何对象,因此将其视为垃圾。

垃圾收集器现在线性地遍历堆,寻找垃圾对象的连续块(现在被认为是可用空间)。然后,垃圾收集器将非垃圾对象向下移动到内存中(使用标准的memcpy函数),从而消除了堆中的所有间隙。当然,在内存中移动对象会使指向该对象的所有指针无效。因此,垃圾收集器必须修改应用程序的根,以便指针指向对象的新位置。另外,如果任何对象包含指向另一个对象的指针,则垃圾回收器还负责更正这些指针。

下图显示了收集后的托管堆。

Memory5.gif

在识别完所有垃圾之后,所有非垃圾都已压缩,所有非垃圾指针都已固定,NextObjPtr定位在最后一个非垃圾对象之后。此时,再次尝试新操作,并成功创建应用程序请求的资源。

GC会对性能产生重大影响,这是使用托管堆的主要缺点。但是,请记住,GC仅在堆已满时才发生,并且在此之前,托管堆要比C运行时堆快得多。运行时的垃圾收集器还使用Generations提供了一些优化,可以大大提高垃圾收集的性能。

您不再需要实现管理应用程序使用的任何资源的生存期的任何代码。现在,不可能泄漏资源,因为可以在某个时候收集从应用程序的根目录无法访问的任何资源。此外,也无法访问已释放的资源,因为如果可访问资源将不会被释放。如果无法访问,则您的应用程序无法访问它。

以下代码演示了如何分配和管理资源:

class Application { public static int Main(String[] args) { // ArrayList object created in heap, myArray is now in root ArrayList myArray = new ArrayList(); // Create 10000 objects in the heap for (int x = 0; x < 10000; x++) { myArray.Add(new Object()); // Object object created in heap } // Right now, myArray is a root (on the thread's stack). So, // myArray is reachable and the 10000 objects it points to are also reachable. Console.WriteLine(myArray.Count); // After the last reference to myArray in the code, myArray is not a root. // Note that the method doesn't have to return, the JIT compiler knows // to make myArray not a root after the last reference to it in the code. // Since myArray is not a root, all 10001 objects are not reachable // and are considered garbage. However, the objects are not // collected until a GC is performed. } }

如果GC非常出色,那么您可能想知道为什么它不在ANSI C ++中。原因是垃圾收集器必须能够标识应用程序的根,还必须能够找到所有对象指针。C ++的问题在于它允许将指针从一种类型转换为另一种类型,并且无法知道指针所指的是什么。在公共语言运行库中,托管堆始终知道对象的实际类型,并且元数据信息用于确定对象的哪些成员引用其他对象。

世代

纯粹为了提高性能而存在的垃圾收集器的一个功能称为“世代”。分代垃圾收集器(也称为临时垃圾收集器)进行以下假设:

  • 对象越新,其生存期就会越短。
  • 对象越旧,其寿命将越长。
  • 较新的对象往往彼此之间具有很强的关系,并且经常在同一时间访问。
  • 压缩一部分堆比压缩整个堆要快。

初始化后,托管堆不包含任何对象。如下图所示,添加到堆中的对象被称为第0代。简而言之,第0代中的对象是从未被垃圾收集器检查过的年轻对象。

Memory6.gif

现在,如果将更多对象添加到堆中,则将填充堆,并且必须进行垃圾回收。垃圾收集器分析堆时,将构建垃圾(此处以绿色显示)和非垃圾对象的图形。可以将收集到的所有对象压缩到堆的最左侧。这些对象在收藏中幸存下来,并且更旧,现在被认为是第一代。

Memory7.gif

随着更多对象添加到堆中,这些新的年轻对象将放置在第0代中。如果再次填充第0代,则会执行GC。这次,将第1代中幸存的所有对象压缩并视为第2代(请参见下图)。现在压缩了第0代中的所有幸存者,并认为它们是第1代。第0代当前不包含任何对象,但是所有新对象将进入第0代。

Memory8.gif

当前,第二代是运行时的垃圾收集器支持的最高一代。当将来发生收集时,当前第2代中尚存的所有对象仅保留在第2代中。

世代GC性能优化

分代垃圾收集提高了性能。当堆填满并发生收集时,垃圾收集器可以选择仅检查第0代中的对象,而忽略任何更大的后代中的对象。毕竟,对象越新,则预期寿命越短。因此,收集和压缩第0代对象很可能会从堆中回收大量空间,并且比收集器检查所有代的对象要快。

分代收集器可以通过不遍历托管堆中的每个对象来提供更多优化。如果根或对象引用的是旧对象,则垃圾收集器可以忽略任何较旧对象的内部引用,从而减少了构建可访问对象图所需的时间。当然,旧对象可能是指新对象。为了检查这些对象,收集器可以利用系统的写监视支持(由Kernel32.dll中的Win32 GetWriteWatch函数提供)。此支持使收集器知道自上次收集以来已将哪些旧对象(如果有)写入了。可以检查这些特定的旧对象的引用,以查看它们是否引用了任何新对象。

如果收集第0代未提供必要的存储量,则收集器可以尝试收集第1代和第0代的对象。如果所有其他操作均失败,则收集器可以收集第2代,第1代和第9代的所有对象。 0。

前面提到的一种假设是,较新的对象之间往往具有很强的关系,并且经常在同一时间访问。由于新对象是在内存中连续分配的,因此您可以从引用的位置获得性能。更具体地说,很可能所有对象都可以驻留在CPU的缓存中。您的应用程序将以惊人的速度访问这些对象,因为CPU将能够执行其大多数操作,而不会导致强制RAM访问的高速缓存未命中。

微软的性能测试表明,托管堆分配比Win32 HeapAlloc函数执行的标准分配更快。这些测试还表明,在200 MHz Pentium上执行第0代完整GC所需的时间少于1毫秒。Microsoft的目标是使GC花费的时间不比普通页面错误多。

Win32堆的缺点:

  • 大多数堆(例如C运行时堆)在找到可用空间的任何地方分配对象。因此,如果我连续创建多个对象,则这些对象很有可能将被兆字节的地址空间分隔开。但是,在托管堆中,连续分配几个对象可确保对象在内存中是连续的。
  • 从Win32堆分配内存时,必须检查该堆以找到可以满足请求的内存块。这在托管堆中不是必需的,因为此处对象在内存中是连续的。
  • 在Win32堆中,必须维护堆维护的数据结构。另一方面,托管堆仅需要增加堆指针。

终接器

垃圾收集器提供了您可能想利用的其他功能:终结处理。最终确定允许资源在被收集后对其进行适当的清理。通过使用终结处理,当垃圾回收器决定释放资源的内存时,代表文件或网络连接的资源便能够正确清理自身。

当垃圾收集器检测到对象是垃圾时,垃圾收集器将调用对象的Finalize方法(如果存在),然后回收该对象的内存。例如,假设您具有以下类型(在C#中):

public class BaseObj
{
public BaseObj()
{
}
protected override void Finalize()
{
// Perform resource cleanup code here
// Example: Close file/Close network connection
Console.WriteLine("In Finalize.");
}
}

现在,您可以通过调用以下内容来创建该对象的实例:

BaseObj bo = new BaseObj();

将来的某个时候,垃圾收集器将确定该对象为垃圾。发生这种情况时,垃圾收集器将看到该类型具有Finalize方法,并将调用该方法,从而使“ In Finalize”出现在控制台窗口中并回收该对象使用的内存块。

许多习惯于使用C ++进行编程的开发人员都会在析构函数和Finalize方法之间建立直接的关联。但是,对象终结处理和析构函数具有非常不同的语义,在考虑终结处理时,最好忘记您对析构函数的了解。受管对象永远不会有析构函数。

设计类型时,最好避免使用Finalize方法。有几个原因:

  • 可终结对象被提升为较早的一代,这增加了内存压力,并在垃圾收集器确定对象为垃圾时阻止了对象的内存被收集。此外,该对象直接或间接引用的所有对象也将得到提升。
  • 可终结对象需要更长的分配时间。
  • 强制垃圾收集器执行Finalize方法会严重影响性能。请记住,每个对象都已完成。因此,如果我有10,000个对象的数组,则每个对象都必须调用其Finalize方法。
  • 终结对象可以引用其他(不可终结)对象,从而不必要地延长其寿命。实际上,您可能需要考虑将类型分为两种不同的类型:一种轻型类型,其具有不引用任何其他对象的Finalize方法,一个单独的类型,其类型不具有引用其他对象的Finalize方法。
  • 您无法控制Finalize方法何时执行。该对象可能会保留资源,直到下一次垃圾收集器运行为止。
  • 当应用程序终止时,某些对象仍然可以访问,并且不会调用其Finalize方法。如果后台线程正在使用对象,或者在应用程序关闭或AppDomain卸载期间创建了对象,则会发生这种情况。此外,默认情况下,应用程序退出时,不可达对象不会调用Finalize方法,因此应用程序可能会迅速终止。当然,将回收所有操作系统资源,但是托管堆中的任何对象都无法正常清理。您可以通过调用System.GC类型的RequestFinalizeOnShutdown方法来更改此默认行为。但是,应谨慎使用此方法,因为调用它意味着您的类型正在控制整个应用程序的策略。
  • 运行时无法保证Finalize方法的调用顺序。例如,假设有一个对象包含一个指向内部对象的指针。垃圾收集器检测到两个对象都是垃圾。此外,假设首先调用内部对象的Finalize方法。现在,允许外部对象的Finalize方法访问内部对象并对其调用方法,但是内部对象已完成,并且结果可能无法预测。因此,强烈建议Finalize方法不要访问任何内部成员对象。

如果确定类型必须实现Finalize方法,则请确保代码尽快执行。避免所有会阻止Finalize方法的操作,包括任何线程同步操作。另外,如果您让任何异常转义了Finalize方法,则系统仅假定Finalize方法已返回,并继续调用其他对象的Finalize方法。

当编译器为构造函数生成代码时,编译器会自动插入对基本类型的构造函数的调用。同样,当C ++编译器为析构函数生成代码时,编译器会自动插入对基本类型的析构函数的调用。终结方法不同于析构函数。编译器对Finalize方法没有特殊知识,因此编译器不会自动生成代码以调用基本类型的Finalize方法。如果您想要这种行为,并且经常这样做,那么必须从类型的Finalize方法中显式调用基本类型的Finalize方法:

public class BaseObj { public BaseObj() { } protected override void Finalize() { Console.WriteLine("In Finalize."); base.Finalize(); // Call base type's Finalize } }

请注意,通常将基类型的Finalize方法称为派生类型的Finalize方法中的最后一条语句。这样可以使基础对象保持尽可能长的生命。由于调用基本类型的Finalize方法很常见,因此C#的语法简化了您的工作。在C#中,以下代码:

class MyObject { MyObject() { } }

终结内部

当应用程序创建新对象时,新运算符将从堆中分配内存。如果对象的类型包含Finalize方法,则将指向该对象的指针放在终结队列中。终结队列是由垃圾收集器控制的内部数据结构。队列中的每个条目都指向一个对象,在可以回收该对象的内存之前,应调用该对象的Finalize方法。

下图显示了包含多个对象的堆。从应用程序的根目录可以访问其中的某些对象,而某些则不能。创建对象C,E,F,I和J时,系统检测到这些对象具有Finalize方法,并将指向这些对象的指针添加到了终结队列中。

Memory9.gif

发生GC时,对象B,E,G,H,I和J被确定为垃圾。垃圾收集器扫描完成队列,以查找指向这些对象的指针。当找到一个指针时,该指针将从终结队列中删除,并附加到易碎队列(发音为“ F-reachable”)。易碎队列是由垃圾收集器控制的另一个内部数据结构。易碎队列中的每个指针都标识一个对象,该对象已准备好调用其Finalize方法。

收集之后,托管堆如下图所示。在这里,您看到对象B,G和H占用的内存已被回收,因为这些对象没有需要调用的Finalize方法。但是,无法回收对象E,I和J占用的内存,因为尚未调用它们的Finalize方法。

Memory10.gif

有一个专用的运行时线程专用于调用Finalize方法。当可访问队列为空时(通常是这种情况),该线程进入睡眠状态。但是,当出现条目时,该线程将唤醒,从队列中删除每个条目,并调用每个对象的Finalize方法。因此,您不应在Finalize方法中执行任何有关执行代码的线程的假设的代码。例如,避免在Finalize方法中访问线程本地存储。

终结队列与易碎队列的交互非常有趣。首先,让我告诉您易碎队列的名称。f很明显,代表定稿;易碎队列中的每个条目都应调用其Finalize方法。名称的“可到达”部分表示对象可到达。换句话说,易碎队列被视为根,就像全局变量和静态变量是根一样。因此,如果对象在易碎队列中,则该对象可访问且不是垃圾。

简而言之,当对象不可访问时,垃圾收集器将其视为对象垃圾。然后,当垃圾收集器将对象的条目从终结队列移到可访问队列时,该对象不再被视为垃圾,并且不回收其内存。至此,垃圾收集器已经完成了对垃圾的识别。某些标识为垃圾的对象已被重新分类为非垃圾。垃圾收集器压缩可回收内存,特殊的运行时线程清空易碎队列,执行每个对象的Finalize方法。

Memory11.gif

下次调用垃圾回收器时,它会看到最终对象是真正的垃圾,因为应用程序的根不指向该对象,并且易碎队列不再指向该对象。现在,只需回收该对象的内存即可。这里要了解的重要一点是,需要两个GC来回收需要终结处理的对象使用的内存。实际上,可能需要两个以上的集合,因为这些对象可以提升为较老的一代。上图显示了第二个GC之后托管堆的外观。

处置方法

使用此方法可以关闭或释放由实现此接口的类的实例持有的非托管资源,例如文件,流和句柄。按照惯例,此方法用于与释放对象拥有的资源或准备对象重用相关的所有任务。

在实现此方法时,对象必须设法通过在包含层次结构中传播调用来确保释放所有保留的资源。例如,如果对象A分配了对象B,而对象B分配了对象C,则A的Dispose实现必须调用B上的Dispose,后者又必须调用C上的Dispose。对象还必须调用其基类的Dispose方法。如果基类实现IDisposable。

如果多次调用对象的Dispose方法,则该对象必须忽略第一个调用之后的所有调用。如果多次调用其Dispose方法,则该对象不得引发异常。如果由于已释放资源并且以前未调用过Dispose而发生错误,则Dispose可能引发异常。

因为必须显式调用Dispose方法,所以实现IDisposable的对象还必须实现终结器,以在不调用Dispose时处理释放资源。默认情况下,垃圾回收器将在回收对象的内存之前自动调用其终结器。但是,一旦调用了Dispose方法,垃圾收集器通常就不需要调用已处理对象的终结器。为了防止自动完成,Dispose实现可以调用GC.SuppressFinalize方法。

通过System.GC直接控制

System.GC类型使您的应用程序可以直接控制垃圾收集器。您可以通过读取GC.MaxGeneration属性来查询托管堆支持的最大生成量。当前,GC.MaxGeneration属性始终返回2。

也可以通过调用此处显示的两个方法之一来强制垃圾收集器执行收集:

void GC.Collect(Int32 Generation) void GC.Collect()

第一种方法允许您指定要收集的世代。您可以将0范围内的任何整数传递给GC.MaxGeneration(含)。传递0导致生成0被收集;传递1导致收集第1代和第0代;传递2会导致生成2、1、0和0。不带参数的Collect方法的版本强制所有世代的完整集合,等效于调用:

GC.Collect(GC.MaxGeneration);

GC类型还提供了WaitForPendingFinalizers方法。此方法只是挂起调用线程,直到处理易碎队列的线程清空了队列,然后调用每个对象的Finalize方法。在大多数应用程序中,您不太可能需要调用此方法。

最后,垃圾收集器提供了两种方法,可让您确定对象当前处于哪个世代:

Int32 GetGeneration(Object obj)
Int32 GetGeneration(WeakReference wr)

GetGeneration的第一个版本将对象引用作为参数,而第二个版本将WeakReference引用作为参数。当然,返回的值将介于0到GC.MaxGeneration之间(含)。

了解.NET中的垃圾回收

Posted on 2020-04-05 | Edited on 2021-04-25 | In 技术

了解.NET中的垃圾回收

一旦了解了.NET的垃圾收集器是如何工作的,那么可能会触及.NET应用程序的一些更为神秘的问题的原因就会变得更加清楚。NET可能已承诺要结束显式内存管理,但在开发.NET应用程序时,仍然有必要分析内存的使用情况,以便避免与内存相关的错误和某些性能问题。

.NET的垃圾收集器已在Windows应用程序中作为显式内存管理和内存泄漏的结束而出售给我们:这个想法是,在后台运行垃圾收集器的情况下,开发人员不再需要担心管理它们创建的对象的生命周期–应用程序完成处理后,垃圾收集器将对其进行处理。

但是,实际情况要复杂得多。垃圾收集器无疑解决了非托管程序中最常见的泄漏-由开发人员在完成使用后忘记释放内存而引起的泄漏。它还解决了内存释放过早的相关问题,但是当垃圾收集器对开发人员对对象是否仍然处于“活动状态”并且能够进行开发时有不同的看法时,解决该问题的方式可能导致内存泄漏。要使用的。解决这些问题之前,您需要对收集器的工作方式有所了解。

垃圾收集器如何工作

那么,垃圾收集器如何实现其魔力?基本思想非常简单:它检查对象在内存中的布局方式,并通过遵循一系列引用来标识正在运行的程序可以“访问”的所有那些对象。

当垃圾回收开始时,它将查看一组称为“ GC根”的引用。这些是由于某种原因总是可以访问的内存位置,并且包含对程序创建的对象的引用。它将这些对象标记为“活动”,然后查看它们引用的所有对象。它也将这些标记为“实时”。它以这种方式继续,遍历它知道是“活动”的所有对象。它将它们引用的所有内容都标记为也被使用,直到找不到其他对象为止。

如果某个对象或其超类之一的字段包含另一个对象,则该对象由垃圾收集器标识为引用另一个对象。

一旦知道了所有这些活动对象,就可以丢弃所有剩余的对象,并将空间重新用于新对象。.NET压缩内存,以确保没有间隙(有效地压缩丢弃的对象不存在)–这意味着空闲内存始终位于堆的末尾,并可以非常快速地分配新对象。

GC根本身不是对象,而是对对象的引用。GC根引用的任何对象将自动在下一个垃圾回收中保留下来。.NET中有四种主要的根:

当前正在运行的方法中的局部变量被视为GC根。这些变量引用的对象始终可以通过声明它们的方法立即访问,因此必须保留它们。这些根的生命周期可以取决于程序的构建方式。在调试版本中,局部变量的持续时间与方法在堆栈上的时间一样长。在发行版本中,JIT能够查看程序结构以找出执行过程中该方法可以使用变量的最后一点,并在不再需要该变量时将其丢弃。这种策略并不总是使用,可以通过例如在调试器中运行程序来关闭。

静态变量也始终被视为GC根。声明它们的类可以随时访问它们引用的对象(如果是公共的,则可以访问程序的其余部分),因此.NET将始终保持它们不变。声明为“线程静态”的变量仅会在该线程运行时持续存在。

如果通过互操作将托管对象传递给非托管COM +库,则该对象也将成为具有引用计数的GC根。这是因为COM +不进行垃圾收集:它使用引用计数系统;通过将引用计数设置为0,一旦COM +库完成了该对象,它将不再是GC根目录,并且可以再次收集。

如果对象具有终结器,则在垃圾回收器确定该对象不再“处于活动状态”时,不会立即将其删除。相反,它成为一种特殊的根,直到.NET调用了finalizer方法。这意味着这些对象通常需要从内存中删除一个以上的垃圾回收,因为它们在第一次发现未使用时仍将生存。

对象图

总体而言,.NET中的内存形成了一个复杂的,打结的引用和交叉引用图。这可能使得很难确定特定对象使用的内存量。例如,List 对象使用的内存非常小,因为List 类只有几个字段。但是,其中之一是列表中的对象数组:如果列表中有许多条目,则这可能会很大。这几乎总是由列表“独占”,因此关系非常简单:列表的总大小是小的初始对象和它引用的大数组的大小。但是,数组中的对象可能完全是另一回事:很可能存在通过内存的其他路径来访问它们。在这种情况下,

当循环引用开始起作用时,事情变得更加混乱。

737-image001.jpg

在开发代码时,通常将内存视为组织为更容易理解的结构:从各个根开始的树:

737-image002.jpg

确实,以这种方式进行思考确实使(更确实可能)思考对象在内存中的布局方式。这也是编写程序或使用调试器时表示数据的方式,但这很容易忘记一个对象可以附加到多个根。这通常是.NET中内存泄漏的来源:开发人员忘记或从未意识到,一个对象锚定到多个根。考虑一下此处所示的情况:将GC root 2设置为null实际上不会允许垃圾收集器删除任何对象,这可以从查看完整图形中看到,而不能从树中看到。

内存剖析器可以从另一个角度查看图形,就像树根植于单个对象并向后跟随引用以将GC根放在叶子上一样。对于根2引用的ClassC对象,我们可以向后跟随引用以获取下图:

737-image003.jpg

通过这种方式的思考表明,ClassC对象具有两个最终的“所有者”,在垃圾收集器将其删除之前,这两个对象都必须放弃它。一旦将GC根目录2设置为null,就可以断开GC根目录3与该对象之间的任何链接,以便将其删除。

在实际的.NET应用程序中,这种情况很容易出现。最常见的是,数据对象被用户界面中的元素引用,但在数据处理完毕后不会被删除。这种情况并不是很泄漏:当用新数据更新UI控件时,将回收内存,但是这可能意味着应用程序使用的内存比预期的要多得多。事件处理程序是另一个常见原因:很容易忘记一个对象的寿命至少与它从中接收事件的对象一样长,对于某些全局事件处理程序(如Application类中的事件),这种情况永远存在。

实际的应用程序,尤其是那些具有用户界面组件的应用程序,具有比这复杂得多的图形。甚至可以从大量不同的地方引用对话框中的标签之类的简单内容…

737-image004.jpg

很容易看到偶然的物体如何在迷宫中丢失。

垃圾收集器的局限性

仍在引用的未使用对象

.NET中垃圾收集器的最大局限性是一个细微的限制:虽然它可以检测和删除未使用的对象,但实际上它会找到未引用的对象。这是一个重要的区别:程序可能永远不会再引用对象。但是,尽管有一些路径导致它可能仍被使用,但它永远不会从内存中释放出来。这导致内存泄漏;在.NET中,当将不再使用的对象保持引用状态时,会发生这些情况。

尽管内存使用率上升的症状很明显,但这些泄漏的来源可能很难发现。有必要确定哪些未使用的对象保留在内存中,然后跟踪引用以找出为什么不收集它们。内存分析器对于此任务至关重要:通过比较发生泄漏时的内存状态,可以找到麻烦的未使用对象,但是没有调试器可以向后跟踪对象引用。

垃圾收集器旨在处理大量资源,也就是说,释放对象的位置无关紧要。在现代系统上,内存属于这一类(何时回收内存无关紧要,只要及时完成以防止新分配失败)。仍然有一些资源不属于此类:例如,需要快速关闭文件句柄以避免引起应用程序之间的共享冲突。这些资源不能由垃圾收集器完全管理,因此.NET为管理这些资源的对象提供Dispose()方法以及using()构造。在这些情况下,对象的稀缺资源可通过实施Dispose 方法,但是紧要的内存要少得多,然后由垃圾回收器释放。

Dispose意味着.NET没有什么特别的,因此仍必须取消引用已处置的对象。这使已处置但尚未回收的对象成为内存泄漏源的良好候选对象。

堆的碎片

.NET中一个鲜为人知的限制是大对象堆的限制。成为该堆一部分的对象不会在运行时移动,这可能导致程序过早地耗尽内存。当某些对象的寿命比其他对象长时,这将导致堆在对象过去所在的位置形成孔-这称为碎片。当程序要求一个大的内存块,但堆变得非常分散,以至于没有单个内存区域足以容纳它时,就会发生问题。内存分析器可以估计程序可以分配的最大对象:如果该对象正在下降,则很可能是原因。一个OutOfMemoryException当程序显然具有大量可用内存时,通常会发生由碎片引起的错误–在32位系统上,进程应至少能够使用1.5Gb,但是由于碎片导致的故障通常会在使用该碎片之前开始发生很多内存。

碎片化的另一个征兆是.NET通常必须保留分配给应用程序的空洞所使用的内存。这显然导致它使用比在任务管理器中查看所需的内存更多的内存。这种效果通常相对来说是无害的:Windows非常擅长于意识到未被占用的孔所占用的内存并将其分页,并且如果碎片没有恶化,则程序将不会耗尽内存。但是,对于用户而言,这看起来并不好,他们可能会认为该应用程序浪费且“ blo肿”。当探查器显示程序分配的对象仅使用少量内存,而任务管理器显示该进程占用大量空间时,通常会发生这种情况。

垃圾收集器的性能

在性能方面,垃圾收集系统的最重要特征是垃圾收集器可以随时开始执行。这使它们不适用于定时至关重要的情况,因为任何操作的定时都可能被收集器的操作所抛弃。

.NET收集器有两种主要的操作模式:并发和同步(有时称为工作站和服务器)。默认情况下,并发垃圾收集用于桌面应用程序,同步用于服务器应用程序(例如ASP.NET)。

在并发模式下,.NET将尝试避免在进行收集时停止正在运行的程序。这意味着在给定的时间内应用程序可以完成的总次数较少,但应用程序不会暂停。这对交互式应用程序很有用,在交互应用程序中,给用户留下印象,即应用程序应立即做出响应,这一点很重要。

在同步模式下,.NET将在垃圾收集器运行时挂起正在运行的应用程序。实际上,这总体上比并发模式更有效–垃圾回收花费相同的时间,但是不必与程序继续运行进行竞争–但是,这意味着必须执行完整的回收时会有明显的暂停。 。

如果默认设置不合适,则可以在应用程序的配置文件中设置垃圾收集器的类型。当更重要的是应用程序具有高吞吐量而不是显示响应时,选择同步收集器可能很有用。

在大型应用程序中,垃圾收集器需要处理的对象数量会变得非常大,这意味着访问和重新排列所有对象都将花费很长时间。为了解决这个问题,.NET使用了“分代”垃圾收集器,该垃圾收集器试图将优先级赋予较小的一组对象。这个想法是,最近创建的对象更有可能被快速释放,因此,当试图释放内存时,分代垃圾收集器会优先处理它们,因此.NET首先查看自上一次垃圾收集以来已分配的对象,并且只会开始如果无法通过这种方式释放足够的空间,请考虑使用较旧的对象。

如果.NET可以自行选择收集时间,则此系统效果最佳,并且如果GC.Collect调用()会中断该系统,因为这通常会导致新对象过早地变旧,这增加了在不久的将来再次进行昂贵的完整收集的可能性。

具有终结器的类也会破坏垃圾收集器的平稳运行。这些类的对象不能立即删除:相反,它们进入终结器队列,并在运行终结器后从内存中删除。这意味着它们所引用的任何对象(以及那些对象所引用的任何对象,依此类推)至少也必须在此之前保留在内存中,并且在内存再次可用之前需要两次垃圾回收。如果该图包含带有终结器的许多对象,则这可能意味着垃圾收集器需要多次通过才能完全释放所有未引用的对象。

有一个避免此问题的简单方法:IDisposable在可终结类上实现,将完成对象所需的操作移到Dispose()方法中并GC.SuppressFinalize()在最后调用。然后可以修改终结器以调用该Dispose()方法。GC.SuppressFinalize()告诉垃圾回收器,该对象不再需要终结,可以立即被垃圾回收,这可以导致更快地回收内存。

结论

如果您花一些时间了解垃圾收集器的工作方式,则更容易理解应用程序中的内存和性能问题。它表明,尽管.NET减轻了内存管理的负担,但并不能完全消除跟踪和管理资源的需求。但是,使用内存分析器来诊断和修复.NET中的问题更加容易。考虑到.NET在开发中尽早管理内存的方式可以帮助减少问题,但是即使那样,由于框架或第三方库的复杂性,此类问题仍然可能出现。

1…345…9

溪源

81 posts
4 categories
4 tags
博客园 CSDN
© 2021 溪源
Powered by Hexo v3.9.0
|
Theme – NexT.Pisces v7.1.2
#
ICP备案号:湘ICP备19001531号-2