C# 多线程

多线程

线程 被定义为程序的执行路径。每个线程都定义了一个独特的控制流。如果您的应用程序涉及到复杂的和耗时的操作,那么设置不同的线程执行路径往往是有益的,每个线程执行特定的工作。

线程是**轻量级进程**。一个使用线程的常见实例是现代操作系统中并行编程的实现。使用线程节省了 CPU 周期的浪费,同时提高了应用程序的效率。

到目前为止我们编写的程序是一个单线程作为应用程序的运行实例的单一的过程运行的。但是,这样子应用程序同时只能执行一个任务。为了同时执行多个任务,它可以被划分为更小的线程。

线程生命周期

  • 一般认为,线程有五种状态:

新建(new 对象) 、就绪(等待CPU调度)、运行(CPU正在运行)、阻塞(等待阻塞、同步阻塞等)、死亡(对象释放)

主线程

在 中,**System.Threading.Thread** 类用于线程的工作。它允许创建并访问多线程应用程序中的单个线程。进程中第一个被执行的线程称为**主线程**。

当 程序开始执行时,主线程自动创建。使用 Thread 类创建的线程被主线程的子线程调用。您可以使用 Thread 类的 CurrentThread 属性访问线程。

下面的程序演示了主线程的执行:

实例

using System;
using System.Threading;

namespace MultithreadingApplication
{
    class MainThreadProgram
    {
        static void Main(string[] args)
        {
            Thread th = Thread.CurrentThread;
            th.Name = "MainThread";
            Console.WriteLine("This is {0}", th.Name);
            Console.ReadKey();
        }
    }
}

当上面的代码被编译和执行时,它会产生下列结果:

This is MainThread

Thread 类常用的属性和方法

下表列出了 Thread 类的一些常用的 属性

属性描述
CurrentContext获取线程正在其中执行的当前上下文。
CurrentCulture获取或设置当前线程的区域性。
CurrentPrincipal获取或设置线程的当前负责人(对基于角色的安全性而言)。
CurrentThread获取当前正在运行的线程。
CurrentUICulture获取或设置资源管理器使用的当前区域性以便在运行时查找区域性特定的资源。
ExecutionContext获取一个 ExecutionContext 对象,该对象包含有关当前线程的各种上下文的信息。
IsAlive获取一个值,该值指示当前线程的执行状态。
IsBackground获取或设置一个值,该值指示某个线程是否为后台线程。
IsThreadPoolThread获取一个值,该值指示线程是否属于托管线程池。
ManagedThreadId获取当前托管线程的唯一标识符。
Name获取或设置线程的名称。
Priority获取或设置一个值,该值指示线程的调度优先级。
ThreadState获取一个值,该值包含当前线程的状态。

下表列出了 Thread 类的一些常用的 方法

序号方法名 & 描述
1public void Abort() 在调用此方法的线程上引发 ThreadAbortException,以开始终止此线程的过程。调用此方法通常会终止线程。
2public static LocalDataStoreSlot AllocateDataSlot() 在所有的线程上分配未命名的数据槽。为了获得更好的性能,请改用以 ThreadStaticAttribute 属性标记的字段。
3**public static LocalDataStoreSlot AllocateNamedDataSlot( string name) ** 在所有线程上分配已命名的数据槽。为了获得更好的性能,请改用以 ThreadStaticAttribute 属性标记的字段。 |
| 4 | public static void BeginCriticalRegion() 通知主机执行将要进入一个代码区域,在该代码区域内线程中止或未经处理的异常的影响可能会危害应用程序域中的其他任务。 |
| 5 | public static void BeginThreadAffinity() 通知主机托管代码将要执行依赖于当前物理操作系统线程的标识的指令。 |
| 6 | public static void EndCriticalRegion() 通知主机执行将要进入一个代码区域,在该代码区域内线程中止或未经处理的异常仅影响当前任务。 |
| 7 | public static void EndThreadAffinity() 通知主机托管代码已执行完依赖于当前物理操作系统线程的标识的指令。 |
| 8 | public static void FreeNamedDataSlot(string name) 为进程中的所有线程消除名称与槽之间的关联。为了获得更好的性能,请改用以 ThreadStaticAttribute 属性标记的字段。 |
| 9 | **public static Object GetData(LocalDataStoreSlot slot) ** 在当前线程的当前域中从当前线程上指定的槽中检索值。为了获得更好的性能,请改用以 ThreadStaticAttribute 属性标记的字段。 |
| 10 | public static AppDomain GetDomain() 返回当前线程正在其中运行的当前域。 |
| 11 | public static AppDomain GetDomainID() 返回唯一的应用程序域标识符。 |
| 12 | **public static LocalDataStoreSlot GetNamedDataSlot(string name) ** 查找已命名的数据槽。为了获得更好的性能,请改用以 ThreadStaticAttribute 属性标记的字段。 |
| 13 | public void Interrupt() 中断处于 WaitSleepJoin 线程状态的线程。 |
| 14 | public void Join() 在继续执行标准的 COM 和 SendMessage 消息泵处理期间,阻塞调用线程,直到某个线程终止为止。此方法有不同的重载形式。 |
| 15 | public static void MemoryBarrier() 按如下方式同步内存存取:执行当前线程的处理器在对指令重新排序时,不能采用先执行 MemoryBarrier 调用之后的内存存取,再执行 MemoryBarrier 调用之前的内存存取的方式。 |
| 16 | public static void ResetAbort() 取消为当前线程请求的 Abort。 |
| 17 | **public static void SetData(LocalDataStoreSlot slot,Object data) ** 在当前正在运行的线程上为此线程的当前域在指定槽中设置数据。为了获得更好的性能,请改用以 ThreadStaticAttribute 属性标记的字段。 |
| 18 | public void Start() 开始一个线程。 |
| 19 | **public static void Sleep(int millisecondsTimeout) ** 让线程暂停一段时间。 |
| 20 | **public static void SpinWait(int iterations) ** 导致线程等待由 iterations 参数定义的时间量。 |
| 21 | **public static byte VolatileRead(ref byte address) public static double VolatileRead(ref double address) public static int VolatileRead(ref int address) public static Object VolatileRead(ref Object address) ** 读取字段值。无论处理器的数目或处理器缓存的状态如何,该值都是由计算机的任何处理器写入的最新值。此方法有不同的重载形式。这里只给出了一些形式。 |
| 22 | **public static void VolatileWrite(ref byte address, byte value) public static void VolatileWrite(ref double address, double value) public static void VolatileWrite(ref int address,int value) public static void VolatileWrite(ref Object address,Object value) ** 立即向字段写入一个值,以使该值对计算机中的所有处理器都可见。此方法有不同的重载形式。这里只给出了一些形式。 |
| 23 | public static bool Yield() 导致调用线程执行准备好在当前处理器上运行的另一个线程。由操作系统选择要执行的线程。 |

创建线程

线程是通过扩展 Thread 类创建的。扩展的 Thread 类调用 Start() 方法来开始子线程的执行。

下面的程序演示了这个概念:

实例

using System;
using System.Threading;

namespace MultithreadingApplication
{
    class ThreadCreationProgram
    {
        public static void CallToChildThread()
        {
            Console.WriteLine("Child thread starts");
        }
        
        static void Main(string[] args)
        {
            ThreadStart childref = new ThreadStart(CallToChildThread);
            Console.WriteLine("In Main: Creating the Child thread");
            Thread childThread = new Thread(childref);
            childThread.Start();
            Console.ReadKey();
        }
    }
}

当上面的代码被编译和执行时,它会产生下列结果:

In Main: Creating the Child thread
Child thread starts

Thread方式

1、不带参数
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace ConsoleApp5
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread thread = new Thread(new ThreadStart(CreateThread));
            thread.Name = "线程一";
            thread.IsBackground = true;
            thread.Start();
        }

        static void CreateThread()
        {
            for (int i=1;i<=20;i++)
            {
                Console.WriteLine(Thread.CurrentThread.Name+":"+i);
            }
        }
    }
}
2、带参数ParameterizedThreadStart
using System;
using System.Threading;

namespace ConsoleApp5
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread thread = new Thread(new ParameterizedThreadStart(CreateThread));
            thread.IsBackground = true;
            thread.Start("线程二");
            Console.ReadLine();
        }

        static void CreateThread(Object threadName)
        {
            Thread.CurrentThread.Name = threadName.ToString();
            for (int i = 1; i <= 20; i++)
            {
                Console.WriteLine(Thread.CurrentThread.Name + ":" + i);
            }
        }
    }
}

Task方式

Task类的表示单个操作不会返回一个值,通常以异步方式执行

1、直接New创建
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp5
{
    class Program
    {
        static void Main(string[] args)
        {
            Task task = new Task(() =>
            {
                Console.WriteLine("可以直接写代码,也可以调用方法");
                CreateTask();
            });
            task.Start();
            Console.ReadLine();
        }

        static void CreateTask()
        {
            Thread.CurrentThread.Name = "线程一";
            for (int i = 1; i <= 20; i++)
            {
                Console.WriteLine(Thread.CurrentThread.Name + ":" + i);
            }
        }
    }
}
2、使用Factory工厂创建
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp5
{
    class Program
    {
        static void Main(string[] args)
        {
            //Factory提供对用于创建Task的工厂方法的访问
            Task.Factory.StartNew(() =>
            {
                Console.WriteLine("可以直接写代码,也可以调用方法");
                CreateTask();
            });
            Console.ReadLine();
        }

        static void CreateTask()
        {
            Thread.CurrentThread.Name = "线程一";
            for (int i = 1; i <= 20; i++)
            {
                Console.WriteLine(Thread.CurrentThread.Name + ":" + i);
            }
        }
    }
}
3、 使用Run方法
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp5
{
    class Program
    {
        static void Main(string[] args)
        {
            //将在线程池上运行的指定工作排队,并返回代表该工作的Task对象
            Task.Run(() =>
            {
                Console.WriteLine("可以直接写代码,也可以调用方法");
                CreateTask();
            });
            Console.ReadLine();
        }

        static void CreateTask()
        {
            Thread.CurrentThread.Name = "线程一";
            for (int i = 1; i <= 20; i++)
            {
                Console.WriteLine(Thread.CurrentThread.Name + ":" + i);
            }
        }
    }
}

线程常用的方法

1、Sleep

当我们创建一个线程后,我们需要调用线程对象的Start()方法来调度那个线程。在这时,CLR将会为作为构造函数参数传递给线程对象的方法地址分配一个时间片。一旦线程开始执行,它就可以在操作系统处理其他线程时回到睡眠状态或者退出状态。我们可以使用线程类的Sleep()方法让一个线程进入睡眠状态。如果你正在等待一个资源并且你想在稍后继续尝试访问这个资源时,Sleep()方法是很重要的。举个例子,假设你的程序由于无法访问需要的资源而导致其不能继续执行时,你可能想要在几毫秒之后尝试继续访问资源,在这种情况下让线程在再次尝试访问资源之前睡眠一段时间是一个很好的方式。

Sleep()方法有两种重载方式。第一种重载方法有一个整型参数,并会按照指定的毫秒时间暂停线程执行。例如,如果你向线程传递值100,那么线程将会暂停100毫秒。这个方法将会让线程进入WaitSleepJoin状态。

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace SimpleThread
{
    public class ThreadSleep
    {
        public static Thread worker;
        public static Thread worker2;

        public static void Main()
        {
            Console.WriteLine("Entering the void Main!");

            worker = new Thread(new ThreadStart(Counter));
            worker2 = new Thread(new ThreadStart(Counter2));

            //Make the worker2 Object as highest priority
            worker2.Priority = ThreadPriority.Highest;

            worker.Start();
            worker2.Start();

            Console.WriteLine("Exiting the void Main!");
            Console.ReadLine();
        }

        public static void Counter()
        {
            Console.WriteLine("Entering Counter");
            for (int i = 1; i < 50; i++)
            {
                Console.Write(i + " ");
                if (i == 10)
                {
                    Console.WriteLine();
                    Thread.Sleep(1000);
                }
            }
            Console.WriteLine("Exiting Counter");
        }

        public static void Counter2()
        {
            Console.WriteLine("Entering Counter2");
            for (int i = 51; i < 100; i++)
            {
                Console.Write(i + " ");
                if (i == 70)
                {
                    Console.WriteLine();
                    Thread.Sleep(5000);
                }
            }
            Console.WriteLine("Exiting Counter2");
        }
    }
}

第二种重载方法有一个TimeSpan类型参数,当前线程会按照TimeSpan的值暂停一段时间。TimeSpan是System命名空间中的一个类。TimeSpan有一些很有用的属性并会返回基于时钟时间间隔。

我们可以使用FromSeconds()和FromMinutes()来确定睡眠时间。

public static void Counter()
{
    Console.WriteLine("Entering Counter");
    for (int i = 1; i < 50; i++)
    {
        Console.Write(i + " ");
        if (i == 10)
        {
            Console.WriteLine();
            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
    }
    Console.WriteLine("Exiting Counter");
}

public static void Counter2()
{
    Console.WriteLine("Entering Counter2");
    for (int i = 51; i < 100; i++)
    {
        Console.Write(i + " ");
        if (i == 70)
        {
            Console.WriteLine();
            Thread.Sleep(TimeSpan.FromSeconds(5));
        }
    }
    Console.WriteLine("Exiting Counter2");
}

2、Interrupt

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace SimpleThread
{
    public class Interrupt
    {
        public static Thread sleeper;
        public static Thread worker;

        public static void Main()
        {
            Console.WriteLine("Entering the void Main!");

            sleeper = new Thread(new ThreadStart(SleepingThread));
            worker = new Thread(new ThreadStart(AwakeThread));

            sleeper.Start();
            worker.Start();

            Console.WriteLine("Exiting the void Main!");
            Console.ReadLine();
        }
        public static void SleepingThread()
        {
            for (int i = 1; i < 50; i++)
            {
                Console.Write(i + " ");
                if (i == 10 || i == 20 || i == 30)
                {
                    Console.WriteLine("Going to sleep at: " + i);
                    try
                    {
                        Thread.Sleep(20);
                    }
                    catch (ThreadInterruptedException ex)
                    {
                        Console.WriteLine(ex.Message);
                    }
                }
            }
        }

        public static void AwakeThread()
        {
            for (int i = 51; i < 100; i++)
            {
                Console.Write(i + " ");
                if (sleeper.ThreadState == ThreadState.WaitSleepJoin)
                {
                    Console.WriteLine("Interrupting the sleeping thread.");
                    sleeper.Interrupt();
                }
            }
        }
    }
}

在上面的例子中,当计数器的值为10, 20 和 30 时第一个线程会睡眠。第二个线程会检查第一个线程是否已经进入睡眠状态。如果是的话,它将中断第一个线程并使它回到调度队列中去。Interrupt()方法是让睡眠线程重新醒来的最好方式,当线程等待的资源可用且你想让线程继续运行时你可以使用这个方法。输出结果与下面显示的类似:

3、Join

Join方法主要是用来阻塞调用线程,直到某个线程终止或经过了指定时间为止。官方的解释比较乏味,通俗的说就是创建一个子线程,给它加了这个方法,其它线程就会暂停执行,直到这个线程执行完为止才去执行(包括主线程)。

class Program
{
    static void Main(string[] args)
    {
        Thread threadA = new Thread(ThreadMethod);      
        threadA.Name = "线程A";
        Thread threadB = new Thread(ThreadMethod);      
        threadB.Name = "线程B";
        threadA.Start();       
        //threadA.Join();      
        threadB.Start();       
        //threadB.Join();

        for (int i = 1; i <=3; i++)
        {
            Console.WriteLine("我是:主线程,循环了{1}次", Thread.CurrentThread.Name, i);
            Thread.Sleep(300);          //休眠300毫秒                                                
        }
        Console.ReadKey();
    }
    public static void ThreadMethod(object parameter)
    {
        for (int i = 1; i <=3; i++)
        {
            Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name, i);
            Thread.Sleep(300);         //休眠300毫秒              
        }
    }
}

前台线程和后台线程

前台线程:只有所有的前台线程都结束,应用程序才能结束。默认情况下创建的线程都是前台线程
后台线程:只要所有的前台线程结束,后台线程自动结束。通过Thread.IsBackground设置后台线程。必须在调用Start方法之前设置线程的类型,否则一旦线程运行,将无法改变其类型。

通过BeginXXX方法运行的线程都是后台线程。

class Program
{
    static void Main(string[] args)
    {
        //演示前台、后台线程
        BackGroundTest background = new BackGroundTest(10);
        //创建前台线程
        Thread fThread = new Thread(new ThreadStart(background.RunLoop));
        //给线程命名
        fThread.Name = "前台线程";


        BackGroundTest background1 = new BackGroundTest(20);
        //创建后台线程
        Thread bThread = new Thread(new ThreadStart(background1.RunLoop));
        bThread.Name = "后台线程";
        //设置为后台线程
        bThread.IsBackground = true;

        //启动线程
        fThread.Start();
        bThread.Start();
    }
}

class BackGroundTest
{
    private int Count;
    public BackGroundTest(int count)
    {
        this.Count = count;
    }
    public void RunLoop()
    {
        //获取当前线程的名称
        string threadName = Thread.CurrentThread.Name;
        for (int i = 0; i < Count; i++)
        {
            Console.WriteLine("{0}计数:{1}", threadName, i.ToString());
            //线程休眠1000毫秒
            Thread.Sleep(1000);
        }
        Console.WriteLine("{0}完成计数", threadName);

    }
}

多线程之线程同步

1、不使用线程同步的实例

我们先来看下面一个例子:

using System;
using System.Threading;

namespace ThreadSynchDemo
{
    class Program
    {
        private static int Counter = 0;
        static void Main(string[] args)
        {
            Thread t1 = new Thread(() => {
                for (int i = 0; i < 1000; i++)
                {
                    Counter++;
                    Thread.Sleep(1);
                }
            });
            t1.Start();

            Thread t2 = new Thread(() => {
                for (int i = 0; i < 1000; i++)
                {
                    Counter++;
                    Thread.Sleep(1);
                }
            });
            t2.Start();

            Thread.Sleep(3000);
            Console.WriteLine(Counter);
            Console.ReadKey();
        }
    }
}

分析:在上面的例子中,t1和t2两个线程里面都是让变量Counter的值自增1,假设这时t1线程读取到Counter的值为200,可能t2线程执行非常快,t1线程读取Counter值的时候,t2线程已经把Counter的值改为了205,等t1线程执行完毕以后,Counter的值又被变为了201,这样就会出现线程同步的问题了。那么该如何解决这个问题呢?

2、解决线程同步问题

(1)Lock

解决线程同步问题最简单的是使用lock。lock可以解决多个线程同时操作一个资源引起的问题。lock是C#中的关键字,它要锁定一个资源,lock的特点是:同一时刻只能有一个线程进入lock的对象的范围,其它lock的线程都要等待。我们看下面优化后的代码:

using System;
using System.Threading;

namespace ThreadSynchDemo
{
    class Program
    {
        private static int Counter = 0;
        // 定义一个locker对象
        private static Object locker = new Object();
        static void Main(string[] args)
        {
            #region 存在线程同步问题
            //Thread t1 = new Thread(() => {
            //    for (int i = 0; i < 1000; i++)
            //    {
            //        Counter++;
            //        Thread.Sleep(1);
            //    }
            //});
            //t1.Start();

            //Thread t2 = new Thread(() => {
            //    for (int i = 0; i < 1000; i++)
            //    {
            //        Counter++;
            //        Thread.Sleep(1);
            //    }
            //});
            //t2.Start(); 
            #endregion

            #region 使用Lock解决线程同步问题
            Thread t1 = new Thread(() => {
                for (int i = 0; i < 1000; i++)
                {
                    lock(locker)
                    {
                        Counter++;
                    }
                    Thread.Sleep(1);
                }
            });
            t1.Start();

            Thread t2 = new Thread(() => {
                for (int i = 0; i < 1000; i++)
                {
                    lock (locker)
                    {
                        Counter++;
                    }
                    Thread.Sleep(1);
                }
            });
            t2.Start();
            #endregion

            Thread.Sleep(3000);
            Console.WriteLine(Counter);
            Console.ReadKey();
        }
    }
}

注意:lock只能锁住同一个对象,如果是不同的对象,还是会有线程同步的问题。lock锁定的对象必须是引用类型的对象。

我们在定义一个Object类型的对象,lock分别锁住两个对象,看看是什么结果:

using System;
using System.Threading;

namespace ThreadSynchDemo
{
    class Program
    {
        private static int Counter = 0;
        // 定义一个locker对象
        private static Object locker = new Object();
        // 定义locker2
        private static Object locker2 = new Object();
        static void Main(string[] args)
        {
            #region 存在线程同步问题
            //Thread t1 = new Thread(() => {
            //    for (int i = 0; i < 1000; i++)
            //    {
            //        Counter++;
            //        Thread.Sleep(1);
            //    }
            //});
            //t1.Start();

            //Thread t2 = new Thread(() => {
            //    for (int i = 0; i < 1000; i++)
            //    {
            //        Counter++;
            //        Thread.Sleep(1);
            //    }
            //});
            //t2.Start(); 
            #endregion

            #region 使用Lock解决线程同步问题
            //Thread t1 = new Thread(() => {
            //    for (int i = 0; i < 1000; i++)
            //    {
            //        lock(locker)
            //        {
            //            Counter++;
            //        }
            //        Thread.Sleep(1);
            //    }
            //});
            //t1.Start();

            //Thread t2 = new Thread(() => {
            //    for (int i = 0; i < 1000; i++)
            //    {
            //        lock (locker)
            //        {
            //            Counter++;
            //        }
            //        Thread.Sleep(1);
            //    }
            //});
            //t2.Start();
            #endregion

            #region 使用lock锁住不同的对象也会有线程同步问题
            Thread t1 = new Thread(() => {
                for (int i = 0; i < 1000; i++)
                {
                    lock (locker)
                    {
                        Counter++;
                    }
                    Thread.Sleep(1);
                }
            });
            t1.Start();

            Thread t2 = new Thread(() => {
                for (int i = 0; i < 1000; i++)
                {
                    lock (locker2)
                    {
                        Counter++;
                    }
                    Thread.Sleep(1);
                }
            });
            t2.Start();
            #endregion
            Thread.Sleep(3000);
            Console.WriteLine(Counter);
            Console.ReadKey();
        }
    }
}

可以看到,这时还是会有线程同步的问题。虽然使用了lock,但是我们锁住的是不同的对象,这样也会有线程同步问题。lock必须锁住同一个对象才可以。

我们下面在来看一个多线程同步问题的例子:

using System;
using System.Threading;

namespace ThreadSynchDemo2
{
    class Program
    {
        static int Money = 100;

        /// <summary>
        /// 定义一个取钱的方法
        /// </summary>
        /// <param name="name"></param>
        static void QuQian(string name)
        {
            Console.WriteLine(name + "查看一下余额" + Money);
            int yue = Money - 1;
            Console.WriteLine(name + "取钱");
            Money = yue;
            Console.WriteLine(name + "取完了,剩" + Money);
        }

        static void Main(string[] args)
        {
            Thread t1 = new Thread(() => {
                for (int i = 0; i < 10; i++)
                {
                    QuQian("t2");
                }
            });
            Thread t2 = new Thread(() => {
                for (int i = 0; i < 10; i++)
                {
                    QuQian("t2");
                }
            });
            t1.Start();
            t2.Start();
            t1.Join();
            t2.Join();
            Console.WriteLine("余额" + Money);
            Console.ReadKey();
        }
    }
}

可以看到,最终的余额并不是80,这也是线程同步带来的问题,如何解决。解决思路就是使用同步的技术避免两个线程同时修改一个余额。

(2)最大粒度——同步方法

在方法上面使用[MethodImpl(MethodImplOptions.Synchronized)],标记该方法是同步方法,这样一个方法只能同时被一个线程访问。我们在QuQian的方法上面标记,修改后的代码如下:

using System;
using System.Runtime.CompilerServices;
using System.Threading;

namespace ThreadSynchDemo2
{
    class Program
    {
        static int Money = 100;

        /// <summary>
        /// 定义一个取钱的方法,在上面标记为同步方法
        /// </summary>
        /// <param name="name"></param>
        [MethodImpl(MethodImplOptions.Synchronized)]
        static void QuQian(string name)
        {
            Console.WriteLine(name + "查看一下余额" + Money);
            int yue = Money - 1;
            Console.WriteLine(name + "取钱");
            Money = yue;
            Console.WriteLine(name + "取完了,剩" + Money);
        }

        static void Main(string[] args)
        {
            Thread t1 = new Thread(() => {
                for (int i = 0; i < 10; i++)
                {
                    QuQian("t2");
                }
            });
            Thread t2 = new Thread(() => {
                for (int i = 0; i < 10; i++)
                {
                    QuQian("t2");
                }
            });
            t1.Start();
            t2.Start();
            t1.Join();
            t2.Join();
            Console.WriteLine("余额" + Money);
            Console.ReadKey();
        }
    }
}

现在的方法就是“线程安全”的了。什么是“线程安全”呢?“线程安全”是指方法可以被多个线程随意调用,而不会出现混乱。如果出现了混乱,那么就是“线程不安全”的。“线程安全”的方法可以在多线程里面随意的使用。

(3)对象互斥锁

对象互斥锁就是我们上面讲的lock。我们在用lock来修改上面QuQian的例子:

using System;
using System.Runtime.CompilerServices;
using System.Threading;

namespace ThreadSynchDemo2
{
    class Program
    {
        static int Money = 100;

        /// <summary>
        /// 定义一个取钱的方法,在上面标记为同步方法
        /// </summary>
        /// <param name="name"></param>
        //[MethodImpl(MethodImplOptions.Synchronized)]
        //static void QuQian(string name)
        //{
        //    Console.WriteLine(name + "查看一下余额" + Money);
        //    int yue = Money - 1;
        //    Console.WriteLine(name + "取钱");
        //    Money = yue;
        //    Console.WriteLine(name + "取完了,剩" + Money);
        //}

        private static object locker = new object();
        static void QuQian(string name)
        {
            Console.WriteLine(name + "查看一下余额" + Money);
            int yue = Money - 1;
            Console.WriteLine(name + "取钱");
            Money = yue;
            Console.WriteLine(name + "取完了,剩" + Money);
        }

        static void Main(string[] args)
        {
            Thread t1 = new Thread(() => {
                for (int i = 0; i < 10; i++)
                {
                    // 使用对象互斥锁
                    lock(locker)
                    {
                        QuQian("t1");
                    }
                }
            });
            Thread t2 = new Thread(() => {
                for (int i = 0; i < 10; i++)
                {
                    lock (locker)
                    {
                        QuQian("t2");
                    }
                }
            });
            t1.Start();
            t2.Start();
            t1.Join();
            t2.Join();
            Console.WriteLine("余额" + Money);
            Console.ReadKey();
        }
    }
}

可以看到,最终的输出结果还是80。

同一时刻只能有一个线程进入同一个对象的lock代码块。必须是同一个对象才能起到互斥的作用。lock后必须是引用类型,不一定是object,只要是对象就行。

锁对象选择很重要,选不对就起不到同步的作用;选不对还有可能会造成其他地方被锁,比如用字符串做锁(因为字符串缓冲池导致导致可能用的是其他地方正在使用的锁),所以不建议使用字符串做锁。下面的代码就是不允许的:

lock("locker")

两个方法如果都用一个对象做锁,那么访问A的时候就不能访问B,因此锁选择很重要。

(4)Monitor

其实lock关键字就是对Monitor的简化调用,lock最终会被编译成Monitor,因此一般不直接使用Monitor类,看下面代码:

using System;
using System.Threading;

namespace MonitorDemo
{
    class Program
    {
        static int Money = 100;
        private static object locker = new object();
        static void QuQian(string name)
        {
            // 等待没有人锁定locker对象,就锁定它,然后继续执行
            Monitor.Enter(locker);
            try
            {
                Console.WriteLine(name + "查看一下余额" + Money);
                int yue = Money - 1;
                Console.WriteLine(name + "取钱");
                Money = yue;
                Console.WriteLine(name + "取完了,剩" + Money);
            }
            finally
            {
                // 释放locker对象的锁
                Monitor.Exit(locker);
            }
        }

        static void Main(string[] args)
        {
            Thread t1 = new Thread(() => {
                for (int i = 0; i < 10; i++)
                {
                        QuQian("t1");
                }
            });
            Thread t2 = new Thread(() => {
                for (int i = 0; i < 10; i++)
                {
                        QuQian("t2");
                }
            });
            t1.Start();
            t2.Start();
            t1.Join();
            t2.Join();
            Console.WriteLine("余额" + Money);
            Console.ReadKey();
        }
    }
}

Monitor类里面还有TryEnter方法,如果Enter的时候有人在占用锁,它不会等待,而是会返回false。看下面的示例代码:

using System;
using System.Threading;

namespace MonitorDemo
{
    class Program
    {
        static int Money = 100;
        private static object locker = new object();
        static void QuQian(string name)
        {
            // 等待没有人锁定locker对象,就锁定它,然后继续执行
            Monitor.Enter(locker);
            try
            {
                Console.WriteLine(name + "查看一下余额" + Money);
                int yue = Money - 1;
                Console.WriteLine(name + "取钱");
                Money = yue;
                Console.WriteLine(name + "取完了,剩" + Money);
            }
            finally
            {
                // 释放locker对象的锁
                Monitor.Exit(locker);
            }
        }

        static void F1(int i)
        {
            if (!Monitor.TryEnter(locker))
            {
                Console.WriteLine("有人在锁着呢");
                return;
            }
            Console.WriteLine(i);
            Monitor.Exit(locker);
        }


        static void Main(string[] args)
        {
            //Thread t1 = new Thread(() => {
            //    for (int i = 0; i < 10; i++)
            //    {
            //            QuQian("t1");
            //    }
            //});
            //Thread t2 = new Thread(() => {
            //    for (int i = 0; i < 10; i++)
            //    {
            //            QuQian("t2");
            //    }
            //});
            Thread t1 = new Thread(() => {
                for (int i = 0; i < 10; i++)
                {
                    F1(i);
                }
            });
            Thread t2 = new Thread(() => {
                for (int i = 0; i < 10; i++)
                {
                    F1(i);
                }
            });

            t1.Start();
            t2.Start();
            t1.Join();
            t2.Join();
            Console.WriteLine("余额" + Money);
            Console.ReadKey();
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值