更新(輸出說明)
我有多個正在運行的任務,它們都試圖完成一項作業,但是我只希望一項任務正在執行或等待執行該作業。
在一種情況下,如果作業正在進行中,其他嘗試執行相同作業的任務將看到它已經在進行中并跳過嘗試執行此操作并立即繼續,因為他們不需要知道是否或何時執行實際上完成。
在另一種情況下,如果作業正在進行中,則其他任務應等待作業完成后再繼續,但不要嘗試自己完成作業,因為它剛剛由他們正在等待的任務完成。一旦作業完成,所有等待的任務可能會繼續,但它們都不應該嘗試完成剛剛完成的作業。
原始問題
我的目標是創建一個async Task擴展方法,該方法將生成類似于此的結果:(不完全是因為SKIP并且WAIT-TO-SKIP不會每次都以完全相同的順序)
使用一個執行緒運行時:
[Task 1] LOCKED
[Task 1] WORK-START
[Task 1] WORK-END
[Task 1] UNLOCKED
使用五個執行緒運行時:
[Task 1] LOCKED
[Task 1] WORK-START
[Task 2] WAIT-TO-SKIP
[Task 3] WAIT-TO-SKIP
[Task 4] WAIT-TO-SKIP
[Task 5] WAIT-TO-SKIP
[Task 1] WORK-END
[Task 1] UNLOCKED
[Task 2] SKIP
[Task 3] SKIP
[Task 4] SKIP
[Task 5] SKIP
它還應該有一個引數,允許它的行為如下:
[Task 1] LOCKED
[Task 1] WORK-START
[Task 2] SKIP
[Task 3] SKIP
[Task 4] SKIP
[Task 5] SKIP
[Task 1] WORK-END
[Task 1] UNLOCKED
這是我想出的方法:
public static async Task FirstThreadAsync(IFirstThread obj, Func<Task> action, TaskCompletionSource? waitTaskSource, string threadName = "")
{
if (obj.Locked)
{
if (waitTaskSource != null && !waitTaskSource.Task.IsCompleted)
{
Log.Debug(Logger, $"[{threadName}] WAIT-TO-SKIP");
await waitTaskSource.Task;
}
Log.Debug(Logger, $"[{threadName}] SKIP-1");
return;
}
var lockWasTaken = false;
var temp = obj;
try
{
if (waitTaskSource == null || waitTaskSource.Task.IsCompleted == false)
{
Monitor.TryEnter(temp, ref lockWasTaken);
if (lockWasTaken) obj.Locked = true;
}
}
finally
{
if (lockWasTaken) Monitor.Exit(temp);
}
if (waitTaskSource?.Task.IsCompleted == true)
{
Log.Debug(Logger, $"[{threadName}] SKIP-3");
return;
}
if (waitTaskSource != null && !lockWasTaken)
{
if (!waitTaskSource.Task.IsCompleted)
{
Log.Debug(Logger, $"[{threadName}] WAIT-TO-SKIP (LOCKED)");
await waitTaskSource.Task;
}
Log.Debug(Logger, $"[{threadName}] SKIP-2");
return;
}
Log.Debug(Logger, $"[{threadName}] LOCKED");
try
{
Log.Debug(Logger, $"[{threadName}] WORK-START");
await action.Invoke().ConfigureAwait(false);
Log.Debug(Logger, $"[{threadName}] WORK-END");
}
catch (Exception ex)
{
waitTaskSource?.TrySetException(ex);
throw;
}
finally
{
obj.Locked = false;
Log.Debug(Logger, $"[{threadName}] UNLOCKED");
waitTaskSource?.TrySetResult();
}
}
這是介面和Example類:
public interface IFirstThread
{
bool Locked { get; set; }
}
public class Example : IFirstThread
{
public bool Locked { get; set; }
public async Task DoWorkAsync(string taskName)
{
for (var i = 0; i < 10; i )
{
await Task.Delay(5);
}
}
}
這是一個單元測驗,所有測驗Task1 - Task5都嘗試同時運行并Task End在全部完成后運行:
[TestMethod]
public async Task DoWorkOnce_AsyncX()
{
var waitTaskSource = new TaskCompletionSource();
var example = new Methods.Example();
var tasks = new List<Task>
{
FirstThreadAsync(example, example.DoWorkAsync, waitTaskSource, "Task 1"),
FirstThreadAsync(example, example.DoWorkAsync, waitTaskSource, "Task 2"),
FirstThreadAsync(example, example.DoWorkAsync, waitTaskSource, "Task 3"),
FirstThreadAsync(example, example.DoWorkAsync, waitTaskSource, "Task 4"),
FirstThreadAsync(example, example.DoWorkAsync, waitTaskSource, "Task 5"),
};
await Task.WhenAll(tasks);
await FirstThreadAsync(example, example.DoWorkAsync, waitTaskSource, "Task End"),
//code to get and compare the output
}
我遇到的問題是 99% 的時間它按預期作業,但有時它允許兩個執行緒同時運行,我似乎無法弄清楚為什么或如何停止它。這是允許兩個執行緒同時運行的罕見情況的單元測驗輸出示例:
[Task 5] LOCKED,
[Task 4] WAIT-TO-SKIP,
[Task 2] LOCKED,
[Task 3] WAIT-TO-SKIP,
[Task 1] WAIT-TO-SKIP,
[Task 5] WORK-START,
[Task 2] WORK-START,
[Task 2] WORK-END,
[Task 5] WORK-END,
[Task 5] UNLOCKED,
[Task 2] UNLOCKED,
[Task 1] SKIP-1,
[Task 4] SKIP-1,
[Task 3] SKIP-1,
[Task End] LOCKED,
[Task End] WORK-START,
[Task End] WORK-END,
[Task End] UNLOCKED
如您所見Task 5,Task 2當只有其中一個應該被鎖定時,兩者都會被鎖定。 Task End只是測驗的一部分,用于驗證同時呼叫完成時類是否處于解鎖狀態。此外,該threadName引數完全不是必需的,僅包含在內,因此我可以判斷哪個任務是哪個用于測驗目的。
uj5u.com熱心網友回復:
你用Monitor.Exit得太早了。當在一個執行緒中
Monitor.TryEnter(temp, ref lockWasTaken);
之后執行
if (lockWasTaken) Monitor.Exit(temp);
在另一個執行緒中執行,兩者都可以為lockWasTaken真。
您不需要Locked用作受保護資源的物件的屬性。該類Monitor直接在原子物質中在物件中設定內部標記。您可以通過僅依賴Monitor功能而無需擁有自己的單獨標志來大大簡化邏輯。
此外,正如另一位用戶所指出的,我們沒有看到您為任務創建明確的單獨執行緒。這些任務可以在同一個執行緒上運行,然后Monitor.TryEnter當它們在同一個執行緒上運行時,將允許多個任務同時進入。
請參閱https://softwareengineering.stackexchange.com/questions/340414/when-is-it-safe-to-use-monitor-lock-with-task了解為什么應該使用SempahoreSlim而不是Monitor異步編程。
這是一些使用SemaphoreSlim. 我沒有看到更深層的目的;)
private async void button1_Click(object sender, EventArgs e)
{
TaskCompletionSource? waitTaskSource = null; // or new TaskCompletionSource();
var example = new Example();
var sempahore = new SemaphoreSlim(1);
var tasks = new List<Task>
{
FirstThreadAsync(sempahore, example.DoWorkAsync, waitTaskSource, "Task 1"),
FirstThreadAsync(sempahore, example.DoWorkAsync, waitTaskSource, "Task 2"),
FirstThreadAsync(sempahore, example.DoWorkAsync, waitTaskSource, "Task 3"),
FirstThreadAsync(sempahore, example.DoWorkAsync, waitTaskSource, "Task 4"),
FirstThreadAsync(sempahore, example.DoWorkAsync, waitTaskSource, "Task 5"),
};
await Task.WhenAll(tasks);
await FirstThreadAsync(sempahore, example.DoWorkAsync, waitTaskSource, "Task End");
}
public static async Task FirstThreadAsync(SemaphoreSlim semaphore, Func<Task> action, TaskCompletionSource? waitTaskSource, string threadName = "")
{
// Try to acquire lock
bool lockAcquired = await semaphore.WaitAsync(TimeSpan.Zero);
if (lockAcquired)
{
// Lock acquired -> Do Work
Debug.WriteLine($"[{threadName}] LOCKED");
try
{
Debug.WriteLine($"[{threadName}] WORK-START");
await action.Invoke();
Debug.WriteLine($"[{threadName}] WORK-END");
semaphore.Release();
Debug.WriteLine($"[{threadName}] UNLOCKED");
}
catch (Exception ex)
{
waitTaskSource?.TrySetException(ex);
}
finally
{
waitTaskSource?.TrySetResult();
}
}
else // No lock acquired
{
// When source is specified, await it.
if(waitTaskSource != null)
{
Debug.WriteLine($"[{threadName}] WAIT-TO-SKIP");
await waitTaskSource.Task;
}
Debug.WriteLine($"[{threadName}] SKIP");
}
}
public class Example
{
public async Task DoWorkAsync()
{
for (var i = 0; i < 10; i )
{
await Task.Delay(5);
}
}
}
我剛剛看到您的要求,即作業包只應完成一次。為此,您可以跳過釋放信號量。
進一步討論我們需要了解更多的實際需求細節。您當前的要求似乎有點奇怪。為什么有多個任務但只有一個作業包?
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/529411.html
下一篇:執行緒不并發執行
