如何使StreamDocumentRepresentation类的异步方法实现线程安全,确保文件仅上传一次
Great question! The core problem with your current code is that multiple concurrent calls to GetDocumentUriAsync() could all pass the initial string.IsNullOrWhiteSpace(_fileName) check before the first thread finishes uploading the document. This leads to duplicate uploads—definitely not what you want.
Let's cover two robust, async-friendly solutions to ensure the document is uploaded exactly once, no matter how many threads call the method.
Solution 1: Async Mutex with SemaphoreSlim (Double-Checked Locking)
Since we're dealing with async code, we can't use a standard lock statement (it blocks threads and doesn't play nice with await). Instead, SemaphoreSlim is perfect here—it supports asynchronous waiting and acts as a lightweight mutex.
Here's how to modify your class:
public class StreamDocumentRepresentation: DocumentRepresentation { private readonly IFileStorageService _fileStorageService; private readonly MemoryStream _documentStream; private string _fileName; // Semaphore with initial count 1 to enforce mutual exclusion private readonly SemaphoreSlim _uploadLock = new SemaphoreSlim(1, 1); public StreamDocumentRepresentation(MemoryStream documentStream, IFileStorageService fileStorageService) { _documentStream = documentStream; _fileStorageService = fileStorageService; } public override async Task<string> GetDocumentUriAsync() { // First quick check to avoid locking if we already have the filename if (string.IsNullOrWhiteSpace(_fileName)) { await _uploadLock.WaitAsync(); try { // Double-check after acquiring the lock—another thread might have finished uploading while we waited if (string.IsNullOrWhiteSpace(_fileName)) { await UploadDocument(); } } finally { // Always release the lock, even if an exception occurs _uploadLock.Release(); } } return _fileName; } private async Task UploadDocument() { _fileName = $"{Guid.NewGuid()}.pdf"; // Optional: Reset stream position to start before uploading (in case the stream was read previously) _documentStream.Position = 0; await _fileStorageService.UploadFile(_fileName, _documentStream); } }
Key Notes:
- Double-Checked Locking: The first check skips locking if we already have the filename. The second check inside the lock ensures we don't re-upload if another thread completed the work while we waited for the lock.
- SemaphoreSlim Cleanup: Always release the lock in a
finallyblock to prevent deadlocks if an exception is thrown during upload. - Stream Position: Added
_documentStream.Position = 0as a safety measure—if the stream was read elsewhere before upload, resetting the position ensures the full content is uploaded.
Solution 2: AsyncLazy (Cleaner, More Maintainable)
If you prefer a more declarative approach, using an AsyncLazy wrapper (either from a NuGet package or a custom implementation) handles the thread safety automatically. This wraps the upload logic in a lazy-initialized task that only runs once.
Step 1: Implement a Custom AsyncLazy (if you don't want to use a package)
public class AsyncLazy<T> { private readonly Lazy<Task<T>> _lazyTask; public AsyncLazy(Func<Task<T>> taskFactory) { _lazyTask = new Lazy<Task<T>>(taskFactory); } public Task<T> GetValueAsync() => _lazyTask.Value; }
Step 2: Modify StreamDocumentRepresentation
public class StreamDocumentRepresentation: DocumentRepresentation { private readonly IFileStorageService _fileStorageService; private readonly MemoryStream _documentStream; // Lazy-initialized task that handles upload exactly once private readonly AsyncLazy<string> _lazyFileName; public StreamDocumentRepresentation(MemoryStream documentStream, IFileStorageService fileStorageService) { _documentStream = documentStream; _fileStorageService = fileStorageService; // Initialize the lazy task with our upload logic _lazyFileName = new AsyncLazy<string>(async () => { var fileName = $"{Guid.NewGuid()}.pdf"; _documentStream.Position = 0; await _fileStorageService.UploadFile(fileName, _documentStream); return fileName; }); } public override Task<string> GetDocumentUriAsync() { // Simply return the lazy task—no manual locking needed! return _lazyFileName.GetValueAsync(); } }
Key Notes:
- Automatic Thread Safety: The
Lazy<Task<T>>under the hood ensures the upload factory method runs exactly once, even with concurrent calls. - Cleaner Code: Eliminates manual lock handling, making the class easier to read and maintain.
- NuGet Alternative: If you don't want to implement
AsyncLazyyourself, use theMicrosoft.VisualStudio.Threadingpackage which includes a robustAsyncLazy<T>implementation.
Which Should You Choose?
- Use SemaphoreSlim if you need fine-grained control over the locking logic or can't add external dependencies.
- Use AsyncLazy for a cleaner, more maintainable solution—especially if you're already using packages like
Microsoft.VisualStudio.Threading.
Both approaches guarantee that your document is uploaded exactly once, no matter how many threads call GetDocumentUriAsync().
内容的提问来源于stack exchange,提问作者Denys Doskach

