首页 > 其他分享 >几个有意思的多线程问题 & 有趣现象笔记

几个有意思的多线程问题 & 有趣现象笔记

时间:2024-11-16 16:32:50浏览次数:1  
标签:Core 有意思 false Thread ConfigureAwait 笔记 线程 NET 多线程

信号量释放的时候线程被带入的问题

SemaphoreSlim 和多线程使用的时候,.Release() 时,应该在新的线程去做 Release 操作同理,因为 Release 时会切换到 await 等待的代码执行,也就是调用 SemaphoreSlim.Release 的线程被带入到了 await SemaphoreSlim.WaitAsync() 的代码执行,如果是一个很长的执行过程,那么原来的线程就无法处理后续的过程了。

TaskCompleteSurce.SetResult() 也是同理。

在队列调度中,不慎将 COM 组件的 STA 线程占用了,导致后续一些列奇怪的问题

公司一共现有程序,调用了某仪器厂商的库,该库底层又会和 COM+ 组件互操作。运行久了出现了奇怪问题(卡住,或者后续运行出现诡异问题,原因后面讲),VS附加调试时,弹出这个错误:

托管调试助手 "ContextSwitchDeadlock":“CLR 无法从 COM 上下文 0x12f6d20 转换为 COM 上下文 0x12f6dd8,这种状态已持续 60 秒。拥有目标上下文/单元的线程很有可能执行的是非泵式等待或者在不发送 Windows 消息的情况下处理一个运行时间非常长的操作。这种情况通常会影响到性能,甚至可能导致应用程序不响应或者使用的内存随时间不断累积。要避免此问题,所有单线程单元(STA)线程都应使用泵式等待基元(如 CoWaitForMultipleHandles),并在运行时间很长的操作过程中定期发送消息。”

问题分析:

  • 前提:厂商库的某些接口,内部使用线程槽获取当前线程数据而非现代化的“以变量形式上下文”,因此是不能够在多线程中被调用的,调用了不会直接报错,会因为出现多个线程槽上下文而出现各种古怪问题
  • 我们同事用了厂商的库,确实是单线程调用的,放在一个专用的后台任务线程,用队列消息泵,单线程处理。
  • 而问题出在队列处理完毕后的回调上,人们都会习惯处理完后,直接回调(调用 TaskCompletionSource.SetResult()
  • 但是在这里不行,直接回调回导致当前的线程被带入到外部去,由于调度的原因,这就导致线程被归还的时候,有可能已经不是原来的线程了,那么上面说的前提条件就无法满足了。

解决方式:

在等待 TaskCompletionSource.Task 的线程中,修改成:

原先是这样:
reportResult = reportToken.TaskCompletionSource.Task.ConfigureAwait().GetAwaiter().GetResult();

改成(注意区别,ConfigureAwait(false) ):
.ConfigureAwait(false).GetAwaiter().GetResult();
这里注意,.NET Core 默认对 task 的 await 已经是ConfigureAwait(false) ,但是 .NET Framework 不是,而这个项目是 .NET Framework 项目。

但是因为上面的修改依赖于调用方,如果调用方忘记设置ConfigureAwait(false) ,还是会出问题,所以再进一步,修改队列完成回调,不再使用当前队列的线程回调,开启新线程回调:

void SetTaskCompleteResult<T>(TaskCompletionSource<T> tsc, T result) {
    // 修改:COM 工作线程运行到这里,拿到结果后,从直接设置 TaskCompletionSource 修改成使用新线程设置:
    Task.Factory.StartNew(() => { tsc.SetResult(result); });
}

void SetTaskCompleteException<T>(TaskCompletionSource<T> tsc, Exception exception) {
    // 修改:COM 工作线程运行到这里,拿到结果后,从直接设置 TaskCompletionSource 修改成使用新线程设置:
    Task.Factory.StartNew(() => { tsc.SetException(exception); });
}

多线程操作设置了 BoundCapacity 的 BlockingCollection的问题

BlockingCollection supports bounding and blocking. Bounding means you can set the maximum capacity of the collection. Bounding is important in certain scenarios because it enables you to control the maximum size of the collection in memory, and it prevents the producing threads from moving too far ahead of the consuming threads.

一个具体的多线程情况,生产者在事件回调(统一线程)中执行 BlockingCollection.Add,消费者有个逻辑,就是消费者执行的一些过程依赖了一个外部的代码库,这个外部库在某些情况下会卡死线程(且不支持取消,但这个外部库必须要用,你又没法改动它的代码),因此消费者内部又维护了一个队列,通多队列执行并检测队列中的任务是否有卡死的情况,如果有卡死的情况,就放弃当前队列和 BlockingCollection 实例,全部重新开始,那么问题来了。

但是由于并发问题,生产者线程回调时获取到的是销毁前的BlockingCollection 实例(即使操作 BlockingCollection 加了 isDispose 判断,由于判断 isDisposed 时未加双重锁还是存在并发问题, BoundedCapacity 较小,而Dispose释放的东西较大时间稍长且没有在方法最前而是最后才设置 isDisposed = true (或者忘记设置isDisposed = true) ,导致短时间内同时多个并发同时通过 isDisposed检测期间BlockingCollection被很快填满,而且 Add 操作存在通过BlockingCollection.Dispose 检测后才被Dispose 的问题),然后由于 bounding 为0(也就是满了),于是生产者线程等待被消费者线程消费,但是由于生产者生产卡住了,消费者的新 BlockingCollection 等不到新数据,于是形成死锁。

Thread.Abort()

.NET Framework 时代支持 Thread.Abort() 强制终止当前线程,但 .NET Core 时代这个接口已经被标记为过时且无效了(调用会发出 PlatformNotSupportedException),转而使用温和的 CancellationTokenSource 机制,“提醒”线程:你可以下班了!

所以,都是让线程下班,区别在于

  • Thread.Abort() 相当于直接拔掉线程的电源(拔掉之前会发车一个 ThreadAbortException 给线程的上次堆栈一个捕获终止的机会,并给它一点处理时间)
  • 可能因为 .NET Core 跨平台的原因,取消了 Thread.Abort() 的支持 https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/5.0/thread-abort-obsolete
  • CancellationTokenSource 是会有一个变量能够让线程知道,我可以下班了,但是线程也可以选择继续加班。

Thread.Interrupt

实测 Thread.Interrupt() 能够打算 Sleep 中的线程,但是执行中的线程不会被打断。

try { Thread.Sleep(60 * 2000); int i = 0; while (i <= 0) { try { foreach (var d in Directory.GetFiles("*.*")) { if (d == "ImpossibleName") { i = 1; } } } catch (Exception e) { } } } catch (ThreadInterruptedException e) { Console.WriteLine(e); throw; } catch (Exception e) { Console.WriteLine(e); throw; }

SpinWait

https://docs.microsoft.com/en-us/dotnet/api/system.threading.thread.spinwait?view=net-6.0

SpinWait 其实就是对 Thread.Sleep Thread.Yield Thread.SpinWait 的简单封装.
Thread.Sleep Thread.Yield 都会切线程,Thread.SpinWait 不会,即为自旋。
如果只是简单自旋,直接调用 Thread.SpinWait(1) 就可以。

ConfigureAwait 在 .NET Framework 和 .NET Core 的区别

.NET Framework

  • .NET Framework 中 .ConfigureAwait(false) 有一定概率,线程id和之前的相等
  • 如果用 await .ConfigureAwait(false); 则是一定概率使用上一个线程
  • 如果用 .GetAwaiter().GetResult(); 则不论 true/false 一定是上一个线程

.NET Core

In .NET Core, Microsoft did away with the SynchronizationContext that caused us to need ConfigureAwait(false) everywhere. Thus your ASP.NET Core app technically doesn’t need any ConfigureAwait(false) logic in them because it’s redundant. However, if you have a library that is using .NET Standard, then it is highly recommended that you still use .ConfigureAwait(false). In .NET Core, this will effectively do nothing. But if someone with .NET Framework ends up using this library and calls it synchronously, they will be in big trouble.

在 .NET Core 中,由于调度机制修改,不再需要调用 ConfigureAwait(false) 方法。

标签:Core,有意思,false,Thread,ConfigureAwait,笔记,线程,NET,多线程
From: https://www.cnblogs.com/darklx/p/18549408

相关文章

  • Java基础——多线程
    1.线程是一个程序内部的一条执行流程程序中如果只有一条执行流程,那这个程序就是单线程的程序2.多线程指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)2.1.如何创建多条线程Java通过java.lang.Thread类的对象来代表线程2.1.1.方式一:继承Thread类//1......
  • 【学习笔记】Segment Tree Beats/吉司机线段树
    链接区间最值操作HDU-5306支持对区间取\(\min\),维护区间\(\max\),查询区间和。很容易想到一个暴力,我们每一次找出这个区间的最大值\(mx\),如果\(mx>x\),那么暴力修改这个位置的值,否则已经修改完毕,退出,时间复杂度为\(O(n^2\logn)\)。打一打补丁,对线段树上的每一个区间维......
  • _app搭建笔记
    逍遥模拟器端口号:21503(3)adbinstall+包名的绝对路径安装apk包案例:adbinstallE:\dcs\two\app\mojibase.apkE:\dcs\two\app\baiduyuedu_5520.apk(4)活动路径名:aaptdbadgingD:\app\baiduyuedu_3760.apk(5)adbuninstall包名:卸载com.baidu.yuedu包名name='com......
  • 【跟着阿舜学音乐-笔记】1.12和弦功能与进行原理
    七和弦七和弦是三和弦的基础上叠加三音构成的和弦(四个音的和弦)。其中小大七和弦(CmM7)很少运用,因为调内没有小大七和弦,同时听感上也不是很好。注:有另一种和弦命名方式,即三和弦与根音呈大小七度的音组成和弦的命名法,该命名法对比上述命名法有个特例——增大七和弦(增三和弦叠加一......
  • JUC---多线程下的数据共享(基于ThreadLocal的思考)
    多线程下的数据共享(基于ThreadLocal的思考)起初实在写项目过程中,在完成超时订单自动取消的任务时,使用xxl-job,整个逻辑是需要从订单表中找出过期的订单,然后将其存入订单取消表。存入订单取消表时需要存储用户的信息。我最开始没想那么多,就直接从ThreadLocal中取出用户信息,但......
  • 设计模式学习笔记之七大原则
    设计模式的七大原则开闭原则(OpenClosedPrinciple,OCP)单一职责原则(SingleResponsibilityPrinciple,SRP)里氏代换原则(LiskovSubstitutionPrinciple,LSP)依赖倒转原则(DependencyInversionPrinciple,DIP)接口隔离原则(InterfaceSegregationPrinciple,ISP)合成/聚合复用原则(Co......
  • 人工智能:原理与技术 学习笔记
    Lecture2Supervisedlearning:regression,classification,...Unsupervisedlearning:clustering,dimensionalityreduction,...Thecanonicalmachinelearningproblem:Givenasetoftrainingdata\(\{(x_i,y_i)\}_{i=1}^m\)andalossfunction\......
  • [笔记]Dijkstra算法正确性证明
    最近做了一些题,感觉对算法更深刻的理解是比套板子更深层次的,在这个层次上解决问题,思路会更加清晰。比如P5687[CSP-S2019江西]网格图(题解)这道题就是网格图的最小生成树,解法就建立在普通Kruskal的基础上,当时想了挺久也没想出来,看了题解才豁然开朗。所以各算法总是要回顾回顾的~......
  • The Missing Semester 第一讲MIT笔记
    幕布链接shell命令-幕布echo打印传入参数echohello\Worldecho"helloworld"echo$PATH(找环境变量在哪)date查看时间which查看程序所在的目录pwdpresentworkingdirectory当前所在的工作目录path在windows中路径一般为反斜杠\masOS和Linux不同为斜杠,下为绝对路径......
  • c语言笔记(鹏哥)课件+上课板书汇总(深入指针1)
    深入指针(1)⽬录:一、内存和地址二、指针变量和地址三、取地址操作符四、指针变量类型的意义(这一讲到这)五、const修饰指针六、指针运算七、野指针八、assert断⾔九、指针的使⽤和传址调⽤内存和地址引例:假设有一个宿舍楼,你在一个房间里,宿舍楼里每一间房间都......