代码改变世界

NET多线程探索-线程同步和通信

2012-03-20 16:53  海不是蓝  阅读(2295)  评论(8编辑  收藏  举报

NET中各种线程同步方法


在NET多线程开发中,有时候需要多个线程协调工作,完成这个步骤的过程称为“同步”。

使用同步的主要原因:
1.多个线程访问同一个共享资源。
2.多线程写入文件时保证只有一个线程使用文件资源。 3.由事件引发线程,线程等待事件,需要挂起线程。

NET中线程同步常见的几种方法:

1.lock


lock 确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。
lock的优点:简单易用,对象的同步几乎透明,轻量级。

使用lock需要注意:
锁定的对于应该是私有的,如果是公有的对象,可能出现超出控制范围的其它代码锁定该对象。
所以应该尽量避免使用lock(this),不保证会有其他线程闯入破坏数据正确性。

一个lock(this)错误的示例:

class Program
{
    static void Main(string[] args)
    {
        A a1 = new A();
        A a2 = new A();
        Thread T1 = new Thread(new ParameterizedThreadStart(a1.Test));
        Thread T2 = new Thread(new ParameterizedThreadStart(a2.Test));
        T1.Start(2);
        T2.Start(5);
        Console.Read();
    }
}
public class A
{
    public static Int32 Count = 2;
    public void Test(object i)
    {
        lock (this)
        {
            Console.WriteLine("Count={0}", Count);
            Count += (Int32)i;
            Thread.Sleep(1 * 1000);
            Console.WriteLine("i={0},?Count+i={1}", i, Count);
            Console.WriteLine("--------------------------");
        }
    }
}

图片1
这里数据和我们期待的不相同。

这里lock锁定的是A的实例,当线程T1执行的时候,lock锁定了a1对象,所以线程t2可以执行a2对象的Test方法。

正确的写法:

public class A
{
    private static object obj = new object();
    public static Int32 Count = 2;
    public void Test(object i)
    {
        lock (obj)
        {
            Console.WriteLine("Count={0}", Count);
            Count += (Int32)i;
            Thread.Sleep(1 * 1000);
            Console.WriteLine("i={0},?Count+i={1}", i, Count);
            Console.WriteLine("--------------------------");
        }
    }
}

图片2

这里的线程已经正常工作了,Count也正常的累加。


lock (obj)怎么错误了?
上面正常运行的程序稍微改动下!

class Program
{
    static void Main(string[] args)
    {
        A a1 = new A();
        A a2 = new A();
        Thread T1 = new Thread(new ParameterizedThreadStart(a1.Test));
        Thread T2 = new Thread(new ParameterizedThreadStart(a2.Test));
        T1.Start(2);
        T2.Start(5);
        Console.Read();
    }
}
public class A
{
    private object obj = new object();
    public static Int32 Count = 2;
    public void Test(object i)
    {
        lock (obj)
        {
            Console.WriteLine("Count={0}", Count);
            Count += (Int32)i;
            Thread.Sleep(1 * 1000);
            Console.WriteLine("i={0},?Count+i={1}", i, Count);
            Console.WriteLine("--------------------------");
        }
    }
}

图片3

分析下:这里我们把

private static object obj = new object();

中的static去掉了,所以obj变成了私有的实例成员,a1,a2都有不同的obj实例,所以lock这里也就没什么作用了!

让lock不再错下去!
这里我们也不再把static写上去了,只需要把调用那里稍微改动下。

class Program
{
    static void Main(string[] args)
    {
        A a1 = new A();
        Thread T1 = new Thread(new ParameterizedThreadStart(a1.Test));
        Thread T2 = new Thread(new ParameterizedThreadStart(a1.Test));
        T1.Start(2);
        T2.Start(5);
        Console.Read();
    }
}

图片4

我们这里让a2对象去见马克思了,a1对象的Test调用了2次,这里lock锁定的obj都是a1的同一个私有对象obj,所以lock是起作用的!

 

Monitor


Monitor实现同步比lock复杂点,lock实际上是Monitor的简便方式,lock最终还是编译成Monitor。
不同处:
1.Monitor在使用的时候需要手动指定锁和手动释放手。
2.Monitor比lock多了几个实用的方法

public static bool Wait(object obj);
public static void Pulse(object obj);
public static void PulseAll(object obj);


Mointor同步实例:

class Program
{
    static void Main(string[] args)
    {
        Int32[] nums = { 1, 2, 3, 4, 5 };
        SumThread ST1 = new SumThread("Thread1");
        SumThread ST2 = new SumThread("Thread2");
        ST1.Run(nums);
        ST2.Run(nums);
        Console.Read();
    }
}

public class SumThread
{
    //注意这里私有静态SA是SumThread共有的,所以考虑同步问题
    private static SumArray SA = new SumArray();
    private Thread t;

    public SumThread(string ThreadName)
    {
        t = new Thread((object nums) =>
        {
            Console.WriteLine("线程{0}开始执行...", t.Name);
            Int32 i = SA.GetSum((Int32[])nums);
            Console.WriteLine("线程{0}执行完毕,sum={1}", t.Name, i);
        });
        t.Name = ThreadName;
    }

    public void Run(Int32[] nums)
    {
        t.Start(nums);
    }
}

public class SumArray
{
    private Int32 sum;
    private  object obj = new object();

    public Int32 GetSum(Int32[] nums)
    {
        Monitor.Enter(obj);
        try
        {
            //初始化sum值,以免获取到其它线程的脏数据
            sum = 0;
            foreach (Int32 num in nums)
            {
                sum += num;
                Console.WriteLine("当前线程是:{0},sum={1}", Thread.CurrentThread.Name, sum);
                Thread.Sleep(1 * 1000);
            }
            return sum;
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
            return 0;
        }
        finally
        {
            //很重要,千万不要忘记释放锁
            Monitor.Exit(obj);
        }
    }
}

图片5


这里线程之间和蔼可亲的执行着,没有去抢着计算。
试着把

Monitor.Enter(obj);
Monitor.Exit(obj);

去掉,那么线程就不会这么听话了!

图片6

 

另外一种同步的写法

前面使用的同步方式并不是所有情况都适合的,假如我们调用的是第三方的组件,我们没有修改源码的权限,那么我们只有考虑下面这种同步的实现。

class Program
{
    static void Main(string[] args)
    {
        Int32[] nums = { 1, 2, 3, 4, 5 };
        SumThread ST1 = new SumThread("Thread1");
        SumThread ST2 = new SumThread("Thread2");
        ST1.Run(nums);
        ST2.Run(nums);
        Console.Read();
    }
}

public class SumThread
{
    //注意这里私有静态SA是SumThread共2有的,所以考虑同步问题
    private static SumArray SA = new SumArray();
    private Thread t;

    public SumThread(string ThreadName)
    {
        t = new Thread((object nums) =>
        {
            Console.WriteLine("线程{0}开始执行...", t.Name);
            Monitor.Enter(SA);
            try
            {
                Int32 i = SA.GetSum((Int32[])nums);
                Console.WriteLine("线程{0}执行完毕,sum={1}", t.Name, i);
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
            finally
            {
                //很重要,千万不要忘记释放锁
                Monitor.Exit(SA);
            }
        });
        t.Name = ThreadName;
    }

    public void Run(Int32[] nums)
    {
        t.Start(nums);
    }
}

public class SumArray
{
    private Int32 sum;

    public Int32 GetSum(Int32[] nums)
    {

        //初始化sum值,以免获取到其它线程的脏数据
        sum = 0;
        foreach (Int32 num in nums)
        {
            sum += num;
            Console.WriteLine("当前线程是:{0},sum={1}", Thread.CurrentThread.Name, sum);
            Thread.Sleep(1 * 1000);
        }
        return sum;
    }
}

图片7

注意:这里锁定的是SA.GetSum()调用本身,不是方法内部代码,SA对象是私有的。

 

Monitor-线程通信


当线程T正在lock块执行,需要访问另外一个线程lock块中的资源R,这个时候如果T等待R可用,有可能会让线程进入死循环。
这里就可以使用线程通信,先让T暂时放弃对lock块中的控制,等R变得可用,那么就通知线程T恢复运行。

public static bool Wait(object obj);
public static void Pulse(object obj);
public static void PulseAll(object obj);

上面就是这里需要用的方法,属于Monitor。 Wait是让线程暂停,这个方法有个重写,多了一个参数指定暂停的毫秒数。
Pulse是唤醒线程。
时钟滴答例子:

class Program
{
    static void Main(string[] args)
    {
        Clock C = new Clock();
        C.RunClock(1);
        Console.Read();
    }
}
public class Clock
{
    private object obj = new object();

    //开始运行时钟,输入运行分钟
    public void RunClock(Int32 Minute)
    {
        Thread T1 = new Thread((object Minute1) =>
        {
            Int32 m = Convert.ToInt32(Minute1) * 60 / 2;
            while (m > 0)
            {
                DI(true);

                m--;
            }
        });
        Thread T2 = new Thread((object Minute1) =>
        {
            Int32 m = Convert.ToInt32(Minute1) * 60 / 2;
            while (m > 0)
            {
                DA(true);
                m--;
            }
        });
        T1.Start(Minute);
        T2.Start(Minute);
    }

    public void DI(bool run)
    {
        lock (obj)
        {
            Console.WriteLine("嘀");
            Thread.Sleep(1000);
            Monitor.Pulse(obj);//执行完毕,唤醒其它线程
            Monitor.Wait(obj);//进入暂停,移交执行权利,等待唤醒
        }
    }

    public void DA(bool run)
    {
        lock (obj)
        {
            Console.WriteLine("嗒");
            Thread.Sleep(1000);
            Monitor.Pulse(obj);//执行完毕,唤醒其它线程
            Monitor.Wait(obj);//进入暂停,移交执行权利,等待唤醒
        }
    }
}

图片8

 

谢谢Shikyoh指出错误,已经修改

就写到这里,下一篇写下互斥锁信号量这些。

 

作者:海不是蓝
博客:hailan2012
邮箱:hailan2012@sina.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。