当前位置: 首页 > news >正文

网站单页设计购物类网站

网站单页设计,购物类网站,企业网站 源码,企业所得税多少钱起征C#中的多线程 - 同步基础 C#中的多线程 - 同步基础 1同步概要在第 1 部分#xff1a;基础知识中#xff0c;我们描述了如何在线程上启动任务、配置线程以及双向传递数据。同时也说明了局部变量对于线程来说是私有的#xff0c;以及引用是如何在线程之间共享#xff0c;允许… C#中的多线程 - 同步基础 C#中的多线程 - 同步基础 1同步概要 在第 1 部分基础知识中我们描述了如何在线程上启动任务、配置线程以及双向传递数据。同时也说明了局部变量对于线程来说是私有的以及引用是如何在线程之间共享允许其通过公共字段进行通信。 下一步是同步synchronization为期望的结果协调线程的行为。当多个线程访问同一个数据时同步尤其重要但是这是一件非常容易搞砸的事情。 同步构造可以分为以下四类 简单的阻塞方法这些方法会使当前线程等待另一个线程结束或是自己等待一段时间。Sleep、Join与Task.Wait都是简单的阻塞方法。锁构造锁构造能够限制每次可以执行某些动作或是执行某段代码的线程数量。排它锁构造是最常见的它每次只允许一个线程执行从而可以使得参与竞争的线程在访问公共数据时不会彼此干扰。标准的排它锁构造是lockMonitor.Enter/Monitor.Exit、Mutex与 SpinLock。非排它锁构造是Semaphore、SemaphoreSlim以及读写锁。信号构造信号构造可以使一个线程暂停直到接收到另一个线程的通知避免了低效的轮询 。有两种经常使用的信号设施事件等待句柄event wait handle 和Monitor类的Wait / Pluse方法。Framework 4.0 加入了CountdownEvent与Barrier类。非阻塞同步构造非阻塞同步构造通过调用处理器指令来保护对公共字段的访问。CLR 与 C# 提供了下列非阻塞构造Thread.MemoryBarrier 、Thread.VolatileRead、Thread.VolatileWrite、volatile关键字以及Interlocked类。阻塞这个概念对于前三类来说都非常重要接下来我们简要的剖析下它。 1.1阻塞 当线程的执行由于某些原因被暂停比如调用Sleep等待一段时间或者通过Join或EndInvoke方法等待其它线程结束时则认为此线程被阻塞blocked。被阻塞的线程会立即出让yields其处理器时间片之后不再消耗处理器时间直到阻塞条件被满足。可以通过线程的ThreadState属性来检查一个线程是否被阻塞 bool blocked  (someThread.ThreadState  ThreadState.WaitSleepJoin) ! 0; 上面例子中线程状态可能在进行状态判断和依据状态进行操作之间发生改变因此这段代码仅可用于调试诊断的场景。 当一个线程被阻塞或是解除阻塞时操作系统会进行上下文切换context switch这会带来几微秒的额外时间开销。 阻塞会在以下 4 种情况下解除电源按钮可不能算╮(╯▽╰)╭ 阻塞条件被满足操作超时如果指定了超时时间通过Thread.Interrupt中断通过Thread.Abort中止通过Suspend方法已过时不应该再使用暂停线程的执行不被认为是阻塞。 1.2阻塞 vs 自旋 有时线程必须暂停直到特定条件被满足。信号构造和锁构造可以通过在条件被满足前阻塞线程来实现。但是还有一种更为简单的方法线程可以通过自旋spinning来等待条件被满足。例如 while (!proceed); 一般来说这会非常浪费处理器时间因为对 CLR 和操作系统来说这个线程正在执行重要的计算就给它分配了相应的资源。 有时会组合使用阻塞与自旋 while (!proceed)  Thread.Sleep (10); 尽管并不优雅但是这比仅使用自旋更高效一般来说。然而这样也可能会出现问题这是由proceed标识上的并发问题引起的。正确的使用和锁构造和信号构造可以避免这个问题。 自旋在等待的条件很快大致几微秒就能被满足的情况下更高效因为它避免了上下文切换带来的额外开销。.NET Framework 提供了专门的方法和类型来辅助实现自旋在第 5 部分会讲到。 1.3线程状态 可以通过线程的ThreadState属性来查询线程状态它会返回一个ThreadState类型的按位方式组合的枚举值其中包含了三“层”信息。然而大多数值都是冗余的、无用的或者过时不建议使用的。下图是其中一“层”信息 下面的代码可以提取线程状态中最有用的 4 个值: Unstarted、Running、WaitSleepJoin和Stopped public static ThreadState SimpleThreadState (ThreadState ts){   return ts  (ThreadState.Unstarted |                ThreadState.WaitSleepJoin |                ThreadState.Stopped);} ThreadState属性在进行调试诊断时有用但不适合用来进行同步因为线程状态可能在判断状态和依据状态进行操作之间发生改变。 2锁 排它锁用于确保同一时间只允许一个线程执行指定的代码段。主要的两个排它锁构造是lock和Mutex互斥体。其中lock更快使用也更方便。而Mutex的优势是它可以跨进程的使用。 在这一节里我们从介绍lock构造开始然后介绍Mutex和信号量semaphore用于非排它场景。稍后在第 4 部分会介绍读写锁reader / writer lock。 Framework 4.0 加入了SpinLock结构体可以用于高并发场景。 让我们从下边这个类开始 class ThreadUnsafe{   static int _val1  1, _val2  1;   static void Go()   {     if (_val2 ! 0) Console.WriteLine (_val1 / _val2);     _val2  0;   }} 这个类不是线程安全的如果Go方法同时被两个线程调用可能会产生除数为零错误因为可能在一个线程刚好执行完if的判断语句但还没执行Console.WriteLine语句时_val2就被另一个线程设置为零。 下边使用lock解决这个问题 class ThreadSafe{   static readonly object _locker  new object();   static int _val1, _val2;   static void Go()   {     lock (_locker)     {       if (_val2 ! 0) Console.WriteLine (_val1 / _val2);       _val2  0;     }   }} 同一时间只有一个线程可以锁定同步对象这里指_locker并且其它竞争锁的线程会被阻塞直到锁被释放。如果有多个线程在竞争锁它们会在一个“就绪队列ready queue”中排队并且遵循先到先得的规则需要说明的是Windows 系统和 CLR 的差别可能导致这个队列在有时会不遵循这个规则。因为一个线程的访问不能与另一个线程相重叠排它锁有时也被这样描述它强制对锁保护的内容进行顺序serialized访问。在这个例子中我们保护的是Go方法的内部逻辑还有_val1与_val2字段。 在竞争锁时被阻塞的线程它的线程状态是WaitSleepJoin。在中断与中止中我们会描述如何通过其它线程强制释放被阻塞的线程这是一种可以用于结束线程的重型技术译者注这里指它们应该被作为在没有其它更为优雅的办法时的最后手段。 锁构造比较Permalink 构造 用途 跨进程 开销* lock Monitor.Enter/Monitor.Exit 确保同一时间只有一个线程可以访问资源或代码 - 20ns Mutex   1000ns SemaphoreSlim Framework 4.0 中加入 确保只有不超过指定数量的线程可以并发访问资源或代码 - 200ns Semaphore   1000ns ReaderWriterLockSlim Framework 3.5 中加入 允许多个读线程和一个写线程共存 - 40ns ReaderWriterLock 已过时 - 100ns * 时间代表在同一线程上一次进行加锁和释放锁假设没有阻塞的开销在 Intel Core i7 860 上测得。 2.1Monitor.Enter 与 Monitor.Exit C# 的lock语句是一个语法糖它其实就是使用了try / finally来调用Monitor.Enter与Monitor.Exit方法。下面是在之前示例中的Go方法内部所发生的事情简化的版本 Monitor.Enter (_locker);try{   if (_val2 ! 0) Console.WriteLine (_val1 / _val2);   _val2  0;}finally { Monitor.Exit (_locker); } 如果在同一个对象上没有先调用Monitor.Enter就调用Monitor.Exit会抛出一个异常。 lockTaken 重载 刚刚所描述的就是 C# 1.0、2.0 和 3.0 的编译器翻译lock语句产生的代码。 然而它有一个潜在的缺陷。考虑这样的情况在Monitor.Enter的实现内部或者在Monitor.Enter与try中间有异常被抛出可能是因为在线程上调用了Abort或者有OutOfMemoryException异常被抛出这时不一定能够获得锁。如果获得了锁那么该锁就不会被释放因为不可能执行到try / finally内这会导致锁泄漏。 为了避免这种危险CLR 4.0 的设计者为Monitor.Enter添加了下面的重载 public static void Enter (object obj, ref bool lockTaken); 当且仅当Enter方法抛出异常锁没有能够获得时lockTaken为false。 下边是正确的使用方式这就是 C# 4.0 对于lock语句的翻译 bool lockTaken  false;try{   Monitor.Enter (_locker, ref lockTaken);   // 你的代码...}finally { if (lockTaken) Monitor.Exit (_locker); } TryEnter Monitor还提供了一个TryEnter方法允许以毫秒或是TimeSpan方式指定超时时间。如果获得了锁该方法会返回true而如果由于超时没有获得锁则会返回false。TryEnter也可以以无参数的形式进行调用这是对锁进行“测试”如果不能立即获得锁就会立即返回false。 类似于Enter方法该方法在 CLR 4.0 中也被重载来接受lockTaken参数。 2.2选择同步对象 对所有参与同步的线程可见的任何对象都可以被当作同步对象使用但有一个硬性规定同步对象必须为引用类型。同步对象一般是私有的因为这有助于封装锁逻辑并且一般是一个实例或静态字段。同步对象也可以就是其要保护的对象如下面例子中的_list字段 class ThreadSafe{   List string _list  new List string();   void Test()   {     lock (_list)     {       _list.Add (Item 1);       // ... 一个只被用来加锁的字段例如前面例子中的_locker可以精确控制锁的作用域与粒度。对象自己this甚至是其类型都可以被当作同步对象来使用 lock (this) { ... }// 或者lock (typeof (Widget)) { ... }    // 保护对静态资源的访问 这种方式的缺点在于并没有对锁逻辑进行封装从而很难避免死锁与过多的阻塞。同时类型上的锁也可能会跨越应用程序域application domain边界在同一进程内。 你也可以在被 lambda 表达式或匿名方法所捕获的局部变量上加锁。 锁在任何情况下都不会限制对同步对象本身的访问。换句话说x.ToString()不会因为其它线程调用lock(x)而阻塞两个线程都要调用lock(x)才能使阻塞发生。 2.3何时加锁 简单的原则是需要在访问任意可写的共享字段any writable shared field时加锁。即使是最简单的操作例如对一个字段的赋值操作都必须考虑同步。在下面的类中Increment与Assign方法都不是线程安全的 class ThreadUnsafe{   static int _x;   static void Increment() { _x; }   static void Assign()    { _x  123; }} 以下是线程安全的版本 class ThreadSafe{   static readonly object _locker  new object();   static int _x;   static void Increment() { lock (_locker) _x; }   static void Assign()    { lock (_locker) _x  123; }} 在非阻塞同步nonblocking synchronization中我们会解释这种需求是如何产生的以及在这些场景下内存屏障memory barrier内存栅栏内存栅障和Interlocked类如何提供替代方法进行锁定。 2.4锁与原子性 如果一组变量总是在相同的锁内进行读写就可以称为原子的atomically读写。假定字段x与y总是在对locker对象的lock内进行读取与赋值 lock (locker) { if (x ! 0) y / x; } 可以说x和y是被原子的访问的因为上面的代码块无法被其它的线程分割或抢占。如果被其它线程分割或抢占x和y就可能被别的线程修改导致计算结果无效。而现在 x和y总是在相同的排它锁中进行访问因此不会出现除数为零的错误。 在lock锁内抛出异常将打破锁的原子性考虑如下代码 decimal _savingsBalance, _checkBalance;void Transfer (decimal amount){   lock (_locker)   {     _savingsBalance  amount;     _checkBalance - amount  GetBankFee();   }} 如果GetBankFee()方法内抛出异常银行可能就要损失钱财了。在这个例子中我们可以通过更早的调用GetBankFee()来避免这个问题。对于更复杂情况解决方案是在catch或finally中实现“回滚rollback”逻辑。 指令原子性是一个相似但不同的概念 如果一条指令可以在 CPU 上不可分割地执行那么它就是原子的。见非阻塞同步 2.5嵌套锁 线程可以用嵌套重入的方式重对相同的对象进行加锁 lock (locker)   lock (locker)     lock (locker)     {        // ...    } 或者 Monitor.Enter (locker);  Monitor.Enter (locker);  Monitor.Enter (locker); Monitor.Exit (locker); Monitor.Exit (locker); Monitor.Exit (locker); 在这样的场景中只有当最外层的lock语句退出或是执行了匹配数目的Monitor.Exit语句时对象才会被解锁。 嵌套锁可以用于在锁中调用另一个方法也使用了同一对象来锁定 static readonly object _locker  new object(); static void Main(){   lock (_locker)   {      AnotherMethod();      //  这里依然拥有锁因为锁是可重入的  }}static void AnotherMethod(){ lock (_locker) { Console.WriteLine (Another method); }} 线程只会在第一个最外层lock处阻塞。 2.6死锁 当两个线程等待的资源都被对方占用时它们都无法执行这就产生了死锁。演示死锁最简单的方法就是使用两个锁 object locker1  new object();object locker2  new object();new Thread (()  {                     lock (locker1)                     {                       Thread.Sleep (1000);                       lock (locker2);      // 死锁                    }                   }).Start();lock (locker2){   Thread.Sleep (1000);   lock (locker1);                          // 死锁} 更复杂的死锁链可能由三个或更多的线程创建。 在标准环境下CLR 不会像SQL Server一样自动检测和解决死锁。除非你指定了锁定的超时时间否则死锁会造成参与的线程无限阻塞。在SQL CLR 集成宿主环境中死锁能够被自动检测并在其中一个线程上抛出可捕获的异常。 死锁是多线程中最难解决的问题之一尤其是在有很多关联对象的时候。这个困难在根本上在于无法确定调用方caller已经拥有了哪些锁。 你可能会锁定类x中的私有字段a而并不知道调用方或者调用方的调用方已经锁住了类y中的字段b。同时另一个线程正在执行顺序相反的操作这样就创建了死锁。讽刺的是这个问题会由于良好的面向对象的设计模式而加剧因为这类模式建立的调用链直到运行时才能确定。 流行的建议“以一致的顺序对对象加锁以避免死锁”尽管它对于我们最初的例子有帮助但是很难应用到刚才所描述的场景。更好的策略是如果发现在锁区域中的对其它类的方法调用最终会引用回当前对象就应该小心同时考虑是否真的需要对其它类的方法调用加锁往往是需要的但是有时也会有其它选择。更多的依靠声明方式declarative与数据并行data parallelism、不可变类型immutable types与非阻塞同步构造 nonblocking synchronization constructs可以减少对锁的需要。 有另一种思路来帮助理解这个问题当你在拥有锁的情况下访问其它类的代码对于锁的封装就存在潜在的泄露。这不是 CLR 或 .NET Framework 的问题而是因为锁本身的局限性。锁的问题在许多研究项目中被分析包括软件事务内存Software Transactional Memory。 另一个死锁的场景是如果已拥有一个锁在调用Dispatcher.Invoke在 WPF 程序中或是Control.Invoke在 Windows Forms 程序中时如果 UI 恰好要运行等待同一个锁的另一个方法就会在这里发生死锁。这通常可以通过调用BeginInvoke而不是Invoke来简单的修复。或者可以在调用Invoke之前释放锁但是如果是调用方获得的锁那么这种方法可能并不会起作用。我们在富客户端应用与线程亲和中来解释Invoke和BeginInvoke。 2.7性能 锁是非常快的在一个 2010 时代的计算机上没有竞争的情况下获取并释放锁一般只需 20 纳秒。如果存在竞争产生的上下文切换会把开销增加到微秒的级别并且线程被重新调度前可能还会等待更久的时间。如果需要锁定的时间很短那么可以使用自旋锁SpinLock来避免上下文切换的开销。 如果获取锁后保持的时间太长而不释放就会降低并发度同时也会加大死锁的风险。 2.8互斥体Mutex 互斥体类似于 C# 的lock不同在于它是可以跨越多个进程工作。换句话说Mutex可以是机器范围computer-wide的也可以是程序范围application-wide的。 没有竞争的情况下获取并释放Mutex需要几微秒的时间大约比lock慢 50 倍。 使用Mutex类时可以调用WaitOne方法来加锁调用ReleaseMutex方法来解锁。关闭或销毁Mutex会自动释放锁。与lock语句一样Mutex只能被获得该锁的线程释放。 跨进程Mutex的一种常见的应用就是确保只运行一个程序实例。下面演示了这是如何实现的 class OneAtATimePlease{   static void Main()   {     // 命名的 Mutex 是机器范围的它的名称需要是唯一的    // 比如使用公司名程序名或者也可以用 URL    using (var mutex  new Mutex (false, oreilly.com OneAtATimeDemo))     {       // 可能其它程序实例正在关闭所以可以等待几秒来让其它实例完成关闭       if (!mutex.WaitOne (TimeSpan.FromSeconds (3), false))       {         Console.WriteLine (Another app instance is running. Bye!);         return;       }       RunProgram();     }   }   static void RunProgram()   {     Console.WriteLine (Running. Press Enter to exit);     Console.ReadLine();   }} 如果在终端服务Terminal Services下运行机器范围的Mutex默认仅对于运行在相同终端服务器会话的应用程序可见。要使其对所有终端服务器会话可见需要在其名字前加上Global\。 2.9信号量Semaphore 信号量类似于一个夜总会它具有一定的容量并且有保安把守。一旦满员就不允许其他人进入这些人将在外面排队。当有一个人离开时排在最前头的人便可以进入。这种构造最少需要两个参数夜总会中当前的空位数以及夜总会的总容量。 容量为 1 的信号量与Mutex和lock类似所不同的是信号量没有“所有者”它是线程无关thread-agnostic的。任何线程都可以在调用Semaphore上的Release方法而对于Mutex和lock只有获得锁的线程才可以释放。 SemaphoreSlim是 Framework 4.0 加入的轻量级的信号量功能与Semaphore相似不同之处是它对于并行编程的低延迟需求做了优化。在传统的多线程方式中也有用因为它支持在等待时指定取消标记 cancellation token。但它不能跨进程使用。 在Semaphore上调用WaitOne或Release会产生大概 1 微秒的开销而SemaphoreSlim产生的开销约是其四分之一。 信号量在有限并发的需求中有用它可以阻止过多的线程同时执行特定的代码段。在下面的例子中五个线程尝试进入一个只允许三个线程进入的夜总会 class TheClub{   static SemaphoreSlim _sem  new SemaphoreSlim (3);    // 容量为 3   static void Main()   {     for (int i  1; i  5; i) new Thread (Enter).Start (i);   }   static void Enter (object id)   {      _sem.Wait();// 同时只能有   Thread.Sleep (1000 * (int) id);               // 3个线程   _sem.Release();   }} 如果Sleep语句被替换为密集的磁盘 I/O 操作由于Semaphore限制了过多的并发硬盘活动就可能改善整体性能。 类似于Mutex命名的Semaphore也可以跨进程使用。 3线程安全 说一个程序或方法是线程安全 thread-safe的是指它在任意的多线程场景中都不存在不确定性。线程安全主要是通过锁以及减少线程交互来实现。 一般的类型很少有完全线程安全的原因如下 完全线程安全的开发负担很重特别是如果一个类型有很多字段的情况在任意多线程并发的情况下每个字段都有交互的潜在可能。线程安全可能会损失性能某种程度上无论类型是否实际被用于多线程都会增加损耗。线程安全的类型并不能确保使用该类型的程序也是线程安全的为了实现程序线程安全所涉及的工作经常会使得类型线程安全成为多余。因此线程安全通常只会在需要时再实现只为了处理特定的多线程场景。 然而有些方法可以用来“作弊” 使庞大和复杂的类在多线程环境中安全运行。一种方法是牺牲粒度将大段代码甚至是访问的整个对象封装在一个排它锁内从而保证在高层上能进行顺序访问。事实上如果我们希望在多线程环境中使用线程不安全的第三方代码或大多数 Framework 的类型时这种策略是十分有用的。它仅仅是简单的使用了相同的排它锁来保护对非线程安全对象上所有属性、方法和字段的访问。这种解决方案适用于对象的方法都能够快速执行的场景否则会导致大量的阻塞。 除基本类型外很少有 .NET Framework 的类型能在比并发读取更高的需求下保证其实例成员是线程安全的。实现线程安全的责任就落在了开发人员身上一般就是使用排它锁。命名空间System.Collections.Concurrent中的类型是个例外它们是线程安全的数据结构。 另一种“作弊”的方法是通过最小化共享数据来减少线程交互。这是一种优秀的方法隐式的用于“ 无状态stateless”的中间层程序和网页服务器中。由于多个客户端请求可以同时到达服务端方法就必须是线程安全的。无状态设计因可伸缩性scalability好而流行在本质上限制了交互的可能性因为类并不需要持久化请求之间的数据。线程交互仅限于静态字段比如在内存中缓存通用数据或者提供认证和审计这样的基础服务时需要考虑。 实现线程安全的最后一种方式是使用自动锁机制automatic locking regime。如果继承 ContextBoundObject 类并使用 Synchronization 特性.NET Framework 就可以实现这种机制。当该对象上的方法或属性被调用时一个对象范围object-wide的锁就会自动作用于整个方法或属性的调用。尽管这样降低了实现线程安全的负担但是也有它的问题它很可能造成死锁、降低并发度并引起并非有意的重入。正是由于这些原因手动加锁通常是更好的选择直到有更好用的自动锁机制出现。 3.1线程安全与 .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对象本身来加锁。如果有两个关联的List就需要选择一个公共对象来加锁可以使用其中一个List对象然而更好的方式是使用一个独立的字段。 枚举 .NET 的集合也不是线程安全的因为如果在枚举的过程中集合被修改则会抛出异常。在这个例子中我们并没有将整个枚举过程加锁而是首先将其中的对象复制到一个数组中。如果我们要进行的枚举可能很耗时那么可以通过上述方式避免过长时间锁定。另一种解决方案是使用读写锁reader / writer lock 对线程安全的对象加锁 有时也需要对线程安全的对象加锁为了举例说明假设 Framework 的List类是线程安全的我们要给它添加一个条目 if (!_list.Contains (newItem)) _list.Add (newItem); 无论List本身是否线程安全上面的语句都不是线程安全的为了防止if条件判断执行后在实际添加条目之前被其它线程抢占修改了_list整个if所包含的代码都需要封装在一个锁中。并且在所有要修改_list的地方都要使用这个锁。例如下面的语句也需要封装在相同的锁中 _list.Clear(); 这也是为了确保了它不会在前面语句的执行过程中抢先执行。换句话说我们不得不像对于非线程安全的集合一样锁定线程安全的集合这使得对于List类是线程安全的假设变得多余。 在高并发的环境下对集合的访问加锁可能会产生大量阻塞为此 Framework 4.0 提供了线程安全的队列、栈和字典。 静态成员 将对对象的访问封装在一个自定义锁中的方式只有当所有参与并发的线程都知道并使用这个锁时才能起作用。然而如果需要加锁的逻辑有更大范围那就不是这么简单了。最糟糕的情况就是public类型中的静态成员。比如我们假设DateTime结构体上的静态属性DateTime.Now不是线程安全的即两个并发线程调用会导致错误的输出或是异常。使用外部加锁进行修正的唯一方法就是在调用DateTime.Now之前对类型本身加锁lock(typeof(DateTime))。这仅适用于所有的程序员都接受这样做这不太可能。此外对类型加锁也有其自身的问题。 因此DateTime结构体的静态成员都经过细致的处理来保证它是线程安全的。这在 .NET Framework 中是一个通用模式静态成员是线程安全的而实例成员则不是。编写类型让别人使用时遵守这种模式就不会令别人感到困惑和遇到难以解决的线程安全问题。换句话说保证静态成员的线程安全就不会妨碍你的类型的使用者实现线程安全。 静态方法的线程安全是必须由明确的编码实现的不是说把方法写成静态的就能自动实现线程安全 只读线程安全 使类型对于并发只读访问是线程安全的会很有益这意味着使用者可以避免使用排它锁。许多 .NET Framework 类型都遵循这一原则例如集合对于并发读是线程安全的。 自己遵循这一愿则也很简单如果我们希望一个类型对于并发只读访问是线程安全的那么不要在使用者期望是只读的方法内修改字段也不要加锁后修改。例如在集合的ToArray()方法的实现中也许会从压紧compacting集合的内部结构开始。然而这会导致使用者认为是只读的操作并非线程安全。 只读线程安全也是枚举器与可枚举类型分离的原因之一两个线程可以在一个集合上同时进行枚举因为它们会分别获得单独的枚举器。 如果缺乏文档在认为一个方法是只读前一定要谨慎。一个很好的例子是Random类当调用Random.Next()时它会更新私有的种子seed值。因此或者对Random类的使用加锁或者每个线程使用单独的实例。 3.2应用服务器中的线程安全 应用服务器需要使用多线程来处理多个客户端的同时请求。WCF、ASP.NET 以及 Web Services 应用都是隐式多线程的。使用 TCP 或 HTTP 之类网络通道的远程Remoting服务应用程序也是如此。这意味着服务端编程必须考虑线程安全考虑在处理客户端请求的线程间是否存在交互的可能。幸运的是这种交互的可能性不大一般服务端类要不然是无状态的无字段要不然就有为每个客户端或每个请求创建单独对象实例的激活模型。交互通常仅在静态字段上出现有时是用于在内存中缓存数据库数据来提高性能。 例如有一个查询数据库的RetrieveUser方法 // User 是一个自定义类型包含用户数据的字段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);   // 从数据库获取数据    lock (_users) _users [id]  u;     return u;   }} 至少必须要在读取和更新字典时加锁来保证线程安全。在这个例子中在加锁的便捷和性能之间进行了平衡。我们的设计略有一些效率问题如果两个线程同时使用未缓存过数据的id调用这个方法RetrieveUser就可能被调用两次并且其中一次对字典的更新是不必要的。对整个方法加锁可以避免这一问题但会导致更糟的效率整个缓存在调用RetrieveUser的期间都会被加锁在这段时间内其它需要这样获取用户信息的线程都会被阻塞。 3.3富客户端应用与线程亲和 译者注这里的 thread affinity 译为线程亲和是指 UI 控件与线程的一种“绑定”关系而不是通常理解中的线程与 CPU 核心的绑定关系。 WPF 与 Windows Forms 库都遵循基于线程亲和的模型。尽管它们有各自的实现但是原理非常相似。 富客户端的构成主要基于DependencyObjectWPF 中或是ControlWindows Forms 中。这些对象具有线程亲和性thread affinity意思是只有创建它们的线程才能访问其成员。违反这一原则会引起不可预料的行为或是抛出异常。 这样的好处是访问 UI 对象时并不需要加锁。而坏处是如果希望调用在另一线程 Y 上创建的对象 X 的成员就必须将请求封送marshal到线程 Y 。通过下列方法显式实现 WPF 中在其Dispatcher对象上调用Invoke或BeginInvoke。Windows Forms 中调用Control对象上的Invoke或BeginInvoke。Invoke和BeginInvoke都接受一个委托代表我们希望在目标控件上运行的的方法。Invoke是同步工作的调用方在封送的委托执行完成前会被阻塞BeginInvoke是异步工作的调用方立即返回封送请求被加入队列使用与处理键盘、鼠标、定时器事件相同的消息队列。 假定窗体包含一个名为txtMessage的文本框我们希望使用一个工作线程更新其内容下面是 WPF 的示例 public partial class MyWindow : Window{   public MyWindow()   {     InitializeComponent();     new Thread (Work).Start();   }   void Work()   {     Thread.Sleep (5000);           // 模拟耗时任务    UpdateMessage (The answer);   }   void UpdateMessage (string message)   {     Action action  ()  txtMessage.Text  message;     Dispatcher.Invoke (action);   }} Windows Forms 的代码类似所不同的是我们调用Form的Invoke方法 void UpdateMessage (string message){   Action action  ()  txtMessage.Text  message;   this.Invoke (action);} Framework 提供了两个构造来简化这个过程 BackgroundWorker任务延续Task continuations工作线程 vs UI 线程 我们可以认为富客户端应用程序中有两种不同的线程类别UI 线程和工作线程。UI 线程创建并“占有” UI 元素工作线程则不会工作线程通常执行长时间任务例如获取数据。 大多数的富客户端应用只有一个 UI 线程它也是应用程序的主线程它再去根据需要创建工作线程可以直接创建或者使用BackgroundWorker。这些工作线程可以将代码封送回主 UI 线程来更新控件或报告工作进度。 那么应用程序何时会需要多个 UI 线程呢主要的应用场景是如果应用具有多个顶级窗口每个顶级窗口都是被称为单文档界面Single Document InterfaceSDI的程序例如 Microsoft Word。每个 SDI 窗口通常会在任务栏上显示为独立的应用程序并且与其它的 SDI 窗口在功能上基本隔离。通过为每一个这样的窗口设置独立的 UI 线程可以使应用程序有更好的响应。 3.4不可变对象 不可变对象的状态不能被修改无论通过外部还是内部。不可变对象的字段通常被声明为只读的并且在构造过程中就会被初始化好。 不变性immutability 是函数式编程的一个标志不是修改对象而是使用不同的属性创建一个新的对象。LINQ 就遵循这种模式。不变性在多线程中也很有价值它可以通过消除或是最小化写入的可能来避免共享可写状态的问题。 使用不可变对象的一个模式是封装一组相关的字段来最小化锁定的时长。下面的例子中假设有两个字段 int _percentComplete;string _statusMessage; 我们希望对其进行原子的读 / 写操作。除了加锁之外也可以定义如下的不可变类 class ProgressStatus    // 代表某活动进度{   public readonly int PercentComplete;   public readonly string StatusMessage;   // 这个类可能还有其它很多字段...   public ProgressStatus (int percentComplete, string statusMessage)   {     PercentComplete  percentComplete;     StatusMessage  statusMessage;   }} 然后我们可以定义一个该类型的字段以及一个用于加锁的对象 readonly object _statusLocker  new object();ProgressStatus _status; 现在我们就可以读 / 写该类型的值而仅需要为一次赋值加锁 var status  new ProgressStatus (50, Working on it);// 想象一下我们曾经在这要处理多少个字段 ... // ...lock (_statusLocker) _status  status;    // 非常短暂的锁 要读取该对象首先获取该对象的一个副本在锁内然后就可以读取其值而不需要继续占有锁 ProgressStatus statusCopy;lock (_locker ProgressStatus)  statusCopy  _status;   // 也是一个短暂的锁 int pc  statusCopy.PercentComplete; string msg  statusCopy.StatusMessage;// ... 译者注上面代码有误lock中应该是_statusLocker。这里的statusCopy也不是真正的副本而仅仅相当于_status的别名这么做是为了通过刷新处理器缓存获取_status当前的一致状态。 技术上讲最后两行代码的线程安全是因为之前的lock进行了隐式的内存屏障memory barrier。 需要注意的是这种无锁译者注这里不是说完全没有用到锁而是指锁非常短暂的方法可以避免一组相关字段出现不一致的情况。但它无法阻止后续的操作修改数据因此通常需要使用锁。在第 5 部分中将会看到使用不变性来简化多线程的更多示例包括PLINQ。 可以根据先前的值安全的创建一个新的ProgressStatus对象例如在其上可以“增加”PercentComplete的值而仅需锁定一行代码。实际上不使用锁都可以我们可以显式使用内存屏障、Interlocked.CompareExchange还有自旋等待。这种高级技术将在稍后的并行编程中讲解。 原文链接: http://blog.gkarch.com/threading/part2.html posted on 2018-08-11 13:46 micwin 阅读(...) 评论(...)  编辑 收藏 转载于:https://www.cnblogs.com/chinanetwind/articles/9459538.html
http://wiki.neutronadmin.com/news/447283/

相关文章:

  • 建设电子商务网站的方法有?学校网站建设策划方案
  • 自助建站优化排名视频网站开发教程
  • wordpress需要登录密码东莞百度快速排名优化
  • 网站制作报价明细表手机网站优点
  • 药检局信息化网站系统建设方案宁夏公路建设局网站
  • 网站开发一次性费用寻求南宁网站建设人员
  • 网站与网址的区别网站建设如何选择良好的服务器
  • 东莞网站建设都找菲凡网络网络培训的功能主要有
  • 广州 网站建设网络推广网页设计常熟经济技术开发区人才网
  • 宜昌网站建设多少钱服装公司网站多少钱
  • 安徽网站设计找哪家wordpress访客统计插件
  • 玉溪市住房城乡建设局网站用html框架做网站
  • 如何为网站做面包屑导航wordpress本地化
  • 南宁网站设计方法徐州最新通知今天
  • 网站建设项目规划书目录wordpress访客
  • 怒江网站建设百度关键词seo优化
  • 网站活动怎么做的网站建设招聘系统
  • 免费网站制作公司毕设做网站的过程
  • 旅游网站建设方案后台网站如何规范的排版编辑
  • 手机怎么安装网站程序创建网站视频
  • 百度注册网站网站页面优化
  • 厦门做企业网站找谁网站建设类图书有哪些
  • 网站站长统计代码世界大学排名
  • 沧浪网站建设方案六枝特区建设局网站
  • 网站推广方案注意事项?网站优化自已做还是请人做
  • 禅城网站建设多少钱招聘简历模板
  • 网站后台构建移动互联网应用程序清理整合情况
  • 网站的栏目规划招聘网站哪个平台比较好
  • 南宁建站公司甘肃住房和城乡建设部网站
  • 做销售在哪个网站找客户端购物商城建设