当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作。直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,这种时候很容易出现问题。

争用条件#

如果两个或多个线程访问相同的对象。并且对共享状态的访问没有同步,就会出现争用条件。

class StateObject
{
    private int state = 5;

    public void ChangeState()
        {
            state++;
            if (state == 5)
            {
                Console.WriteLine("value=5");
            }
            state = 5;
        }
}
public static void ThreadSync_Lock_Example_01()
{
    StateObject m = new StateObject();
    Thread t1 = new Thread(ChangeState);
    t1.Start(m);
    Console.ReadKey();

    static void ChangeState(object o)
    {
        StateObject m = o as StateObject;
        while (true)
        {
            m.ChangeState();
        }
    }
}

此时运行程序是没有输出的,因为 StateObject 类中 state 初始值是5,if 条件不会进入,随后又将 state 重新初始化为5。

两个线程执行

public static void ThreadSync_Lock_Example_02()
{
    StateObject m = new StateObject();
    Thread t1 = new Thread(ChangeState);
    Thread t2 = new Thread(ChangeState);
    t1.Start(m);
    t2.Start(m);
    Console.ReadKey();

    static void ChangeState(object o)
    {
        StateObject m = o as StateObject;
        while (true)
        {
            m.ChangeState();
        }
    }
}

此时会不停的打印"value=5",原因在于:一个线程在判断语句处时,另一个线程可能又将 state 的值改为了5,而导致输出合法。

死锁#

class Deadlock
{
    static StateObject o1 = new StateObject();
    static StateObject o2 = new StateObject();
    public static void DeadlockA(object o)
    {
        lock (o1)
        {
            Console.WriteLine("我是线程{0},我锁定了对象o1", o);
            lock (o2)
            {
                Console.WriteLine("我是线程{0},我锁定了对象o2", o);
            }
        }
    }

    public static void DeadlockB(object o)
    {
        lock (o2)
        {
            Console.WriteLine("我是线程{0},我锁定了对象o2", o);
            lock (o1)
            {
                Console.WriteLine("我是线程{0},我锁定了对象o1", o);
            }
        }
    }
}
 Thread t1 = new Thread(Deadlock.DeadlockA);
 Thread t2 = new Thread(Deadlock.DeadlockB);
        t1.Start("t1");
        t2.Start("t2");

t1 线程执行 DeadlockA() 方法顺序锁定o1o2 t2 线程执行 DeadlockB() 方法顺序锁定o2o1 当前结果显示t1线程锁定了o1后,t2线程在t1线程锁定o1后抢占进来,锁定了o2t2在等t1解锁,t1在等t2解锁,都处于挂起状态在等对方解锁,这就形成了死锁,线程将无限等待下去。

这个问题应该从一开始就设计好锁定顺序,也可以为锁定义超时时间来处理,保证“上锁”这个操作在一个线程上执行也是避免死锁的方法之一。

C#中有多个用于多线程的同步技术

  • lock语句
  • Interlocked类
  • Monitor类
  • SpinLock类
  • WaitHandle类
  • Mutex类
  • Semapphore类
  • Event类
  • Barrier类
  • ReaderWriteLockSlim类

其中lock语句/Interlocked类/Monitor类可用于进程内存的同步,其它几个提供了多进程之间的线程同步。

Lock#

C#使用 lock 语句锁定在线程中共享的变量,如果一个线程锁定了变量,另一个线程就必须等待该锁定的解除。

static void ChangeState(object o)
{
    StateObject m = o as StateObject;
    while (true)
       {
       //给变量m加锁
          lock (m)
            {
              m.ChangeState();
            }
       }
}

因为实例的对象也可以用于外部的同步访问,而且不能在类自身控制这种访问,所以应采用SyncRoot模式,创建私有对象,将这个对象用于 lock 语句。

private static object syncRoot= new object();
static void ChangeState(object o)
{
    StateObject m = o as StateObject;
    while (true)
         {
           lock (aync)
           {
               m.ChangeState();
           }
         }
}

注意

  1. lock只能锁定对象,即引用类型,不能锁定值类型
  2. lock不能锁定空值,因为null是不需要被释放的
  3. 不能锁定string类型,虽然它也是引用类型的。因为字符串类型被CLR暂留,这意味着整个程序中任何给定字符串都只有一个实例,具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例
  4. 避免锁定public类型,如果该实例可以被公开访问,则 lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象

锁是否必须是静态类型?#

如果被锁定的方法是静态的,那么这个锁必须是静态类型。这样就是在全局锁定了该方法,不管该类有多少个实例,都要排队执行。

如果被锁定的方法不是静态的,那么不能使用静态类型的锁,因为被锁定的方法是属于实例的。只要该实例调用锁定方法不产生损坏就可以,不同实例间是不需要锁的。这个锁只锁该实例的方法,而不是锁所有实例的方法。

class ThreadSafe
{
 private static object _locker = new object();
 
  void Go()
  {
    lock (_locker)
    {
      ......//共享数据的操作 (Static Method),使用静态锁确保所有实例排队执行
    }
  }

private object _locker2=new object();
  void GoTo()
  {
    lock(_locker2)
    //共享数据的操作,非静态方法,是用非静态锁,确保同一个实例的方法调用者排队执行
  }
}

同步对象可以兼作它 lock 的对象。比如:

class ThreadSafe
{
 private List <string> _list = new List <string>(); 
  void Test()
  {
    lock (_list)
    {
      _list.Add ("Item 1");
    }
  }
}

Monitors#

lock 其实是 Monitors 的简洁写法。

lock (syncRoot)
{
    m.ChangeState();
}

两者其实是一样的

Monitor.Enter(syncRoot);
try
{
    m.ChangeState();
}
finally
{
    Monitor.Exit(syncRoot);
}

Mutex#

互斥锁是一个互斥的同步对象,同一时间有且仅有一个线程可以获取它。可以实现进程级别上线程的同步。

public static void ThreadSync_Mutex_Example_01()
{
    for (int i = 0; i < 3; i++)
    {
        //在不同的线程中调用受互斥锁保护的方法
        Thread test = new Thread(MutexMethod);
        test.Start();
    }
    Console.Read();

    static void MutexMethod()
    {
        //实例化一个互斥锁
        Mutex mutex = new Mutex();

        Console.WriteLine("{0} 请求获取互斥锁", Thread.CurrentThread.ManagedThreadId);
        mutex.WaitOne();
        Console.WriteLine("{0} 已获取到互斥锁", Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine("{0} 准备释放互斥锁", Thread.CurrentThread.ManagedThreadId);
        // 释放互斥锁
        mutex.ReleaseMutex();
        Console.WriteLine("{0} 已经释放互斥锁", Thread.CurrentThread.ManagedThreadId);
    }
}

互斥锁的带有三个参数的构造函数

  • initiallyOwned : 如果 initiallyOwnedtrue,互斥锁的初始状态就是被所实例化的线程所获取,否则实例化的线程处于未获取状态
  • name:该互斥锁的名字,在操作系统中只有一个命名为name的互斥锁mutex,如果一个线程得到这个name的互斥锁,其他线程就无法得到这个互斥锁了,必须等待那个线程对这个线程释放
  • createNew:如果指定名称的互斥体已经存在就返回false,否则返回true