本地 vs 共享的状态 Local vs Shared State
Local本地独立
- CLR为每个线程分配自己的内存栈(Stack),以便使本地变量保持独立。 ```csharp using System; using System.Threading;
namespace ThreadTest
{
class Program
{
static void Main()
{
new Thread(Go).Start(); // 在新线程上调用Go()
Go(); // 在main线程上调用Go()
//输出10个?
}
static void Go()
{
// cycles是本地变量
// 在每个线程的内存栈上,都会创建cycles独立的副本
for (int cycles = 0; cycles < 5; cycles++)
{
Console.WriteLine("?");
}
}
}
}
<a name="EZsiG"></a>
#### Shared共享
- 如果多个线程都引用到同一个对象的实例,那么他们就共享了数据。
```csharp
using System;
using System.Threading;
namespace ThreadTest
{
class Program
{
bool _done;
static void Main()
{
Thread.CurrentThread.Name = "Main";
Program t = new Program(); // 创建了一个共同的实例
new Thread(t.Go) { Name = "New" }.Start();
t.Go();
}
void Go() // 这是一个实例方法
{
Console.WriteLine(Thread.CurrentThread.Name);
if (!_done)
{
_done = true;
Console.WriteLine("Done");
}
}
// 由于两个线程是在同一个Program实例上调用的Go(),所以它们共享_done
// 结果就是打印一次Done
}
}
- 被Lambda表达式或匿名委托所捕获的本地变量,会被编译器转化为字段(field),所以也会被共享。 ```csharp using System; using System.Threading;
namespace ThreadTest { class Program { static void Main() { bool done = false; ThreadStart action = () => { if (!done) { done = true; Console.WriteLine(“Done”); } }; new Thread(action).Start(); action(); } } }
- 静态字段(field)也会在线程间共享数据。
<a name="wDuoN"></a>
## 线程安全Thread Safety
- 上面三个例子就引出了**线程安全**这个关键概念(或者说缺乏线程安全)
- 上述例子的输出实际上无法确定的:
- 有可能(理论上)“Done”会被打印两次。
- 如果交换Go方法里语句的顺序,那么“Done”被打印两次的几率会大大增加
```csharp
using System;
using System.Threading;
using Console = System.Console;
namespace ThreadTest
{
class Program
{
private static bool _done;
static void Main()
{
new Thread(Go).Start();
Go();
}
static void Go()
{
if (!_done)
{
Console.WriteLine("Done");
Thread.Sleep(100);
_done = true;
}
}
}
}
// 输出 Done Done
- 因为一个线程可能正在评估if,而另外一个线程在执行WriteLine语句,它还没来得及把done设为true。
尽可能的避免使用共享状态。
锁定与线程安全 简介 Locking & Thread Safety
- 在读取和写入共享数据的时候,通过使用一个互斥锁(exckysive lock),就可以修复前面例子的问题。
- C#使用lock语句来加锁
- 当强哥线程同时竞争一个锁的时候(锁可以基于任何引用类型),一个线程会等待或阻塞,直到锁变成可用状态。 ```csharp using System; using System.Threading; using Console = System.Console;
namespace ThreadTest { class Program { private static bool _done;
private static readonly object _locker = new object();
static void Main()
{
new Thread(Go).Start();
Go();
// 输出一个Done
}
static void Go()
{
// 通过一个引用类型的对象,在lock大括号里面这段代码,同一时刻最多只有一个线程执行。
lock (_locker)
{
if (!_done)
{
Console.WriteLine("Done");
Thread.Sleep(100);
_done = true;
}
}
}
}
} ```
- 在多线程上下文中,以这种方式避免不确定性的代码就叫做线程安全。
- Lock不是线程安全的银弹,很容易忘记对字段加锁,lock也会引起一些问题(死锁)
思考题i++是线程安全的吗?
不是,因为每个线程都有自己的内存,在执行的时候会把变量加载到自己的内存中,进行+1操作之后再把变量保存到主内容中去。而进行这些操作的时候其它线程就有可能抢先执行了。