【发布时间】:2017-04-04 21:07:40
【问题描述】:
为了进行异步文件 IO,我创建了一个类,让我可以根据字符串键进行锁定,以防止同时对同一个文件进行多次写入或防止同时进行写入和读取。然而,我面临的问题是可以通过每次发送不同的密钥来增加 _lockDict。未清理旧的未使用锁,但我不知道如何以线程安全的方式执行此操作。这可能会导致非常大的内存消耗。
根据密钥返回锁实例的类:
public class AsyncNamedReaderWriterLocker
{
private readonly object _mutex = new object();
private readonly Dictionary<string, AsyncReaderWriterLock> _lockDict = new Dictionary<string, AsyncReaderWriterLock>();
public Task<IDisposable> EnterReaderLockAsync(string name)
{
var locker = GetLock(name);
return locker.EnterReaderLockAsync();
}
public Task<IDisposable> EnterWriterLockAsync(string name)
{
var locker = GetLock(name);
return locker.EnterWriterLockAsync();
}
private AsyncReaderWriterLock GetLock(string name)
{
lock (_mutex)
{
if (!_lockDict.TryGetValue(name, out AsyncReaderWriterLock locker))
{
locker = new AsyncReaderWriterLock();
_lockDict.Add(name, locker);
}
return locker;
}
}
public class AsyncReaderWriterLock
{
private readonly Queue<TaskCompletionSource<IDisposable>> _writerQueue = new Queue<TaskCompletionSource<IDisposable>>();
private readonly Queue<TaskCompletionSource<IDisposable>> _readerQueue = new Queue<TaskCompletionSource<IDisposable>>();
private readonly WriterLocker _writerLocker;
private readonly ReaderLocker _readerLocker;
private readonly object _mutex = new object();
private int _locksHeld;
public AsyncReaderWriterLock()
{
_writerLocker = new WriterLocker(this);
_readerLocker = new ReaderLocker(this);
}
public Task<IDisposable> EnterReaderLockAsync()
{
lock (_mutex)
{
if (_locksHeld >= 0 && _writerQueue.Count == 0)
{
_locksHeld++;
return Task.FromResult<IDisposable>(_readerLocker);
}
var tcs = new TaskCompletionSource<IDisposable>();
_readerQueue.Enqueue(tcs);
return tcs.Task;
}
}
public Task<IDisposable> EnterWriterLockAsync()
{
lock (_mutex)
{
if (_locksHeld == 0)
{
_locksHeld = -1;
return Task.FromResult<IDisposable>(_writerLocker);
}
var tcs = new TaskCompletionSource<IDisposable>();
_writerQueue.Enqueue(tcs);
return tcs.Task;
}
}
private void ReleaseLocks()
{
if (_locksHeld != 0)
return;
// Give priority to writers.
if (_writerQueue.Count != 0)
{
_locksHeld = -1;
var tcs = _writerQueue.Dequeue();
tcs.TrySetResult(_writerLocker);
return;
}
// Then to readers.
while (_readerQueue.Count != 0)
{
var tcs = _readerQueue.Dequeue();
tcs.TrySetResult(_readerLocker);
++_locksHeld;
}
}
private void ReleaseReaderLock()
{
lock (_mutex)
{
_locksHeld--;
ReleaseLocks();
}
}
private void ReleaseWriterLock()
{
lock (_mutex)
{
_locksHeld = 0;
ReleaseLocks();
}
}
private class ReaderLocker : IDisposable
{
private readonly AsyncReaderWriterLock _asyncReaderWriterLock;
internal ReaderLocker(AsyncReaderWriterLock asyncReaderWriterLock)
{
_asyncReaderWriterLock = asyncReaderWriterLock;
}
public void Dispose()
{
_asyncReaderWriterLock.ReleaseReaderLock();
}
}
private class WriterLocker : IDisposable
{
private readonly AsyncReaderWriterLock _asyncReaderWriterLock;
internal WriterLocker(AsyncReaderWriterLock asyncReaderWriterLock)
{
_asyncReaderWriterLock = asyncReaderWriterLock;
}
public void Dispose()
{
_asyncReaderWriterLock.ReleaseWriterLock();
}
}
}
使用 AsyncNamedReaderWriterLocker 类的示例:
public class AsyncFileIO
{
private static readonly AsyncNamedReaderWriterLocker AsyncNamedReaderWriterLocker = new AsyncNamedReaderWriterLocker();
public async Task CreateFile(string filename, byte[] data)
{
using (await AsyncNamedReaderWriterLocker.EnterWriterLockAsync(filename))
{
var directory = Path.GetDirectoryName(filename);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
using (var stream = File.Create(filename))
{
await stream.WriteAsync(data, 0, data.Length);
}
}
}
public async Task<byte[]> ReadFile(string filename)
{
using (await AsyncNamedReaderWriterLocker.EnterReaderLockAsync(filename))
{
if (!File.Exists(filename)) return null;
using (var stream = File.OpenRead(filename))
{
var data = new byte[stream.Length];
await stream.ReadAsync(data, 0, 0);
return data;
}
}
}
public async Task DeleteFile(string filename)
{
using (await AsyncNamedReaderWriterLocker.EnterWriterLockAsync(filename))
{
var directoryName = Path.GetDirectoryName(filename);
if (!Directory.Exists(directoryName)) return;
File.Delete(filename);
}
}
}
请注意,我最感兴趣的是出于学习目的,并且完全意识到这对于大多数应用程序来说都是多余的。
【问题讨论】:
-
您需要添加一种方法来将锁标记为未使用。这可能会在任何客户端分发文件名时完成。一旦确定不再需要文件名,它就可以告诉您的
AsyncNamedReaderWriterLocker转储锁。话虽如此,同一个参与者可以很容易地协调谁读取和写入什么文件,这样您就不需要并发文件访问。要获得更具体的信息,准确了解您要完成的工作会有所帮助,以及为什么需要并发文件访问而不是所述的异步访问? -
异步处理也带来了并发性。您可以尝试同时读取和写入同一个文件,从而导致并发。这就是为什么需要锁来防止同时写入和读取同一个文件的原因。此代码将在服务器中用于提供文件和一些额外的逻辑,例如生成图像的缩略图。我正在寻找的是一旦不再使用锁就会被清理(以线程方式)。
-
你将不得不跟踪有多少线程正在持有或等待锁(你的锁似乎已经有这个信息),然后当一个锁被释放时,如果没有其他线程等待,从字典中删除锁。当然,这将需要在释放步骤中进行额外的同步,以确保在释放锁的过程中没有线程从字典中检索锁。我怀疑这是否比只对所有资源使用一个锁更好(特别是考虑到所有 I/O 操作都可能在磁盘上出现瓶颈)。
标签: c# multithreading asynchronous thread-safety locking