在C#中使用信号量解决多线程访问共享资源的冲突问题

  目前在我写的233篇原创文章中,有两篇是粉丝可见的,其中《C#线程的参数传递、获取线程返回值以及处理多线程冲突》这篇文章有179个粉丝关注,看到不断有人关注这篇文章,这表明学习C#的人还是挺多的,感觉文章内容不够厚实,对不起粉丝的关注,加上文章末尾说了要写一篇详细的多线程通讯,今天就写了使用信号量来解决多线程访问共享资源可能导致的冲突或者错误。
  解决这样的问题还有很多手段,比如可以使用锁、自旋锁、事件、管道、互斥量、原子操作等等,而不仅仅是只有使用信号量这一手段。
  如果后面再使用C#,就写其他的,这一篇主要涉及信号量,包括使用信号量解决多线程访问共享资源的冲突问题以及在线程池中使用信号量。
  目录:
  1、问题:有两个任务同时进行,它们的任务内容都是在10秒内每隔一秒随机产生一个1到10的随机数,第三个任务随时统计并显示两个任务所产生1~10数字的个数。
  2、问题:有两个任务同时进行,它们的任务内容都是随机产生1000个一个1到10的随机数,第三个任务统计并显示两个任务所产生1~10数字的个数。
  3、使用锁来解决多线程访问共享资源的冲突(最常见的做法)。
  4、使用信号量解决多线程访问共享资源所可能产生的冲突问题。
  5、使用线程池与信号量解决多线程访问共享资源可能导致的冲突问题。

  在C#中,信号量(Semaphore)是一种同步原语,它可以用来控制多个线程对共享资源的访问。信号量维护了一个计数器,当有线程访问共享资源时,计数器减1;当线程释放共享资源时,计数器加1。如果计数器为0,表示没有可用的资源,此时线程需要等待,直到有其它线程释放资源。
  信号量的主要作用是实现对共享资源的控制和同步,以避免多个线程同时访问共享资源而导致的冲突。通过使用信号量,我们可以确保同一时间只有指定数量的线程能够访问共享资源,从而避免冲突。除了线程管理之外,信号量还可以应用于进程管理、网络编程、并发控制等领域。
  在实际开发中,如果需要协调对共享资源的访问,避免多个线程或进程同时对共享资源进行操作,信号量是一个很有用的工具。
  信号量其实是一种操作系统的原语,它不仅应用在C#中,其他的编程语言或者操作系统也有它的实现。在操作系统中,信号量主要用于进程间的同步和通信,它可以用来协调对共享资源的访问,避免多个进程同时对共享资源进行操作导致的冲突。
  百度百科
  信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量VI,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端。确认这些信号量VI引用的是初始创建的信号量。


  1问题:有两个任务同时进行,它们的任务内容都是在10秒内每隔一秒随机产生一个1到10的随机数,第三个任务随时统计并显示两个任务所产生1~10数字的个数。

  这个问题实现简单,代码如下:

using System;
using System.Threading;

namespace MultiThread20230224
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            Control.CheckForIllegalCrossThreadCalls = false;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            int[] Arr = new int[11];

            // 创建线程
            Thread t1 = new Thread(() => {
                // 对共享资源的操作
                textBox1.Text = "";
                int count = 0;
                while (count++ < 10)
                {
                    Random r = new Random();
                    int number = r.Next(1, 11);
                    textBox1.Text = textBox1.Text + number.ToString() + Environment.NewLine;
                    Arr[number] ++ ;
                    Thread.Sleep(1000);
                }
            });
            t1.Start();

            Thread t2 = new Thread(() => {
                textBox2.Text = "";
                // 对共享资源的操作
                int count = 0;
                while (count++ < 10)
                {
                    Random r = new Random();
                    int number = r.Next(1, 11);
                    textBox2.Text = textBox2.Text + number.ToString() + Environment.NewLine;
                    Arr[number]++;
                    Thread.Sleep(1000);
                }
            });
            t2.Start();

            Thread t3 = new Thread(() => {
                // 显示统计数据
                int count = 0;
                while (count++ < 10)
                {
                    textBox3.Text = "";
                    for (int i = 1; i < Arr.GetLength(0); i++)
                    {
                        textBox3.Text = textBox3.Text +i.ToString()+" ==> "+Arr[i].ToString()+ Environment.NewLine;
                    }
                    Thread.Sleep(1000);
                }
            });
            t3.Start();
        }
    }
}

  运行后发现最后结果是正确的,程序也没有报告错误,哪怕运行多次也是这样,但是代码中没有使用任何线程同步机制来确保线程安全性,因此在多次运行时,可能会产生意外的结果,比如有些数字没有被计算,或者计数器的值不准确等。
  为了验证多线程对共享资源访问可能发生的冲突或者错误,将需求更改为:
  2、问题:有两个任务同时进行,它们的任务内容都是随机产生1000个一个1到10的随机数,第三个任务统计并显示两个任务所产生1~10数字的个数。

  实现代码如下:

using System;
using System.Threading;

namespace MultiThread20230224
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            Control.CheckForIllegalCrossThreadCalls = false;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            int[] Arr = new int[11];
            int[] Arr1 = new int[11];//记录任务一的1到10所产生的个数统计
            int[] Arr2 = new int[11];//记录任务二的1到10所产生的个数统计

            // 创建线程
            Thread t1 = new Thread(() => {
                textBox1.Text = "";
                int count = 0;
                while (count++ < 1000)
                {
                    //semaphore.WaitOne();
                    Random r = new Random();
                    int number = r.Next(1, 11);
                    //textBox1.Text = textBox1.Text + number.ToString() + Environment.NewLine;
                    Arr[number] ++ ;
                    Arr1[number]++;
                    //Thread.Sleep(1);
                    //semaphore.Release();
                }
            });
            t1.Start();

            Thread t2 = new Thread(() => {
                textBox2.Text = "";
                int count = 0;
                while (count++ < 1000)
                {
                    //semaphore.WaitOne();
                    Random r = new Random();
                    int number = r.Next(1, 11);
                    //textBox2.Text = textBox2.Text + number.ToString() + Environment.NewLine;
                    Arr[number]++;
                    Arr2[number]++;
                    //Thread.Sleep(1);
                    //semaphore.Release();
                }
            });
            t2.Start();
            t1.Join();
            t2.Join();

            Thread t3 = new Thread(() => {
                    textBox1.Text = "";
                    textBox2.Text = "";
                    textBox3.Text = "";
                    for (int i = 1; i < Arr.GetLength(0); i++)
                    {
                        textBox1.Text = textBox1.Text + i.ToString() + " ==> " + Arr1[i].ToString() + Environment.NewLine;
                        textBox2.Text = textBox2.Text +i.ToString()+" ==> "+Arr2[i].ToString()+ Environment.NewLine;
                        textBox3.Text = textBox3.Text + i.ToString() + " ==> " + Arr[i].ToString() + Environment.NewLine;
                    }
            });
            t3.Start();
        }
    }
}

  所产生的结果显示:

在C#中使用信号量解决多线程访问共享资源的冲突问题_第1张图片

  可以看到结果有很多错误。这就表明了多线程访问的数据竞争问题。数据竞争可能会导致不可预测的结果,例如应用程序崩溃或不正确的行为、结果。
  3、使用锁来解决多线程访问共享资源的冲突(最常见的做法)

using System;
using System.Threading;

namespace MultiThread20230224
{
    public partial class Form1 : Form
    {
        private object lockObj = new object();
        int[] Arr = new int[11];
        int[] Arr1 = new int[11];
        int[] Arr2 = new int[11];
        public Form1()
        {
            InitializeComponent();
            Control.CheckForIllegalCrossThreadCalls = false;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Thread t1 = new Thread(() => {
                int count = 0;
                while (count++ < 1000)
                {
                    Random r = new Random();
                    int number = r.Next(1, 11);
                    lock (lockObj)
                    {
                        Arr[number]++;
                    }                        
                    Arr1[number]++;
                }
            });
            t1.Start();

            Thread t2 = new Thread(() => {
                int count = 0;
                while (count++ < 1000)
                {
                    Random r = new Random();
                    int number = r.Next(1, 11);
                    lock (lockObj)
                    {
                        Arr[number]++;
                    }
                    Arr2[number]++;
                }
            });
            t2.Start();
            t1.Join();
            t2.Join();

            Thread t3 = new Thread(() => {
                    textBox1.Text = "";
                    textBox2.Text = "";
                    textBox3.Text = "";
                    for (int i = 1; i < Arr.GetLength(0); i++)
                    {
                        textBox1.Text = textBox1.Text + i.ToString() + " ==> " + Arr1[i].ToString() + Environment.NewLine;
                        textBox2.Text = textBox2.Text +i.ToString()+" ==> "+Arr2[i].ToString()+ Environment.NewLine;
                        textBox3.Text = textBox3.Text + i.ToString() + " ==> " + Arr[i].ToString() + Environment.NewLine;
                    }
            });
            t3.Start();
        }
    }
}

  现在的结果就是正确的。

在C#中使用信号量解决多线程访问共享资源的冲突问题_第2张图片

   4、使用信号量解决多线程访问共享资源所可能产生的冲突问题

  先看实现的代码:

using System;
using System.Threading;

namespace MultiThread20230224
{
    public partial class Form1 : Form
    {
        //private object lockObj = new object();
        int[] Arr = new int[11];//记录两个任务所产生的1到10的个数统计
        int[] Arr1 = new int[11];//记录任务一的1到10所产生的个数统计
        int[] Arr2 = new int[11];//记录任务二的1到10所产生的个数统计
        private SemaphoreSlim semaphore = new SemaphoreSlim(1);  // 声明一个信号量对象

        public Form1()
        {
            InitializeComponent();
            Control.CheckForIllegalCrossThreadCalls = false;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            // 创建线程
            Thread t1 = new Thread(() => {
                // 对共享资源的操作
                int count = 0;
                while (count++ < 1000)
                {
                    semaphore.Wait();  // 请求信号量
                    int number = new Random().Next(1, 11);
                    Arr[number]++;
                    Arr1[number]++;
                    semaphore.Release();  // 释放信号量
                }
            });
            t1.Start();

            Thread t2 = new Thread(() => {
                // 对共享资源的操作
                int count = 0;
                while (count++ < 1000)
                {
                    semaphore.Wait();  // 请求信号量
                    int number = new Random().Next(1, 11);
                    Arr[number]++;
                    Arr2[number]++;
                    semaphore.Release();  // 释放信号量
                }
            });
            t2.Start();
            t1.Join();
            t2.Join();

            Thread t3 = new Thread(() => {
                // 显示统计数据
                    string S1 = "";
                    textBox1.Text = "";
                    textBox2.Text = "";
                    textBox3.Text = "";
                    for (int i = 1; i < Arr.GetLength(0); i++)
                    {
                        textBox1.Text = textBox1.Text + i.ToString() + " ==> " + Arr1[i].ToString() + Environment.NewLine;
                        textBox2.Text = textBox2.Text +i.ToString()+" ==> "+Arr2[i].ToString()+ Environment.NewLine;
                        if (Arr[i]== Arr1[i]+ Arr2[i])
                        {
                            S1 = "√";
                        }else{
                            S1 = "×";
                        }
                        textBox3.Text = textBox3.Text + i.ToString() + " ==> " + Arr[i].ToString()+" "+S1 + Environment.NewLine;
                    }
            });
            t3.Start();
        }
    }
}

  结果显示:

在C#中使用信号量解决多线程访问共享资源的冲突问题_第3张图片

  上面的实现比较简单,可以改动程序:
  声明信号量:

static Semaphore semaphore = new Semaphore(1, 1);

  改写等待信号语句:

semaphore.WaitOne();

  出来的结果也是一样的正确。
  说明:
  ⑴ 在使用 SemaphoreSlim 时,构造函数中的参数表示信号量的初始计数。0 表示信号量一开始没有可用的许可证,需要等待另一个线程调用 Release 方法来增加计数并释放许可证。如果初始值为1或更高,则表示初始情况下有可用的许可证,其他线程可以直接调用 Wait 方法并获得许可证而不必等待。
  ⑵ 在使用SemaphoreSlim的情况下,通过Wait()和Release()方法来控制线程的同步。
  ⑶ SemaphoreSlim 和 Semaphore 都是用来控制多个线程对共享资源的访问的工具,但它们的实现方式不同,有一些细微的差别。SemaphoreSlim 是一个轻量级的 Semaphore 实现,与 Semaphore 相比,它更快、更节省资源。 
  ⑷ Semaphore(1, 1) 是 Semaphore 的一个构造函数,它创建了一个初始计数为1、最大计数为1的信号量。这意味着在任何时刻只能有一个线程获得该信号量并访问共享资源。 

   延续上面的问题,如果是100个这样的任务同时进行,显然,代码就不可能这样写,需要使用线程池来解决了。
  问题:有100个任务同时进行,它们的任务内容都是随机产生一个1到10的随机数,第三个任务统计并显示两个任务所产生1~10数字的个数。

   5、使用线程池与信号量解决多线程访问共享资源可能导致的冲突问题

  实现代码:

using System;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

namespace MultiThread20230224
{
    public partial class Form1 : Form
    {
        static int[] Arr = new int[11];
        static SemaphoreSlim semaphore = new SemaphoreSlim(1); // 用于保证对Arr数组的操作是线程安全的

        public Form1()
        {
            InitializeComponent();
            Control.CheckForIllegalCrossThreadCalls = false;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            // 启动100个任务
            for (int i = 0; i < 100; i++)
            {
                ThreadPool.QueueUserWorkItem(new WaitCallback(DoTask), i);
            }

            // 等待所有任务完成
            Thread.Sleep(5000);

            textBox3.Text = "";
            int count = 0;
            for (int i = 1; i < 11; i++)
            {
                count += Arr[i];
                textBox3.Text = textBox3.Text + i.ToString() + " ==> " + Arr[i].ToString() + Environment.NewLine;
            }
            textBox3.Text = textBox3.Text + "总数 ==> " + count.ToString() + Environment.NewLine;
        }

        static void DoTask(object TaskNum)
        {
            int index = (int)TaskNum;
            Random rand = new Random();
            for (int i = 0; i < 10; i++)
            {
                //这里可以记录每个任务(index)所产生的数据,这里忽略
                //比如记录本次任务的数据,用于整体的对比
                int number = rand.Next(1, 11); // 产生一个1~10之间的随机数
                // 使用信号量保证对Arr数组的操作是线程安全的
                semaphore.Wait();
                Arr[number]++;
                semaphore.Release();
            }
        }
    }
}

  显示结果:(因为每个线程里产生10个数,100个线程应该产生1000个数,而程序中的总数是累加了各个线程所产生的个数总计,所以也应该是1000才对)

在C#中使用信号量解决多线程访问共享资源的冲突问题_第4张图片

   上面的代码中同时创建了 100 个线程,并将它们全部加入到线程池中。然后,使用线程池的 QueueUserWorkItem 方法将 100 个任务分配给这 100 个线程去执行。使用semaphore.Wait()和semaphore.Release()保证对Arr数组的访问不发生冲突。

你可能感兴趣的:(C#,C#,多线程,信号量,多线程冲突)