漫谈多线程(中)

这一篇接着上一篇来继续学习多线程。

线程同步

在大多数情况下,计算机中的线程会并发运行。有些线程之间没有联系,独立运行,像这种线程我们称为无关线程。但也有一些线程,之间需要传递结果,需要共享资源。像这种线程,我们称为有关线程。比如,我们网上观看电影,一个线程负责下载电影,一个线程负责播放电影。它们只有共同合作我们才能观看到电影,它们之间共享资源。由此,我们可以看出,线程的相关性体现在对同一资源的访问上。我们把这种供多个线程访问的资源成为临界源(Critical Resource)、访问临界源的代码称为临界区(Critical Region)。我们看个程序:

        //缓冲区,只能容纳一个字符
          private static char buffer;
        static void Main(string[] args)
         {

            //线程:写者
            Thread writer = new Thread(delegate()
            {
                string sentence = "无可奈何花落去,似曾相识燕归来,小园香径独徘徊。";
                for (int i = 0; i < 24; i++)
                {
                    buffer = sentence[i]; //向缓冲区写入字符
                    Thread.Sleep(25);
                }
            });

            //线程:读者
            Thread Reader = new Thread(delegate()
            {
                for (int i = 0; i < 24; i++)
                {
                    char ch = buffer; //从缓存区读取数据

                    Console.Write(ch);
                    Thread.Sleep(25);
                }
            });

            //启动线程
            writer.Start();
            Reader.Start();
        }
    }

我们创建两个线程,一个Writer线程负责向缓存区写入字符,一个Reader线程负责从缓存区读取字符。我们假设,缓存区一次只能存放一个字符。也就说,如果Reader不能及时从缓存区读取字符,那么就会被Writer下次要写入的字符覆盖掉。我们来看一下程序的运行效果,如下图:

出现了混乱,为什么会这样呢?原因很简单,因为Writer向缓存中写入字符,Reader要马上去读取,Writer、Reader步调一致,才能得出完整的效果。但是,线程往往是交替执行的,不能确保时间的一致。像这种需要两个线程协同合作才能完成一项任务的情况叫做线程同步(Synchronization)。如何才能确保两个线程的同步呢?C#为我们提供了一系列的同步类.

互锁(Interlocked类)

看程序:

         //缓冲区,只能容纳一个字符
        private static char buffer;

        //标识量(缓冲区中已使用的空间,初始值为0)
        private static long numberofUsedSpace = 0;
        
         static void Main(string[] args)
           {

            string sentence = "无可奈何花落去,似曾相识燕归来,小园香径独徘徊。";

            //线程:写者
            Thread writer = new Thread(delegate()
            {

                for (int i = 0; i < 24; i++)
                {
                    //写入程序前检查缓冲区中是否已满
                    //如果已满,就进行等待。如果未满,就写入字符.
                    while (Interlocked.Read(ref numberofUsedSpace) == 1)
                    {
                        Thread.Sleep(10);
                    }
                    buffer = sentence[i]; //向缓冲区写入字符
                    Thread.Sleep(25);

                    //写入数据后,将numberofUsedSpace由0变1
                    Interlocked.Increment(ref numberofUsedSpace);
                }
            });

            //线程:读者
            Thread Reader = new Thread(delegate()
            {
                for (int i = 0; i < 24; i++)
                {
                    //读取之前检查缓冲区是否已满
                    //如果已满,进行读取,如果未满,进行等待。
                    while (Interlocked.Read(ref numberofUsedSpace) == 0)
                    {
                        Thread.Sleep(25);

                    }
                    char ch = buffer; //从缓存区读取数据
                    Console.Write(ch);

                    //读取完字符,将numberofUsedSpace由1设为0
                    Interlocked.Decrement(ref numberofUsedSpace);
                }
            });

            //启动线程
            writer.Start();
            Reader.Start();
        }

我们通过一个numerofUsedSpace的变量作为计数器,假设numberofUsedSpace=1已满,numberofUsedSpace=0未满。每当Writer线程向缓存区写入字符时,需要通过Interlocked的Read方法来检查numberofUsedSpace是否已满。如果未满,吸入字符,如果已满,进行等待。同样,当Read线程需要向缓冲区读取字符时,也是通过Interlocked的Rread方法来检查numberofUsedSpace是否已满,已满,进行读取,未满进行等待。

 

管程(Monitor类)

另一种实现线程同步的方法,是通过Monitor类。看程序:

        //缓冲区,只能容纳一个字符
        private static char buffer;
        //用于同步的对象(独占锁)
        private static object lockForBuffer = new object();
        static void Main(string[] args)
        {
            //线程:写者
            Thread writer = new Thread(delegate()
            {
                string sentence = "无可奈何花落去,似曾相识燕归来,小园香径独徘徊。";

                for (int i = 0; i < 24; i++)
                {
                    try
                    {
                        //进入临界区
                        Monitor.Enter(lockForBuffer);

                        buffer = sentence[i]; //向缓冲区写入字符

                        //唤醒睡眠在临界资源上的线程
                        Monitor.Pulse(lockForBuffer);

                        //让当前的线程睡眠在临界资源上
                        Monitor.Wait(lockForBuffer);

                    }
                    catch (ThreadInterruptedException)
                    {
                        Console.WriteLine("线程writer被中止");
                    }
                    finally
                    {
                        //推出临界区
                        Monitor.Exit(lockForBuffer);
                    }

                }
            });

            //线程:读者
            Thread Reader = new Thread(delegate()
            {
                for (int i = 0; i < 24; i++)
                {
                    try
                    {
                        //进入临界区
                        Monitor.Enter(lockForBuffer);
                        char ch = buffer; //从缓存区读取数据
                        Console.Write(ch);

                        //唤醒睡眠在临界资源上的线程
                        Monitor.Pulse(lockForBuffer);

                        //让当前线程睡眠在临界资源上
                        Monitor.Wait(lockForBuffer);
                    }
                    catch (ThreadInterruptedException)
                    {
                        Console.WriteLine("线程reader被中止");
                    }
                    finally
                    {
                        //退出临界区
                        Monitor.Exit(lockForBuffer);
                    }
                }
            });

            //启动线程
            writer.Start();
            Reader.Start();
        }

     

当线程进入临界区,会调用Monitor的Entry方法来获取独占锁,如果得到,就进行操作,如果被别的线程占用,就睡眠在临界资源上,直到独占锁被释放。如果此时,别的线程进入临界区,会发现独占锁被占用,他们会睡眠在临界资源上。Monitor会记录有哪些线程睡眠在临界资源上,当线程执行完操作,调用Pulse()方法,唤醒睡眠在临界资源上的线程。因为,线程还需要下次操作,所以需要调用Wait()方法,令自己睡眠在临界资源上。最后通过调用Exit()方法释放独占锁。

Note that:Monitor只能锁定引用类型的变量,如果使用值类型变量。每调用一次Entry()方法,就进行一次装箱操作,每进行一次装箱操作就会得到一个新的object对象。相同的操作执行在不同的对象上,得不得同步的效果。为了确保退出临界区时临界资源得到释放,我们应把Monitor类的代码放入Try语句,把调用Exit()方法放入finally语句。为了方便,C#为我们提供了更加简洁的语句。

lock(要锁定的对象)
{
//临界区的代码
    。。。。。
   。。。。。。
}

lock语句执行完,会自动调用Exit()方法,来释放资源。它完全等价于:

try
{
     Monitor.Entry(要锁定的对象);
      //临界区代码
    。 。。。。。
   。。。。。。
   。。。。。。
}
finally
{
    Monitor.Exit(要锁定的对象);
}

当线程以独占锁的方式去访问资源时,其他线程是不能访问的。只有当lock语句结束后其他线程才可以访问。从某种方面可以说,lock语句相当于暂定了程序的多线程功能。这就相当于在资源上放了一把锁,其他线程只能暂定,这样会使程序的效率大打折扣。所以只有必要时才可以设置独占锁。(我们回想一下Interlocked,当通过Interlocked的Read()方法来读取计数器,如果不符合条件,就会等待,线程状态变为SleepWaitJoin状态。但是,Monitor的Entry()方法获取独占锁,如果得不到,线程会被中止,状态会变为Stopped。这是二者的一点区别)。

互斥体(Mutex类)

在操作系统中,线程往往需要共享资源,而这些资源往往要求排他性的使用。即一次只能由一个线程使用,这种排他性的使用资源称为线程之间的互斥(Mutual Exclusion)。互斥,从某种角度也起到了线程同步的目的,所以互斥是一种特殊的同步。与Monitor类似,只有获得Mutex对象的所属权的线程才可以进入临界区,没有获得所属权的只能在临界区外等候。使用Mutex要比使用Monitor消耗资源。但它可以在系统中的不同程序间实现线程同步。

互斥分为局部互斥、系统互斥。顾名思义,局部互斥,只在创建的程序中有效。系统互斥,会在整个系统中有效。

看程序:

         static  void Main(string[] args)
        {

            Thread threadA = new Thread(delegate()
                {
                    //创建互斥体
                    Mutex fileMutex = new Mutex(false, "MutexForTimeRecordFile");

                    string fileName = @"E:\TimeRecord.txt";
                    for (int i = 1; i <= 10; i++)
                    {
                        try
                        {
                            //请求互斥体的所属权,若成功,则进入临界区,若不成功,则等待
                            fileMutex.WaitOne();

                            //在临界区中操作临界资源,即向文件中写入数据
                            File.AppendAllText(fileName, "threadA: " + DateTime.Now + "\r\n");
                        }
                        catch (ThreadInterruptedException)
                        {
                            Console.WriteLine("线程A被中断。");
                        }
                        finally
                        {
                            fileMutex.ReleaseMutex();  //释放互斥体的所属权
                        }
                        Thread.Sleep(1000);
                    }
                });
            threadA.Start();
        }

 

static void Main(string[] args)
        {
            Thread threadB = new Thread(delegate()
            {
                //创建互斥体
                Mutex fileMutex = new Mutex(false, "MutexForTimeRecordFile");

                string fileName = @"E:\TimeRecord.txt";
                for (int i = 1; i <= 10; i++)
                {
                    try
                    {
                        //请求互斥体的所属权,若成功,则进入临界区,若不成功,则等待。
                        fileMutex.WaitOne();

                        //在临界区中操作临界资源,即向文件中写入数据
                        File.AppendAllText(fileName, "ThreadB: " + DateTime.Now + "\r\n");

                    }
                    catch (ThreadInterruptedException)
                    {
                        Console.WriteLine("线程B被中断.");
                    }
                    finally
                    {
                        fileMutex.ReleaseMutex(); //释放互斥体的所属权
                    }
                    Thread.Sleep(1000);
                }
            });

            threadB.Start();

            Process.Start("MutecA.exe"); //启动程序MutexA.exe
        }

这是两个程序,我们创建了一个系统互斥体(“MutexForTimeRecordFile”,在整个系统中有效,可以跨程序)。在程序B中有执行程序A的代码,通过编译,我们把两个编译后的可执行文件放在一起,执行B。效果如下图:

通过效果图,我们可以看出,两个不同的程序通过相同的系统互斥体名,达到了跨程序线程同步的效果。

我们来总结一下C#为我们带来的这三种实现多线程同步的类。

1.Interlocked类

是通过调用Read()方法,来读取计数器,判断计数器,以此达到线程同步的目的。

2.Monitor类

通过调用Entry()方法来获取独占锁,当执行完代码,要调用Pulse()方法唤醒睡眠在临界资源上的线程,同时自己调用Wait()方法,睡眠在临界资源,以便下次访问临界区。Monitor与Interlocked的不同点是,当调用Monitor.Entry()方法,未获得独占锁时,线程状态会变为Stopped。而Interlocked正是将线程处于SleepWaitJoin状态。

3.Mutex类

调用Mutex的WaitOne()方法来获取所属权,获得所属权的线程可以进入临界区,没有所属权的线程在临界区外等待。Mutex对象比Monitor对象消耗资源,但是Mutex对象可以实现跨程序的线程同步。

Mutex分为局部互斥、系统互斥。

 

时间: 2024-08-03 19:05:21

漫谈多线程(中)的相关文章

c#语言-多线程中的锁系统

介绍 平常在多线程开发中,总避免不了线程同步.这次就对net多线程中的锁系统做个简单描述. 目录 一:lock.Monitor 1:基础. 2: 作用域. 3:字符串锁. 二: mutex 三:Semaphore 四:总结 一:lock.Monitor 1:基础 Lock是Monitor语法糖简化写法.Lock在IL会生成Monitor. //======Example 1===== string obj = "helloworld"; lock (obj) { Console.Wri

Java核心知识点学习----多线程中的阻塞队列,ArrayBlockingQueue介绍

1.什么是阻塞队列? 所谓队列,遵循的是先进先出原则(FIFO),阻塞队列,即是数据共享时,A在写数据时,B想读同一数据,那么就将发生阻塞了. 看一下线程的四种状态,首先是新创建一个线程,然后,通过start方法启动线程--->线程变为可运行可执行状态,然后通过数据产生共享,线程产生互斥---->线程状态变为阻塞状态---->阻塞状态想打开的话可以调用notify方法. 这里Java5中提供了封装好的类,可以直接调用然后构造阻塞状态,以保证数据的原子性. 2.如何实现? 主要是实现Blo

编写高质量代码改善C#程序的157个建议——建议66:正确捕获多线程中的异常

建议66:正确捕获多线程中的异常 多线程的异常处理需要采用特殊的方式.一下这种方式会存在问题: try { Thread t = new Thread((ThreadStart)delegate { throw new Exception("多线程异常"); }); t.Start(); } catch (Exception error) { MessageBox.Show(error.Message + Environment.NewLine + error.StackTrace);

thread.join函数,java多线程中的join函数解析

join函数的作用,是让当前线程等待,直到调用join()的 线程结束或者等到一段时间,我们来看以下代码 1 package mian; 2 3 4 public class simpleplela { 5 static void threadMessage(String message) { 6 String threadName = 7 Thread.currentThread().getName(); 8 9 System.out.println(threadName+" "+m

(单例设计模式中)懒汉式与饿汉式在多线程中的不同

/* 目的:分析一下单例设计模式中,懒汉式与饿汉式在多线程中的不同! 开发时我们一般选择饿汉式,因为它简单明了,多线程中不会出现安全问题! 而饿汉式需要我们自己处理程序中存在的安全隐患,但是饿汉式的程序技术含量更高! */ /* class SinglePerson implements Runnable{ private static SinglePerson ss = new SinglePerson("hjz", 22);//恶汉式 private int age; privat

定时器在多线程中的使用

在多线程中使用定时器必须开启Runloop,因为只有开启Runloop保持现成为活动状态,才能保持定时器不断执行 - (void)viewDidLoad { [super viewDidLoad]; [self performSelectorInBackground:@selector(testMultiThread) withObject:nil]; } - (void) testMultiThread { NSAutoreleasePool * pool = [[NSAutoreleaseP

iOS开发——多线程OC篇&amp;多线程中的单例

多线程中的单例 1 #import "DemoObj.h" 2 3 @implementation DemoObj 4 5 static DemoObj *instance; 6 7 8 9 // 在iOS中,所有对象的内存空间的分配,最终都会调用allocWithZone方法 10 // 如果要做单例,需要重写此方法 11 // GCD提供了一个方法,专门用来创建单例的 12 + (id)allocWithZone:(struct _NSZone *)zone 13 { 14 sta

谨慎使用多线程中的fork

前言 在单核时代,大家所编写的程序都是单进程/单线程程序.随着计算机硬件技术的发展,进入了多核时代后,为了降低响应时间,重复充分利用多核cpu的资源,使用多进程编程的手段逐渐被人们接受和掌握.然而因为创建一个进程代价比较大,多线程编程的手段也就逐渐被人们认可和喜爱了. 记得在我刚刚学习线程进程的时候就想,为什么很少见人把多进程和多线程结合起来使用呢,把二者结合起来不是更好吗?现在想想当初真是too young too simple,后文就主要讨论一下这个问题. 进程与线程模型 进程的经典定义就是

多线程 中 对多次初始化问题解决方案

今天在看MSDN 库源代码时发现了一个类 LazyInitializer.EnsureInitialized 并行计算时用到的. MSdn代码 // Used to hold any exceptions encountered during action processing ConcurrentQueue<Exception> exceptionQ = null; // will be lazily initialized if necessary // This is more effi