- 本地状态和共享状态(Local And Shared State)
(1)Local本地独立
CLR为每个线程分配自己的内存栈(Stack)以便使本地变量保持独立
示例:
static void Main(string[] args)
{
new Thread(Go).Start();//在新线程上调用Go()
Go();//在主线程中调用Go()
}
static void Go()
{
//i是本地变量
//在每个线程的内存栈上,都会创建i独立副本
for (int i = 0; i < 5; i++)
{
Console.Write("X");
}
}
//会打印输出10个x
Shared共享
(1).如果多个线程都引用到同一个对象的实例,那么他们就共享了数据
bool _done = true;
static void Main(string[] args)
{
Program pp = new Program();//创建了一个共同的实例
new Thread(pp.Go).Start();
pp.Go();
}
//由于连个线程是在一个共同实例上调用的Go2(),他们共享_done的值,只会打印一个Done
void Go()
{
if (_done)
{
_done = false;
Console.WriteLine("Done");
}
}
打印一次
(2).被Lambda表达式或匿名委托所捕获的本地变量,会被编辑器转化为字段(fieid),所以也会被共享
bool _isOK = true;
ThreadStart pro = () =>
{
if (_isOK)
{
_isOK = false;
Console.WriteLine("OK");
}
};
new Thread(pro).Start();
pro();
打印一次
(3).静态字段(fieid)也会在线程之间共享数据
static void Main(string[] args)
{
new Thread(Go).Start();
Go();
}
static void Go()
{
if (_done2)
{
_done2 = false;
Console.WriteLine("Done");
}
}
打印一次
- 线程安全
上面三个例子会引发缺乏线程安全的问题,
因为打印的输出结果实际上是无法确定的
有可能会将Done打印两次(理论上)
如果将IF里面的两条语句的位置调换,那么被打印两次的概率会大大增加
因为一个语句正在执行输出,还没来得及将Bool的值进行改变
所以要尽可能避免使用共享状态
static void Main(string[] args)
{
new Thread(Go).Start();
Go();
}
static void Go4()
{
if (_done2)
{
Console.WriteLine("Done");
//模拟代码过多时的等待时间
Thread.Sleep(100);
_done2 = false;
}
}
输出两次Done
3. 解决线程安全问题
(1)在读取和写入共享数据的时候,通过一个互斥锁(exclusive lock) ,就可以修复前面的例子问题
在C#中使用lock语句来加锁
(2)当两个线程同时竞争到一个锁的时候(锁可以基于任何引用类型对象),一个线程会等待或阻塞,知道锁变成可用状态.
static bool _done;
static readonly object _loker = new object();
static void Main(string[] args)
{
//在多线程上下文中,以这种方式避免不确定性的代码就叫线程安全
//Lock不是线程安全的银弹,很容易忘记对字段加锁,lock也会引起一些问题(死锁)
new Thread(Go).Start();
Go();
Console.ReadKey();
}
static void Go()
{
lock (_loker)
{
if (!_done)
{
Console.WriteLine("Done");
//模拟代码过多时的等待时间
Thread.Sleep(1000);
_done = true;
}
}
}
打印一次