C#多线程编程系列(二)- 线程基础

目录

  • C#多线程编程系列(二)- 线程基础
    • 1.1 简介
    • 1.2 创建线程
    • 1.3 暂停线程
    • 1.4 线程等待
    • 1.5 终止线程
    • 1.6 检测线程状态
    • 1.7 线程优先级
    • 1.8 前台线程和后台线程
    • 1.9 向线程传递参数
    • 1.10 C# Lock关键字的使用
    • 1.11 使用Monitor类锁定资源
    • 1.12 多线程中处理异常
  • 参考书籍
  • 笔者水平有限,如果错误欢迎各位批评指正!

C#多线程编程系列(二)- 线程基础


1.1 简介#

线程基础主要包括线程创建、挂起、等待和终止线程。关于更多的线程的底层实现,CPU时间片轮转等等的知识,可以参考《深入理解计算机系统》一书中关于进程和线程的章节,本文不过多赘述。

1.2 创建线程#

在C#语言中,创建线程是一件非常简单的事情;它只需要用到 System.Threading命名空间,其中主要使用Thread类来创建线程。

演示代码如下所示:

 

Copy

using System; using System.Threading; // 创建线程需要用到的命名空间 namespace Recipe1 { class Program { static void Main(string[] args) { // 1.创建一个线程 PrintNumbers为该线程所需要执行的方法 Thread t = new Thread(PrintNumbers); // 2.启动线程 t.Start(); // 主线程也运行PrintNumbers方法,方便对照 PrintNumbers(); // 暂停一下 Console.ReadKey(); } static void PrintNumbers() { // 使用Thread.CurrentThread.ManagedThreadId 可以获取当前运行线程的唯一标识,通过它来区别线程 Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 开始打印..."); for (int i = 0; i < 10; i++) { Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 打印:{i}"); } } } }

运行结果如下图所示,我们可以通过运行结果得知上面的代码创建了一个线程,然后主线程和创建的线程交叉输出结果,这说明PrintNumbers方法同时运行在主线程和另外一个线程中。

1533090931719

1.3 暂停线程#

暂停线程这里使用的方式是通过Thread.Sleep方法,如果线程执行Thread.Sleep方法,那么操作系统将在指定的时间内不为该线程分配任何时间片。如果Sleep时间100ms那么操作系统将至少让该线程睡眠100ms或者更长时间,所以Thread.Sleep方法不能作为高精度的计时器使用。

演示代码如下所示:

 

Copy

using System; using System.Threading; // 创建线程需要用到的命名空间 namespace Recipe2 { class Program { static void Main(string[] args) { // 1.创建一个线程 PrintNumbers为该线程所需要执行的方法 Thread t = new Thread(PrintNumbersWithDelay); // 2.启动线程 t.Start(); // 暂停一下 Console.ReadKey(); } static void PrintNumbersWithDelay() { Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 开始打印... 现在时间{DateTime.Now.ToString("HH:mm:ss.ffff")}"); for (int i = 0; i < 10; i++) { //3. 使用Thread.Sleep方法来使当前线程睡眠,TimeSpan.FromSeconds(2)表示时间为 2秒 Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 打印:{i} 现在时间{DateTime.Now.ToString("HH:mm:ss.ffff")}"); } } } }

运行结果如下图所示,通过下图可以确定上面的代码是有效的,通过Thread.Sleep方法,使线程休眠了2秒左右,但是并不是特别精确的2秒。验证了上面的说法,它的睡眠是至少让线程睡眠多长时间,而不是一定多长时间。

1533091915863

1.4 线程等待#

在本章中,线程等待使用的是Join方法,该方法将暂停执行当前线程,直到所等待的另一个线程终止。在简单的线程同步中会使用到,但它比较简单,不作过多介绍。

演示代码如下所示:

 

Copy

class Program { static void Main(string[] args) { Console.WriteLine($"-------开始执行 现在时间{DateTime.Now.ToString("HH:mm:ss.ffff")}-------"); // 1.创建一个线程 PrintNumbersWithDelay为该线程所需要执行的方法 Thread t = new Thread(PrintNumbersWithDelay); // 2.启动线程 t.Start(); // 3.等待线程结束 t.Join(); Console.WriteLine($"-------执行完毕 现在时间{DateTime.Now.ToString("HH:mm:ss.ffff")}-------"); // 暂停一下 Console.ReadKey(); } static void PrintNumbersWithDelay() { Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 开始打印... 现在时间{DateTime.Now.ToString("HH:mm:ss.ffff")}"); for (int i = 0; i < 10; i++) { Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 打印:{i} 现在时间{DateTime.Now.ToString("HH:mm:ss.ffff")}"); } } }

运行结果如下图所示,开始执行和执行完毕两条信息由主线程打印;根据其输出的顺序可见主线程是等待另外的线程结束后才输出执行完毕这条信息。

1533095197008

1.5 终止线程#

终止线程使用的方法是Abort方法,当该方法被执行时,将尝试销毁该线程。通过引发ThreadAbortException异常使线程被销毁。但一般不推荐使用该方法,原因有以下几点。

  1. 使用Abort方法只是尝试销毁该线程,但不一定能终止线程。
  2. 如果被终止的线程在执行lock内的代码,那么终止线程会造成线程不安全。
  3. 线程终止时,CLR会保证自己内部的数据结构不会损坏,但是BCL不能保证。

基于以上原因不推荐使用Abort方法,在实际项目中一般使用CancellationToken来终止线程。

演示代码如下所示:

 

Copy

static void Main(string[] args) { Console.WriteLine($"-------开始执行 现在时间{DateTime.Now.ToString("HH:mm:ss.ffff")}-------"); // 1.创建一个线程 PrintNumbersWithDelay为该线程所需要执行的方法 Thread t = new Thread(PrintNumbersWithDelay); // 2.启动线程 t.Start(); // 3.主线程休眠6秒 Thread.Sleep(TimeSpan.FromSeconds(6)); // 4.终止线程 t.Abort(); Console.WriteLine($"-------执行完毕 现在时间{DateTime.Now.ToString("HH:mm:ss.ffff")}-------"); // 暂停一下 Console.ReadKey(); } static void PrintNumbersWithDelay() { Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 开始打印... 现在时间{DateTime.Now.ToString("HH:mm:ss.ffff")}"); for (int i = 0; i < 10; i++) { Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 打印:{i} 现在时间{DateTime.Now.ToString("HH:mm:ss.ffff")}"); } }

运行结果如下图所示,启动所创建的线程3后,6秒钟主线程调用了Abort方法,线程3没有继续执行便结束了;与预期的结果一致。

1533096132246

1.6 检测线程状态#

线程的状态可通过访问ThreadState属性来检测,ThreadState是一个枚举类型,一共有10种状态,状态具体含义如下表所示。

成员名称说明
Aborted线程处于 Stopped 状态中。
AbortRequested已对线程调用了 Thread.Abort 方法,但线程尚未收到试图终止它的挂起的 System.Threading.ThreadAbortException
Background线程正作为后台线程执行(相对于前台线程而言)。此状态可以通过设置 Thread.IsBackground 属性来控制。
Running线程已启动,它未被阻塞,并且没有挂起的 ThreadAbortException
Stopped线程已停止。
StopRequested正在请求线程停止。这仅用于内部。
Suspended线程已挂起。
SuspendRequested正在请求线程挂起。
Unstarted尚未对线程调用 Thread.Start 方法。
WaitSleepJoin由于调用 WaitSleep 或 Join,线程已被阻止。

下表列出导致状态更改的操作。

操作ThreadState
在公共语言运行库中创建线程。Unstarted
线程调用 StartUnstarted
线程开始运行。Running
线程调用 SleepWaitSleepJoin
线程对其他对象调用 Wait。WaitSleepJoin
线程对其他线程调用 Join。WaitSleepJoin
另一个线程调用 InterruptRunning
另一个线程调用 SuspendSuspendRequested
线程响应 Suspend 请求。Suspended
另一个线程调用 ResumeRunning
另一个线程调用 AbortAbortRequested
线程响应 Abort 请求。Stopped
线程被终止。Stopped

演示代码如下所示:

 

Copy

static void Main(string[] args) { Console.WriteLine("开始执行..."); Thread t = new Thread(PrintNumbersWithStatus); Thread t2 = new Thread(DoNothing); // 使用ThreadState查看线程状态 此时线程未启动,应为Unstarted Console.WriteLine($"Check 1 :{t.ThreadState}"); t2.Start(); t.Start(); // 线程启动, 状态应为 Running Console.WriteLine($"Check 2 :{t.ThreadState}"); // 由于PrintNumberWithStatus方法开始执行,状态为Running // 但是经接着会执行Thread.Sleep方法 状态会转为 WaitSleepJoin for (int i = 1; i < 30; i++) { Console.WriteLine($"Check 3 : {t.ThreadState}"); } // 延时一段时间,方便查看状态 Thread.Sleep(TimeSpan.FromSeconds(6)); // 终止线程 t.Abort(); Console.WriteLine("t线程被终止"); // 由于该线程是被Abort方法终止 所以状态为 Aborted或AbortRequested Console.WriteLine($"Check 4 : {t.ThreadState}"); // 该线程正常执行结束 所以状态为Stopped Console.WriteLine($"Check 5 : {t2.ThreadState}"); Console.ReadKey(); } static void DoNothing() { Thread.Sleep(TimeSpan.FromSeconds(2)); } static void PrintNumbersWithStatus() { Console.WriteLine("t线程开始执行..."); // 在线程内部,可通过Thread.CurrentThread拿到当前线程Thread对象 Console.WriteLine($"Check 6 : {Thread.CurrentThread.ThreadState}"); for (int i = 1; i < 10; i++) { Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine($"t线程输出 :{i}"); } }

运行结果如下图所示,与预期的结果一致。

1533107472877

1.7 线程优先级#

Windows操作系统为抢占式多线程(Preemptive multithreaded)操作系统,是因为线程可在任何时间停止(被枪占)并调度另一个线程。

Windows操作系统中线程有0(最低) ~ 31(最高)的优先级,而优先级越高所能占用的CPU时间就越多,确定某个线程所处的优先级需要考虑进程优先级相对线程优先级两个优先级。

  1. 进程优先级:Windows支持6个进程优先级,分别是Idle、Below Normal、Normal、Above normal、High 和Realtime。默认为Normal
  2. 相对线程优先级:相对线程优先级是相对于进程优先级的,因为进程包含了线程。Windows支持7个相对线程优先级,分别是Idle、Lowest、Below Normal、Normal、Above Normal、Highest 和 Time-Critical.默认为Normal

下表总结了进程的优先级线程的相对优先级优先级(0~31)的映射关系。粗体为相对线程优先级,斜体为进程优先级

 IdleBelow NormalNormalAbove NormalHighRealtime
Time-Critical151515151531
Highest6810121526
Above Normal579111425
Normal468101324
Below Normal35791223
Lowest24681122
Idle1111116

而在C#程序中,可更改线程的相对优先级,需要设置ThreadPriority属性,可设置为ThreadPriority枚举类型的五个值之一:Lowest、BelowNormal、Normal、AboveNormal 或 Highest。CLR为自己保留了IdleTime-Critical优先级,程序中不可设置。

演示代码如下所示。

 

Copy

static void Main(string[] args) { Console.WriteLine($"当前线程优先级: {Thread.CurrentThread.Priority} \r\n"); // 第一次测试,在所有核心上运行 Console.WriteLine("运行在所有空闲的核心上"); RunThreads(); Thread.Sleep(TimeSpan.FromSeconds(2)); // 第二次测试,在单个核心上运行 Console.WriteLine("\r\n运行在单个核心上"); // 设置在单个核心上运行 System.Diagnostics.Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(1); RunThreads(); Console.ReadLine(); } static void RunThreads() { var sample = new ThreadSample(); var threadOne = new Thread(sample.CountNumbers); threadOne.Name = "线程一"; var threadTwo = new Thread(sample.CountNumbers); threadTwo.Name = "线程二"; // 设置优先级和启动线程 threadOne.Priority = ThreadPriority.Highest; threadTwo.Priority = ThreadPriority.Lowest; threadOne.Start(); threadTwo.Start(); // 延时2秒 查看结果 Thread.Sleep(TimeSpan.FromSeconds(2)); sample.Stop(); } class ThreadSample { private bool _isStopped = false; public void Stop() { _isStopped = true; } public void CountNumbers() { long counter = 0; while (!_isStopped) { counter++; } Console.WriteLine($"{Thread.CurrentThread.Name} 优先级为 {Thread.CurrentThread.Priority,11} 计数为 = {counter,13:N0}"); } }

运行结果如下图所示。Highest占用的CPU时间明显多于Lowest。当程序运行在所有核心上时,线程可以在不同核心同时运行,所以HighestLowest差距会小一些。

1533109869998

1.8 前台线程和后台线程#

在CLR中,线程要么是前台线程,要么就是后台线程。当一个进程的所有前台线程停止运行时,CLR将强制终止仍在运行的任何后台线程,不会抛出异常。

在C#中可通过Thread类中的IsBackground属性来指定是否为后台线程。在线程生命周期中,任何时候都可从前台线程变为后台线程。线程池中的线程默认为后台线程

演示代码如下所示。

 

Copy

static void Main(string[] args) { var sampleForeground = new ThreadSample(10); var sampleBackground = new ThreadSample(20); var threadPoolBackground = new ThreadSample(20); // 默认创建为前台线程 var threadOne = new Thread(sampleForeground.CountNumbers); threadOne.Name = "前台线程"; var threadTwo = new Thread(sampleBackground.CountNumbers); threadTwo.Name = "后台线程"; // 设置IsBackground属性为 true 表示后台线程 threadTwo.IsBackground = true; // 线程池内的线程默认为 后台线程 ThreadPool.QueueUserWorkItem((obj) => { Thread.CurrentThread.Name = "线程池线程"; threadPoolBackground.CountNumbers(); }); // 启动线程 threadOne.Start(); threadTwo.Start(); } class ThreadSample { private readonly int _iterations; public ThreadSample(int iterations) { _iterations = iterations; } public void CountNumbers() { for (int i = 0; i < _iterations; i++) { Thread.Sleep(TimeSpan.FromSeconds(0.5)); Console.WriteLine($"{Thread.CurrentThread.Name} prints {i}"); } } }

运行结果如下图所示。当前台线程10次循环结束以后,创建的后台线程和线程池线程都会被CLR强制结束。

1533116008700

1.9 向线程传递参数#

向线程中传递参数常用的有三种方法,构造函数传值、Start方法传值和Lambda表达式传值,一般常用Start方法来传值。

演示代码如下所示,通过三种方式来传递参数,告诉线程中的循环最终需要循环几次。

 

Copy

static void Main(string[] args) { // 第一种方法 通过构造函数传值 var sample = new ThreadSample(10); var threadOne = new Thread(sample.CountNumbers); threadOne.Name = "ThreadOne"; threadOne.Start(); threadOne.Join(); Console.WriteLine("--------------------------"); // 第二种方法 使用Start方法传值 // Count方法 接收一个Object类型参数 var threadTwo = new Thread(Count); threadTwo.Name = "ThreadTwo"; // Start方法中传入的值 会传递到 Count方法 Object参数上 threadTwo.Start(8); threadTwo.Join(); Console.WriteLine("--------------------------"); // 第三种方法 Lambda表达式传值 // 实际上是构建了一个匿名函数 通过函数闭包来传值 var threadThree = new Thread(() => CountNumbers(12)); threadThree.Name = "ThreadThree"; threadThree.Start(); threadThree.Join(); Console.WriteLine("--------------------------"); // Lambda表达式传值 会共享变量值 int i = 10; var threadFour = new Thread(() => PrintNumber(i)); i = 20; var threadFive = new Thread(() => PrintNumber(i)); threadFour.Start(); threadFive.Start(); } static void Count(object iterations) { CountNumbers((int)iterations); } static void CountNumbers(int iterations) { for (int i = 1; i <= iterations; i++) { Thread.Sleep(TimeSpan.FromSeconds(0.5)); Console.WriteLine($"{Thread.CurrentThread.Name} prints {i}"); } } static void PrintNumber(int number) { Console.WriteLine(number); } class ThreadSample { private readonly int _iterations; public ThreadSample(int iterations) { _iterations = iterations; } public void CountNumbers() { for (int i = 1; i <= _iterations; i++) { Thread.Sleep(TimeSpan.FromSeconds(0.5)); Console.WriteLine($"{Thread.CurrentThread.Name} prints {i}"); } } }

运行结果如下图所示,与预期结果相符。

1533124776052

1.10 C# Lock关键字的使用#

在多线程的系统中,由于CPU的时间片轮转等线程调度算法的使用,容易出现线程安全问题。具体可参考《深入理解计算机系统》一书相关的章节。

在C#中lock关键字是一个语法糖,它将Monitor封装,给object加上一个互斥锁,从而实现代码的线程安全,Monitor会在下一节中介绍。

对于lock关键字还是Monitor锁定的对象,都必须小心选择,不恰当的选择可能会造成严重的性能问题甚至发生死锁。以下有几条关于选择锁定对象的建议。

  1. 同步锁定的对象不能是值类型。因为使用值类型时会有装箱的问题,装箱后的就成了一个新的实例,会导致Monitor.Enter()Monitor.Exit()接收到不同的实例而失去关联性
  2. 避免锁定this、typeof(type)和stringthistypeof(type)锁定可能在其它不相干的代码中会有相同的定义,导致多个同步块互相阻塞。string需要考虑字符串拘留的问题,如果同一个字符串常量在多个地方出现,可能引用的会是同一个实例。
  3. 对象的选择作用域尽可能刚好达到要求,使用静态的、私有的变量。

以下演示代码实现了多线程情况下的计数功能,一种实现是线程不安全的,会导致结果与预期不相符,但也有可能正确。另外一种使用了lock关键字进行线程同步,所以它结果是一定的。

 

Copy

static void Main(string[] args) { Console.WriteLine("错误的多线程计数方式"); var c = new Counter(); // 开启3个线程,使用没有同步块的计数方式对其进行计数 var t1 = new Thread(() => TestCounter(c)); var t2 = new Thread(() => TestCounter(c)); var t3 = new Thread(() => TestCounter(c)); t1.Start(); t2.Start(); t3.Start(); t1.Join(); t2.Join(); t3.Join(); // 因为多线程 线程抢占等原因 其结果是不一定的 碰巧可能为0 Console.WriteLine($"Total count: {c.Count}"); Console.WriteLine("--------------------------"); Console.WriteLine("正确的多线程计数方式"); var c1 = new CounterWithLock(); // 开启3个线程,使用带有lock同步块的方式对其进行计数 t1 = new Thread(() => TestCounter(c1)); t2 = new Thread(() => TestCounter(c1)); t3 = new Thread(() => TestCounter(c1)); t1.Start(); t2.Start(); t3.Start(); t1.Join(); t2.Join(); t3.Join(); // 其结果是一定的 为0 Console.WriteLine($"Total count: {c1.Count}"); Console.ReadLine(); } static void TestCounter(CounterBase c) { for (int i = 0; i < 100000; i++) { c.Increment(); c.Decrement(); } } // 线程不安全的计数 class Counter : CounterBase { public int Count { get; private set; } public override void Increment() { Count++; } public override void Decrement() { Count--; } } // 线程安全的计数 class CounterWithLock : CounterBase { private readonly object _syncRoot = new Object(); public int Count { get; private set; } public override void Increment() { // 使用Lock关键字 锁定私有变量 lock (_syncRoot) { // 同步块 Count++; } } public override void Decrement() { lock (_syncRoot) { Count--; } } } abstract class CounterBase { public abstract void Increment(); public abstract void Decrement(); }

运行结果如下图所示,与预期结果相符。

1533126787553

1.11 使用Monitor类锁定资源#

Monitor类主要用于线程同步中, lock关键字是对Monitor类的一个封装,其封装结构如下代码所示。

 

Copy

try { Monitor.Enter(obj); dosomething(); } catch(Exception ex) { } finally { Monitor.Exit(obj); }

以下代码演示了使用Monitor.TyeEnter()方法避免资源死锁和使用lock发生资源死锁的场景。

 

Copy

static void Main(string[] args) { object lock1 = new object(); object lock2 = new object(); new Thread(() => LockTooMuch(lock1, lock2)).Start(); lock (lock2) { Thread.Sleep(1000); Console.WriteLine("Monitor.TryEnter可以不被阻塞, 在超过指定时间后返回false"); // 如果5S不能进入同步块,那么返回。 // 因为前面的lock锁定了 lock2变量 而LockTooMuch()一开始锁定了lock1 所以这个同步块无法获取 lock1 而LockTooMuch方法内也不能获取lock2 // 只能等待TryEnter超时 释放 lock2 LockTooMuch()才会是释放 lock1 if (Monitor.TryEnter(lock1, TimeSpan.FromSeconds(5))) { Console.WriteLine("获取保护资源成功"); } else { Console.WriteLine("获取资源超时"); } } new Thread(() => LockTooMuch(lock1, lock2)).Start(); Console.WriteLine("----------------------------------"); lock (lock2) { Console.WriteLine("这里会发生资源死锁"); Thread.Sleep(1000); // 这里必然会发生死锁 // 本同步块 锁定了 lock2 无法得到 lock1 // 而 LockTooMuch 锁定了 lock1 无法得到 lock2 lock (lock1) { // 该语句永远都不会执行 Console.WriteLine("获取保护资源成功"); } } } static void LockTooMuch(object lock1, object lock2) { lock (lock1) { Thread.Sleep(1000); lock (lock2) ; } }

运行结果如下图所示,因为使用Monitor.TryEnter()方法在超时以后会返回,不会阻塞线程,所以没有发生死锁。而第二段代码中lock没有超时返回的功能,导致资源死锁,同步块中的代码永远不会被执行。

1533127789225

1.12 多线程中处理异常#

在多线程中处理异常应当使用就近原则,在哪个线程发生异常那么所在的代码块一定要有相应的异常处理。否则可能会导致程序崩溃、数据丢失。

主线程中使用try/catch语句是不能捕获创建线程中的异常。但是万一遇到不可预料的异常,可通过监听AppDomain.CurrentDomain.UnhandledException事件来进行捕获和异常处理。

演示代码如下所示,异常处理 1 和 异常处理 2 能正常被执行,而异常处理 3 是无效的。

 

Copy

static void Main(string[] args) { // 启动线程,线程代码中进行异常处理 var t = new Thread(FaultyThread); t.Start(); t.Join(); // 捕获全局异常 AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; t = new Thread(BadFaultyThread); t.Start(); t.Join(); // 线程代码中不进行异常处理,尝试在主线程中捕获 AppDomain.CurrentDomain.UnhandledException -= CurrentDomain_UnhandledException; try { t = new Thread(BadFaultyThread); t.Start(); } catch (Exception ex) { // 永远不会运行 Console.WriteLine($"异常处理 3 : {ex.Message}"); } } private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { Console.WriteLine($"异常处理 2 :{(e.ExceptionObject as Exception).Message}"); } static void BadFaultyThread() { Console.WriteLine("有异常的线程已启动..."); Thread.Sleep(TimeSpan.FromSeconds(2)); throw new Exception("Boom!"); } static void FaultyThread() { try { Console.WriteLine("有异常的线程已启动..."); Thread.Sleep(TimeSpan.FromSeconds(1)); throw new Exception("Boom!"); } catch (Exception ex) { Console.WriteLine($"异常处理 1 : {ex.Message}"); } }

运行结果如下图所示,与预期结果一致。

1533129416654

参考书籍

本文主要参考了以下几本书,在此对这些作者表示由衷的感谢你们提供了这么好的资料。

  1. 《CLR via C#》
  2. 《C# in Depth Third Edition》
  3. 《Essential C# 6.0》
  4. 《Multithreading with C# Cookbook Second Edition》

线程基础这一章节终于整理完了,是笔者学习过程中的笔记和思考。计划按照《Multithreading with C# Cookbook Second Edition》这本书的结构,一共更新十二个章节,先立个Flag。


源码下载点击链接 示例源码下载

笔者水平有限,如果错误欢迎各位批评指正!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/438440.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C#多线程编程系列(三)- 线程同步

目录 1.1 简介1.2 执行基本原子操作1.3 使用Mutex类1.4 使用SemaphoreSlim类1.5 使用AutoResetEvent类1.6 使用ManualResetEventSlim类1.7 使用CountDownEvent类1.8 使用Barrier类1.9 使用ReaderWriterLockSlim类1.10 使用SpinWait类参考书籍笔者水平有限&#xff0c;如果错误…

C#多线程编程系列(四)- 使用线程池

目录 1.1 简介1.2 在线程池中调用委托1.3 向线程池中放入异步操作1.4 线程池与并行度1.5 实现一个取消选项1.6 在线程池中使用等待事件处理器及超时1.7 使用计时器1.8 使用BackgroundWorker组件参考书籍笔者水平有限&#xff0c;如果错误欢迎各位批评指正&#xff01;1.1 简介…

C#线程模型脉络

今天在看同事新买到的《C#本质论 Edition 4》的时候&#xff0c;对比下以前Edtion3的新特性时&#xff0c;针对Async/Await关键字时发现对一些线程方面的定义还理解的不是很透彻&#xff0c;脉络还不是很清晰&#xff0c;这样有了本文&#xff0c;希望对有同样困惑的朋友有些帮…

“菜”鸟理解.NET Framework(CLI,CLS,CTS,CLR,FCL,BCL)

既然要学.NET&#xff0c;就要先认识认识她&#xff0c;我不喜欢大段大段文字的东西&#xff0c;自己通过理解&#xff0c;画个图&#xff0c;来看看.NET的沉鱼落雁&#xff0c;闭月羞花之容。 最下层蓝色部分是.NET Framework的基础&#xff0c;也是所有应用软件的基础。.NET …

QQ炫舞手游显示进入服务器失败6,qq炫舞手游进不去怎么办 游戏进不去方法详解[多图]...

qq炫舞手游是新出的游戏&#xff0c;在近期非常的火爆&#xff0c;不过有不少的玩家都有进不了游戏的情况&#xff0c;下面安族小编给大家介绍一下游戏进不去方法详解。qq炫舞手游玩不了解决方法1.第一种方式就是内测还没有开启咯&#xff0c;所以玩家一般都是下载不到包的&…

那些年我们一起追逐的多线程(Thread、ThreadPool、委托异步调用、Task/TaskFactory、Parallerl、async和await)

一. 背景 在刚接触开发的头几年里&#xff0c;说实话&#xff0c;根本不考虑多线程的这个问题&#xff0c;貌似那时候脑子里也有没有多线程的这个概念&#xff0c;所有的业务都是一个线程来处理&#xff0c;不考虑性能问题&#xff0c;当然也没有考虑多线程操作一条记录存在的并…

asp.net mvc webform和razor的page基类区别

接触过asp.net mvc的都知道&#xff0c;在传统的webform的模式下&#xff0c;page页面的基类是这样声明的&#xff1a; <% Page Language"C#" MasterPageFile"~/Views/Shared/Site.Master" Inherits"ViewPage" %> 如果是partial view的话…

frameset ajax,js控制frameSet示例

js控制frameSet示例1&#xff1a;js修改frameset的cols属性来达到修改各个页面所占的宽高&#xff0c;例如隐藏当前frame页。复制代码 代码如下:window.parent.document.getElementsByTagName("frameset")[0].cols"0,*";2&#xff1a;js调用其他frame页面的…

人生的抉择—aspx、ashx、asmx文件处理请求效率比较

总结&#xff1a; ashx&#xff1a;只是实现IHttpHandler接口 aspx&#xff1a;public class Page : TemplateControl, IHttpHandler 而TemplateControl是&#xff1a; abstract class TemplateControl : Control, INamingContainer, IFilterResolutionService 所以aspx是重型…

ajax jsp模糊查询源码,Ajax动态执行模糊查询功能

Ajax动态执行模糊查询功能 内容精选换一换Profiling采集的数据较多&#xff0c;同时解析后展示的性能指标项也比较多&#xff0c;为方便用户快捷查找到具体性能指标的含义&#xff0c;提供命令行查询功能&#xff1a;不包含metric_name参数时&#xff0c;打印hiprof.sh脚本帮助…

一般处理程序(ashx)和页面处理程序(aspx)的区别

客官请看图 图中的Httphandler就是处理程序。 两者的共同点 如果把aspx处理程序和ashx处理程序放到上图中&#xff0c;他们是处在相同的位置的&#xff0c; 他们都实现了IHttphandler接口。实现了IHttphandler才具备处理请求的能力 两者的不同点 微软对aspx下足了功夫&#…

怎么在微云服务器找一个文件,微云文件在哪里打开_怎么快速找到微云文件

微云文件在哪里打开&#xff0c;这个问题困扰了许多的朋友&#xff0c;今天小编带着教程走向大家&#xff0c;我们可以使用微云可以快速连接和预览在线文档&#xff1b;同时加载和卸载多终端、手机、计算机、同步PAD数据传输&#xff0c;无需数据线。对于微云客户端&#xff0c…

ASP.NET Core真实管道详解[1]

ASP.NET Core管道虽然在结构组成上显得非常简单&#xff0c;但是在具体实现上却涉及到太多的对象&#xff0c;所以我们在 《ASP.NET Core管道深度剖析[共4篇]》 中围绕着一个经过极度简化的模拟管道讲述了真实管道构建的方式以及处理HTTP请求的流程。在这个系列 中&#xff0c;…

Wayland 显示服务器,wayland 1.8.0 发布,显示服务器

Wayland 1.8.0 发布&#xff0c;该版本现已提供下载&#xff1a; wayland-1.8.0.tar.xz&#xff1a;该版本与 RC2 相比的变化&#xff1a;publish-doc: Add script for publishing docs to the websiteconfigure.ac: bump to version 1.8.0 for the official releasescanner: d…

华为把服务器虚拟底层锁了,华为全面关闭解码锁服务:马上升级到很吓人的技术!...

原标题&#xff1a;华为全面关闭解码锁服务&#xff1a;马上升级到很吓人的技术&#xff01;华为余承东被网友称之为余大嘴&#xff0c;华为P20 Pro发布之后&#xff0c;余大嘴豪言华为P20的销量将超过2000万部&#xff0c;很就会推出一项“很吓人的技术”&#xff0c;没想到仅…

ASP.NET Core真实管道详解[2]:Server是如何完成针对请求的监听、接收与响应的【上】

Server是ASP .NET Core管道的第一个节点&#xff0c;负责完整请求的监听和接收&#xff0c;最终对请求的响应同样也由它完成。Server是我们对所有实现了IServer接口的所有类型以及对应对象的统称&#xff0c;如下面的代码片段所示&#xff0c;这个接口具有一个只读属性Features…

pos机未能连接服务器,pos 机链接不了服务器

pos 机链接不了服务器 内容精选换一换在本章节中&#xff0c;您将运行已部署好的游戏&#xff0c;登录游戏客户端。已准备好Windows机器&#xff0c;硬盘至少20G&#xff0c;且必须安装有显卡。服务器地址&#xff1a;节点的弹性IP地址&#xff0c;请登录CCE控制台&#xff0c;…

ASP.NET Core管道深度剖析(1):采用管道处理HTTP请求

之所以称ASP.NET Core是一个Web开发平台&#xff0c;源于它具有一个极具扩展性的请求处理管道&#xff0c;我们可以通过这个管道的定制来满足各种场景下的HTTP处理需求。ASP. NET Core应用的很多特性&#xff0c;比如路由、认证、会话、缓存等&#xff0c;也同时定制消息处理管…

emui消息推送服务器,别再抱怨,这次或许真的轮到你了,EMUI9.1推送进度再次更新...

最近几年里&#xff0c;华为在系统方面下的功夫可谓是大家有目共睹的。以往大家在使用华为EMUI操作系统的时候&#xff0c;或许会感觉到卡顿、应用启动时间过长、运行不流畅以及UI界面毫无亮点可言等&#xff0c;但那已经是过去的EMUI系统了&#xff0c;今日的EMUI系统已与往日…

ASP.NET Core管道深度剖析(2):创建一个“迷你版”的管道来模拟真实管道请求处理流程

从《ASP.NET Core管道深度剖析&#xff08;1&#xff09;&#xff1a;采用管道处理HTTP请求》我们知道ASP.NET Core请求处理管道由一个服务器和一组有序的中间件组成&#xff0c;所以从总体设计来讲是非常简单的&#xff0c;但是就具体的实现来说&#xff0c;由于其中涉及很多对…