找回密码
 立即注册
快捷导航

[.NET] NET的并发冲突

[复制链接]
妙笔生花 2023-9-1 15:09:40 | 显示全部楼层
本帖最后由 妙笔生花 于 2023-9-5 05:19 编辑

在Net6开发过程中,解决并发冲突有以下几种方法:

  1. 使用锁:可以使用互斥锁(Mutex)或读写锁(ReaderWriterLock)来保护共享资源,确保同一时间只有一个线程可以访问该资源。使用锁的关键是在访问共享资源之前获取锁,并在使用完毕后释放锁。

  2. 使用并发集合:Net6提供了一些线程安全的并发集合类,如ConcurrentDictionary、ConcurrentQueue和ConcurrentBag等。这些集合类在内部使用了锁或其他机制来保证线程安全,可以在多个线程之间安全地进行并发操作。

  3. 使用异步编程模型:Net6提供了异步编程模型(Async/Await),可以使用异步方法来执行耗时的操作,而不会阻塞主线程。通过使用异步方法,可以提高系统的并发性能,减少线程竞争和冲突。

  4. 使用事务:如果涉及到多个操作需要保持一致性,可以使用数据库事务或分布式事务来解决并发冲突。事务可以确保多个操作要么全部成功,要么全部失败,从而保持数据的一致性。

  5. 使用乐观并发控制(乐观锁):乐观并发控制是一种无锁的并发控制方式,认为并发操作的冲突是较少发生的,因此不会立即获取锁,而是在操作完成时检查是否发生了冲突。通过在更新操作之前检查数据的版本号或时间戳来判断是否发生冲突。如果发生冲突,可以选择重试操作或者放弃操作。

  6. 使用悲观并发控制(悲观锁):悲观锁是一种悲观的思想,认为并发操作会导致冲突,因此在访问共享资源之前就会获取锁,并在操作完成后释放锁。

需要根据具体的应用场景和需求选择合适的并发冲突解决方法,综合考虑性能、数据一致性和代码复杂度等因素。

回复

使用道具 举报

主题

0

回帖

598

积分

自成一派

 楼主| 妙笔生花 2023-9-1 15:25:50 | 显示全部楼层
本帖最后由 妙笔生花 于 2023-9-1 22:20 编辑

实现 悲观锁 的方式有哪些?

悲观并发控制一般采用行锁、表锁等 排它性对资源进行锁定,确保一个时间点只有一个用户在操作被锁定的资源。

数据库提供的事务

[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
    if (id != todoItem.Id) return BadRequest();

    // 使用悲观锁
    var existingTodoItem = await _context.TodoItems
        .FirstOrDefaultAsync(t => t.Id == id, 
            new QueryOptions<TodoItem>().WithLock(LockType.Update));

    if (existingTodoItem == null) return NotFound();

    // 更新数据
    existingTodoItem.Name = todoItem.Name;
    existingTodoItem.Description = todoItem.Description;

    try await _context.SaveChangesAsync();
    catch (DbUpdateConcurrencyException)
    {
        // 处理并发冲突
        return Conflict();
    }
    return NoContent();
}

使用Monitor类

Monitor类是.NET中用于实现互斥锁的类,可以使用它来实现悲观锁。通过调用Monitor.Enter方法获取锁,在临界区内执行操作,然后调用Monitor.Exit方法释放锁。这样可以确保同一时间只有一个线程可以进入临界区。

private static readonly object _lockObject = new object();

[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
    if (id != todoItem.Id) return BadRequest();
    lock (_lockObject)
    {
        _context.Entry(todoItem).State = EntityState.Modified;
        _context.SaveChanges();
    }
    return NoContent();
}

使用了一个静态的lockObject作为锁对象。在执行更新操作之前,使用lock关键字来获取锁,确保同一时间只有一个线程可以进入临界区。在临界区内执行更新操作,并调用SaveChanges方法来保存更改。然后,释放锁,让其他线程可以继续访问。

注意:避免死锁的情况,确保在获取锁后能够正常释放锁。避免过度使用锁,以免影响性能。

使用Mutex类

Mutex类是.NET中用于实现互斥锁的类,可以使用它来实现悲观锁。通过创建一个命名的Mutex实例,在临界区内调用WaitOne方法获取锁,执行操作,然后调用ReleaseMutex方法释放锁。这样可以确保同一时间只有一个线程可以获取到命名的Mutex实例。

private static readonly Mutex _mutex = new Mutex();

[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
    if (id != todoItem.Id) return BadRequest();

    _mutex.WaitOne();

    try
    {
        _context.Entry(todoItem).State = EntityState.Modified;
        await _context.SaveChangesAsync();
    }
    finally
    {
        _mutex.ReleaseMutex();
    }

    return NoContent();
}

使用了一个静态的Mutex对象来实现锁。在执行更新操作之前,调用WaitOne方法获取锁,确保同一时间只有一个线程可以进入临界区。在临界区内执行更新操作,并调用SaveChangesAsync方法来保存更改。然后,使用ReleaseMutex方法释放锁,让其他线程可以继续访问。

注意:避免死锁的情况,确保在获取锁后能够正常释放锁。避免过度使用锁,以免影响性能。

使用Semaphore类

Semaphore类是.NET中用于实现信号量的类,可以使用它来实现悲观锁。通过创建一个Semaphore实例,设置初始计数器值为1,在临界区内调用WaitOne方法获取锁,执行操作,然后调用Release方法释放锁。这样可以确保同一时间只有一个线程可以获取到信号量。

在类的顶部声明一个Semaphore对象:

private static SemaphoreSlim semaphore = new SemaphoreSlim(1);

[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
    if (id != todoItem.Id) return BadRequest();

    await semaphore.WaitAsync();

    try
    {
        _context.Entry(todoItem).State = EntityState.Modified;
        await _context.SaveChangesAsync();
        return NoContent();
    }
    finally
    {
        semaphore.Release();
    }
}

在方法开始时,调用semaphore.WaitAsync()来获取信号量,如果没有其他线程正在访问该方法,则会立即获取到信号量。如果有其他线程正在访问该方法,则会被阻塞,直到有一个线程释放了信号量。

在方法结束时,使用semaphore.Release()释放信号量,以便其他线程可以获取到信号量并访问该方法。

这样就可以确保在同一时间只有一个线程可以访问PutTodoItem方法,从而解决并发冲突的问题。

使用分布式锁

如果应用程序部署在多台服务器上,并且需要对共享资源进行并发控制,可以使用分布式锁来解决并发冲突。可以使用第三方库如RedLock.NET或DistributedLock等来实现分布式锁。

[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
    // 使用Redis分布式锁
    using (var distributedLock = await _distributedLockProvider.AcquireLockAsync($"todoitem:{id}"))
    {
        if (!distributedLock.IsAcquired)
        {
            // 无法获取锁,返回冲突错误
            return Conflict();
        }

        if (id != todoItem.Id) return BadRequest();
        _context.Entry(todoItem).State = EntityState.Modified;
        await _context.SaveChangesAsync();
        return NoContent();
    }
}

使用了一个名为_distributedLockProvider的分布式锁提供程序来获取一个名为 todoitem:{id} 的锁。如果无法获取锁,则返回冲突错误。如果成功获取锁,则继续执行更新操作,并在完成后释放锁。

注意,_distributedLockProvider和AcquireLockAsync是示例中的占位符。需要根据使用的分布式锁库和实际情况进行适当的更改。


注意

悲观并发控制的使用比较简单,仅对要进行并发控制的资源加上锁即可,但是这种锁是独占排它的,如果系统并发量很大,锁会严重影响性能,如果使用不当,甚至会导致死锁。因此,对于高并发系统,要尽量避免使用悲观策略,改为NoSQL(Redis)。如果必须使用数据库控制并发,尽量采用乐观并发控制,因为乐观并发控制不会阻塞其他线程的访问,并且可以更好地处理并发冲突。

   
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

1楼
2楼
温馨提示

关于 注册码 问题

      由于近期经常大量注册机器人注册发送大量广告,本站开启免费入群领取注册码注册网站账号,注册码在群公告上贴着...

关于 注册码 问题

      由于近期经常大量注册机器人注册发送大量广告,本站开启免费入群领取注册码注册网站账号,注册码在群公告上贴着...

Archiver|手机版|小黑屋|DLSite

GMT+8, 2024-11-22 16:32

Powered by Discuz! X3.5 and PHP8

快速回复 返回顶部 返回列表