Ich habe viel über die neue Task-Funktionalität in .net 4.0 gelesen, aber ich habe keine Lösung für das folgende Problem gefunden:
Ich schreibe eine Serveranwendung, die Anfragen von vielen Benutzern verarbeitet, und ich möchte Tasks verwenden, um diese Anfragen auf mehrere Kerne zu verteilen. Diese Tasks sollten jedoch mit Objekten - zunächst mit Benutzern - synchronisiert werden, so dass für jedes Objekt jeweils nur eine Aufgabe bearbeitet wird. Dies wäre mit Task.ContinueWith() einfach zu erreichen, aber es sollte auch möglich sein, eine Aufgabe auf mehrere Objekte zu synchronisieren (z. B. wenn ein Benutzer einem anderen Benutzer Geld überweist, sollte eine Variable bei Benutzer A dekrementiert und bei Benutzer B inkrementiert werden, ohne dass andere Aufgaben dazwischenfunken).
Mein erster Versuch ist also eine Klasse, die Delegierte empfängt, Aufgaben erstellt und sie in einem Wörterbuch mit den zu synchronisierenden Objekten als Schlüssel speichert. Wenn eine neue Aufgabe geplant wird, kann sie mit Task.ContinueWith() an die letzte Aufgabe des gegebenen Objekts angehängt werden. Wenn sie mit mehreren Objekten synchronisiert werden soll, wird die neue Aufgabe mit TaskFactory.ContinueWhenAll() erstellt. Die erstellte Aufgabe wird für jedes Objekt, auf das sie synchronisiert wird, im Wörterbuch gespeichert. Hier ist mein erster Entwurf:
public class ActionScheduler:IActionScheduler
{
private readonly IDictionary<object, Task> mSchedulingDictionary = new Dictionary<object, Task>();
private readonly TaskFactory mTaskFactory = new TaskFactory();
/// <summary>
/// Schedules actions synchonized on one or more objects. Only one action will be processed for each object at any time.
/// </summary>
/// <param name="synchronisationObjects">Array of objects the current action is synchronized on</param>
/// <param name="action">The action that will be scheduled and processed</param>
public void ScheduleTask(object[] synchronisationObjects, Action action)
{
// lock the dictionary in case two actions are scheduled on the same object at the same time
// this is necessary since reading and writing to a dictionary can not be done in an atomic manner
lock(mSchedulingDictionary)
{
// get all current tasks for the given synchronisation objects
var oldTaskList = new List<Task>();
foreach (var syncObject in synchronisationObjects)
{
Task task;
mSchedulingDictionary.TryGetValue(syncObject, out task);
if (task != null)
oldTaskList.Add(task);
}
// create a new task for the given action
Task newTask;
if (oldTaskList.Count > 1)
{
// task depends on multiple previous tasks
newTask = mTaskFactory.ContinueWhenAll(oldTaskList.ToArray(), t => action());
}
else
{
if (oldTaskList.Count == 1)
{
// task depends on exactly one previous task
newTask = oldTaskList[0].ContinueWith(t => action());
}
else
{
// task does not depend on any previous task and can be started immediately
newTask = new Task(action);
newTask.Start();
}
}
// store the task in the dictionary
foreach (var syncObject in synchronisationObjects)
{
mSchedulingDictionary[syncObject] = newTask;
}
}
}
}
Dies funktioniert auch, wenn eine Aufgabe "multiSyncTask" für mehrere Objekte erstellt wurde und anschließend Aufgaben für jedes der Objekte geplant werden. Da sie alle mit multiSyncTask.ContinueWith() erstellt werden, starten sie synchron:
static void Main()
{
IActionScheduler actionScheduler = new ActionScheduler();
var syncObj1 = new object();
var syncObj2 = new object();
// these two start and complete simultaneously:
actionScheduler.ScheduleTask(new[] { syncObj1 }, () => PrintTextAfterWait("1"));
actionScheduler.ScheduleTask(new[] { syncObj2 }, () => PrintTextAfterWait("2"));
// this task starts after the first two and "locks" both objects:
actionScheduler.ScheduleTask(new[] { syncObj1, syncObj2 }, () => PrintTextAfterWait("1 and 2"));
// these two - again - start and complete simultaneously after the task above:
actionScheduler.ScheduleTask(new[] { syncObj1 }, () => PrintTextAfterWait("1"));
actionScheduler.ScheduleTask(new[] { syncObj2 }, () => PrintTextAfterWait("2"));
}
static void PrintTextAfterWait(string text)
{
Thread.Sleep(3000);
Console.WriteLine(text);
}
Was denken Sie - ist das eine gute Lösung für mein Problem? Ich bin etwas skeptisch, was die große Sperre für das Wörterbuch angeht, aber sie ist notwendig, wenn zwei Aufgaben gleichzeitig auf einem Objekt geplant werden, um Race Conditions zu verhindern. Natürlich ist das Wörterbuch nur für die Zeit gesperrt, die es braucht, um eine Aufgabe zu erstellen, nicht wenn sie verarbeitet wird.
Außerdem würde ich gerne wissen, ob es bereits existierende Lösungen oder Programmierparadigmen gibt, die mein Problem besser lösen, indem sie .net 4.0 Tasks verwenden, die ich nicht aufgespürt habe.
Vielen Dank und mit freundlichen Grüßen, Johannes