本文主要针对 GKarch 相关文章留作笔记,仅在原文基础上记录了自己的理解与摘抄部分片段。

遵循原作者的 CC 3.0 协议

如果想要了解更加详细的文章信息内容,请访问下列地址进行学习。

原文章地址:https://blog.gkarch.com/threading/part3.html

一、基于事件的异步模式

  1. 基于事件的异步模式 (event-based asynchronous pattern) 提供了简单的方式,让类型提供多线程的能力而不需要显式启动线程。

    • 协作取消模型。
    • 工作线程完成时安全更新 UI 的能力。
    • 转发异常到完成事件。
  2. EAP 仅是一个模式,需要开发人员自己实现。

  3. 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; } // 指示是否仍在运行
  4. 通过 Task 可以很方便的实现 EAP 模式类似的功能。

二、BackgroundWorker

  1. BackgroundWorker 是一个通用的 EAP 实现,提供了下列功能。

    • 协作取消模型。
    • 工作线程完成时安全更新 UI 的能力。
    • 转发异常到完成事件。
    • 报告工作进度的协议。
  2. BackgroundWorker 使用线程池来创建线程,所以不应该在 BackgroundWorker 的线程上调用 Abort() 方法。

2.1 使用方法

  1. 实例化 BackgroundWorker 对象,并且挂接 DoWork 事件。

  2. 调用 RunWorkerAsync() 可以传递一个 object 参数,以上则是 BackgroundWorker 的最简使用方法。

  3. 可以为 BackgroundWorker 对象挂接 RunWorkerCompleted 事件,在该事件内部可以对工作线程执行后的异常与结果进行检查,并且可以直接在该事件内部安全地更新 UI 组件。

  4. 如果需要支持取消功能,则需要将 WorkerSupportsCancellation 属性置为 true。这样在 DoWork() 事件当中就可通过检查对象的 CancellationPending 属性来确定是否被取消,如果是则将 Cancel 置为 true 并结束工作事件。

  5. 调用 CancelAsync 来请求取消。

  6. 开发人员不一定需要在 CancellationPendingtrue 时才取消任务,随时可以通过将 Cancel 置为 true 来终止任务。

  7. 如果需要添加工作进度报告,则需要将 WorkerReportsProgress 属性置为 true,并在 DoWork 事件中周期性地调用 ReportProcess() 方法来报告工作进度。同时挂接 ProgressChanged 事件,在其内部可以安全地更新 UI 组件,例如设置进度条 Value 值。

  8. 下列代码即是上述功能的完整实现。

    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;
    }
    }
  9. BackgroundWorker 不是密闭类,用户可以继承自 BackgroundWorker 类型,并重写其 DoWork() 方法以达到自己的需要。

三、线程的中断与中止

  1. 所有 阻塞 方法在解除阻塞的条件没有满足,并且其没有指定超时时间的情况下,会永久阻塞。

  2. 开发人员可以通过 Thread.Interrupt()Thread.Abort() 方法来解除阻塞。

  3. 在使用线程中断与中止方法的时候,应该十分谨慎,这可能会导致一些意想不到的情况发生。

  4. 为了演示上面所说的概念,可以编写如下代码进行测试。

    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 中断

  1. 在一个阻塞线程上调用 Thread.Interrupt() 方法,会导致该线程抛出 ThreadInterruptedException 异常,并且强制释放线程。
  2. 中断线程时,除非没有对 ThreadInterruptedException 进行处理,否则是不会导致阻塞线程结束的。
  3. 随意中断一个线程是十分危险的,我们可以通过信号构造或者取消构造。哪怕是使用 Thread.Abort() 来中止线程,都比中断线程更加安全。
  4. 因为随意中断线程会导致调用栈上面的任何框架,或者第三方的方法意外接收到中断。

3.2 中止

Thread.Abort() 方法在 .NET Core 当中无法使用,调用该方法会抛出 Thread abort is not supported on this platform. 错误。

  1. 在一个阻塞线程上调用 Thread.Abort() 方法,效果与中断相似,但会抛出一个 ThreadAbortException 异常。
  2. 该异常在 catch 块结束之后会被重新抛出。
  3. 未经处理的 ThreadAbortException 是仅有的两个不会导致应用程序关闭的异常之一。
  4. 中止与中断最大的不同是,中止操作会立即在执行的地方抛出异常。例如中止发生在 FileStream 的构造期间,可能会导致一个非托管文件句柄保持打开状态导致内存泄漏。

四、安全取消

  1. 与实现了 EAP 模式的 BackgroundWorker 类型一样,我们可以通过协作模式,使用一个标识来优雅地中止线程。

  2. 其核心思路就是封装一个取消标记,将其传入到线程当中,在线程执行时可以通过这个取消标记来优雅中止。

    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 取消标记

  1. 在 .NET 提供了 CancellationTokenSourceCancellationToken 来简化取消操作。

  2. 如果需要使用这两个类,则只需要实例化一个 CancellationTokenSource 对象,并将其 Token 属性传递给支持取消的方法,在需要取消的使用调用 Source 的 Cancel() 即可。

    // 伪代码
    var cancelSource = new CancellationTokenSource(); // 启动线程
    new Thread(() => work(cancelSource.Token)).Start(); // Work 方法的定义
    void Work(CancellationToken cancelToken)
    {
    cancelToken.ThrowIfCancellationRequested();
    } // 需要取消的时候,调用 Cancel 方法。
    cancelSource.Cancel();

五、延迟初始化

  1. 延迟初始化的作用是缓解类型构造的开销,尤其是某个类型的构造开销很大的时候可以按需进行构造。

    // 原始代码
    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();
    }
    }
    }
    }
    }
  2. 在 .NET 4.0 之后提供了一个 Lazy<T> 类型,可以免去上面复杂的代码编写,并且也实现了双重锁定模式。

  3. 通过在创建 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

  1. LazyInitializer 是一个静态类,基本与 Lazy<T> 相似,但是提供了一系列的静态方法,在某些极端情况下可以改善性能。

    public class LazyFactoryTest
    {
    private LazyExpensive _lazyExpensive; // 双重锁定模式。
    public LazyExpensive LazyExpensive
    {
    get
    {
    LazyInitializer.EnsureInitialized(ref _lazyExpensive, () => new LazyExpensive());
    return _lazyExpensive;
    }
    } }
  2. LazyInitializer 提供了一个竞争初始化的版本,这种在多核处理器(线程数与核心数相等)的情况下速度比双重锁定技术要快。

    volatile Expensive _expensive;
    public Expensive Expensive
    {
    get
    {
    if (_expensive == null)
    {
    var instance = new Expensive();
    Interlocked.CompareExchange (ref _expensive, instance, null);
    }
    return _expensive;
    }
    }

六、线程局部存储

  1. 某些数据不适合作为全局遍历和局部变量,但是在整个调用栈当中又需要进行共享,是与执行路径紧密相关的。所以这里来说,应该是在代码的执行路径当中是全局的,这里就可以通过线程来达到数据隔离的效果。例如线程 A 调用链是这样的 A() -> B() -> C()。

  2. 对静态字段增加 [ThreadStatic] ,这样每个线程就会拥有独立的副本,但仅适用于静态字段。

    [ThreadStatic] static int _x;
  3. .NET 提供了一个 ThreadLocal<T> 类型可以用于静态字段和实例字段的线程局部存储。

    // 静态字段存储
    static ThreadLocal<int> _x = new ThreadLocal<int>(() => 3); // 实例字段存储
    var localRandom = new ThreadLocal<Random>(() => new Random());
  4. ThreadLocal<T> 的值是 延迟初始化 的,第一次被使用的时候 才通过工厂进行初始化。

  5. 我们可以使用 Thread 提供的 Thread.GetData()Thread.SetData() 方法来将数据存储在线程数据槽当中。

  6. 同一个数据槽可以跨线程使用,而且它在不同的线程当中数据仍然是独立的。

  7. 通过 LocalDataStoreSolt 可以构建一个数据槽,通过 Thread.GetNamedDataSlot("securityLevel") 来获得一个命名槽,可以通过 Thread.FreeNameDataSlot("securityLevel") 来释放。

  8. 如果不需要命名槽,也可以通过 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 多线程定时器

  1. 多线程定时器使用线程池触发时间,也就意味着 Elapsed 事件可能会在不同线程当中触发。
  2. System.Threading.Timer 是最简单的多线程定时器,而 System.Timers.Timer 则是对于该计时器的封装。
  3. 多线程定时器的精度大概在 10 ~ 20 ms。

7.2 单线程定时器

  1. 单线程定时器依赖于 UI 模型的底层消息循环机制,所以其 Tick 事件总是在创建该定时器的线程触发。
  2. 单线程定时器关联的事件可以安全地操作 UI 组件。
  3. 精度比多线程定时器更低,而且更容易使 UI 失去响应。

C# 多线程学习笔记 - 3的更多相关文章

  1. java多线程学习笔记——详细

    一.线程类  1.新建状态(New):新创建了一个线程对象.        2.就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法.该状态的线程位于可运行线程池中, ...

  2. JAVA多线程学习笔记(1)

    JAVA多线程学习笔记(1) 由于笔者使用markdown格式书写,后续copy到blog可能存在格式不美观的问题,本文的.mk文件已经上传到个人的github,会进行同步更新.github传送门 一 ...

  3. 多线程学习笔记九之ThreadLocal

    目录 多线程学习笔记九之ThreadLocal 简介 类结构 源码分析 ThreadLocalMap set(T value) get() remove() 为什么ThreadLocalMap的键是W ...

  4. java进阶-多线程学习笔记

    多线程学习笔记 1.什么是线程 操作系统中 打开一个程序就是一个进程 一个进程可以创建多个线程 现在系统中 系统调度的最小单元是线程 2.多线程有什么用? 发挥多核CPU的优势 如果使用多线程 将计算 ...

  5. Java多线程学习笔记(一)——多线程实现和安全问题

    1. 线程.进程.多线程: 进程是正在执行的程序,线程是进程中的代码执行,多线程就是在一个进程中有多个线程同时执行不同的任务,就像QQ,既可以开视频,又可以同时打字聊天. 2.线程的特点: 1.运行任 ...

  6. java 多线程学习笔记

    这篇文章主要是个人的学习笔记,是以例子来驱动的,加深自己对多线程的理解. 一:实现多线程的两种方法 1.继承Thread class MyThread1 extends Thread{ public ...

  7. Java多线程学习笔记--生产消费者模式

    实际开发中,我们经常会接触到生产消费者模型,如:Android的Looper相应handler处理UI操作,Socket通信的响应过程.数据缓冲区在文件读写应用等.强大的模型框架,鉴于本人水平有限目前 ...

  8. Java多线程学习笔记

    进程:正在执行中的程序,其实是应用程序在内存中运行的那片空间.(只负责空间分配) 线程:进程中的一个执行单元,负责进程汇总的程序的运行,一个进程当中至少要有一个线程. 多线程:一个进程中时可以有多个线 ...

  9. C# 多线程学习笔记 - 2

    本文主要针对 GKarch 相关文章留作笔记,仅在原文基础上记录了自己的理解与摘抄部分片段. 遵循原作者的 CC 3.0 协议. 如果想要了解更加详细的文章信息内容,请访问下列地址进行学习. 原文章地 ...

随机推荐

  1. DDD - 概述 - (一)

    本片将介绍以下内容: 1).DDD是什么? 2).怎么使用DDD? 3).使用DDD应该规避或者注意什么? 一.DDD是什么? 简言之:领域驱动设计(domain driven design),顾名思 ...

  2. app:利用HBuilder打包webpack项目

    1.安装HBuilder 2.将你的项目在HBuilder中打开 3.控制台 打包编译 npm run build 4.新建一个app项目,将项目编译生成的dist文件夹 ,复制到app项目中 5.双 ...

  3. Java基础知识总结(超级经典)

    Java基础知识总结(超级经典) 写代码: 1,明确需求.我要做什么? 2,分析思路.我要怎么做?1,2,3. 3,确定步骤.每一个思路部分用到哪些语句,方法,和对象. 4,代码实现.用具体的java ...

  4. Linux多线程编程,为什么要使用线程,使用线程的理由和优点等

    线程?为什么有了进程还需要线程呢,他们有什么区别?使用线程有什么优势呢?还有多线程编程的一些细节问题,(http://www.0830120.com)如线程之间怎样同步.互斥,这些东西将在本文中介绍. ...

  5. python:PATH、PYTHONPATH 和 sys.path 的区别

    python:PATH.PYTHONPATH 和 sys.path 的区别 共同点 所有在它们的路径里面的 moduel 都可以被 import PATH 在 PATH 中的一些命令,例如 *.exe ...

  6. 机器学习之--线性回归sigmoid函数分类

    import numpy as np import matplotlib as mpl import matplotlib.pyplot as plt import random #sigmoid函数 ...

  7. Linux突然无法使用,是内存不足的问题

    昨晚发现这个问题是网站请求连不到Linux服务器了,后来发现是内存不足导致docker进程被杀掉了,docker的Status为Exit(137),是被内核杀掉的意思, 具体解释看这里,https:/ ...

  8. CSS特例定位方式

    同级向下一个元素定位,一个+表示下一个元素,++表格下下个元素 input[name='name1'] +input td:eq(0)表示第一个td元素,此定位方式限于执行js,在selenium时用 ...

  9. vs2012,打开早期版本窗体错误

    <runtime>  <NetFx40_LegacySecurityPolicy enabled="true"/></runtime>

  10. vue安装使用

    一.安装(cmd) 1.全局安装vue cnpm install --global vue-cli 2.cd到需要创建项目的文件夹下 3.创建项目 项目是基于webpack的 vue init web ...