C#线程安全

如果程序或方法在面对任何多线程场景时没有不确定性,则它是线程安全的。线程安全主要通过锁定和减少线程交互的可能性来实现。

通用类型很少在整体上是线程安全的,原因如下:

·  全线程安全的开发负担可能很大,特别是如果一个类型有很多字段(每个字段都可能在任意多线程上下文中进行交互)。

·  线程安全可能会带来性能成本(部分是支付的,无论该类型是否实际被多个线程使用)。

·  线程安全类型并不一定会使使用它的程序成为线程安全的,而后者所涉及的工作常常使前者变得多余。.

因此,线程安全通常在需要的地方实现,以便处理特定的多线程场景。

然而,有几种方法可以“欺骗”并让大型复杂的类在多线程环境中安全运行。一种是通过将大段代码(甚至对整个对象的访问)包装在单个排他锁中来牺牲粒度,从而在高级别强制执行序列化访问。事实上,如果你想在多线程上下文中使用线程不安全的第三方代码(或大多数框架类型),这种策略是必不可少的。诀窍就是使用相同的排他锁来保护对线程不安全对象上所有属性、方法和字段的访问。如果对象的方法都执行得很快(否则会出现很多阻塞),该解决方案效果很好。

除了基本类型之外,很少有 .NET Framework 类型在实例化时对于并发只读访问之外的任何东西都是线程安全的。开发人员有责任叠加线程安全,通常使用排他锁。(其中的集合System.Collections.Concurrent 是一个例外。)

另一种作弊方法是通过最小化共享数据来最小化线程交互。这是一种极好的方法,隐含在“无状态”中间层应用程序和网页服务器中。由于多个客户端请求可以同时到达,它们调用的服务器方法必须是线程安全的。无状态设计(由于可扩展性的原因而流行)本质上限制了交互的可能性,因为类不会在请求之间保留数据。然后,线程交互仅限于人们可能选择创建的静态字段,用于在内存中缓存常用数据以及提供基础设施服务(如身份验证和审计)。

实现线程安全的最后一种方法是使用自动锁定机制。.NET Framework 正是这样做的,如果您ContextBoundObject and apply the Synchronization attribute将其子类化为该类。每当随后调用此类对象上的方法或属性时,都会自动获取对象范围的锁以用于方法或属性的整个执行。尽管这减少了线程安全负担,但它也产生了自己的问题:否则不会发生死锁、并发性不足和意外重入。由于这些原因,手动锁定通常是更好的选择——至少在不太简单的自动锁定机制可用之前是这样。

线程安全和 .NET Framework 类型

锁定可用于将线程不安全的代码转换为线程安全的代码。.NET Framework 是一个很好的应用:它的几乎所有非原始类型在实例化时都不是线程安全的(除了只读访问之外的任何东西),但是如果对任何给定的所有访问都可以在多线程代码中使用它们对象通过锁保护。这是一个示例,其中两个线程同时将一个项目添加到同一个List集合中,然后枚举列表:

class ThreadSafe
{
  static List <string> _list = new List <string>();
 
  static void Main()
  {
    new Thread (AddItem).Start();
    new Thread (AddItem).Start();
  }
 
  static void AddItem()
  {
    lock (_list) _list.Add ("Item " + _list.Count);
 
    string[] items;
    lock (_list) items = _list.ToArray();
    foreach (string s in items) Console.WriteLine (s);
  }
}

在这种情况下,我们锁定_list 对象本身。如果我们有两个相互关联的列表,我们将不得不选择一个共同的对象来锁定(我们可以指定一个列表,或者更好:使用一个独立的字段)。

枚举 .NET 集合也是线程不安全的,因为如果在枚举期间修改了列表,则会引发异常。在此示例中,我们首先将项目复制到数组中,而不是在枚举期间锁定。如果我们在枚举期间所做的事情可能很耗时,这可以避免过度持有锁。(另一种解决方案是使用读/写锁。)

锁定线程安全对象

有时您还需要锁定访问线程安全对象。为了说明,假设框架的List类确实是线程安全的,我们想向列表中添加一个项目:

if (!_list.Contains (newItem)) _list.Add (newItem);

不管这个列表是否是线程安全的,这个声明肯定不是!整个if语句必须被包裹在一个锁中,以防止在测试集装箱船和添加新项目之间抢占。然后,我们修改该列表的任何地方都需要使用相同的锁。例如,以下语句也需要包含在相同的锁中:

_list.Clear();

以确保它不会抢占之前的声明。换句话说,我们必须像使用我们的线程不安全集合类一样进行锁定(使List该类的假设线程安全变得多余)。

在高并发环境中锁定访问集合可能会导致过度阻塞。为此,Framework 4.0 提供了线程安全的队列、堆栈和字典。

静态成员

仅当所有并发线程都知道并使用该锁时,才能围绕自定义锁包装对对象的访问。如果对象的范围很广,则情况可能并非如此。最坏的情况是公共类型中的静态成员。例如,假设DateTime结构上的静态属性DateTime.Now, 不是线程安全的,并且两个并发调用可能导致输出乱码或异常。用外部锁定解决这个问题的唯一方法可能是锁定类型本身lock(typeof(DateTime))——在调用DateTime.Now. 这只有在所有程序员都同意这样做的情况下才有效(这不太可能)。此外,锁定类型会产生其自身的问题。

出于这个原因,DateTime结构上的静态成员已被仔细编程为线程安全的。这是整个 .NET Framework 中的常见模式:静态成员是线程安全的;实例成员不是。在为公共消费编写类型时遵循这种模式也很有意义,以免造成不可能的线程安全难题。换句话说,通过使静态方法线程安全,您正在编程以便不排除该类型使用者的线程安全性。

静态方法中的线程安全是必须明确编码的:它不会因为方法是静态的而自动发生!

只读线程安全

为并发只读访问(如果可能)使类型线程安全是有利的,因为这意味着消费者可以避免过度锁定。许多 .NET Framework 类型都遵循这一原则:例如,集合对于并发读者来说是线程安全的。

自己遵循这个原则很简单:如果您将一个类型记录为对并发只读访问是线程安全的,则不要写入消费者期望只读的方法中的字段(或锁定这样做)。例如,在实现ToArray() 集合中的方法时,您可以从压缩集合的内部结构开始。但是,对于期望这是只读的消费者来说,这将使其成为线程不安全的。

只读线程安全是枚举器与“可枚举”分开的原因之一:两个线程可以同时枚举一个集合,因为每个线程都有一个单独的枚举器对象。

应用服务器中的线程安全

应用程序服务器需要多线程来处理同时的客户端请求。WCF、http://ASP.NET 和 Web 服务应用程序是隐式多线程的;对于使用 TCP 或 HTTP 等网络通道的远程处理服务器应用程序也是如此。这意味着在服务器端编写代码时,如果处理客户端请求的线程之间存在交互的可能性,则必须考虑线程安全。幸运的是,这种可能性很少;典型的服务器类要么是无状态的(无字段),要么具有为每个客户端或每个请求创建单独的对象实例的激活模型。交互通常仅通过静态字段产生,有时用于缓存在数据库的内存部分以提高性能。

例如,假设您有一个RetrieveUser 查询数据库的方法:

internal User RetrieveUser (int id) { ... }

如果此方法被频繁调用,您可以通过将结果缓存在静态文件中来提高性能Dictionary。这是一个考虑线程安全的解决方案:

static class UserCache
{
  static Dictionary <int, User> _users = new Dictionary <int, User>();
 
  internal static User GetUser (int id)
  {
    User u = null;
 
    lock (_users)
      if (_users.TryGetValue (id, out u))
        return u;
 
    u = RetrieveUser (id);   // Method to retrieve user from database
    lock (_users) _users [id] = u;
    return u;
  }

客户端应用程序和线程关联

WPF和Windows窗体库都遵循基于线程关联的模型。尽管每个都有单独的实现,但它们的功能都非常相似。

组成富客户端的对象主要基于DependencyObjectWPF 或ControlWindows 窗体。这些对象具有线程亲和性,这意味着只有实例化它们的线程才能随后访问它们的成员。违反此规则会导致不可预测的行为或引发异常。

从积极的方面来说,这意味着您无需锁定访问 UI 对象。不利的一面是,如果要调用在另一个线程 Y 上创建的对象 X 上的成员,则必须将请求编组到线程 Y。您可以显式执行此操作,如下所示:

·  在 WPF 中,调用Invoke或BeginInvoke在元素的Dispatcher 对象上。

·  在 Windows 窗体中,调用Invoke或BeginInvoke在控件上。

Invoke并且BeginInvoke 都接受一个委托,该委托引用您要运行的目标控件上的方法。同步Invoke工作:调用者阻塞直到编组完成。异步工作:调用者立即返回并且封送处理的请求排队(使用处理键盘、鼠标和计时器事件的相同消息队列)。BeginInvoke

假设我们有一个包含名为 的文本框的窗口,txtMessage我们希望工作线程更新其内容,这是 WPF 的示例:

public partial class MyWindow : Window{  public MyWindow()  {  

public partial class MyWindow : Window
{
  public MyWindow()
  {
    InitializeComponent();
    new Thread (Work).Start();
  }
 
  void Work()
  {
    Thread.Sleep (5000);           // Simulate time-consuming task
    UpdateMessage ("The answer");
  }
 
  void UpdateMessage (string message)
  {
    Action action = () => txtMessage.Text = message;
    Dispatcher.Invoke (action);
  }
}