C# 如何处理和避免并发冲突?

科技   2024-11-23 08:27   北京  

并发冲突是指多个线程同时访问或修改共享资源时可能引发的不一致性或错误。在 C# 开发中,尤其是在多线程和并发环境下,处理并发冲突显得尤为重要。以下是一些常用的技术和方法,并结合案例详细说明:


1. 使用锁定 (Locking)

锁定机制可以确保同一时间只有一个线程访问共享资源,从而避免并发冲突。在 C# 中,使用 lock 关键字可以创建锁定区域。

示例代码:

private readonly object _lock = new object();
private int _sharedCounter = 0;

public void IncrementCounter()
{
    lock (_lock)
    {
        _sharedCounter++;
        Console.WriteLine($"Counter incremented to {_sharedCounter} by thread {Thread.CurrentThread.ManagedThreadId}");
    }
}

说明:

  • _lock 对象用作锁定的标识符。
  • lock 块内的代码是线程安全的,确保同一时间只有一个线程可以执行。

2. 使用线程安全的集合类

.NET 提供了线程安全的集合类,如 ConcurrentDictionaryConcurrentQueue 等。这些集合内置了并发控制,适合在高并发场景下使用。

示例代码:

using System.Collections.Concurrent;

var concurrentDictionary = new ConcurrentDictionary<intstring>();

Parallel.For(010, i =>
{
    concurrentDictionary.TryAdd(i, $"Value{i}");
    Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} added: {i}");
});

// 遍历集合
foreach (var item in concurrentDictionary)
{
    Console.WriteLine($"Key: {item.Key}, Value: {item.Value}");
}

说明:
ConcurrentDictionary 自动处理线程间的数据同步,无需手动加锁,降低了代码复杂度。


3. 使用并发性原语

对于更复杂的同步需求,可以使用并发性原语,如 MonitorMutexSemaphore

示例代码:

using System.Threading;

private static Semaphore semaphore = new Semaphore(33);

public void AccessResource()
{
    Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} waiting to enter...");
    semaphore.WaitOne(); // 获取信号量
    try
    {
        Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} is working...");
        Thread.Sleep(1000); // 模拟工作
    }
    finally
    {
        Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} is leaving.");
        semaphore.Release(); // 释放信号量
    }
}

说明:
Semaphore 限制同时访问资源的线程数,例如本例中最多允许 3 个线程同时访问资源。


4. 使用事务

在数据库操作中,事务是一种强有力的工具,用于确保一组操作要么全部成功,要么全部失败。

示例代码:

using (var connection = new SqlConnection("YourConnectionString"))
{
    connection.Open();
    using (var transaction = connection.BeginTransaction())
    {
        try
        {
            var command = connection.CreateCommand();
            command.Transaction = transaction;

            command.CommandText = "UPDATE Accounts SET Balance = Balance - 100 WHERE Id = 1";
            command.ExecuteNonQuery();

            command.CommandText = "UPDATE Accounts SET Balance = Balance + 100 WHERE Id = 2";
            command.ExecuteNonQuery();

            transaction.Commit();
            Console.WriteLine("Transaction committed successfully.");
        }
        catch
        {
            transaction.Rollback();
            Console.WriteLine("Transaction rolled back due to an error.");
        }
    }
}

说明:
通过事务,可以确保多个数据库操作的原子性,即使在并发情况下,也能避免数据不一致。


5. 避免共享状态

通过减少或避免共享状态,可以大幅降低并发冲突的可能性。常见方法包括使用不可变对象或线程本地存储 (ThreadLocal)。

示例代码:

// 使用不可变对象
public class ImmutableData
{
    public int Value { get; }
    public ImmutableData(int value)
    {
        Value = value;
    }
}

// 使用线程本地存储
private static ThreadLocal<int> threadLocalCounter = new ThreadLocal<int>(() => 0);

public void UpdateCounter()
{
    threadLocalCounter.Value++;
    Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId}: Counter = {threadLocalCounter.Value}");
}

说明:

  • 不可变对象的状态一旦创建,就不会改变,从而避免了数据竞争。
  • ThreadLocal 确保每个线程拥有独立的变量副本。

总结

  • 锁定:适用于需要精确控制共享资源的访问。
  • 线程安全集合:简化并发编程,适合数据共享。
  • 并发性原语:应对更复杂的同步需求。
  • 事务:保障数据库操作的一致性。
  • 避免共享状态:从根本上减少冲突的可能。

选择适当的技术需要根据实际场景权衡性能和复杂度。例如,高频访问的共享资源可以优先考虑线程安全集合,而需要精准控制的场景则使用锁或并发性原语。

参考:内容由AI辅助生成


关注公众号DotNet开发跳槽    


DotNet开发跳槽
本公众号专注为.net开发工程师提供一个学习技术及求职/跳槽的交流平台。不定期分享NET技术类文章、面试题、求助技巧等干货,原创文章300+篇,让.net开发工程师学习/面试不再迷茫。ps: 后台回复“跳槽”,免费领取.NET开发面试题!
 最新文章