本地 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个? }

  1. static void Go()
  2. {
  3. // cycles是本地变量
  4. // 在每个线程的内存栈上,都会创建cycles独立的副本
  5. for (int cycles = 0; cycles < 5; cycles++)
  6. {
  7. Console.WriteLine("?");
  8. }
  9. }
  10. }

}

  1. <a name="EZsiG"></a>
  2. #### Shared共享
  3. - 如果多个线程都引用到同一个对象的实例,那么他们就共享了数据。
  4. ```csharp
  5. using System;
  6. using System.Threading;
  7. namespace ThreadTest
  8. {
  9. class Program
  10. {
  11. bool _done;
  12. static void Main()
  13. {
  14. Thread.CurrentThread.Name = "Main";
  15. Program t = new Program(); // 创建了一个共同的实例
  16. new Thread(t.Go) { Name = "New" }.Start();
  17. t.Go();
  18. }
  19. void Go() // 这是一个实例方法
  20. {
  21. Console.WriteLine(Thread.CurrentThread.Name);
  22. if (!_done)
  23. {
  24. _done = true;
  25. Console.WriteLine("Done");
  26. }
  27. }
  28. // 由于两个线程是在同一个Program实例上调用的Go(),所以它们共享_done
  29. // 结果就是打印一次Done
  30. }
  31. }
  • 被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操作之后再把变量保存到主内容中去。而进行这些操作的时候其它线程就有可能抢先执行了。