C# 多线程学习笔记 - 3
本文主要针对 GKarch 相关文章留作笔记,仅在原文基础上记录了自己的理解与摘抄部分片段。
遵循原作者的 CC 3.0 协议。
如果想要了解更加详细的文章信息内容,请访问下列地址进行学习。
一、基于事件的异步模式
- 基于事件的异步模式 (event-based asynchronous pattern) 提供了简单的方式,让类型提供多线程的能力而不需要显式启动线程。 - 协作取消模型。
- 工作线程完成时安全更新 UI 的能力。
- 转发异常到完成事件。
 
- EAP 仅是一个模式,需要开发人员自己实现。 
- EAP 一般会提供一组成员,在其内部管理工作线程,例如 - WebClient类型就使用的 EAP 模式进行设计。- // 下载数据的同步版本。
 public byte[] DownloadData (Uri address);
 // 下载数据的异步版本。
 public void DownloadDataAsync (Uri address);
 // 下载数据的异步版本,支持传入 token 标识任务。
 public void DownloadDataAsync (Uri address, object userToken);
 // 完成时候的事件,当任务取消,出现异常或者更新 UI 操作都可以才该事件内部进行操作。
 public event DownloadDataCompletedEventHandler DownloadDataCompleted; public void CancelAsync (object userState); // 取消一个操作
 public bool IsBusy { get; } // 指示是否仍在运行
 
- 通过 - Task可以很方便的实现 EAP 模式类似的功能。
二、BackgroundWorker
- BackgroundWorker是一个通用的 EAP 实现,提供了下列功能。- 协作取消模型。
- 工作线程完成时安全更新 UI 的能力。
- 转发异常到完成事件。
- 报告工作进度的协议。
 
- BackgroundWorker使用线程池来创建线程,所以不应该在- BackgroundWorker的线程上调用- Abort()方法。
2.1 使用方法
- 实例化 - BackgroundWorker对象,并且挂接- DoWork事件。
- 调用 - RunWorkerAsync()可以传递一个- object参数,以上则是- BackgroundWorker的最简使用方法。
- 可以为 - BackgroundWorker对象挂接- RunWorkerCompleted事件,在该事件内部可以对工作线程执行后的异常与结果进行检查,并且可以直接在该事件内部安全地更新 UI 组件。
- 如果需要支持取消功能,则需要将 - WorkerSupportsCancellation属性置为- true。这样在- DoWork()事件当中就可通过检查对象的- CancellationPending属性来确定是否被取消,如果是则将- Cancel置为- true并结束工作事件。
- 调用 - CancelAsync来请求取消。
- 开发人员不一定需要在 - CancellationPending为- true时才取消任务,随时可以通过将- Cancel置为- true来终止任务。
- 如果需要添加工作进度报告,则需要将 - WorkerReportsProgress属性置为- true,并在- DoWork事件中周期性地调用- ReportProcess()方法来报告工作进度。同时挂接- ProgressChanged事件,在其内部可以安全地更新 UI 组件,例如设置进度条 Value 值。
- 下列代码即是上述功能的完整实现。 - class Program
 {
 static void Main()
 {
 var backgroundTest = new BackgroundWorkTest();
 backgroundTest.Run();
 Console.ReadLine();
 }
 } public class BackgroundWorkTest
 {
 private readonly BackgroundWorker _bw = new BackgroundWorker(); public BackgroundWorkTest()
 {
 // 绑定工作事件
 _bw.DoWork += BwOnDoWork; // 绑定工作完成事件
 _bw.WorkerSupportsCancellation = true;
 _bw.RunWorkerCompleted += BwOnRunWorkerCompleted; // 绑定工作进度更新事件
 _bw.WorkerReportsProgress = true;
 _bw.ProgressChanged += BwOnProgressChanged;
 } private void BwOnProgressChanged(object sender, ProgressChangedEventArgs e)
 {
 Console.WriteLine($"当前进度:{e.ProgressPercentage}%");
 } private void BwOnRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
 {
 if (e.Cancelled)
 {
 Console.WriteLine("任务已经被取消。");
 } if (e.Error != null)
 {
 Console.WriteLine("执行任务的过程中出现了异常。");
 } // 在当前线程可以直接更新 UI 组件的数据 Console.WriteLine($"执行完成的结果:{e.Result}");
 } public void Run()
 {
 _bw.RunWorkerAsync(10);
 } private void BwOnDoWork(object sender, DoWorkEventArgs e)
 {
 // 这里是工作线程进行执行的 Console.WriteLine($"需要计算的数据值为:{e.Argument}"); for (int i = 0; i <= 100; i += 20)
 {
 if (_bw.CancellationPending)
 {
 e.Cancel = true;
 return;
 } _bw.ReportProgress(i);
 } // 传递完成的数据给完成事件
 e.Result = 1510;
 }
 }
 
- BackgroundWorker不是密闭类,用户可以继承自- BackgroundWorker类型,并重写其- DoWork()方法以达到自己的需要。
三、线程的中断与中止
- 所有 阻塞 方法在解除阻塞的条件没有满足,并且其没有指定超时时间的情况下,会永久阻塞。 
- 开发人员可以通过 - Thread.Interrupt()与- Thread.Abort()方法来解除阻塞。
- 在使用线程中断与中止方法的时候,应该十分谨慎,这可能会导致一些意想不到的情况发生。 
- 为了演示上面所说的概念,可以编写如下代码进行测试。 - class Program
 {
 static void Main()
 {
 var test = new ThreadInterrupt();
 test.Run();
 Console.ReadLine();
 }
 } public class ThreadInterrupt
 {
 public void Run()
 {
 var testThread = new Thread(WorkThread); testThread.Start();
 // 中断指定的线程
 testThread.Interrupt();
 } private void WorkThread()
 {
 try
 {
 // 永远阻塞
 Thread.Sleep(Timeout.Infinite);
 }
 catch (ThreadInterruptedException e)
 {
 Console.WriteLine("产生了中断异常.");
 } Console.WriteLine("线程执行完成.");
 }
 }
 
3.1 中断
- 在一个阻塞线程上调用 Thread.Interrupt()方法,会导致该线程抛出ThreadInterruptedException异常,并且强制释放线程。
- 中断线程时,除非没有对 ThreadInterruptedException进行处理,否则是不会导致阻塞线程结束的。
- 随意中断一个线程是十分危险的,我们可以通过信号构造或者取消构造。哪怕是使用 Thread.Abort()来中止线程,都比中断线程更加安全。
- 因为随意中断线程会导致调用栈上面的任何框架,或者第三方的方法意外接收到中断。
3.2 中止
Thread.Abort()方法在 .NET Core 当中无法使用,调用该方法会抛出Thread abort is not supported on this platform.错误。
- 在一个阻塞线程上调用 Thread.Abort()方法,效果与中断相似,但会抛出一个ThreadAbortException异常。
- 该异常在 catch块结束之后会被重新抛出。
- 未经处理的 ThreadAbortException是仅有的两个不会导致应用程序关闭的异常之一。
- 中止与中断最大的不同是,中止操作会立即在执行的地方抛出异常。例如中止发生在 FileStream的构造期间,可能会导致一个非托管文件句柄保持打开状态导致内存泄漏。
四、安全取消
- 与实现了 EAP 模式的 - BackgroundWorker类型一样,我们可以通过协作模式,使用一个标识来优雅地中止线程。
- 其核心思路就是封装一个取消标记,将其传入到线程当中,在线程执行时可以通过这个取消标记来优雅中止。 - class Program
 {
 static void Main()
 {
 var test = new CancelTest();
 test.Run();
 Console.ReadLine();
 }
 } public class CancelToken
 {
 private readonly object _selfLocker = new object();
 private bool _cancelRequest = false; /// <summary>
 /// 当前操作是否已经被取消。
 /// </summary>
 public bool IsCancellationRequested
 {
 get
 {
 lock (_selfLocker)
 {
 return _cancelRequest;
 }
 }
 } /// <summary>
 /// 取消操作。
 /// </summary>
 public void Cancel()
 {
 lock (_selfLocker)
 {
 _cancelRequest = true;
 }
 } /// <summary>
 /// 如果操作已经被取消,则抛出异常。
 /// </summary>
 public void ThrowIfCancellationRequested()
 {
 lock (_selfLocker)
 {
 if (_cancelRequest)
 {
 throw new OperationCanceledException("操作被取消.");
 }
 }
 }
 } public class CancelTest
 {
 public void Run()
 {
 var cancelToken = new CancelToken(); var workThread = new Thread(() =>
 {
 try
 {
 Work(cancelToken);
 }
 catch (OperationCanceledException e)
 {
 Console.WriteLine("任务已经被取消。");
 }
 }); workThread.Start(); Thread.Sleep(1000);
 cancelToken.Cancel();
 } private void Work(CancelToken token)
 {
 // 模拟耗时操作
 while (true)
 {
 token.ThrowIfCancellationRequested();
 try
 {
 RealWork(token);
 }
 finally
 {
 // 清理资源
 }
 }
 } private void RealWork(CancelToken token)
 {
 token.ThrowIfCancellationRequested();
 Console.WriteLine("我是真的在工作...");
 }
 }
 
4.1 取消标记
- 在 .NET 提供了 - CancellationTokenSource和- CancellationToken来简化取消操作。
- 如果需要使用这两个类,则只需要实例化一个 - CancellationTokenSource对象,并将其- Token属性传递给支持取消的方法,在需要取消的使用调用 Source 的- Cancel()即可。- // 伪代码
 var cancelSource = new CancellationTokenSource(); // 启动线程
 new Thread(() => work(cancelSource.Token)).Start(); // Work 方法的定义
 void Work(CancellationToken cancelToken)
 {
 cancelToken.ThrowIfCancellationRequested();
 } // 需要取消的时候,调用 Cancel 方法。
 cancelSource.Cancel();
 
五、延迟初始化
- 延迟初始化的作用是缓解类型构造的开销,尤其是某个类型的构造开销很大的时候可以按需进行构造。 - // 原始代码
 public class Foo
 {
 public readonly Expensive Expensive = new Expensive();
 } public class Expensive
 {
 public Expensive()
 {
 // ... 构造开销极大
 }
 } // 按需构造
 public class LazyFoo
 {
 private Expensive _expensive; public Expensive Expensive
 {
 get
 {
 if(_expensive == null) _expensive = new Expensive();
 }
 }
 } // 按需构造的线程安全版本
 public class SafeLazyFoo
 {
 private Expensive _expensive;
 private readonly object _lazyLocker = new object(); public Expensive Expensive
 {
 get
 {
 lock(_lazyLocker)
 {
 if(_expensive == null)
 {
 _expensive = new Expensive();
 }
 }
 }
 }
 }
 
- 在 .NET 4.0 之后提供了一个 - Lazy<T>类型,可以免去上面复杂的代码编写,并且也实现了双重锁定模式。
- 通过在创建 - Lazy<T>实例时传递不同的- bool参数来决定是否创建线程安全的初始化模式,传递了- true则是线程安全的,传递了- false则不是线程安全的。- public class LazyExpensive
 { } public class LazyTest
 {
 // 线程安全版本的延迟初始化对象。
 private Lazy<LazyExpensive> _lazyExpensive = new Lazy<LazyExpensive>(()=>new LazyExpensive(),true); public LazyExpensive LazyExpensive => _lazyExpensive.Value;
 }
 
5.1 LazyInitializer
- LazyInitializer是一个静态类,基本与- Lazy<T>相似,但是提供了一系列的静态方法,在某些极端情况下可以改善性能。- public class LazyFactoryTest
 {
 private LazyExpensive _lazyExpensive; // 双重锁定模式。
 public LazyExpensive LazyExpensive
 {
 get
 {
 LazyInitializer.EnsureInitialized(ref _lazyExpensive, () => new LazyExpensive());
 return _lazyExpensive;
 }
 } }
 
- LazyInitializer提供了一个竞争初始化的版本,这种在多核处理器(线程数与核心数相等)的情况下速度比双重锁定技术要快。- volatile Expensive _expensive;
 public Expensive Expensive
 {
 get
 {
 if (_expensive == null)
 {
 var instance = new Expensive();
 Interlocked.CompareExchange (ref _expensive, instance, null);
 }
 return _expensive;
 }
 }
 
六、线程局部存储
- 某些数据不适合作为全局遍历和局部变量,但是在整个调用栈当中又需要进行共享,是与执行路径紧密相关的。所以这里来说,应该是在代码的执行路径当中是全局的,这里就可以通过线程来达到数据隔离的效果。例如线程 A 调用链是这样的 A() -> B() -> C()。 
- 对静态字段增加 - [ThreadStatic],这样每个线程就会拥有独立的副本,但仅适用于静态字段。- [ThreadStatic] static int _x;
 
- .NET 提供了一个 - ThreadLocal<T>类型可以用于静态字段和实例字段的线程局部存储。- // 静态字段存储
 static ThreadLocal<int> _x = new ThreadLocal<int>(() => 3); // 实例字段存储
 var localRandom = new ThreadLocal<Random>(() => new Random());
 
- ThreadLocal<T>的值是 延迟初始化 的,第一次被使用的时候 才通过工厂进行初始化。
- 我们可以使用 - Thread提供的- Thread.GetData()与- Thread.SetData()方法来将数据存储在线程数据槽当中。
- 同一个数据槽可以跨线程使用,而且它在不同的线程当中数据仍然是独立的。 
- 通过 - LocalDataStoreSolt可以构建一个数据槽,通过- Thread.GetNamedDataSlot("securityLevel")来获得一个命名槽,可以通过- Thread.FreeNameDataSlot("securityLevel")来释放。
- 如果不需要命名槽,也可以通过 - Thread.AllocateDataSlot()来获得一个匿名槽。- class Program
 {
 static void Main()
 {
 var test = new ThreadSlotTest();
 test.Run();
 Console.ReadLine();
 }
 } public class ThreadSlotTest
 {
 // 创建一个命名槽。
 private LocalDataStoreSlot _localDataStoreSlot = Thread.GetNamedDataSlot("命名槽");
 // 创建一个匿名槽。
 private LocalDataStoreSlot _anonymousDataStoreSlot = Thread.AllocateDataSlot(); public void Run()
 {
 new Thread(NamedThreadWork).Start();
 new Thread(NamedThreadWork).Start(); new Thread(AnonymousThreadWork).Start();
 new Thread(AnonymousThreadWork).Start(); // 释放命名槽。
 Thread.FreeNamedDataSlot("命名槽");
 } // 命名槽测试。
 private void NamedThreadWork()
 {
 // 设置命名槽数据
 Thread.SetData(_localDataStoreSlot,DateTime.UtcNow.Ticks); var data = Thread.GetData(_localDataStoreSlot);
 Console.WriteLine($"命名槽数据:{data}"); ContinueNamedThreadWork();
 } private void ContinueNamedThreadWork()
 {
 Console.WriteLine($"延续方法中命名槽的数据:{Thread.GetData(_localDataStoreSlot)}");
 } // 匿名槽测试。
 private void AnonymousThreadWork()
 {
 // 设置匿名槽数据
 Thread.SetData(_anonymousDataStoreSlot,DateTime.UtcNow.Ticks); var data = Thread.GetData(_anonymousDataStoreSlot);
 Console.WriteLine($"匿名槽数据:{data}"); ContinueAnonymousThreadWork();
 } private void ContinueAnonymousThreadWork()
 {
 Console.WriteLine($"延续方法中匿名槽的数据:{Thread.GetData(_anonymousDataStoreSlot)}");
 }
 }
 
七、定时器
7.1 多线程定时器
- 多线程定时器使用线程池触发时间,也就意味着 Elapsed事件可能会在不同线程当中触发。
- System.Threading.Timer是最简单的多线程定时器,而- System.Timers.Timer则是对于该计时器的封装。
- 多线程定时器的精度大概在 10~20ms。
7.2 单线程定时器
- 单线程定时器依赖于 UI 模型的底层消息循环机制,所以其 Tick事件总是在创建该定时器的线程触发。
- 单线程定时器关联的事件可以安全地操作 UI 组件。
- 精度比多线程定时器更低,而且更容易使 UI 失去响应。
C# 多线程学习笔记 - 3的更多相关文章
- java多线程学习笔记——详细
		一.线程类 1.新建状态(New):新创建了一个线程对象. 2.就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法.该状态的线程位于可运行线程池中, ... 
- JAVA多线程学习笔记(1)
		JAVA多线程学习笔记(1) 由于笔者使用markdown格式书写,后续copy到blog可能存在格式不美观的问题,本文的.mk文件已经上传到个人的github,会进行同步更新.github传送门 一 ... 
- 多线程学习笔记九之ThreadLocal
		目录 多线程学习笔记九之ThreadLocal 简介 类结构 源码分析 ThreadLocalMap set(T value) get() remove() 为什么ThreadLocalMap的键是W ... 
- java进阶-多线程学习笔记
		多线程学习笔记 1.什么是线程 操作系统中 打开一个程序就是一个进程 一个进程可以创建多个线程 现在系统中 系统调度的最小单元是线程 2.多线程有什么用? 发挥多核CPU的优势 如果使用多线程 将计算 ... 
- Java多线程学习笔记(一)——多线程实现和安全问题
		1. 线程.进程.多线程: 进程是正在执行的程序,线程是进程中的代码执行,多线程就是在一个进程中有多个线程同时执行不同的任务,就像QQ,既可以开视频,又可以同时打字聊天. 2.线程的特点: 1.运行任 ... 
- java 多线程学习笔记
		这篇文章主要是个人的学习笔记,是以例子来驱动的,加深自己对多线程的理解. 一:实现多线程的两种方法 1.继承Thread class MyThread1 extends Thread{ public ... 
- Java多线程学习笔记--生产消费者模式
		实际开发中,我们经常会接触到生产消费者模型,如:Android的Looper相应handler处理UI操作,Socket通信的响应过程.数据缓冲区在文件读写应用等.强大的模型框架,鉴于本人水平有限目前 ... 
- Java多线程学习笔记
		进程:正在执行中的程序,其实是应用程序在内存中运行的那片空间.(只负责空间分配) 线程:进程中的一个执行单元,负责进程汇总的程序的运行,一个进程当中至少要有一个线程. 多线程:一个进程中时可以有多个线 ... 
- C# 多线程学习笔记 - 2
		本文主要针对 GKarch 相关文章留作笔记,仅在原文基础上记录了自己的理解与摘抄部分片段. 遵循原作者的 CC 3.0 协议. 如果想要了解更加详细的文章信息内容,请访问下列地址进行学习. 原文章地 ... 
随机推荐
- Asp.net并发请求导致的数据重复插入问题
			前段时间工作中,有客户反应了系统中某类待办重复出现两次的情况.我核实了数据之后,分析认为是并发请求下导致的数据不一致性问题,并做了重现.其实这并不是一个需要频繁调用的功能,但是客户连续点击了两次,导致 ... 
- c# List列表数据转换成树形结构
			把List列表结构 转换成树形结构 /// <summary> /// 构造树形Json /// </summary> public static class TreeJson ... 
- 第二项目AIaisell(易销宝)
			一.什么是报表 向上级报告情况的表格.简单的说:报表就是用表格.图表等格式来动态显示数据,可以用公式表示为:“报表 = 多样的格式 + 动态的数据” 表格:详细数据 图表: 直观 二.表格数据展示 2 ... 
- android studio 开发免安装的app之目录结构
			尚未深入分析,暂且外链到我看到的,对此有帮助的博客,在此,感谢你们. 1.https://blog.csdn.net/tscyds/article/details/74331085 2.https:/ ... 
- 向mysql中导入向导时如表xlsx
			如果出现这种问题那么是因为没有打开这个文件,如果想导入这个文件需要到开这个文件,然后再导入 
- hdu1814 Peaceful Commission
			hdu1814 Peaceful Commission 题意:2-sat裸题,打印字典序最小的 我写了三个 染色做法,正解 scc做法,不管字典序 scc做法,错误的字典序贪心 #include &l ... 
- [AGC017D]Game on Tree
			[AGC017D]Game on Tree 题目大意: 一棵\(n(n\le10^5)\)个结点的树.A和B轮流进行游戏,A先手.每次删掉一棵子树,根结点不能删.最先不能操作的人输,问最后谁赢. 思路 ... 
- VS之设置文件编码格式
			VS2012默认格式为 "GB2312-80",很多时候可能出现乱码情况,就是编码问题,如何在VS里修改呢? 文件->“高级保存选项 ” 选择gb2312 
- java的3大特性
			java的3大特性 1.继承: * 继承是从已有类得到继承信息创建新类的过程. * 提供继承信息的类被称为父类(超类.基类):得到继承信息的类被称为子类(派生类). * 继承让变化中的软件系统有定的延 ... 
- ssm知识点整理
			第1章 resultType和resultMap的区别是什么? MyBatis中在查询进行select映射的时候,返回类型可以用resultType,也可以用resultMap,resultType ... 
