在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();
        }
    }
}

  所产生的结果显示:

  可以看到结果有很多错误。这就表明了多线程访问的数据竞争问题。数据竞争可能会导致不可预测的结果,例如应用程序崩溃或不正确的行为、结果。
  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();
        }
    }
}

  现在的结果就是正确的。

   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();
        }
    }
}

  结果显示:

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

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才对)

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

  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
多线程编程资源共享是一个常见的问题。当多个线程同时访问和修改共享资源时,如果没有正确的同步机制,就会出现数据竞争和不可预测的结果。以下是一些处理多线程资源共享问题的常用方法: 1. 使用互斥锁(Mutex)或锁(lock):通过在访问共享资源的代码块上加锁,确保同一时间只有一个线程可以访问该资源。这样可以避免数据竞争和并发修改的问题。 ```csharp private static readonly object lockObject = new object(); lock (lockObject) { // 访问共享资源的代码 } ``` 2. 使用线程安全的集合类:在C#,有一些线程安全的集合类,例如`ConcurrentQueue`、`ConcurrentStack`、`ConcurrentDictionary`等。它们内部实现了适当的同步机制,可以在多线程环境下安全地进行读写操作。 ```csharp ConcurrentQueue<int> queue = new ConcurrentQueue<int>(); // 线程1往队列添加元素 queue.Enqueue(1); // 线程2从队列取出元素 int item; if (queue.TryDequeue(out item)) { // 处理取出的元素 } ``` 3. 使用互斥体(Monitor):通过使用`Monitor`类来创建临界区,确保只有一个线程可以进入临界区访问共享资源。 ```csharp private static readonly object lockObject = new object(); Monitor.Enter(lockObject); try { // 访问共享资源的代码 } finally { Monitor.Exit(lockObject); } ``` 4. 使用原子操作:C#提供了一些原子操作的方法,例如`Interlocked`类的方法,可以在多线程环境下进行原子性的读写操作,避免数据竞争和并发修改的问题。 ```csharp private static int counter = 0; Interlocked.Increment(ref counter); // 原子性地增加计数器 ``` 以上方法可以帮助你处理多线程资源共享问题,确保线程安全和数据一致性。根据具体情况选择合适的方法来处理资源共享,以满足程序的需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值