贺州同城购物网站建设,网站建设的淘宝模板,网站制作公司推荐,班级网站建设活动方案话不多#xff0c;但比较中肯#xff0c;本文参照c# 线程暂停继续的实现方式_哔哩哔哩_bilibili
一、老方式 1、这是一个老的实现方式#xff0c;基本不推荐#xff0c;背后控制的原理需要了解。 界面#xff1a;三个button一个textbox … 话不多但比较中肯本文参照c# 线程暂停继续的实现方式_哔哩哔哩_bilibili
一、老方式 1、这是一个老的实现方式基本不推荐背后控制的原理需要了解。 界面三个button一个textbox 代码 private volatile bool isPause false;//fprivate void BtnStart_Click(object sender, EventArgs e){Task.Run(() {int i 0;while (true){if (isPause)//a{ continue; }i;Invoke(new Action(() {TxtInfo.AppendText($[{i.ToString(000]).PadRight(8, )}{DateTime.Now.TimeOfDay}{Environment.NewLine});}));//bThread.Sleep(50);//c//线程要做事放这里...}});}private void BtnPause_Click(object sender, EventArgs e){ this.isPause true; }private void BtnContinue_Click(object sender, EventArgs e){ this.isPause false; } 上面一直在异步线程中无限循环由UI线程的isPause来控制异步线程中的循环分支。 1、问f处为什么要用volatile 答volatile关键字用于修饰isPause变量它的作用是确保多个线程对该变量的读取和写入操作都是从主内存中进行的而不是从线程的本地缓存中进行的。这样可以保证多个线程对该变量的操作是可见的避免了由于线程之间的数据不一致性而引发的问题。 在多线程环境中为了提高性能每个线程会将共享变量存储在自己的线程本地缓存中而不是直接从主内存中读取和写入。这样可以减少对主内存的访问提高了程序的执行效率。 然而这也带来了一个问题即线程之间的共享变量可能存在数据不一致性的问题。当一个线程修改了共享变量的值时其他线程可能无法立即感知到这个变化因为它们仍然在使用自己本地缓存中的旧值。这就导致了线程之间的数据不一致可能会引发错误的行为。 为了解决这个问题可以使用volatile关键字来修饰共享变量。它的作用是告诉编译器和处理器这个变量可能会被其他线程修改因此每次读取和写入该变量时都要直接从主内存中进行操作而不是使用线程本地缓存。这样可以确保线程之间对共享变量的操作是可见的避免了数据不一致性带来的问题。 如果在程序中使用volatile关键字修饰了isPause变量那么异步CPU不会一直访问该变量。volatile关键字的作用是保证共享变量的可见性当一个线程修改了volatile变量的值时其他线程能够立即感知到这个变化。这样可以避免线程一直访问旧值提高程序的执行效率。 上面明显的Task创建的异步线程一直在访问isPause而UI线程也在访问修改isPause所以有必要加上volatile。 2、问a处为什么占用CPU比较高 答对于使用volatile关键字来实现暂停或取消操作的方式每次都需要在异步线程中判断isPause变量的值这会导致CPU不断进行判断操作从而占用较高的CPU资源。 优化的方案就是用CancellationTokensource一个控制线程标志位的类它是.NET中提供的一种用于取消操作的机制。通过使用CancellationTokensource可以创建一个CancellationToken对象用于监视和取消异步操作。 CancellationTokensource可以降低对CPU的占用主要是因为它使用了一种基于事件的机制来实现取消操作。在异步操作中可以通过检查CancellationToken对象的IsCancellationRequested属性来判断是否需要取消操作。当调用CancellationTokensource的Cancel方法时会触发CancellationToken对象的取消事件从而通知异步操作需要取消。 相比于使用volatile关键字来取消异步操作CancellationTokensource的机制更加高效。因为CancellationTokensource使用了事件来通知取消操作而不是简单地通过轮询检查volatile变量的值。这样可以避免不必要的CPU占用提高程序的性能。 总结起来使用CancellationTokensource可以降低对CPU的占用因为它使用了基于事件的机制来实现取消操作相比于使用volatile关键字来取消操作更加高效。 3、问没触发时还不是要Cpu来处理就不占用cpu了这时是谁来处理呢 答在使用CancellationTokensource的情况下当没有触发取消事件时异步操作并不会一直占用CPU资源。相反异步操作会进入等待状态直到有新的任务或事件触发。 具体来说当没有触发取消事件时异步操作会进入等待状态并且不会占用CPU资源。当取消事件被触发时异步操作会被唤醒并且开始处理取消操作。这个过程是由系统的线程调度器来管理和控制的。 在异步操作等待的过程中CPU资源会被释放可以用于处理其他任务。这样可以避免不必要的CPU占用提高系统的整体性能。 因此使用CancellationTokensource的方式可以在没有取消事件发生时避免异步操作一直占用CPU资源而是由系统的线程调度器来控制异步操作的执行。这样可以更好地利用CPU资源提高程序的性能。 4、问为什么c处要加入延时 答b处的操作在UI线程中执行耗时的操作会导致UI线程阻塞使得界面无法响应用户的操作甚至可能导致界面卡顿或崩溃。 为了避免UI线程的阻塞可以将耗时的操作放在后台线程中执行然后通过合适的方式将结果传递给UI线程进行更新。在这种情况下可以使用异步操作或线程池来执行耗时的操作并在操作完成后通过合适的方式通知UI线程进行更新。 通过在c处使用等待来缓解UI线程可以让UI线程有空闲的时间来进行绘制、移动等操作从而提高界面的响应性能。等待的时间可以根据实际情况来设置以确保UI线程有足够的空闲时间来处理其他任务。 通过将耗时的操作放在后台线程中执行并采取合适的方式来缓解UI线程的压力可以确保UI线程的响应性能提高用户体验。这是在处理耗时操作时常用的一种策略。 去除c句或将c处改为Thread.Sleep(500)时用鼠标拖动主界面form1会有明显的反应效果。 5、问当异步线程不处于暂停时关闭form1会出错 答当程序关闭时如果异步操作仍在进行中可能会导致一些问题例如在追加文本的操作中出现错误(b处)。 如果在程序关闭时异步操作仍在执行追加文本的操作可能会导致文件句柄被关闭或文件访问被中断从而导致错误的发生。这是因为程序关闭时会终止所有的线程包括异步操作所在的线程。 为了避免这种情况可以在程序关闭之前确保所有的异步操作都已经完成。可以通过等待异步操作完成或者取消异步操作来确保程序关闭时不会出现错误。 后面其实实现方式中可以在程序关闭时先调用CancellationTokensource的Cancel方法来取消异步操作然后等待异步操作完成。这样可以确保在程序关闭之前异步操作已经被取消或完成避免出现错误。 总之为了避免在程序关闭时出现错误需要确保所有的异步操作都已经完成或取消。可以通过适当的方式来等待异步操作完成或者在程序关闭时取消异步操作。这样可以确保程序关闭时的稳定性和正确性。 6、问:如何解决上面关闭时提示错误的情况 答b处应该改为BeginInvoke可以避免关闭时提示错误。 BeginInvoke方法会将指定的方法放入UI线程的消息队列中由UI线程在适当的时候执行。这样可以确保在程序关闭时异步操作已经完成或取消避免出现错误。 复习invoke与begineinvoke的区别 Invoke方法是同步等待执行的它会阻塞当前线程直到被调用的方法执行完成并返回结果。这意味着在UI线程中使用Invoke方法执行耗时的操作时会导致UI线程阻塞界面无法响应用户的操作。 而BeginInvoke方法是异步等待执行的它会将指定的方法放入UI线程的消息队列中并立即返回。UI线程会在适当的时候执行该方法。这样可以避免UI线程的阻塞保持界面的响应性能。 另外BeginInvoke方法还可以通过IAsyncResult接口来实现预先取消的功能。通过调用IAsyncResult接口的AsyncWaitHandle属性的WaitOne方法可以等待异步操作完成或取消。如果异步操作已经开始执行但在等待期间被取消如程序关闭WaitOne方法会返回一个取消的信号而不会导致错误。 因此使用BeginInvoke方法可以实现异步等待执行并具有预先取消的功能。这样可以确保在程序关闭时异步操作会被正确地执行或取消避免出现错误。 7、上面程序有个小bug在开始线程后可以暂停或继续但在暂停后不能再点开始点了没效果。 因为开始中无法修改isPause如果此时是暂停或继续将不受影响。因此应该有一个标志来判断当前线程是否开启或未开再判断现在的状况进行处理。
二、事件方式 1、仍然前面的。界面 程序 private ManualResetEvent resetEvent new ManualResetEvent(true);//aprivate void BtnStart_Click(object sender, EventArgs e){Task.Run(() {int i 0;while (true){resetEvent.WaitOne();//bi;BeginInvoke(new Action(() {TxtInfo.AppendText($[{i.ToString(000]).PadRight(8, )}{DateTime.Now.TimeOfDay}\r\n);}));Thread.Sleep(100);//线程要做事放这里...}});}private void BtnPause_Click(object sender, EventArgs e){ resetEvent.Reset(); }private void BtnContinue_Click(object sender, EventArgs e){ resetEvent.Set(); } 现在去除占用cpu较高的isPause方式,使用基于事件激活方式的ManualResetEvent。 1、问什么是ManualResetEvent 答ManualResetEvent有点类似保一道门要么同步阻塞关闭门要么同步放行打开门。Manual是手动的意思也就是打开了就一直打开不会自动关闭关闭了就一直关闭不会自动又去打开每次都得人工操作。 ManualResetEvent是一个线程同步原语用于在多线程环境下进行线程间的同步和通信。它提供了一种简单的方式来阻塞一个或多个线程直到接收到信号或超时。 ManualResetEvent有两种状态有信号状态(Set)和无信号状态。当处于有信号状态时调用WaitOne方法的线程会立即继续执行。当处于无信号状态时调用WaitOne方法的线程会被阻塞直到接收到信号。 ManualResetEvent的主要方法和属性包括 - WaitOne()阻塞当前线程直到接收到信号。 - WaitOne(int millisecondsTimeout)阻塞当前线程直到接收到信号或超时。 - Set()将ManualResetEvent的状态设置为有信号状态唤醒等待的线程。 - Reset()将ManualResetEvent的状态设置为无信号状态。 - WaitHandle属性获取ManualResetEvent的WaitHandle对象可以用于在多个等待句柄之间进行等待。 使用ManualResetEvent可以实现多线程的同步和通信。例如可以在一个线程中调用Set方法来设置信号然后在其他线程中调用WaitOne方法来等待信号。当信号被设置时等待的线程会被唤醒并继续执行。 注意ManualResetEvent是一次性的即一旦信号被设置等待的线程会被唤醒但信号不会自动重置。需要调用Reset方法来将信号重置为无信号状态以便下一次等待。 2、问ManualResetEvent里面有Event它内部是事件激活机制吗 答是的ManualResetEvent内部使用了事件Event来实现线程的同步和通信。 在.NET中事件Event是一种基于委托的异步通信机制用于线程间的通信。事件由一个事件源Event Source和一个或多个事件处理程序Event Handler组成。事件源可以触发事件而事件处理程序可以订阅事件并在事件触发时执行相应的操作。 在ManualResetEvent中使用了一个内部的ManualResetEventSlim对象来实现事件的激活机制。ManualResetEventSlim是ManualResetEvent的轻量级版本它使用了更高效的同步机制来实现线程的等待和唤醒。 当调用Set方法时ManualResetEventSlim会激活内部的事件通知等待的线程可以继续执行。而调用Reset方法时ManualResetEventSlim会重新等待事件的触发。 通过使用事件激活机制ManualResetEvent可以实现线程的同步和通信。一个线程可以在某个条件满足时调用Set方法激活事件而其他线程可以在事件触发时被唤醒并继续执行。 注意ManualResetEvent中的事件激活机制是基于内部的ManualResetEventSlim对象实现的而不是直接使用.NET中的事件Event机制。 总之ManualResetEvent使用了事件激活机制来实现线程的同步和通信。通过激活事件它可以通知等待的线程可以继续执行。这种机制可以用于实现线程间的协调和通信。 3、问ManualResetEvent与AutoResetEvent有什么区别 答AutoResetEvent和ManualResetEvent是两种不同的线程同步机制主要区别在于事件的自动重置行为。 AutoResetEvent会在一个线程等待事件后只允许一个线程通过并自动将事件状态重置为无信号状态。也就是说一旦有一个线程通过了事件事件会自动重置为无信号状态其他线程需要重新等待事件的触发。 而ManualResetEvent则允许多个线程同时通过事件并且不会自动重置事件状态。也就是说一旦有一个线程通过了事件事件会保持有信号状态其他线程仍然可以通过事件而不需要重新等待。 因此AutoResetEvent适用于一次只允许一个线程通过的场景例如生产者-消费者模型中的消费者线程。每当一个消费者线程从缓冲区中取走一个数据后AutoResetEvent会自动将事件状态重置为无信号状态其它消费者线程需要重新等待。 而ManualResetEvent适用于多个线程可以同时通过的场景例如某个条件满足时多个线程需要同时执行某个操作。ManualResetEvent可以保持事件状态为有信号状态以允许多个线程通过。 总结起来AutoResetEvent在一个线程通过事件后会自动重置事件状态而ManualResetEvent允许多个线程通过事件并且不会自动重置事件状态。选择使用哪种类型的事件取决于具体的需求和场景。 4、问AutoResetEvent类似自动门打开后会自动关闭主要用在什么场景 答AutoResetEvent类似为一个自动门。一旦有一个线程通过了门即事件被激活门会自动关闭其他线程需要重新等待门再次打开。 AutoResetEvent通常用于以下场景 (1)生产者-消费者模型多个消费者线程等待生产者线程生产数据并且一次只能有一个消费者线程取走数据。生产者线程生产完数据后通过AutoResetEvent激活事件唤醒一个消费者线程来处理数据。 (2)线程池管理线程池中的线程等待任务的到来一旦有任务到达通过AutoResetEvent激活事件唤醒一个线程来执行任务。 (3)任务协调多个线程在某个条件满足时需要同时执行某个操作通过AutoResetEvent可以实现线程的同步和协调。 例如: 假设有一个餐厅里面有多个服务员和多个顾客。服务员需要等待顾客点菜一旦有顾客点菜服务员就会去为该顾客服务然后再等待下一个顾客点菜。这个场景可以使用AutoResetEvent来实现。服务员等待的事件可以看作是一个自动门一旦有顾客点菜门会自动打开服务员可以通过门去为顾客服务然后门会自动关闭服务员需要重新等待下一个顾客点菜。 总之AutoResetEvent适用于一次只允许一个线程通过的场景可以用于实现线程的同步和协调。 如果只有一个线程去操作另一个线程AutoResetEvent的作用会比较有限。因为AutoResetEvent主要用于线程间的同步和协调当只有一个线程在操作时就没有其他线程需要等待或被唤醒的需求。 在同一时间只允许一个线程通过因此在这个意义上可以将其视为一个线程。它提供了一种线程间的同步机制确保只有一个线程能够访问共享资源或执行某个操作。尽管AutoResetEvent本身不是并发执行的但它可以用于实现线程的同步和协调以确保在同一时间只有一个线程能够执行某个操作。 5、问:a处new ManualResetEvent(true)是什么意思 答创建对象时用true进行构造也就是给它有信号相当于Set让b处开门放行让后面跑起来。 6、问b处的resetEvent.WaitOne()是什么意思 答它是对当前线程的阻塞不占用CPU资源等待事件的激活打开. resetEvent.WaitOne()是AutoResetEvent类的一个方法用于使当前线程进入阻塞状态直到AutoResetEvent对象的状态变为有信号signaled。 当调用WaitOne()方法时如果AutoResetEvent的状态为无信号nonsignaled则当前线程会被阻塞一直等待直到AutoResetEvent的状态变为有信号。这意味着调用WaitOne()方法的线程会暂时停止执行并且不会占用CPU资源。它会等待其他线程通过Set()方法将AutoResetEvent的状态设置为有信号。 当AutoResetEvent的状态变为有信号时调用WaitOne()方法的线程会被唤醒然后继续执行后续的代码。 因此调用resetEvent.WaitOne()会使当前线程进入阻塞状态直到AutoResetEvent的状态变为有信号。在阻塞期间该线程是空闲的不会占用CPU资源。一旦AutoResetEvent的状态变为有信号该线程会被唤醒并继续执行后续的代码。所以可以说该线程在等待期间是暂时“死”了但并不是真正的“死”它只是在等待事件的发生。 它与Thread.Sleep()的阻塞是有区别的前面是基于事件的阻塞后者是基于时间的阻塞。
三、经典的线程控制模型 1、上面基本成型微软又进行总结归纳对线程的暂停继续取消专门用了相应的类来处理。 CancellationTokenSource ManualResetEvent/AutoResetEvent来控制。 界面 程序 private CancellationTokenSource cancelTSource;//aprivate ManualResetEvent resetEvent new ManualResetEvent(true);//bprivate void BtnStart_Click(object sender, EventArgs e){cancelTSource new CancellationTokenSource();CancellationToken ct cancelTSource.Token;TxtInfo.Clear();Task.Run(() {int i 0;while (!ct.IsCancellationRequested)//e{resetEvent.WaitOne();//ci;BeginInvoke(new Action(() {TxtInfo.AppendText($[{i.ToString(000]).PadRight(8, )}{DateTime.Now.TimeOfDay}\r\n);}));Thread.Sleep(100);}}, ct);//d}private void BtnPause_Click(object sender, EventArgs e){ resetEvent.Reset(); }private void BtnContinue_Click(object sender, EventArgs e){ resetEvent.Set(); }private void BtnStop_Click(object sender, EventArgs e){ cancelTSource.Cancel(); }//f 增加了一个取消改变了while(true)用更科学归纳的while。 1、问a处CancellationTokenSource是什么 答CancellationTokenSource是用于创建和管理CancellationToken的类。CancellationToken用于在多线程或异步操作中通知取消请求。 CancellationTokenSource相当于交通局创建和管理整个城市程序每个交通路口(异步操作)的信号灯而CancellationToken就是某个具体路口的交通信号灯用于通知异步操作是否应该被取消。这样的好处是避免以前程序员每个人都设置bool型的标志来控制用一个统一的信号源和信号来统一管理编写程序和阅读程序都会很轻松。此外CancellationTokenSource还提供了一些方便的方法如Cancel()方法来触发取消信号以及可以注册回调函数来在取消发生时执行特定的操作。 CancellationTokenSource提供了以下几个主要的成员 (1)CancellationTokenSource()创建一个新的CancellationTokenSource实例。 (2)CancellationTokenSource(int millisecondsDelay)创建一个新的CancellationTokenSource实例并在指定的延迟时间后自动取消。 (3)CancellationTokenSource(TimeSpan delay)创建一个新的CancellationTokenSource实例并在指定的延迟时间后自动取消。与(2)相同仅参数的类型不一样。 (4)CancellationToken Token获取与CancellationTokenSource关联的CancellationToken实例。通过Token属性可以将CancellationToken传递给需要取消支持的方法或操作。 (5)void Cancel()请求取消与CancellationTokenSource关联的CancellationToken。一旦调用Cancel()方法CancellationToken将进入取消状态可以通过检查IsCancellationRequested属性来判断是否已请求取消。 (6)void CancelAfter(int millisecondsDelay)在指定的延迟时间后请求取消。 (7)void CancelAfter(TimeSpan delay)在指定的延迟时间后请求取消。 CancellationTokenSource和CancellationToken通常用于在长时间运行的操作中实现取消功能。通过在操作中轮询CancellationToken的IsCancellationRequested属性可以检查是否已请求取消并相应地终止操作。CancellationTokenSource提供了一种方便的方式来创建和管理CancellationToken并在适当的时候请求取消。 注意: CancellationTokenSource是一种信号机制用于在多线程或异步操作中通知取消请求。它提供了一种更方便和集成的方式来管理取消操作避免了手动创建多个bool标志(信号)的麻烦。CancellationTokenSource本身并不直接控制线程的暂停或停止继续它只是提供了一个取消请求的标志。线程可以通过轮询与CancellationToken关联的IsCancellationRequested属性来检查是否已请求取消并根据需要采取相应的操作。 2、问如何达到控制多个线程 答上面因为是一个线程所以创建了一个公有变量如果要控制多个线程可以创建多个公有变量这样每个公有变量对应一个异步线程比如上面的cancelTSource对应的就是目前想控制的异步线程。比如另外创建一个异步线程时可以再用创建另一个公有变量来管理控制这个异步线程。 3、问为什么要用两个类来做事 答:(1)CancellationTokenSource从名字上来看它主要管理取消异步线程的操作。 (2)ManualResetEvent从名字上看主要是手动复位事件的操作即手动开信号和关信号的作用让异步线程在自己线程内进行暂停和继续的作用并不能进行取消本异步线程。 综合两者所以上面用两个类来操作四个启动暂停继续停止。 4、问:CancellationTokenSource与CancellationToken的区别 答CancellationTokenSource可以看作是管理者负责生成和取消CancellationToken。而CancellationToken则是具体执行落实者用于请求取消操作并传递取消状态。 CancellationTokenSource取消标记源和CancellationToken取消标记区别 (1)CancellationTokenSourceCancellationTokenSource 是一个类用于创建和管理取消标记。它负责保存取消请求的状态并提供 Cancel() 方法来触发取消请求。每个 CancellationTokenSource 实例可以创建一个与之关联的 CancellationToken 对象。 (2)CancellationTokenCancellationToken 是一个结构体代表具体的取消标记。它是 CancellationTokenSource 的 Token 属性的返回值通过这个 CancellationToken 对象可以检查取消请求的状态。您可以使用 CancellationToken 的 IsCancellationRequested 属性来检查是否发出了取消请求并在需要时停止执行异步操作。 简言之CancellationTokenSource 用于创建和管理取消标记而 CancellationToken 则用于实际检查和响应取消请求的状态。通过 CancellationTokenSource您可以触发取消请求并通过 CancellationToken 来检查这些请求并采取相应的操作。 通常情况下您将使用 CancellationTokenSource 创建和跟踪取消标记源以及通过 CancellationToken 实际监听和处理取消请求。这两个组件结合使用提供了一种有效的方式来控制和取消异步操作。 5、问:CancellationToken介绍 答CancellationToken取消标记是C#中用于在异步操作中传递取消信号的一种机制。它是一个结构体用于协调多个线程之间的取消操作。使用CancellationToken方法 (1)创建 CancellationTokenSource取消标记的源是 CancellationTokenSource取消标记源。您可以通过在代码中创建 CancellationTokenSource 实例来创建取消标记。例如 CancellationTokenSource cancellationTokenSource new CancellationTokenSource(); (2)获取 CancellationToken从 CancellationTokenSource 获取 CancellationToken取消标记。通过访问 CancellationTokenSource 的 Token 属性您可以获取与该取消标记源关联的 CancellationToken。例如 CancellationToken cancellationToken cancellationTokenSource.Token; (3)取消操作调用 CancellationTokenSource 的 Cancel() 方法可以触发取消操作。一旦取消被请求CancellationToken 检查的操作将会收到一个取消请求从而可以根据需要中断异步操作。请对照前面代码f处。 (4)监视取消请求在异步操作中使用 CancellationToken 进行轮询或监视来检查取消请求。您可以使用 CancellationToken.Register() 方法来注册一个回调函数该函数将在取消请求时被调用。在异步操作中的适当位置检查 CancellationToken 的 IsCancellationRequested 属性代码中e处)以便根据需要停止操作的执行。 CancellationTokenSource cts new CancellationTokenSource();//取消标志源CancellationToken token cts.Token;//取消标志token.Register(() Console.WriteLine(Token has been cancelled.));//a 注册回调函数Task task Task.Run(() {try{while (!token.IsCancellationRequested)//是否取消请求{Console.WriteLine(Working...);//bThread.Sleep(1000);//c}//d}catch (OperationCanceledException){Console.WriteLine(Task cancelled.);}}token);Thread.Sleep(5000);cts.Cancel();//e 取消命令task.Wait();Console.ReadKey(); 在a处注册取消回调函数后在e处发出取消,因为有第二个参数token的原因异步线程直接急刹立即取消终止异步线程然后马上使用前面注册的回调函数。 (5)传播取消使用 CancellationToken 可以在异步操作的各个层次之间传播取消请求。例如可以将 CancellationToken 传递给其他方法或在 LINQ 查询中使用它来中断执行。 这里的不同层次就是不同的位置把CancellationToken放在不同的位置上实现不同的控制。例如 CancellationTokenSource cts new CancellationTokenSource();CancellationToken token cts.Token;Task task Task.Run(() {try{Thread.Sleep(2000);//做事token.ThrowIfCancellationRequested();//检测是否取消有则抛异步}catch (Exception){Console.WriteLine(Task cancelled.);}}, token);Thread.Sleep(1000);cts.Cancel();//取消命令task.Wait(); 在linq时可以设置takewhile的条件以便后面检索元素时检测这个取消。 CancellationTokenSource cts new CancellationTokenSource();CancellationToken token cts.Token;IEnumerableint en Enumerable.Range(1, 999);//生成1到999整数Listint list en.TakeWhile(n !token.IsCancellationRequested).ToList();try{for (int i 0; i list.Count; i){Console.WriteLine(i);if (list[i] 500){cts.Cancel();token.ThrowIfCancellationRequested();}}}catch (Exception){Console.WriteLine(已经取消);}Console.ReadKey(); 除了这些外还有 在Where方法中使用CancellationToken来筛选元素 var query numbers.Where(num num % 2 0 !cancellationToken.IsCancellationRequested); 在Select方法中使用CancellationToken来转换元素 var query numbers.Select(num num * 2); 在GroupBy方法中使用CancellationToken来分组元素
var query numbers.GroupBy(num num % 2 0 !cancellationToken.IsCancellationRequested); 在OrderBy或OrderByDescending方法中使用CancellationToken来排序元素 var query numbers.OrderBy(num num, new MyComparer(cancellationToken)); 在Join方法中使用CancellationToken来连接两个序列 var query numbers.Join(otherNumbers, num num, otherNum otherNum, (num, otherNum) num otherNum, (num, otherNum) !cancellationToken.IsCancellationRequested); 注意 LINQ查询的延迟执行特性使得程序能够更加高效地运行避免了不必要的内存占用和计算开销。 当你定义一个LINQ查询时实际上只是在创建一个查询计划而不会立即执行查询。查询计划包含了查询的逻辑和操作序列但不会立即执行实际的查询操作。只有当你对查询结果进行迭代或调用执行方法时才会触发查询的执行。 这种延迟执行的机制使得程序能够更加灵活地处理数据。你可以根据需要动态构建查询只有在需要时才执行查询操作从而减少了不必要的计算和内存占用。 另外LINQ还提供了一些操作符如Take、Skip、Where等它们可以进一步优化查询的性能。这些操作符可以在查询执行之前对数据进行筛选、分页、排序等操作从而减少了需要处理的数据量提高了查询的效率。 总而言之LINQ的延迟执行特性和优化操作符使得程序能够更加高效地处理数据减少了内存占用和计算开销提高了程序的性能。 CancellationToken 提供了一种优雅和可靠的方式来处理异步操作的取消。它可以帮助您更好地管理异步代码的执行状态并在需要时准确地中断操作。通过适当地使用 CancellationToken您可以避免不必要的计算开销、资源浪费和匹配性能的提高。 6、问:在d处不加上ct一样可以取消这里为什么要加上 答如果将CancellationToken作为第二个参数传递给Task.Run方法那么当取消操作被触发时任务会立即响应并停止执行。这就相当于在汽车上安装了紧急刹车系统一旦发生危险情况刹车系统会立即发挥作用以避免事故的发生。 如果不将CancellationToken作为第二个参数传递给Task.Run方法任务可能会继续执行一段时间直到它自己检测到取消操作并停止。这就像汽车没有安装紧急刹车系统需要依靠驾驶员的反应来避免事故。在这种情况下即使取消操作已经被触发任务也可能会继续执行多次循环直到它自己检测到取消操作并停止。 因此将CancellationToken作为第二个参数传递给Task.Run方法是一种更安全、更可靠的做法可以确保任务在取消操作被触发时立即停止执行从而避免潜在的错误和资源浪费。 这种方式可以实现更及时和精确的任务取消。而不是等待整个循环体执行完毕后再判断是否需要取消任务这样可以避免不必要的计算和延迟任务取消的响应时间。因此通过将TokenSource.Token传递给Task.Run的第二个参数可以在每次迭代之前对取消进行监控提供更好的任务取消控制。 7、问这“及时和精确的任务取消”如何理解 答如果没第二个参数循环可能多次后才“意识”到取消而加上第二参数相当增加了一个监视和眼睛以便及时终止。 这个终止 (1)如果取消时Run中的Action委托在循环体内那么需要本次循环完成才进行终止 while (!token.IsCancellationRequested){// 代码A//代码B} 若在Run中的A处遇到取消需要执行到B完成本次循环完成才能取消 (2)如果没有循环就会执行执行到本次的逻辑代码块。 Task.Run(() {// 代码块 A// 代码块 B// 代码块 C}); 如果取消操作被触发任务会执行代码块 A但不会继续执行代码块 B 和代码块 C。任务会在代码块 A 执行完毕后立即返回并取消。 (3)最终是只认逻辑代码块比如在循环体内有多个逻辑代码块 Task.Run(() {for (int i 0; i 10; i){// 逻辑代码块 A// 逻辑代码块 B// 逻辑代码块 C}}); 如果取消操作被触发任务会执行循环的第一个迭代的逻辑代码块 A但不会继续执行逻辑代码块 B 和逻辑代码块 C也不会进行后续的循环迭代。任务会在逻辑代码块 A 执行完毕后立即返回并取消。 8、问什么叫逻辑代码块 答逻辑代码块指的是一组相关的代码语句通常由花括号{}括起来。在C#中常见的逻辑代码块包括if语句块、while循环块、for循环块、switch语句块等。例如以下是一个包含多个逻辑代码块的示例 if (condition){// 逻辑代码块 A}else{// 逻辑代码块 B}while (condition){// 逻辑代码块 C}for (int i 0; i 10; i){// 逻辑代码块 D}switch (variable){case value1:// 逻辑代码块 Ebreak;case value2:// 逻辑代码块 Fbreak;default:// 逻辑代码块 Gbreak;} 在上面的示例中逻辑代码块 A、B、C、D、E、F和G分别是if语句块、while循环块、for循环块和switch语句块中的代码块。当取消操作被触发时任务会执行当前的逻辑代码块但不会继续执行后续的代码块。任务会在当前逻辑代码块执行完毕后立即返回并取消。 9、问Run第二参数添加了监视以便及时取消必定会消耗更多资源 答是的使用第二个参数来监视取消请求会增加一些资源的耗费。当你在创建任务时传递一个CancellationToken对象作为第二个参数时任务会定期检查该取消标记是否被触发。 这种定期检查会引入一些额外的开销因为任务需要花费一些时间和计算资源来检查取消标记。如果取消标记被触发任务需要执行相应的取消操作。 因此使用第二个参数来监视取消请求会增加一些资源的耗费尤其是在长时间运行的任务或高频率的取消请求的情况下。在设计任务时需要权衡取消请求的频率和任务的执行时间以确保资源的合理利用和性能的平衡。 如果任务不需要监视取消请求或者取消请求的频率较低可以选择不使用第二个参数以减少资源的耗费。但是需要注意取消请求仍然可以通过CancellationTokenSource.Cancel()方法来触发无论是否使用了第二个参数。
四、两个线程由B控制A 1、两个线程由B线程控制A线程B接收数据发出指令A完成操作。 界面 程序为了简化用了前面的公有变量但程序运行时不要运行前面只测试后面两个按钮. private ManualResetEvent resetEvent new ManualResetEvent(true);//bprivate ManualResetEvent resetB new ManualResetEvent(true);private volatile int bState -1;//B线程状态-1private Liststring list new Liststring() { Zero, Left, Right, Up, Down };private void BtnBtoA_Click(object sender, EventArgs e)//B线程控制A线程{//线程ATask.Run(() {while (true){resetEvent.WaitOne();if (bState 0){BeginInvoke(new Action(() {TxtInfo.AppendText($B线程消息{list[bState]}\r\n);}));//abState -1;//防止第二次进入,必须由B线程修改后进入}else{}Thread.Sleep(50);//防止上面UI操作阻塞}});//线程BTask.Run(() {Random r new Random();while (true){resetB.WaitOne();//Thread.Sleep(1000);resetEvent.Reset();//暂停A线程bState r.Next(5);resetEvent.Set();//产生数据后才开辟A线程}});}private void BtnBPause_Click(object sender, EventArgs e)//控制线程B{if (BtnBPause.Text B线程故障){resetB.Reset();BtnBPause.Text B线程正常;}else{resetB.Set();BtnBPause.Text B线程故障;}} B线程正常产生的bState来控制A的运行通过追加文本来显示。 1、问为什么点击运行B控A就会在a处出错 答BeginInvoke是异步执行把消息发送给UI就返回a处继续向下执行然后bState就成了-1在某个时间UI线程的消息队列更新txtInfo时提取的list[bState]里的bState可能正好是-1,这样就超出索引范围而出错。 修改BeginInvoke为Invoke则表示是同步执行不会发生bState传入UI线程提取问题。 2、问有一个窗体就有一个UI线程控件不会有线程 答是的每个窗体通常会有一个与之关联的UI线程。UI线程负责处理窗体的创建、显示、用户交互等操作并维护窗体中包含的控件的状态和渲染。 控件并不拥有自己的线程。在大多数GUI框架中包括Windows Forms和WPF控件的创建、更新和事件处理都是在UI线程上进行的。当您在UI线程上对控件进行操作时系统会自动通过消息循环将相应的消息传递给控件并在UI线程中执行相关的操作。 这种单线程模型可以确保界面操作的一致性和安全性。它保证了所有的UI操作都在同一个线程上执行避免了多线程间的竞态条件和数据同步问题。 但请注意如果您在非UI线程上尝试直接访问或修改控件的属性可能会引发线程冲突和异常。为了在非UI线程上进行UI操作通常需要使用跨线程调用如Invoke方法来将操作请求提交给UI线程由UI线程代为执行。 总结起来窗体通常对应一个UI线程而控件没有独立的线程它们的操作都在UI线程上执行遵循单线程模型以确保界面操作的一致性和安全性。 3、问什么是消息循环 答消息循环Message Loop也称为消息泵Message Pump是在图形用户界面GUI应用程序中实现事件处理和消息传递的核心机制之一。它是一种用于处理用户输入、窗口事件和异步请求等消息的循环结构。 在Windows操作系统中每个窗口都有一个与之关联的消息循环。当应用程序启动时Windows会为每个打开的窗口创建一个UI线程并在该线程上运行消息循环。消息循环不断地从消息队列中取出消息并将其分发给对应的窗口进行处理。 基本上消息循环可以被视为一个无限循环它由以下几个步骤组成 (1)接收消息 消息循环通过调用操作系统提供的函数如GetMessage或PeekMessage来接收并获取消息。这些消息可能来自用户的输入、窗口事件、定时器、系统通知等等。 (2)分发消息 一旦获取到消息消息循环会将消息传递给对应的窗口进行处理。每个窗口都有一个消息处理函数称为窗口过程它会根据消息的类型和内容来执行相应的操作。例如用户点击按钮时窗口过程会接收到一个鼠标点击事件的消息并触发相应的按钮点击事件处理代码。 (3)处理消息 在窗口过程中根据消息的类型如鼠标事件、键盘事件、定时器事件等窗口会调用相应的处理函数或执行相应的操作。这些操作可能包括更新界面、响应用户输入、执行业务逻辑等。 (4)等待消息 如果当前没有消息需要处理消息循环会进入一个等待状态等待新的消息到达。在等待期间消息循环会将CPU控制权还给操作系统让其他程序继续执行。 通过不断循环上述步骤消息循环实现了对用户输入和系统事件的响应并保证了应用程序的流畅运行。它是GUI应用程序开发中非常重要的一部分负责管理和分发消息确保正确的事件处理和交互。 2、问在运行期间如果直接关闭form1会在a处的Invoke出错 答是的运行时用invoke同步更新UI,但这时关闭form1会异步异步线程关闭一个失去父母的invoke再更新UI会出错。 为了控制取消因为我们加入前面的CancellationTokenSource并加入第二个参数参与精确地控制 private ManualResetEvent resetEvent new ManualResetEvent(true);private ManualResetEvent resetB new ManualResetEvent(true);private volatile int bState -1;private Liststring list new Liststring() { Zero, Left, Right, Up, Down };private CancellationTokenSource cts new CancellationTokenSource();private void BtnBtoA_Click(object sender, EventArgs e){CancellationToken ct cts.Token;//线程ATask taskA Task.Run(() {while (!ct.IsCancellationRequested){resetEvent.WaitOne();if (bState 0){Invoke(new Action(() {TxtInfo.AppendText($B线程消息{list[bState]}\r\n);}));//abState -1;}else{}Thread.Sleep(100);}}, ct);//线程BTask taskB Task.Run(() {Random r new Random();while (true){resetB.WaitOne();resetEvent.Reset();bState r.Next(5);resetEvent.Set();}});}private void BtnBPause_Click(object sender, EventArgs e){if (BtnBPause.Text B线程故障){resetB.Reset();BtnBPause.Text B线程正常;}else{resetB.Set();BtnBPause.Text B线程故障;}}private void Form1_FormClosing(object sender, FormClosingEventArgs e){cts.Cancel();} 3、问上面在运行期间如果关闭form1时a处仍然报错为什么 答上面在台式机上并不报错但在笔记本上会报错暂时不清楚什么原因造成。 两种修改 (1)直接在Task.Run(后面添加async,哪怕Thread.Sleep(1)设置1毫秒也不会报错 Task.Run(async () {int idx 0;while (!ct.IsCancellationRequested){Invoke(new Action(() {TxtInfo.AppendText($[{idx}]-----\r\n);}));//aThread.Sleep(1);//b}}, ct); (2)把Thread.Sleep改为异步等待增加异步线程中的反应响应也不会报错. Task.Run(async () {int idx 0;while (!ct.IsCancellationRequested){Invoke(new Action(() {TxtInfo.AppendText($[{idx}]-----\r\n);}));//aawait Task.Delay(1);//b}}, ct); 4、问IsHandleCreated介绍与invokerequired的区别 答IsHandleCreated是Control类的一个属性用于检查控件的句柄是否已经创建。在Windows Forms应用程序中控件的句柄是与控件关联的底层Windows窗口的标识符。当控件被创建后它的句柄就会被分配。通过检查IsHandleCreated属性可以确定控件是否已经创建从而避免在控件还没有创建的情况下对其进行操作。 InvokeRequired是Control类的另一个属性用于检查当前代码是否正在非UI线程上执行。当代码在非UI线程上执行时调用Invoke或BeginInvoke方法来更新UI是一种常见的模式。通过检查InvokeRequired属性可以确定当前代码是否在非UI线程上执行从而决定是否需要使用Invoke或BeginInvoke方法来在UI线程上执行操作。 这两个属性的区别在于它们的作用和检查的对象。IsHandleCreated属性用于检查控件的句柄是否已经创建而InvokeRequired属性用于检查当前代码是否在非UI线程上执行。 使用IsHandleCreated属性可以确保在控件已经创建的情况下才对其进行操作从而避免在控件还没有创建的情况下引发异常。而使用InvokeRequired属性可以确保在非UI线程上执行的代码通过Invoke或BeginInvoke方法在UI线程上执行从而避免在非UI线程上直接操作UI引发的跨线程访问异常。 IsHandleCreated和InvokeRequired是用于不同目的的属性但它们在处理UI操作时通常会一起使用以确保在正确的线程上执行UI操作。 如果你确定在UI上有这个控件并且你能够确保在异步线程上执行的代码只会在控件已经创建的情况下运行那么你可能不需要检查IsHandleCreated属性。然而如果存在控件被销毁的可能性或者你不能完全控制异步操作的执行时机那么检查IsHandleCreated属性仍然是一个良好的做法以确保在正确的时机停止异步操作。 5、问跨线程操作UI时invoke与control.invoke有什么区别 答Invoke和Control.Invoke方法的效果是相同的它们都用于在UI线程上执行指定的委托或方法。无论是使用Invoke还是Control.Invoke都可以确保在UI线程上执行操作避免跨线程访问UI引发的异常。 Invoke方法是Control类的一个方法它可以在任何控件上调用而不仅仅是在Form类或其他派生自Control的类上。当你调用Invoke方法时你需要传递一个委托或方法该委托或方法将在UI线程上执行。 Control.Invoke是Control类的一个特殊方法它是通过继承自Control的类如Form上的一个简化的方法。当你在一个派生自Control的类上调用Invoke方法时它会自动将当前实例作为控件参数传递给Invoke方法。这样你就不需要显式地指定控件了。 所以无论是使用Invoke还是Control.Invoke它们的效果是相同的。它们都用于在UI线程上执行操作并且都可以达到相同的目的。你可以根据个人喜好和代码的可读性来选择使用哪个方法。 6、问:Task.Run()与Await Task.Run()的返回值有什么区别 答Task.Run()方法返回的是一个Task或TaskT对象它代表了一个异步操作的执行。当你使用await Task.Run()时await操作符会等待异步操作完成并且返回异步操作的结果。 如果异步操作是一个返回void的方法那么await Task.Run()的返回类型就是Task因为没有具体的结果可以返回。你可以使用await Task.Run()来等待异步操作的完成而不需要关心具体的结果。 如果异步操作是一个返回某个具体类型的方法例如Taskint那么await Task.Run()的返回类型就是该具体类型例如int。在这种情况下await操作符会等待异步操作完成并且返回异步操作的结果。 注意使用await Task.Run()时如果异步操作抛出了异常await操作符会将异常重新抛出而不是返回Task对象或具体的结果。 因此,当Task.Run()返回是void时对应的await Task.Run()将返回一个Task而当Task.Run()返回是T时对应的await Task.Run()将返回一个T类型。这样的设计确实更加合理和灵活无论哪种情况都可以检查异步操作的状态、是否完成、是否出现异常等 7、问如果函数返回类型为async Taskint内部却没有await是什么情况 private async Taskint Test(){return 3;} 答上面返回类型是Taskint内部构造是这样吗 private async Taskint Test(){return await Task.Run(() {return 3;});} 注意必须要用await一是与前面的async配套二是如果不加async它实际返回的是Taskint,直接报错这是一个异步方法返回值是int而不是Taskint。 因此我们可以把async Taskint看作是int类型实际最终结果也必须是int否则也会报错。但你不能直接写int我猜测它是一种特殊的返回值尽管最终的结果是int但必须标注好它的形成的原由。 实际return 3;等效的是return await Task.FromResult(3);它是创建一个已经成功异步执行后的结果所以它的任务状态是 RanToCompletion并不是先“创建一个异步执行”“成功返回结果”。 Task.FromResult(3)是直接创建一个已完成的Taskint对象并将指定的结果这里是3作为该任务的结果。它并不会创建一个异步任务而是创建一个表示已完成的任务并将指定的结果作为该任务的返回值。 在使用Task.FromResult()方法时你可以将其看作是一个快速创建已完成任务的便捷方式。它适用于那些不需要进行真正的异步操作而只需要返回一个已知结果的情况。 注意Task.FromResult()方法创建的任务是已完成的即任务的状态为RanToCompletion并且不会进行任何实际的异步操作。因此当你使用Task.FromResult(3)返回一个已完成的任务时可以立即获取任务的结果而无需等待异步操作的完成。
五、三线程ABC逐个控制 1、三个线程A完成后让B完成B完成后让C完成象工厂流水线一样逐个完成。起始给A一个任务。 界面: 程序 private AutoResetEvent autoResetA new AutoResetEvent(false);private AutoResetEvent autoResetB new AutoResetEvent(false);private AutoResetEvent autoResetC new AutoResetEvent(false);private void BtnStart_Click(object sender, EventArgs e){//线程ATask.Run(async () {while (true){autoResetA.WaitOne();await Task.Delay(300);AppendMsg(线程A任务结束。);autoResetB.Set();}});//线程BTask.Run(async () {while (true){autoResetB.WaitOne();await Task.Delay(300);AppendMsg(线程B任务结束。);autoResetC.Set();}});//线程CTask.Run(async () {while (true){autoResetC.WaitOne();await Task.Delay(300);AppendMsg(线程C任务结束。);}});//线程s起点发出任务Task.Run(async () {while (true){autoResetA.Set();AppendMsg(给线程A派发任务.);await Task.Delay(2300);}});}private void AppendMsg(string s){Invoke(new Action(() {TxtInfo.AppendText(${s}\r\n);TxtInfo.ScrollToCaret();TxtInfo.Refresh();}));} 2、问:AutoResetEvent只能控制一个线程吗 答AutoResetEvent相当于一个自动门默认是关闭状态一旦set打开它马上又会关闭。即使创建这个对象时用true那么它创建完成后马上又会恢复关闭false. AutoResetEvent 是一个同步原语它通常用于线程间的同步操作。它的工作方式是当一个线程调用 WaitOne 方法时如果事件处于关闭状态则线程会阻塞等待。而另一个线程调用 Set 方法时这个事件会打开唤醒等待的线程并且在被唤醒的线程继续执行后事件会自动恢复为关闭状态。 如果你在两个 Task.Run 中使用同一个 AutoResetEvent确保只有一个线程能够通过 WaitOne 方法可以避免两个线程同时运行。 AutoResetEvent autoResetEvent new AutoResetEvent(false);Task.Run(() {// 线程1执行的代码// ...autoResetEvent.Set(); // 打开事件});Task.Run(() {autoResetEvent.WaitOne(); // 等待事件被打开// 线程2执行的代码// ...}); 它的设计初衷是只允许一个线程通过 WaitOne 方法但实际上也可以由一个信号同时唤醒多个线程。当 AutoResetEvent 被设置为打开状态时所有正在等待的线程都会被唤醒并且所有的线程都可以继续执行。 AutoResetEvent autoResetEvent new AutoResetEvent(false);Task.Run(() {autoResetEvent.Set(); // 打开事件});Task.Run(() {autoResetEvent.WaitOne(); // 等待事件被打开// 线程2执行的代码// ...});Task.Run(() {autoResetEvent.WaitOne(); // 等待事件被打开// 线程3执行的代码// ...}); 如果你需要确保只有一个线程能够继续执行可以考虑使用其他的同步原语例如 Mutex 或 Semaphore这些原语可以更精确地控制线程的执行。 3、问什么是同步原语 答原语一词是原始操作primitive operation的缩写指的是一组基本的操作或指令它们是构建更复杂操作的基础。在计算机科学中原语通常用于描述一组基本的操作或指令这些操作或指令是不可再分的不能再进一步分解。 在同步原语的上下文中原语指的是一组基本的操作或指令用于实现多线程或多进程之间的同步和协作。这些原语提供了一些基本的机制例如互斥访问、条件等待、信号通知等用于控制线程或进程之间的操作顺序和并发访问。 因此原语可以理解为一种基本的操作或指令用于构建更复杂的操作或实现特定的功能。它是计算机科学中的基本术语用于描述一组基本的操作或指令以及它们的组合和应用。 4、问SemaphoreSlim比AutoResetEvent更轻量 答是的。 SemaphoreSlim是一个轻量级的信号量实现用于控制并发访问的数量。它可以用来限制同时访问某个资源的线程数量或者用于实现线程之间的顺序执行。 SemaphoreSlim提供了两个主要的方法WaitAsync()和Release()。 - WaitAsync()方法用于获取信号量如果当前信号量的计数器大于0则立即返回并将计数器减1如果计数器为0则线程会被阻塞直到有其他线程调用Release()方法释放信号量。 - Release()方法用于释放信号量将计数器加1并且唤醒一个等待的线程。 await semaphoreA.WaitAsync()表示当前线程正在等待获取semaphoreA信号量。如果semaphoreA的计数器大于0则当前线程可以继续执行如果计数器为0则当前线程会被阻塞直到有其他线程调用semaphoreA.Release()释放信号量。 通过使用await关键字可以将异步等待和信号量的使用结合起来使得代码能够在等待期间继续执行其他任务而不会阻塞线程。这样可以提高程序的性能和响应性。 SemaphoreSlim比AutoResetEvent更轻量的原因是因为它是基于信号量的同步原语而AutoResetEvent是基于事件的同步原语。信号量是一种更通用的同步机制可以用于控制多个线程的访问权限而事件只能用于控制单个线程的访问权限。由于SemaphoreSlim是基于信号量的轻量级实现所以在某些情况下它比AutoResetEvent更高效。 5、问能用SemophoreSlim来优化上面程序吗 答优化后 private SemaphoreSlim semA new SemaphoreSlim(0);private SemaphoreSlim semB new SemaphoreSlim(0);private SemaphoreSlim semC new SemaphoreSlim(0);private void BtnStart_Click(object sender, EventArgs e){//线程ATask.Run(async () {while (true){await semA.WaitAsync();await Task.Delay(300);AppendMsg(线程A任务结束。);semC.Release();}});//线程BTask.Run(async () {while (true){await semB.WaitAsync();await Task.Delay(300);AppendMsg(线程B任务结束。);semC.Release();}});//线程CTask.Run(async () {while (true){await semC.WaitAsync();await Task.Delay(300);AppendMsg(线程C任务结束。);}});//线程s起点发出任务Task.Run(async () {while (true){semA.Release();AppendMsg(给线程A派发任务.);await Task.Delay(1300);}});}private void AppendMsg(string s){Invoke(new Action(() {TxtInfo.AppendText(${s}\r\n);TxtInfo.ScrollToCaret();TxtInfo.Refresh();}));} 最开始new SemaphoreSlim(0);为0是关闭信号大门用release对信号加1当信号量0时信号大门打开程序跑起来。另外await semC.WaitAsync();是一个异步阻塞等待不会影响当前线程的一个等待同时semC.WaitAsync()见钱眼开看到有0的信号来了就不再阻塞、放行同时对信号量减1完成对信号大门的关闭。