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 ...