大家好。在“ C#开发者”课程开始的前夕,我们准备了另一本有用的材料的翻译。享受阅读。
自从我最近列出了Criteo的C#最佳实践列表以来,我认为最好将其公开共享。本文的目的是提供一个不完整的代码模板列表,应避免使用这些代码模板,这可能是因为它们有问题,或者是因为它们工作不佳。该列表似乎有点随机,因为它是脱离上下文的,但是它的所有元素有时都在我们的代码中找到,并在生产中引起了问题。我希望这将是一个很好的预防方法,并防止您将来犯错。还要注意,Criteo Web服务依赖于高性能代码,因此需要避免效率低下的代码。在大多数应用程序中,替换其中一些模板不会有明显的明显差异。最后但并非最不重要的一点ConfigureAwait
是,许多文章已经讨论了一些要点(例如),因此我将不对其进行详细介绍。目的是形成您需要注意的要点的紧凑列表,而不是对每个要点进行详细的技术计算。同步等待异步代码
永远不要期望同步完成的任务。这适用于但不限于:Task.Wait, Task.Result, Task.GetAwaiter().GetResult(), Task.WaitAny, Task.WaitAll
。概括地说:两个池线程之间的任何同步关系都可能导致池耗尽。本文描述了这种现象的原因。配置等待
如果可以从同步上下文中调用您的代码,则将其ConfigureAwait(false)
用于每个await调用。请注意,这ConfigureAwait
仅在使用关键字时有用await
。例如,以下代码是没有意义的:
var result = ProcessAsync().ConfigureAwait(false).GetAwaiter().GetResult();
异步无效
永远不要使用async void
。在async void
方法中引发的异常传播到同步上下文,通常会导致整个应用程序崩溃。如果您无法在方法中返回任务(例如,因为您正在实现接口),则将异步代码移至另一个方法并调用它:interface IInterface
{
void DoSomething();
}
class Implementation : IInterface
{
public void DoSomething()
{
_ = DoSomethingAsync();
}
private async Task DoSomethingAsync()
{
await Task.Delay(100);
}
}
尽可能避免异步
出于习惯或由于肌肉记忆,您可以编写以下内容:public async Task CallAsync()
{
var client = new Client();
return await client.GetAsync();
}
尽管代码在语义上是正确的,但是async
此处不需要使用关键字,并且在高负载的环境中可能会导致大量开销。尽量避免这种情况:public Task CallAsync()
{
var client = new Client();
return _client.GetAsync();
}
但是,请记住,当代码包装在块中时(例如 try/catch
或using
),您将无法求助于此优化:public async Task Correct()
{
using (var client = new Client())
{
return await client.GetAsync();
}
}
public Task Incorrect()
{
using (var client = new Client())
{
return client.GetAsync();
}
}
在错误的版本(Incorrect()
)中,客户端可能会在GetAsync
调用完成之前被删除,因为using块中的任务不会等待。区域比较
如果您没有理由使用区域比较,请始终使用顺序比较。尽管由于内部优化,这对于en-US数据表示形式并没有多大影响,但对于其他区域的表示形式而言,此比较要慢一个数量级(在Linux上,最多两个数量级!)。由于字符串比较是大多数应用程序中的常见操作,因此开销会大大增加。并发<
袋>
切勿在ConcurrentBag<
T>
没有基准测试的情况下使用。此集合专为非常特定的用例而设计(大多数情况下,排队的线程将其从队列中排除),如果将其用于其他目的,则会遇到严重的性能问题。如果你需要一个线程安全的集合,喜欢ConcurrentQueue <
牛逼>
。ReaderWriterLock / ReaderWriterLockSlim <
T >
切勿在没有基准测试的情况下使用。ReaderWriterLock<T>
/ReaderWriterLockSlim<T>
尽管在与读者和作家合作时使用这种专门的同步原语可能很诱人,但它的成本比简单Monitor
(与关键字一起使用lock
)要高得多。如果同时执行关键部分的读取器的数量不是很大,那么并发将不足以吸收增加的开销,并且代码将变得更糟。
首选Lambda函数而不是方法组
考虑以下代码:public IEnumerable<int> GetItems()
{
return _list.Where(i => Filter(i));
}
private static bool Filter(int element)
{
return i % 2 == 0;
}
Resharper建议在不使用lambda函数的情况下重写代码,这看起来会更简洁一些:public IEnumerable<int> GetItems()
{
return _list.Where(Filter);
}
private static bool Filter(int element)
{
return i % 2 == 0;
}
不幸的是,这导致为每个调用分配动态内存。实际上,该调用编译为:public IEnumerable<int> GetItems()
{
return _list.Where(new Predicate<int>(Filter));
}
private static bool Filter(int element)
{
return i % 2 == 0;
}
如果在重载部分中调用代码,则可能会对性能产生重大影响。使用lambda函数可启动编译器优化,该优化可将委托缓存在静态字段中,从而避免分配。这仅在Filter
静态时有效。如果没有,您可以自己缓存该委托:private Predicate<int> _filter;
public Constructor()
{
_filter = new Predicate<int>(Filter);
}
public IEnumerable<int> GetItems()
{
return _list.Where(_filter);
}
private bool Filter(int element)
{
return i % 2 == 0;
}
将枚举转换为字符串
调用Enum.ToString
中.net
是相当昂贵的,因为反射被用于内部转换,并呼吁结构所引发包装虚拟方法。应尽可能避免这种情况。枚举通常可以用常量字符串替换:
public enum Numbers
{
One,
Two,
Three
}
public static class Numbers
{
public const string One = "One";
public const string Two = "Two";
public const string Three = "Three";
}
如果您确实需要使用枚举,请考虑将转换后的值缓存在字典中以分摊开销。枚举比较
注意:在.net core中,这不再相关,因为从2.1版开始,优化是由JIT自动执行的。
当使用枚举作为标志时,可能很想使用以下方法Enum.HasFlag
:[Flags]
public enum Options
{
Option1 = 1,
Option2 = 2,
Option3 = 4
}
private Options _option;
public bool IsOption2Enabled()
{
return _option.HasFlag(Options.Option2);
}
此代码激发了两个带有分配的包:一个用于转换Options.Option2
为Enum
,另一个用于虚拟调用HasFlag
该结构。这使得此代码不成比例地昂贵。相反,您应该牺牲可读性并使用二进制运算符:public bool IsOption2Enabled()
{
return (_option & Options.Option2) == Options.Option2;
}
结构比较方法的实现
在比较中使用结构时(例如,用作字典的键时),您需要覆盖方法Equals/GetHashCode
。默认实现使用反射,并且非常慢。Resharper生成的实现通常非常好。您可以在此处了解更多信息:devblogs.microsoft.com/premier-developer/performance-implications-of-default-struct-equality-in-c使用带接口的结构时,避免包装不当
考虑以下代码:public class IntValue : IValue
{
}
public void DoStuff()
{
var value = new IntValue();
LogValue(value);
SendValue(value);
}
public void SendValue(IValue value)
{
}
public void LogValue(IValue value)
{
}
使IntValue
结构化很容易避免分配动态内存。但是由于AddValue
和SendValue
接口是期望的,并且接口具有引用语义,因此每次调用都会打包该值,从而抵消了这种“优化”的好处。实际上,IntValue
与类相比,将有更多的内存分配,因为该值将为每个调用独立打包。如果您正在编写API并期望某些值成为结构,请尝试使用通用方法:public struct IntValue : IValue
{
}
public void DoStuff()
{
var value = new IntValue();
LogValue(value);
SendValue(value);
}
public void SendValue<T>(T value) where T : IValue
{
}
public void LogValue<T>(T value) where T : IValue
{
}
尽管乍看之下将这些方法转换为通用方法似乎毫无用处,但实际上它使您避免在IntValue
结构化的情况下进行分配打包。CancellationToken订阅始终为内联
当您取消时CancellationTokenSource
,所有订阅都将在当前线程内执行。这可能会导致计划外的暂停甚至隐式的死锁。var cts = new CancellationTokenSource();
cts.Token.Register(() => Thread.Sleep(5000));
cts.Cancel();
您无法逃避这种行为。因此,在取消时CancellationTokenSource
,请问自己是否可以安全地捕获当前线程。如果答案是否定的,包裹调用Cancel
内部Task.Run
的线程池来执行它。TaskCompletionSource延续通常是内联的
像订阅一样CancellationToken
,延续TaskCompletionSource
通常是内联的。这是一个很好的优化,但是会导致隐式错误。例如,考虑以下程序:class Program
{
private static ManualResetEventSlim _mutex = new ManualResetEventSlim();
public static async Task Deadlock()
{
await ProcessAsync();
_mutex.Wait();
}
private static Task ProcessAsync()
{
var tcs = new TaskCompletionSource<bool>();
Task.Run(() =>
{
Thread.Sleep(2000);
tcs.SetResult(true);
_mutex.Set();
});
return tcs.Task;
}
static void Main(string[] args)
{
Deadlock().Wait();
Console.WriteLine("Will never get there");
}
}
该调用tcs.SetResult
导致继续await ProcessAsync()
在当前线程中执行。因此,该语句_mutex.Wait()
由应调用的同一线程执行_mutex.Set()
,从而导致死锁。这可以通过传递参数TaskCreationsOptions.RunContinuationsAsynchronously
c 来避免TaskCompletionSource
。如果您没有充分的理由忽略它,请TaskCreationsOptions.RunContinuationsAsynchronously
在创建时始终使用该选项TaskCompletionSource
。请注意:如果您TaskContinuationOptions.RunContinuationsAsynchronously
改用TaskCreationOptions.RunContinuationsAsynchronously
,代码也将编译,但是参数将被忽略,并且继续将继续内联。这是一个令人惊讶的常见错误,因为它TaskContinuationOptions
先于TaskCreationOptions
自动完成功能。Task.Run / Task.Factory.StartNew
如果没有理由使用Task.Factory.StartNew
,请始终选择Task.Run
运行后台任务。Task.Run
使用更安全的默认值,更重要的是,它会自动解包返回的任务,这可以防止异步方法产生明显的错误。考虑以下程序:class Program
{
public static async Task ProcessAsync()
{
await Task.Delay(2000);
Console.WriteLine("Processing done");
}
static async Task Main(string[] args)
{
await Task.Factory.StartNew(ProcessAsync);
Console.WriteLine("End of program");
Console.ReadLine();
}
}
尽管出现了外观,程序结束将在处理完成之前显示。这是因为它将Task.Factory.StartNew
返回Task<Task>
,并且代码仅需要外部任务。正确的代码可以是await Task.Factory.StartNew(ProcessAsync).Unwrap()
或await Task.Run(ProcessAsync)
。只有三个有效的用例Task.Factory.StartNew
:- 在另一个调度程序中运行任务。
- 在专用线程中执行任务(使用
TaskCreationOptions.LongRunning
)。 - (
TaskCreationOptions.PreferFairness
).
.