详解C#多线程之线程同步

yizhihongxing

详解C#多线程之线程同步

前言

在多线程编程中,线程同步是一个非常重要的概念。当多个线程并发执行同一段代码时,由于线程执行顺序和时机的不确定性,可能会导致各种不可预测的结果,比如死锁、竞态条件等问题。因此,为了确保多线程程序的正确性,我们必须使用正确的线程同步机制来协调线程之间的访问。

本文将详细讲解C#中的线程同步机制,包括锁、互斥量、信号量和事件等。

锁是最基本的线程同步机制,通常用于保护临界区(critical section)。临界区是一段需要互斥访问的代码区域,例如对共享资源的读写操作。

C#中的锁可以使用lock关键字来实现。lock语句可以将一段代码标记为临界区,并且确保在任意时刻只有一个线程可以进入这个区域执行代码,其他线程必须等待当前线程退出临界区后才能进入。

下面是一个简单的示例,演示了如何使用lock来实现线程同步:

class Counter {
    private int count = 0;

    public void Increment() {
        lock (this) {
            count++;
        }
    }

    public void Decrement() {
        lock (this) {
            count--;
        }
    }

    public int GetCount() {
        return count;
    }
}

class Program {
    static void Main(string[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread(() => {
            for (int i = 0; i < 100000; i++) {
                counter.Increment();
            }
        });

        Thread t2 = new Thread(() => {
            for (int i = 0; i < 100000; i++) {
                counter.Decrement();
            }
        });

        t1.Start();
        t2.Start();

        t1.Join();
        t2.Join();

        Console.WriteLine(counter.GetCount()); // 应该输出0
    }
}

在上面的例子中,我们定义了一个Counter类,这个类有一个私有的计数器countIncrement方法将计数器加1,Decrement方法将计数器减1,GetCount方法返回计数器的值。注意,IncrementDecrement方法都是使用lock语句来标记临界区,以确保任意时刻只有一个线程可以进入这个区域执行代码。

Main方法中,我们创建了两个新线程t1t2,分别执行IncrementDecrement方法。我们使用Join方法等待这两个线程执行完毕。最后,我们输出计数器的值,这个值应该是0。

互斥量

除了锁,互斥量也是一种常用的线程同步机制。互斥量可以用于实现多个线程之间的互斥访问,保护临界区。

C#中的互斥量可以使用Mutex类来实现。Mutex类是一个系统级别的同步原语,可以跨进程使用。在创建Mutex对象时,需要指定一个名称。如果在同一台计算机上创建了多个名称相同的Mutex对象,这些对象将被视为是互斥的,即只有一个线程可以获得这些对象的锁。

下面是一个示例,演示了如何使用Mutex来实现线程同步:

class Counter {
    private int count = 0;
    private static Mutex mutex = new Mutex();

    public void Increment() {
        mutex.WaitOne();
        count++;
        mutex.ReleaseMutex();
    }

    public void Decrement() {
        mutex.WaitOne();
        count--;
        mutex.ReleaseMutex();
    }

    public int GetCount() {
        return count;
    }
}

class Program {
    static void Main(string[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread(() => {
            for (int i = 0; i < 100000; i++) {
                counter.Increment();
            }
        });

        Thread t2 = new Thread(() => {
            for (int i = 0; i < 100000; i++) {
                counter.Decrement();
            }
        });

        t1.Start();
        t2.Start();

        t1.Join();
        t2.Join();

        Console.WriteLine(counter.GetCount()); // 应该输出0
    }
}

在上面的例子中,我们使用了一个静态的Mutex对象mutex,这个对象在整个程序中是共享的。在IncrementDecrement方法内部,我们分别调用了WaitOneReleaseMutex方法来获取和释放锁。

与锁不同的是,将mutex.WaitOne()mutex.ReleaseMutex()包含在锁定块内是不合法的。因为Mutex并非C#语言级别的构造,而是由系统提供的同步原语,使用WaitOne方法可以跨越方法边界对代码进行同步。

信号量

信号量是一种常用的线程同步机制,可以用于控制多个线程之间的并发数量。信号量可以看作是一个计数器,当信号量的计数器大于零时,允许某个线程继续执行;当计数器等于零时,阻塞该线程。当另一个线程释放了一个信号量时,计数器加1,唤醒一个(或多个)被阻塞的线程。

C#中的信号量可以使用Semaphore类来实现。Semaphore类有两个构造函数:一个接受一个初始计数器,另一个接受一个初始计数器和一个最大计数器。Semaphore.WaitOne方法用来获取信号量,Semaphore.Release方法用来释放信号量。

下面是一个示例,演示了如何使用Semaphore来实现线程同步:

class Counter {
    private int count = 0;
    private static Semaphore semaphore = new Semaphore(1, 1);

    public void Increment() {
        semaphore.WaitOne();
        count++;
        semaphore.Release();
    }

    public void Decrement() {
        semaphore.WaitOne();
        count--;
        semaphore.Release();
    }

    public int GetCount() {
        return count;
    }
}

class Program {
    static void Main(string[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread(() => {
            for (int i = 0; i < 100000; i++) {
                counter.Increment();
            }
        });

        Thread t2 = new Thread(() => {
            for (int i = 0; i < 100000; i++) {
                counter.Decrement();
            }
        });

        t1.Start();
        t2.Start();

        t1.Join();
        t2.Join();

        Console.WriteLine(counter.GetCount()); // 应该输出0
    }
}

在上面的例子中,我们使用了一个静态的Semaphore对象semaphore,这个对象在整个程序中是共享的。在IncrementDecrement方法内部,我们分别调用了WaitOneRelease方法来获取和释放信号量。这里我们使用了一个计数器为1的Semaphore对象,表示同时只允许一个线程进入临界区。

事件

事件是一种多线程同步机制,可以用来在多个线程之间传递信号。事件有两种状态:已触发和未触发。已触发的事件等价于一个开关,打开开关后,所有通过WaitOne方法等待该事件的线程都可以开始执行;未触发的事件则表示等待状态,直到有线程通过Set方法将事件触发为止。

C#中的事件可以使用ManualResetEventAutoResetEvent类来实现。这两个类都继承自EventWaitHandle类。两者的区别在于:ManualResetEvent需要显式地调用Reset方法将事件重置到未触发状态;而AutoResetEvent会在一个线程通过WaitOne方法等待该事件时自动触发,无需再调用Set方法。

下面是一个示例,演示了如何使用ManualResetEvent来实现线程同步:

class Counter {
    private int count = 0;
    private static ManualResetEventSlim mre = new ManualResetEventSlim();

    public void Increment() {
        count++;
        mre.Set();
    }

    public void Decrement() {
        count--;
        mre.Set();
    }

    public int GetCount() {
        return count;
    }

    public void WaitForChange() {
        mre.Wait();
        mre.Reset();
    }
}

class Program {
    static void Main(string[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread(() => {
            Random r = new Random();
            for (int i = 0; i < 1000; i++) {
                counter.Increment();
                Thread.Sleep(r.Next(10));
            }
        });

        Thread t2 = new Thread(() => {
            Random r = new Random();
            for (int i = 0; i < 1000; i++) {
                counter.Decrement();
                Thread.Sleep(r.Next(10));
            }
        });

        Thread t3 = new Thread(() => {
            for (int i = 0; i < 10; i++) {
                counter.WaitForChange();
                Console.WriteLine(counter.GetCount());
            }
        });

        t1.Start();
        t2.Start();
        t3.Start();

        t1.Join();
        t2.Join();
        t3.Join();
    }
}

在上面的例子中,我们使用了一个静态的ManualResetEventSlim对象mre,这个对象在整个程序中是共享的。在IncrementDecrement方法中,我们分别将计数器加1或减1,然后调用Set方法来触发事件。在WaitForChange方法中,我们使用Wait方法等待事件的触发,并在事件触发后调用Reset方法将事件重置到未触发状态。在Main函数中,我们创建了三个线程t1t2t3,分别执行IncrementDecrementWaitForChange方法。在t3线程中,我们使用WaitForChange方法等待计数器的变化,并在计数器变化后输出计数器的值。

结语

本文详细讲解了C#中的线程同步机制,包括锁、互斥量、信号量和事件。在编写多线程程序时,根据实际情况选择合适的同步机制非常重要,以确保程序的正确性和健壮性。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:详解C#多线程之线程同步 - Python技术站

(0)
上一篇 2023年5月15日
下一篇 2023年5月15日

相关文章

  • ASP.NET Core实现文件上传和下载

    ASP.NET Core 实现文件上传和下载 在 ASP.NET Core 中,可以使用多种方式实现文件上传和下载。本攻略将详细介绍如何在 ASP.NET Core 中实现文件上传和下载,并提供多种上传方式的示例。 步骤一:编写文件上传代码 在 ASP.NET Core 中,可以使用 IFormFile 接口实现文件上传。以下是一个简单的文件上传示例: [H…

    C# 2023年5月17日
    00
  • 深入讲解.Net Core中的Api版本控制

    在 .NET Core 中,API 版本控制是一种常见的需求。API 版本控制可以帮助我们管理 API 的演变,确保客户端和服务器之间的兼容性。本文将深入讲解 .NET Core 中的 API 版本控制,包括路由、策略和文档。 路由 在 .NET Core 中,可以使用路由来实现 API 版本控制。以下是一个示例: [ApiController] [Rout…

    C# 2023年5月17日
    00
  • C#中public变量不能被unity面板识别的解决方案

    C#中public变量是可以被Unity面板识别的,但需要满足以下条件: 1.该变量所在的类必须继承自MonoBehaviour类; 2.该变量需要被标记为[SerializeField],以告诉Unity需要将其序列化并显示在面板上。 如果你遇到了public变量不能被Unity面板识别的问题,可能是因为你没有满足以上两个条件或者其他原因造成的。接下来,我…

    C# 2023年5月15日
    00
  • C#中的虚函数virtual

    当我们在派生类中重写基类的同名函数时,若基类指针或引用指向派生类对象,这时若基类函数被调用,会调用派生类中的函数而不是基类中的函数。但是,如果将基类指针或引用指向派生类对象的实例时,如果使用基类指针或引用来访问这个函数,则只会调用基类中的函数,而不会调用派生类中的函数。为了解决这个问题,C#中引入了虚函数virtual的机制。 虚函数用来实现多态,将基类中的…

    C# 2023年6月7日
    00
  • C#读写config配置文件的方法

    以下是关于C#读写config配置文件的完整攻略。 1. 创建配置文件 首先,我们需要创建一个配置文件,可以使用Visual Studio自带的配置管理器创建,也可以手动创建一个XML文件并修改后缀为.config。下面是一个简单的配置文件示例: <?xml version="1.0" encoding="utf-8&qu…

    C# 2023年6月1日
    00
  • C#中is和as用法实例分析

    C#中is和as用法实例分析 is关键字 is关键字是用来判断某个对象是否是指定类型的实例,如果是则返回true,否则返回false。语法格式如下: obj is type 其中obj表示需要判断的对象,type表示需要判断的类型。如果obj是type类型的实例,返回true,否则返回false。 示例1:判断对象是否是某个类型的实例 object obj …

    C# 2023年5月15日
    00
  • C#中读取App.config配置文件代码实例

    下面就给您详细讲解一下在C#中读取App.config配置文件的完整攻略。 什么是App.config? 在C#项目中,App.config是存放配置信息的文件,经常用来保存应用程序的配置信息,比如数据库连接字符串、路径等等。在项目中对于一些数据的统一管理是非常有用的,修改方便,且使用配置文件时只需要修改App.config即可不用修改代码。 读取App.c…

    C# 2023年6月1日
    00
  • C#实现String字符串转化为SQL语句中的In后接的参数详解

    介绍 在编写SQL语句的时候,经常需要使用到In条件查询,而In后接的参数需要转化为字符串。本文主要介绍如何使用C#将String字符串转化为SQL语句中In后接的参数。 实现步骤 第一步:定义一个List集合存储需要转化的参数。 List<string> list = new List<string>(); list.Add(&qu…

    C# 2023年6月8日
    00
合作推广
合作推广
分享本页
返回顶部