先贴代码:
for (int i = 0; i < 10; i++)
{
System.Threading.ThreadPool.QueueUserWorkItem((o) =>
{
Console.WriteLine(i);
});
}
上面这段代码,照理是进行异步处理,开了线程池来打印。理想输出是0~10这样。
但结果:
这里就是闭包现象。
这里有篇博文也介绍这个:https://www.cnblogs.com/jiejie_peng/p/3701070.html 上面的概念是复制该博文过来的。
这里简单理解,就是变量i,在lamdb表达式的时候,传的是地址,而实际上,这个地址上的值已经执行完for变成10了(理想情况下,传的应该是值0~9)。
照这样理解,可以搞一个中间缓冲值(即每次传的都不是同一个地址)来处理:(输出没按照顺序,是因为多核处理问题,这里不进行讨论)
明显已经没有重复值了,但是记住,地址!就是说tmp的声明必须在for内,如果放到外面(或者放在线程池里面):
神秘现象,直接是9,不是10了???好吧,不小心给自己挖了个坑,接下来开始填坑吧。分析为什么是9,不是10:
循环中确实是0~9,但是最后i++变成10,进行i<10的判断后false,不进入循环了,所以i最后结果为10,tmp=9. 坑解决。
但这里又有新坑,按前面的理解,不是全部应该“in :9___10”,怎么还出现"in :6___6"的?
其实是因为console占用了时间,导致线程池的在for还未执行完的时候就开始了(线程池创建后还得等待分配资源才能启动,所以有延迟),所以就可以看到,前面数字很乱(for还在执行,所以数字还在变),到后面才变成全部9(for执行完毕)。
这里有多线程的知识,这里暂时不讨论,简单讲,int类的,在多核系统中只会有一个内存占位,所以不必担心出现A线程修改值的时候B也来修改,导致脏数据;其他如string类型的,因为是每个线程会复制一份到自己环境中操作,修改完毕后才通知回主地址,如果这时出现A、B线程同时拷贝了数据去操作,又同时通知回来,就导致脏数据产生(互相覆盖),所以就得用多线程锁(实质是内存片段锁的处理,锁住主地址的访问,不允许拷贝,变成类似单线程操作)来处理。
下面贴一下内存地址部分测试的demo,证明用的是同一个地址:
public class test
{
//地址不变
public int num;
//string的内容变时,实际是重新占用内存,然后存入新值,所以只要内容发生变化,内存肯定变
public string str;
public void MethodA()
{
for (int i = 0; i < 10; i++)
{
System.Threading.ThreadPool.QueueUserWorkItem((o) =>
{
num = i;
str = i.ToString();
GetAddr();
});
}
}
public void MethodB()
{
for (int i = 0; i < 10; i++)
{
//System.Threading.ThreadPool.QueueUserWorkItem((o) =>
//{
num = i;
str = i.ToString();
GetAddr();
//});
}
}
unsafe void GetAddr()
{
//这个是类对象,放在堆里面
fixed (int* p = &num)
{
Console.WriteLine("{0}__:Address of numbe:0x{1:x}", num, (int)p);
}
//fixed(char* p= str)
//{
// Console.WriteLine("{0}__:Address of char:0x{1:x}", str, (int)p);
//}
//GetAddrZ(num);
}
////弄成函数传值的话,地址是变化的,因为是一份拷贝
//unsafe void GetAddrZ(int n)
//{
// //获取栈上变量的地址
// int* p = &n;
// Console.WriteLine("{0}__:Address of n:0x{1:x}\n", num, (int)p);
//}
}
先使用int:
接下来是string:
这里的main函数是:
static void Main(string[] args)
{
//int tmp;
//for (int i = 0; i < 10; i++)
//{
// tmp = i;
// //Console.WriteLine("out :{0}____{1}", tmp,i);
// System.Threading.Thread.Sleep(1);
// System.Threading.ThreadPool.QueueUserWorkItem((o) =>
// {
// Console.WriteLine("in :{0}____{1}", tmp, i);
// });
//}
test t = new test();
t.MethodA();
Console.WriteLine();
Console.WriteLine();
t.MethodB();
Console.ReadLine();
}
当然这里也会出现这种情况:
发现这里上面本应该一致的地址,也会出现不同。
重复多次执行经常出现这种情况。
这里就是引用类型在多线程情况下的问题。
这里简单提及“原子操作volatile”:
原子操作 volatile。通知编译器,不允许拷贝,全部访问都是去主地址拿。算是最简单的多线程处理操作,实质这里没使用到加锁,还是会出现问题的:
这里放一下简单的加锁操作:
private object lockstr = new object();
private volatile string _str = "";
public string Str
{
get { return _str; }
set
{
lock (lockstr)
{
_str = value;
}
}
}
多线程内容超级多,等我整理好后再发专门的主题。这里只提初级的加锁方式lock(obj)。