码迷,mamicode.com
首页 > 编程语言 > 详细

线程同步

时间:2016-04-29 16:26:30      阅读:221      评论:0      收藏:0      [点我收藏+]

标签:

线程同步

 

CLR为每个线程分配了线程栈,用于保存本地变量,这样可以保证本地变量是独立的,案例:

        static void Main(string[] args)
        {
            ThreadStart ts = new ThreadStart(Print);
            new Thread(ts).Start();
            Print();
        }
        static void Print()
        {
            for (int i = 0; i < 3; i++)
            {
                Console.Write(i);
            }
        }


在主线程和工作线程中都使用了i变量,但它们是相互独立的,彼此不会影响.线程之间也能共享资源,比如前面的那个shareValue变量的案例.当多个线程访问同一对象时,如果不加以协调,可能会出现结果不正确的情况:

        static int i = 0;
        static void Main(string[] args)
        {
            ThreadEntry();
            ThreadEntry();
        }
        static void ThreadEntry()
        {
            Console.WriteLine("i={0}",i);
            i++;
        }


这段代码在主线程上执行的结果为:

i=0
i=1

 

如果让一个ThreadEntry(0方法由主线程执行,另一个由工作线程执行:

        static void Main(string[] args)
        {
            new Thread(ThreadEntry).Start();
            ThreadEntry();
        }


结果可能是:

i=0
i=0


这是因为两个线程同时进入了ThreadEntry()方法,向控制台输出后采取修改i的值.如果i作为if或者while的判断语句,例如(ifi==1){//},那么这段代码能否执行时不确定的.

 

线程同步就是协调多个线程间的并发操作,已获得符合预期的,确定的执行结果,消除多线程应用程序中的不确定性,它包含两个方面:

(1).保护资源(或代码),即确保资源(或代码)同时只能由一个线程(或指定个数的线程)访问,一般措施是获取锁和释放锁(后面简称锁机制).

(2).协调线程对资源(或代码)的访问顺序,即确定某一资源(或代码)只能先有线程T1访问,再由线程T2访问,一般措施是采用信号量(后面简称信号量机制).T2线程访问资源时,必须等待线程T1先访问,线程T2访问完后,发出信号量,通知线程T2可以访问.

 

 

使用Monitor

 

System.Threading.Monitor对资源进行保护的思路很简单,即使用排它锁.当线程A需要访问某一资源(对象,方法,类型成员,代码段),对其进行加锁,线程A获取到锁以后,任何其他线程吐过再次对资源进行访问,则将其放到等待队列中,直到线程A释放锁以后,再将线程从队列中取出.

 

MonitorEnter()静态方法用于获取锁,Exit()静态方法用于释放锁.

public static void Enter(object obj);

public static void Exit(object obj);

 

使用对象本身作为锁对象

 

现在使用Monitor解决和上面类似的问题,创建一个自定义的类型Resource,然在主线程和worker线程上调用他的Record()方法:

    public class Resource
    {
        public string Called;
        public void Record()
        {
            this.Called += string.Format("{0}[{1}]",Thread.CurrentThread.Name,DateTime.Now.Millisecond);
            Console.WriteLine(Called);
        }
    }
    class Program
    {
 
        private Resource res = new Resource();
 
        static void Main(string[] args)
        {
            Thread.CurrentThread.Name = "Main";
 
            Program p = new Program();
            Thread worker = new Thread(p.ThreadEntry);
            worker.Name = "Worker";
            worker.Start();
            p.ThreadEntry();
        }
        void ThreadEntry()
        {
            Monitor.Enter(res);
            res.Record();
            Monitor.Exit(res);
            
        }
    }


上面的代码将ThreadEntry()改为了实例方法,因此要先创建一个Program的实例.ThreadEntry()方法中,在访问res对象前对其进行枷锁,在访问之后释放锁.

 

通过输出结果可以看到Main()线程先占用了res,并调用Record(),worker线程后占用res,两个线程的切换时间还是存在的.如果注释掉Monitro.Enter()Monitor.Exit()语句,此时再看一下输出.

 

因为两个线程同时进入到了Record()并获取到了Called,而此时Called为空.

 

使用System.Object作为锁对象

 

Monitor有一个限制,就是只能对引用类型加锁.如果将上面的Resource类型改为结构类型,则会抛出异常.解决的办法是将锁加在其他的引用类型上,比如System.Object.此时,线程并不会访问该引用类型的任何属性和方法,该对象的作用仅仅是协调各个线程.换言之,之前的操作流程是占用A,操作A,释放A.现在的流程是占用B,操作A,释放B.当这样做的时候,要保证所有线程在加锁和释放锁的时候都是针对同一个对象B.

 

 

    public struct Resource
    {
        public string Called;
        public void Record()
        {
            this.Called += string.Format("{0}[{1}]",Thread.CurrentThread.Name,DateTime.Now.Millisecond);
            Console.WriteLine(Called);
        }
    }
    class Program
    {
 
        private Resource res = new Resource();
        private object lockObj = new object();
        static void Main(string[] args)
        {
            Thread.CurrentThread.Name = "Main";
 
            Program p = new Program();
            Thread worker = new Thread(p.ThreadEntry);
            worker.Name = "Worker";
            worker.Start();
            p.ThreadEntry();
        }
        void ThreadEntry()
        {
            Monitor.Enter(lockObj);
            res.Record();
            Monitor.Exit(lockObj);            
        }
    }

使用这种方式的时候,需要创建一个没有实际意义的,专门用于协调线程的对象.对上面的案例进行以下修改,让他们锁定不同的对象:

    class Program
    {
 
        private Resource res = new Resource();
        private object lockObj = new object();
        private object lockObj2 = new object();
        static void Main(string[] args)
        {
            Thread.CurrentThread.Name = "Main";
            Program p = new Program();
            ParameterizedThreadStart ts = new ParameterizedThreadStart(p.ThreadEntry);            
            Thread worker = new Thread(p.ThreadEntry);
            worker.Name = "Worker";
            worker.Start(p.lockObj);
            p.ThreadEntry(p.lockObj2);
        }
        void ThreadEntry(object obj)
        {
            Monitor.Enter(obj);
            res.Record();
            Monitor.Exit(obj);            
        }
    }


观察一下输出又不符合预期了.

 

 

使用System.Type作为锁对象

 

采用上面的方式时,需要创建一个专门用于协调线程的System.Object对象,有时候显得代码不够简洁.另一种做法是使用System.Type对象作为锁,使用System.Type对象是基于这样一个事实:多次调用typeof(Type)获取的是同一个对象:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
namespace 线程同步
{
 
    public static class Resource
    {
        public static string Called;
        public static void Record()
        {
            Called += string.Format("{0}[{1}]",Thread.CurrentThread.Name,DateTime.Now.Millisecond);
            Console.WriteLine(Called);
        }
    }
    class Program
    {
 
        static void Main(string[] args)
        {
            Program p = new Program();
            Type t1 = typeof(Resource);
            Type t2 = typeof(Resource);
            Type t3 = typeof(Resource);
 
            Console.WriteLine(t1==t2);
            Console.WriteLine(t2==t3);
        }
        void ThreadEntry()
        {
            Monitor.Enter(typeof(Resource));
            Resource.Record();
            Monitor.Exit(typeof(Resource));            
        }
    }
}


这样就又可以了.

 

 

使用lock语句

 

考虑这样一种情况:Record()方法抛出了异常.假设主线程先执行ThreadEntry()方法,那么当Record()抛出异常之后,将不会执行Monitor.Exit()方法.程序的输出结果可能是:Main[569],并且会一直等待下去.因为woker线程是前台线程,他一直在等待释放锁,而主线程知道结束都未释放锁.解决的方法是将worker线程设为后台线程,但前面已经说过这是不妥的做法,用这种方式结束线程是不合适的.还有一种方法是将Monitor.Exit()方法放到finally块中:

        void ThreadEntry()
        {
            Monitor.Enter(res);
            try
            {
                res.Record();
            }
            finally
            {
                Monitor.Exit(res);
            }                               
        }


由于Monitor的这种规模是太常见了,.NET提供了lock语句进行简化,上面的代码等价于:

        void ThreadEntry()
        {
            lock (res)
            {
                res.Record();
            }                             
        }


lock语句只专注于获取锁,释放锁,并不提供处理异常的简写方法,如果要处理异常,还需要使用try/catch:

        void ThreadEntry()
        {
            lock (res)
            {
                try
                {
                    res.Record();
                }
                catch (Exception ex)
                {
 
                    Console.WriteLine(ex.Message);
                }
                
            }                             
        }


 

 

 

创建线程安全类型

 

类型Resource不是类型安全的,它的内部并没有采取线程安全(Thread-Safe)的措施.最好将获取锁,释放锁的逻辑放到Resource内部实现:

    public  class Resource
    {
        public  string Called;
        public  void Record()
        {
            lock (this)
            {
                this.Called += string.Format("{0}[{1}]",Thread.CurrentThread.Name,DateTime.Now.Millisecond);
                Console.WriteLine(Called);
            }
        }
    }


 

因为这种模式常见,因此,.NET提供了内置的编译器支持,称作同步方法(synchronized methods),只要为方法添加System.Runtime.CompilerServices.MethodImpl标记,并将位置参数设置为MethodImplOptions.Synchronized即可,下面方法的效果与上面的相同的:

        [MethodImpl(MethodImplOptions.Synchronized)]
        public void Record()
        {
            this.Called += string.Format("{0}[{1}]", Thread.CurrentThread.Name, DateTime.Now.Millisecond);
            Console.WriteLine(Called);
        }


也能对类型的成员访问加锁:

    public class Resource
    {
        private int _index;
        public int Index
        {
            get
            {
                lock (this)
                {
                    return _index;
                }
            }
            set
            {
                lock (this)
                {
                    _index = value;
                }
            }
        }
 
    }


上面的锁的粒度太小了,如果一个类的字段太多,那就会麻烦死,也容易形成BUG,所以将ThreadEntry()改为:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Runtime.CompilerServices;
namespace 线程同步
{
 
    public class Resource
    {
        private int _index;
        public int Index
        {
            get
            {
                lock (this)
                {
                    return _index;
                }
            }
            set
            {
                lock (this)
                {
                    _index = value;
                }
            }
        }
 
    }
    class Program
    {
        static Resource res = new Resource();
        void ThreadEntry()
        {
            for (int i = 0; i <= 2; i++)
            {
                res.Index = res.Index + 1;
            }
        }
        static void Main(string[] args)
        {
            Thread.CurrentThread.Name = "Main ";
 
            Program p = new Program();
            Thread worker = new Thread(p.ThreadEntry);
            worker.Name = "Worker";
            worker.Start();//worker线程执行一遍
            p.ThreadEntry();//主线程执行一遍
            Console.WriteLine(res.Index);
        }
 
    }
}


res.Index=res.Index+1语句先调用了get代码段,随后调用了set代码段.我盟期望的结果是6,但实际结果是不确定的.因为两个线程可能先后进入了get语句,获取了相同的Index,然后又先后进入了set语句段,最终的结果可能是3.如果想获取期望的结果,那么应该讲res.Index=res.Index+1视为操作单元为其加锁,此时getset代码段才会顺序执行.

        void ThreadEntry()
        {
            for (int i = 0; i <= 2; i++)
            {
                lock (res)
                {
                    res.Index = res.Index + 1;
                    Console.WriteLine("{0}: {1}",Thread.CurrentThread.Name,res.Index);
                }
            }
        }


能看到输出结果是交替的,如果想要他们顺序输出,则需要再次提高锁的粒度,将锁加在for循环外部:

        void ThreadEntry()
        {
            lock (res)
            {
                for (int i = 0; i <= 2; i++)
                {
 
                    res.Index = res.Index + 1;
                    Console.WriteLine("{0}: {1}", Thread.CurrentThread.Name, res.Index);
                }
            }
        }


可见,锁的粒度对于程序执行的顺序和结果是很重要的.

 

 

使用Moinitor协调线程执行顺序

 

前面使用Monitor保证了资源只能同时由一个线程访问,但是没有限制资源先由T1访问还是T2访问,由于Start()方法实际执行时间的不确定,因此结果可能是主线程先访问,也可能是worker线程先访问.通常,两个线程执行的是不同的任务,比如工作线程获取并计算数据,主线程显示数据.那么此时顺就很重要,来看下面的案例:

 

    public class Resource
    {
        public string Data;
    }
    class Program
    {
        private Resource res = new Resource();
        void ThreadEntry()
        {
            res.Data = "Retrived";
            Monitor.Pulse(res);
        }
        static void Main(string[] args)
        {
            Thread.CurrentThread.Name = "Main ";
 
            Program p = new Program();
            Thread worker = new Thread(p.ThreadEntry);
            worker.Name = "Worker";
            worker.Start();
            Console.WriteLine("Data={0}",p.res.Data);
        }
 
    }

其中worker线程用于获取数据(设置res.Data属性的值),主线程则用于显示数据,上面的结果虽然会报错,但是第一行可能是”Data=”,因为主线程运行到Console.WriteLine()语句的时候,worker线程尚未执行.咱们使用Monitor类型的Wait()Pulse()静态方法:

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


 

Wait(0用于暂停当前线程并等待信号(调用了Wait()方法的线程,状态为WaitSleepJoin),Pulse()方法则用于发出信号,收到信号的新城将会执行后续代码.这两个方法都必须置于lock块内,并且两个方法接收的对象与lock接受的对象相同.修改上面的代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Runtime.CompilerServices;
namespace 线程同步
{
 
    public class Resource
    {
        public string Data;
    }
    class Program
    {
        private Resource res = new Resource();
        void ThreadEntry()
        {
            lock (res)
            {
                res.Data = "Retrived";
                Monitor.Pulse(res);
            }
        }
        static void Main(string[] args)
        {
            Thread.CurrentThread.Name = "Main ";
 
            Program p = new Program();
            Thread worker = new Thread(p.ThreadEntry);
            worker.Name = "Worker";
            worker.Start();
            lock (p.res)
            {
                if (string.IsNullOrEmpty(p.res.Data))
                {
                    Monitor.Wait(p.res);
                }
                Console.WriteLine("Data={0}",p.res.Data);
            }
        }
 
    }
}


Main()方法中的if语句很重要.因为有可能worker线程已经向res.Data赋值并执行过了Monitor.Pulse(),如果此时至宣城再去执行Mointor.Wait(),那么它永远也等不到这个信号量,会一直等待下去.可以删除if语句,然后在worker.Start()语句后添加Thread.Sleep(100)来模拟这种情况:

            worker.Start();

            Thread.Sleep(100);

            lock (p.res)

            {

                Monitor.Wait(p.res);

                Console.WriteLine("Data={0}", p.res.Data);

            }

此时程序会阻塞在Wait()方法的位置,为了避免这种情况,可以调用Wait()的重载方法,传入一个int类型的参数,该参数指示线程等待的时间,以毫秒为单位.如果在等待期间收到信号(其他线程调用了Monitor.Pulse()),则返回true;如果超时,则返回false.修改lock块中的语句:

            lock (p.res)
            {
                bool isTimeOut = Monitor.Wait(p.res,100);
                Console.WriteLine(isTimeOut);                
                Console.WriteLine("Data={0}", p.res.Data);
            }
 


有时候,多个线程等待同一个数据,此时可以调用Monitor.PulseAll()方法,通知所有等待的线程.

 

 

死锁

 

使用Monitor进行线程同步,可能会出现的一种比较棘手的问题就是死锁,简单来说死锁就是两个或两个以上的线程在执行过程中,因为争夺资源而造成的一种互相等待的现象,如果没有外力进行干预(例如调用Abort()),则它们都将无法推进下去.一个线程T1占用资源R1,同时试图访问资源R2,另一个线程T2占用资源R2,同时试图访问资源R1.结果就是两个线程彼此等待,谁也无法进行下去.案例如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Runtime.CompilerServices;
namespace 线程同步
{
 
    public class Resource
    {
        public string Data;
    }
    class Program
    {
        private Resource mainRes = new Resource() { Data="mainRes"};
        private Resource workerRes = new Resource() { Data="workerRes"};
 
        static void Main(string[] args)
        {
 
 
        }
        static void T1(Program p)
        {
            lock (p.mainRes)
            {
                Thread.Sleep(10);
                lock (p.workerRes)
                {
                    Console.WriteLine(p.workerRes.Data);//死锁
                }
            }
        }
 
        void T2()
        {
            lock (workerRes)
            {
                Thread.Sleep(10);
                lock (mainRes)
                {
                    Console.WriteLine(mainRes.Data);//死锁
                }
            }
        }
    }
}


主线程占用了mainRes,同时尝试访问workerRes;工作线程占用了workerRes,同时尝试访问mainRes,最后的结果是两个线程都卡在了内部的elock语句的位置.上面问题解决方案是使用Monitor.TryEnter()方法,这个方法不会像Monitor.Enter()方法那样一直等待锁,而是立即返回.如果能获取锁,返回true,否则返回false.可以修改内部的lock语句,使用for循环,尝试三次获取锁,如果失败则放弃并退出.修改T1,T2.

        static void T1(Program p)
        {
            lock (p.mainRes)
            {
                Thread.Sleep(10);
                int i = 0;
                while (i < 3)
                {
                    if (Monitor.TryEnter(p.workerRes))
                    {
                        Console.WriteLine(p.workerRes.Data);
                        Monitor.Exit(p.workerRes);
                        break;
                    }
                    else
                    {
                        Thread.Sleep(1000);
                    }
                    i++;
                }
                if (i == 3)
                {
                    Console.WriteLine("{0}: Tried 3 times , Deadlock", Thread.CurrentThread.Name);
                }
            }
        }


在实际开发中,在哪个位置产生了死锁事先不知道,但可以使用这里的方法来对可能产生死锁的位置进行测试.

 

线程的东西就先说这么多,这里楼主关于.NET的东西就告一段落了,以后楼主的打算是学学Linux!!!还有Docker!!!顺道学学Ruby,这三个东西以前楼主都是学过的,Linux上玩玩Docker,Kali Linux的大部分文件都是Ruby写的,所以这三个东西的应该一起学起来不难.

线程同步

标签:

原文地址:http://blog.csdn.net/shanyongxu/article/details/51264482

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!