C#.NET应用操作Sqlite互斥同步锁解决并发问题 – 三郎君的日常

C# / 面试 · 2024年4月25日 0

C#.NET应用操作Sqlite互斥同步锁解决并发问题

编程中,同步是一种机制,用于控制多个线程或任务之间的顺序执行和数据访问,以确保数据的一致性和安全性。当多个线程访问共享资源时,如果没有适当的同步机制,可能会导致数据不一致或竞态条件的问题。

同步的基本目的是确保在并发执行的多个操作中,每个操作按照预期的顺序和正确的方式执行,从而避免潜在的错误和不确定性。

同步的几种常见方式:

  1. 互斥锁(Mutex):确保在任何给定时刻只有一个线程可以访问某个资源或临界区。
  2. 信号量(Semaphore):允许多个线程访问临界区,但有一个限制数量的许可证。例如,一个信号量可以设置为允许最多三个线程同时访问。
  3. 事件(Event):用于线程间的通信和同步,一个线程可以通知另一个或多个线程某个事件已发生。
  4. 互斥量(Mutex):与互斥锁类似,但更适用于跨进程同步。
  5. 条件变量(Condition Variable):在某些同步场景中,线程需要等待某个条件满足后才能继续执行,条件变量提供了这种等待机制。
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Data.Sqlite;

public class SqliteDatabase
{
    private readonly string _connectionString;
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

    public SqliteDatabase(string connectionString)
    {
        _connectionString = connectionString;
    }

    public async Task ExecuteNonQueryAsync(string query)
    {
        await _semaphore.WaitAsync();
        try
        {
            using (var connection = new SqliteConnection(_connectionString))
            {
                await connection.OpenAsync();
                var command = connection.CreateCommand();
                command.CommandText = query;
                await command.ExecuteNonQueryAsync();
            }
        }
        finally
        {
            _semaphore.Release();
        }
    }

    public async Task<object> ExecuteScalarAsync(string query)
    {
        await _semaphore.WaitAsync();
        try
        {
            using (var connection = new SqliteConnection(_connectionString))
            {
                await connection.OpenAsync();
                var command = connection.CreateCommand();
                command.CommandText = query;
                return await command.ExecuteScalarAsync();
            }
        }
        finally
        {
            _semaphore.Release();
        }
    }

    public async Task<SqliteDataReader> ExecuteReaderAsync(string query)
    {
        await _semaphore.WaitAsync();
        try
        {
            var connection = new SqliteConnection(_connectionString);
            await connection.OpenAsync();
            var command = connection.CreateCommand();
            command.CommandText = query;
            return await command.ExecuteReaderAsync(CommandBehavior.CloseConnection);
        }
        finally
        {
            _semaphore.Release();
        }
    }
}

SemaphoreSlim构造函数有两个重载:

  1. SemaphoreSlim(int initialCount, int maxCount)
  2. SemaphoreSlim(int initialCount)

参数解释:

  • initialCount: 初始信号量计数。这决定了在没有任何线程等待信号量时可以获取信号量的线程数量。
  • maxCount: 信号量的最大计数。这是信号量可以增加到的最大值。默认为Int32.MaxValue

区别:

  1. new SemaphoreSlim(1, 1):
    • initialCount为1,表示初始时信号量为1。
    • maxCount为1,表示信号量的最大值也是1。
    这种设置创建了一个二进制信号量,也就是只允许一个线程同时访问受信号量保护的资源。
  2. new SemaphoreSlim(1):
    • initialCount为1,表示初始时信号量为1。
    • 由于未提供maxCount参数,所以使用默认值Int32.MaxValue,表示信号量的最大值是Int32.MaxValue
    这种设置也是创建一个二进制信号量,只允许一个线程同时访问受信号量保护的资源,但最大值是Int32.MaxValue,实际上没有限制。

总结:

  • 如果你想明确地限制信号量的最大值,使用new SemaphoreSlim(1, 1)
  • 如果你只关心信号量是否被锁定,而不关心最大值,可以使用new SemaphoreSlim(1)

在实际应用中,如果你只需要二进制信号量(只允许一个线程访问资源),那么这两种方式都是等效的。但是,new SemaphoreSlim(1, 1)提供了更明确的语义,可以帮助读者更好地理解你的意图。

这意味着:


new SemaphoreSlim(2, 1) 创建了一个信号量,其初始计数为2,最大计数为1。

  • 初始时,有两个许可证可供使用。
  • 最多可以有一个许可证。

这种设置似乎有些不一致。通常,最大计数应该大于或等于 初始计数。在这种情况下,最大计数小于初始计数,这样的设置在实际应用中可能会导致问题。

正确的方式应该是设置最大计数大于或等于初始计数,以便更好地控制并发访问。如果需要两个并发访问,可以设置为new SemaphoreSlim(2, 2)

new SemaphoreSlim(1, 2) 创建了一个信号量,其初始计数为1,最大计数为2。

这意味着:

  • 初始时,有一个许可证可供使用。
  • 最多可以有两个许可证。

这种设置允许多达两个线程同时访问受信号量保护的资源。当有两个线程获取许可证时,其他线程将被阻塞,直到有一个或两个许可证被释放为止。

这种信号量设置适用于那些需要允许一定数量的并发访问,但不是无限制的并发访问的场景

互斥锁(Mutex)

using System;
using System.Threading;

class Program
{
    // 创建一个互斥锁
    private static Mutex mutex = new Mutex();

    static void Main(string[] args)
    {
        // 创建两个线程来模拟并发访问
        Thread thread1 = new Thread(DoWork);
        Thread thread2 = new Thread(DoWork);

        // 启动线程
        thread1.Start("Thread 1");
        thread2.Start("Thread 2");

        // 等待两个线程完成
        thread1.Join();
        thread2.Join();

        Console.WriteLine("Main thread exiting...");
    }

    static void DoWork(object threadName)
    {
        Console.WriteLine($"{threadName} is waiting for the mutex.");

        // 等待获取互斥锁
        mutex.WaitOne();

        Console.WriteLine($"{threadName} has acquired the mutex.");

        // 模拟工作
        Thread.Sleep(2000);

        Console.WriteLine($"{threadName} is releasing the mutex.");
        
        // 释放互斥锁
        mutex.ReleaseMutex();
    }
}

在上面的例子中:

  • 我们创建了一个名为mutexMutex实例。
  • 我们启动了两个线程(thread1thread2)来模拟并发访问。
  • 每个线程在尝试获取互斥锁之前都会等待。
  • 一旦获得互斥锁,线程会进行一些模拟的工作。
  • 完成工作后,线程会释放互斥锁。

这样,我们就确保了在任何给定时刻,只有一个线程可以访问受互斥锁保护的资源。