在异步调用匿名函数时明智地使用局部变量

C#学习笔记-方法参数params、ref和in、out 目录params用法ref用法按引用传递参数引用返回值引用局部变量in用法特殊注意点优势out用法版本变迁信息其他ref、in和out三者异同方法重载 C#中ref和out关键字的应用以及区别。 官方文档 编写安全有效的 C# 代码 params 指定此参数采用可变数量的参数。 in 指定此参数由引用传递,但只由调用方法读取。 ref 指定此参数由引用传递,可能由调用方法读取或写入。 out 指定此参数由引用传递,由调用方法写入。 params 使用 params 关键字可以指定采用数目可变的参数 阅读详情

问题:由于在多线程中使用了匿名函数外的局部变量而导致的Bug

执行代码

static void Main(string[] args)
{
    
for (int i = 0; i < 10; i++)
    {
        Thread t 
= new Thread(delegate()
        {
            Thread.Sleep(
new Random().Next(1, 10000));
            Console.Write(
i + ", ");
        });
        t.Start();
    }
}


将得到输出:10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
而不是我们期望的类似于:3, 5, 6, 1, 0, 7, 9, 8, 4, 2, 这样的输出。这是为什么呢?(在实际项目中出现这个Bug的代码请参考[1])

分析:使用Reflector查看编译之后的代码

下面是编译前后的代码对比。(使用Reflector的具体方法请参考[2],这里仅展现结果)



可以发现,i 对于匿名方法来说算得上是“全局”变量,如果在线程处理 i 之前,i 的值就被改变了的话,就会出现我们不希望出现的结果。

解决方法1:使用更小范围的局部变量

我们做一个小小的更改,在for循环里面定义一个变量 j ,让匿名函数只访问这个 j



所以运行程序,可得到正确的输出:3, 5, 6, 1, 0, 7, 9, 8, 4, 2,
由此,我们可以做出一个假设:编译器会在声明匿名函数所使用的局部变量的地方声明AutoGenClass的实例。那么,可以推出另一个结论:如果匿名函数里面使用了成员变量,那么ThreadStart对象也会变成成员变量,有兴趣的话可以自己用Reflector看一下。
      这个方法虽然能有效解决问题,但有一个缺点:声明临时变量的意图不明显。为了避免哪天有个十分热心又不明就里的程序员觉得“变量 j 根本和 i 一样嘛”而把 j 给移除了,我强烈建议在 j 的后面加一个注释:“这里声明一个临时变量 j 是有深意的,谁敢动它老子跟谁玩命!!”。为避免这个缺点,可考虑使用下面的解决方法。

解决方法2:在匿名函数中只使用它的参数



这次匿名函数压根就没使用“全局”变量,所以同样可得到正确的结果:9, 7, 0, 5, 3, 8, 1, 6, 4, 2,

如果需要使用线程池,代码大同小异:

static void Main(string[] args)
{
    
for (int i = 0; i < 10; i++)
    {
        ThreadPool.QueueUserWorkItem(
delegate(object arg)
        {
            Thread.Sleep(
new Random().Next(1, 1000));
            Console.Write(arg 
+ ", ");
        }, i);
    }
    Thread.Sleep(
10000);
}


      让我们再一次仔细思考一下为什么会出现Bug,以及解决方法1和解决方法2是如何生效的。为什么会出现Bug呢?表面上看是因为使用了多线程。但为什么使用了多线程结果就不对了呢?是因为我们期望的执行顺序是“把i的值增加1、输出i、把i的值再增加1、再输出i……”,而实际上由于输出 i 的操作由另一个线程来执行,导致输出 i 的操作进入了另一个平行宇宙——呃,我是说异步操作之中。这样,i 的值在不停地改变,而输出 i 的操作随时都可能被执行,所以输出的 i 的值就成了随机的,程序的行为也成了随机的,这可真是个不折不扣的Bug。想想看,如果另一个平行宇宙里的1-2-3脱掉裤子,会影响到我的裤子也一同掉下来,这是件多可怖的事情!
      要解决这个问题,就要使 i 一但被传递给线程来执行,就不再受到主线程里对 i 的改变的影响。解决方法1通过每次创建新的线程前都创建一个新的AutoGenClass实例,并Copy i 的值(因为 i 是值类型)给AutoGenClass实例的成员变量达到这个目标。解决方法2通过在启动线程时把 i 的值压入匿名方法的参数堆栈来达到这个目标。
      既然确保 i 的值在传递给线程执行之后就不再改变这么重要,我们是否应该去微软总部门前示威,要求在匿名函数里只能使用只读的局部变量呢?就这么定了!机票钱老赵出,现在报名……
      冷静。不应该做这样的限制,因为 1)匿名函数既可以用作同步执行,也可被异步执行。在同步执行的时候更改局部变量是没问题的,而且同步执行的情况比较多,我们哪能因噎废食呢?2)即使限制成只读变量也没用。因为如果 i 是个复杂类型的实例的话,即使声明成只读的,一样可以更改它的属性的值,而如果匿名函数正巧依赖它的属性值,Bug还是会发生。3)有时我们需要故意让线程共享可修改的对象,请看下文。

      “这TMD是怎么回事?”突然听到 Boss 吼道,“为什么你的程序输出的结果是无序的?马上给我改成输出 0,1,2,3,4,5,6,7,8,9, !”
      可是我们用的多线程呀,怎么能保证各个线程按顺序取得数据呢?这种时候,我们就需要故意使用一个可被主线程和其它线程共享、修改的对象,当然一些同步操作也是必须的,请看下面的例子。


解决方法3:使用泛型Queue传递数据

代码如下:

static void Main(string[] args)
{
    Queue
<int> q = new Queue<int>();
    
for (int i = 0; i < 10; i++)
    {
        q.Enqueue(i);

        Thread t 
= new Thread(delegate()
        {
            Thread.Sleep(
new Random().Next(1, 10000));
            
lock (q)
            {
                
if (q.Count > 0)
                    Console.WriteLine(Thread.CurrentThread.Name 
+ ": do " + q.Dequeue());
            }
        });
        t.Name 
= "线程" + i;
        t.Start();
    }
}

输出:

线程2: do 0
线程1: do 1
线程7: do 2
线程9: do 3
线程3: do 4
线程5: do 5
线程6: do 6
线程8: do 7
线程0: do 8
线程4: do 9
请按任意键继续. . .


      使用了线程同步之后,线程们排着队去Queue里取数据,然后执行,在效率上就体现不出多线程的优势了。不过,如果换成用线程池利用后台空闲线程还是有意义的。
      聪明的你一定想到了,泛型Queue一样可以通过参数传递进去。

static void Main(string[] args)
{
    Queue
<int> q = new Queue<int>();
    
for (int i = 0; i < 10; i++)
    {
        q.Enqueue(i);

        Thread t 
= new Thread(delegate(object arg)
        {
            Thread.Sleep(
new Random().Next(1, 10000));
            Queue
<int> qq = arg as Queue<int>;
            
lock (qq)
            {
                
if (qq.Count > 0)
                    Console.WriteLine(Thread.CurrentThread.Name 
+ ": do " + qq.Dequeue());
            }
        });
        t.Name 
= "线程" + i;
        t.Start(q);
    }
}


结论

      既然.net提供了由线程向匿名函数传递参数值的功能,你想要定下一条“多线程回调的匿名函数只允许使用它的参数,禁止使用函数外的变量”的规矩是可以理解的。不过即使这样做,当变量是复杂类型的实例的时候,同样会有产生Bug的危险。所以,要理解为什么以及如何同步变量和线程的执行,灵活运用,别莫名其妙地掉了裤子。
      由于Thread.Start()的参数只能有一个,所以需要传递多个数值的时候就必须提前构造一个数组或Struct,这多少还是有些不便。让我们去微软门前游行,要求为Thread.Start()提供一个可变参数的重载吧

.NET 匿名函数引用局部变量导致的问题 问题 写C#窗口程序,今天遇到的问题。在工作线程(非UI线程)要操作ListView,因此使用了跨线程调用方式。 for (int i = 0; i < videos.Count; ++i) { this.BeginInvoke(new Action(() => { var item = lvwVideos.FindItemWithText(vid... 阅读详情

相关推荐

Docker网络教程(四)macvlan 网络教程

在这种网络下,Docker 宿主机在它的IP之上,允许接受多个 MAC 地址的请求,然后再将这些请求路由至对应的容器里。流通,Docker 会用它的 MAC 地址,把流量路由进你的容器里。对于网络上的网络设备来说,你的容器似乎是物理上连接到网络上的。),Docker使用其MAC地址将流量路由到你的容器。对于网络上的网络设备来说,你的容器似乎是物理上连接到网络上的。你需要 3.9 或者更高版本的 Linux 内核,4.0 以及以上的版本是更好的。如果你的设备叫别的名字,用对应的名字代替。

weixin_42445065的博客 4132

C# 7.0 - C# 7.3 中的新增功能

C# 7.0 - C# 7.3 为 C# 开发体验带来了大量功能和增量改进。 本文概述了新的语言功能和编译器选项。 说明中描述了 C# 7.3 的行为,C# 7.3 是基于 .NET Framework 的应用程序支持的最新版本。 C# 7.1 中添加了语言版本选择配置元素,因此你可以在项目文件中指定编译器语言版本。 C# 7.0-7.3 将这些功能和主题添加到了 C# 语言中: 元组和弃元 可以创建包含多个公共字段的轻量级未命名类型。 编译器和 IDE 工具可理解这些类型的语义。 弃元是指在不关心所赋

玖拾说架构 1203

Vmware Vcenter7.0证书web续期发生错误

生成证书,选择第三个(PS,如果没有域名的,一定要写IP,不然很容易卡在85%,服务不能起来)这里大概率可能是证书里面的FQDN和主机不匹配,又或者是主机解析FQDN有问题。vCenter _MACHINE_CERT快到期了,通过web界面更新证书失败。vSphere Client 版本 7.0.2.00200。登陆ssh到vcenter,重新生成证书。先关掉HA,不然证书管理会报错。第一步先这样,重新续订一下证书。

m0_74823595的博客 3944

局部变量和成员变量(C#)

局部变量 ## 标题 ## 成员变量

qgnczmnmn的博客 1159

Materials Studio与LAMMPS联合作战:如何用msi2lmp.exe完美转换聚合物模型(含PCFF力场配置)

本文详细解析了如何利用msi2lmp.exe工具将Materials Studio中的聚合物模型高效、准确地转换为LAMMPS可识别的data文件,并重点配置PCFF力场。文章提供了从MS模型构建、文件导出到命令行转换、参数调整及转换后验证的全流程实战指南与避坑技巧,旨在帮助研究者解决模型转换中的常见问题,实现跨平台分子动力学模拟的无缝衔接。

weixin_29228781的博客 520

unity中VideoPlayer播放视频,使用Shader除去黑底且无黑边

1. 创建一个Shader 和 一个 材质球;Shader中复制如下代码:(此Shader代码为“ChromaKeyKit”插件中的ChromaKey_Alpa_Genera代码,但因只需要除去视频黑底而挂一个插件有点不合适) // Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)' Shader "ChromaKeyKit/ChromaKey/ChromaKey_Alpha_General"

weixin_42065078的博客 1747

异步匿名函数

创建异步匿名函数,与创建其他匿名方法或Lambda表达式类似,不同的是要在前面加上async修饰符,例如 Func<Task> lambda = async () => await Task.Delay(10); Func<Task<int>> anonMethod = async delegate () { Console.WriteLine...

早起吃虫的博客 3260

利用PLECS实现Boost变换器的闭环控制仿真与优化

本文详细介绍了如何利用PLECS仿真软件实现Boost变换器的闭环控制与优化。从基础原理到开环系统搭建,再到闭环控制设计与动态优化,提供了完整的仿真实践指南。通过PLECS的仿真功能,工程师可以有效诊断问题、优化参数,并实现高性能的DC-DC变换器设计。

weixin_42527401的博客 325

ajax请求返回值赋值问题

在发起异步ajax请求的时候,期望在返回回调函数中将返回值赋值给前面的全局变量出现的问题: var a; $.post(url,function(data){     a = data; }); console.log(a); 这里的a打出来是undefined的; 究其原因,因为ajax请求默认是异步请求,所以js解释执行的时候是不会进行等待ajax的返回,而是会直接进行下一步解释 执行,所...

csdn15679160266的博客 4094

Agile Modbus使用和学习笔记

i < len;这里仅仅需要提供获取函数即可,离散输入是只读类型i++) {return 0;只读类型寄存器i++) {return 0;i < len;

Car12 167

基于 InceptionNeXt 模块增强的 YOLOv8 模型:针对多尺度目标检测的改进与实现

modules.py。

know_F0727的博客 203

异步方法不能使用ref和out的解决方法

 异常处理汇总-后端系列:http://www.cnblogs.com/dunitian/p/4523006.html 应用场景==》后端现在都是用异步方法,那么分页是必不可少的,于是就有了这个问题 这个是官方说明:https://social.msdn.microsoft.com/Forums/en-US/d2f48a52-e35a-4948-844d-828a1a6deb74/why-...

weixin_33785972的博客 734

【反封禁策略】集成 Undetected-Chromedriver:让 Browser-Use 绕过 Cloudflare 五秒盾

摘要:AI Agent如何突破Cloudflare检测的实战指南 本文针对AI Agent在Cloudflare检测下频繁受阻的问题,深入分析了undetected-chromedriver的工作原理与局限性。文章指出,Cloudflare 2026年的检测体系已升级为"浏览器指纹+IP信誉+协议指纹+行为模式"的多维交叉验证,单一伪装方案难以奏效。 核心内容包括: undetected-chromedriver的三层补丁体系解析 v2与v3版本的适用场景对比 两种Browser-Use集成方案:Selen

ZDQ58818的博客 284

微信小程序——Promise异步简化回调,简易回调,代理接口回调,await的用法,匿名函数

Promise异步简化回调 使用的背景:同时存在两个或者多个网络请求,且第二个请求要使用第一个请求返回的数据 代码: const app = getApp() Page({ data: { }, onLoad: function () { this.function_1().then(res=>{ console.log('方法1结果:',res) //这里可以把结果放到GetUserEntity_2作为参数 this.function_2

wy313622821的博客 1812

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

js处理异步的几种方式

1、回调函数 封装了异步操作的函数接受一个匿名函数作为参数,当异步操作执行完后调用这个传递进来的匿名函数; function getData(callback){ setTimeout(() => { let name = 'xxx' callback(name) }, 1000); } getData((d)=>{ console.log(d) }) 缺...

withwz的博客 4794

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

一个简单效果用到的js特性(异步编程,匿名函数,闭包)

效果:轮询显示隐藏所有p标签,显示用打印机效果一个字符一个字符地显示,淡出隐藏准备知识 js异步编程(setInterval setTimeout) JS 本身是单线程运行的,不可能在一段代码还未结束运行时去运行别的代码,因此也就不存在异步执行的概念。但是,如果某个函数做的事情是创建一个别的线程或进程,并与JS主线程并行地做一些事情,并在事情做完后通知 JS 主线程,就能异步。 function

u013561386的博客 1626

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

javaScript 自执行函数—保留异步方法调用的内部变量

1.什么是自执行函数其形如:(function(a){})(a)2.用处javascript中没用私有作用域的概念,如果在多人开发的项目上,你在全局或局部作用域中声明了一些变量,可能会被其他人不小心用同名的变量给覆盖掉,根据javascript函数作用域链的特性,可以使用这种技术可以模仿一个私有作用域,用匿名函数作为一个“容器”,“容器”内部可以访问外部的变量,而外部环境不能访问“容器”内部的变量...

曲径通幽 2220

javascript之异步操作理解---回调函数,async,await以及promise对象

javascript之异步操作理解---回调函数,async,await以及promise对象概述 概述 写在前面:虽然平时做项目,但是发现自己写的代码还是很烂。最近接触了一个对性能要求比较高的项目,于是重新开始审视自己写的代码是不是达到尽量较小系统性能了。加上前几天学到express框架了解到了回调函数的操作。作为拓展,查了许多资料,再次总结一下javascript的异步操作。 javascript是一门单线程语言,即一次只能完成一个任务,若有多个任务要执行,则必须排队按照队列来执行(前一个任务完成,

qq_44774831的博客 4384

C++核心准则F.52:在lambda表达式中使用引用形式捕捉局部变量

F.52: Prefer capturing by reference in lambdas that will be used locally, including passed to algorithms F.52 在lambda表达式中使用引用形式捕捉局部变量,包含向算法传递变量的情况。 译者注:如果你不清楚捕捉(capture)是什么,请参考 https://mp.weixin...

面向对象思考 3243

java lambda 局部变量,Java中的异步Lambda如何将局部变量

I have a method (can be concurrently called by different threads) that creates an asynchronous task and returns a CompletableFuture. I want to measure the time it takes to run the task by chaining it ...

weixin_36352432的博客 338

[C++]引用参数与临时变量

对于引用参数,什么情况下会产生临时变量了?C++为什么对于产生临时变量的引用参数要加const?

Jamienstar的博客 3989

微软技术日报 11-09:.NET 8 与 9 明日同时停止支持,注册表编辑器出了 WinUI 3 版

今天这四条消息的共同点是收口。.NET 8 和 9 的支持期到点,注册表编辑器的新界面只放在仓库里给人看,GitHub 把一类危险触发器按住默认,微软在安全报告里把防线从地址黑名单挪到行为。收口并不等于关门。.NET 10 还在支持期里,被按住的触发器可以用策略按文件放回来,实验室里的样例随时可能变成产品。区别在于这些动作要有人签字,不再是默认开着。如果今天只做一件事,去看一眼构建机上的 .NET 版本。明天之后,8 和 9 的漏洞补丁不会再来了。

海盗Sharp的博客 302

11-01-演进-.NET GC发展史

.NET GC发展史映射硬件演进:从单核STW到多核并发,再到区域化细粒度回收。历经分代管理、ServerGC、BackgroundGC、LOH自动压缩、POH隔离、DATC自适应线程数及Region-based管理等关键革新,代码量从5000行增至4万+,复杂度指数上升。每一步优化均在暂停时间、吞吐量与资源开销间权衡,体现“实用主义”哲学。核心启示:GC演进由硬件驱动,方向明确——更短暂停、更高效率,但代价是工程复杂性持续攀升。

设计 AI 玩游戏🎮 360

微软技术周报 11-02~11-09:VS Code 混装 Claude 与 Codex、EF Core 11 静默改行为、.NET 11 倒计时

11 月 6 日,Power Platform 通过消息中心通知管理员,Copilot Studio、Power Automate 和 Power Apps 里所有跑在 Anthropic Claude、xAI Grok 以及旧版 GPT 上的 prompt,12 月 2 日自动迁移到 GPT-6 系列,档位价格不变,模型选择收窄到一条产品线。效果是这样的:让 Claude 评审一个 Copilot chat 写出的方案,或者把同样的任务交给 Codex,两边并排跑在同一个会话里。更要紧的是下周的时间线。

海盗Sharp的博客 414
上一篇: C#引用类型
下一篇: C#操作SQLServer数据库
CodePointer
博客等级 码龄17年 12粉丝 11原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值