编程中,同步是一种机制,用于控制多个线程或任务之间的顺序执行和数据访问,以确保数据的一致性和安全性。当多个线程访问共享资源时,如果没有适当的同步机制,可能会导致数据不一致或竞态条件的问题。
同步的基本目的是确保在并发执行的多个操作中,每个操作按照预期的顺序和正确的方式执行,从而避免潜在的错误和不确定性。
同步的几种常见方式:
- 互斥锁(Mutex):确保在任何给定时刻只有一个线程可以访问某个资源或临界区。
- 信号量(Semaphore):允许多个线程访问临界区,但有一个限制数量的许可证。例如,一个信号量可以设置为允许最多三个线程同时访问。
- 事件(Event):用于线程间的通信和同步,一个线程可以通知另一个或多个线程某个事件已发生。
- 互斥量(Mutex):与互斥锁类似,但更适用于跨进程同步。
- 条件变量(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
构造函数有两个重载:
SemaphoreSlim(int initialCount, int maxCount)
SemaphoreSlim(int initialCount)
参数解释:
initialCount
: 初始信号量计数。这决定了在没有任何线程等待信号量时可以获取信号量的线程数量。maxCount
: 信号量的最大计数。这是信号量可以增加到的最大值。默认为Int32.MaxValue
。
区别:
new SemaphoreSlim(1, 1)
:initialCount
为1,表示初始时信号量为1。maxCount
为1,表示信号量的最大值也是1。
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();
}
}
在上面的例子中:
- 我们创建了一个名为
mutex
的Mutex
实例。 - 我们启动了两个线程(
thread1
和thread2
)来模拟并发访问。 - 每个线程在尝试获取互斥锁之前都会等待。
- 一旦获得互斥锁,线程会进行一些模拟的工作。
- 完成工作后,线程会释放互斥锁。
这样,我们就确保了在任何给定时刻,只有一个线程可以访问受互斥锁保护的资源。