私は非同期プログラミングが長年にわたって多くの変化を見てきたことを知っています。わずか34歳でこのさびた髪を手に入れることができたのは少し恥ずかしいですが、StackOverflowを使ってスピードアップを期待しています。
私がやろうとしているのは、「作業」のキューを別のスレッドで管理することですが、一度に処理されるのは1つのアイテムだけです。このスレッドで作業を投稿したいのですが、呼び出し元に何も返す必要はありません。もちろん、新しいThread
オブジェクトを単にスピンアップし、スリープ、割り込み、待機ハンドルなどを使用して共有Queue
オブジェクトをループさせることができます。 。 BlockingCollection
、Task
、async
/await
があり、その多くを抽象化するNuGetパッケージは言うまでもありません。
「何が最高か」という質問は一般的に眉をひそめていることを知っているので、組み込みの.NETメカニズムを使用してこのようなことを達成するために、「現在推奨されているものは何ですか」と言い替えます。しかし、サードパーティのNuGetパッケージが物事を単純化するのであれば、それも同じです。
最大同時実行数が1に固定されたTaskScheduler
インスタンスを検討しましたが、おそらく今までにそれを行うためのはるかに不格好な方法があるようです。
背景
具体的には、この場合にしようとしているのは、Webリクエスト中にIPジオロケーションタスクをキューに入れることです。同じIPがジオロケーションのキューに複数回入る可能性がありますが、タスクはそれを検出し、すでに解決されている場合は早期にスキップする方法を知っています。ただし、リクエストハンドラはこれらの() => LocateAddress(context.Request.UserHostAddress)
はキューを呼び出し、LocateAddress
メソッドに重複作業の検出を処理させます。私が使用しているジオロケーションAPIは、リクエストで攻撃されたくないため、一度に1つの同時タスクに制限したいのです。ただし、簡単なパラメーター変更でより多くの並行タスクに簡単にスケールできるアプローチが許可されていれば、素晴らしいことです。
非同期の単一並列作業キューを作成するには、SemaphoreSlim
を作成し、1に初期化してから、要求された作業を開始する前にそのセマフォの取得時にエンキューメソッドawait
を使用します。 。
public class TaskQueue
{
private SemaphoreSlim semaphore;
public TaskQueue()
{
semaphore = new SemaphoreSlim(1);
}
public async Task<T> Enqueue<T>(Func<Task<T>> taskGenerator)
{
await semaphore.WaitAsync();
try
{
return await taskGenerator();
}
finally
{
semaphore.Release();
}
}
public async Task Enqueue(Func<Task> taskGenerator)
{
await semaphore.WaitAsync();
try
{
await taskGenerator();
}
finally
{
semaphore.Release();
}
}
}
もちろん、1以外の一定の並列度を持たせるには、セマフォを他の数に初期化します。
私が見るようにあなたの最良のオプションは TPL Dataflow
のActionBlock
:
var actionBlock = new ActionBlock<string>(address =>
{
if (!IsDuplicate(address))
{
LocateAddress(address);
}
});
actionBlock.Post(context.Request.UserHostAddress);
TPL Dataflow
は堅牢で、スレッドセーフで、async
に対応しており、アクターベースのフレームワークが非常に構成可能です(nugetとして利用可能)
以下は、より複雑なケースの簡単な例です。あなたがしたいと仮定しましょう:
LocateAddress
とキューの挿入の両方をasync
にします。var actionBlock = new ActionBlock<string>(async address =>
{
if (!IsDuplicate(address))
{
await LocateAddressAsync(address);
}
}, new ExecutionDataflowBlockOptions
{
BoundedCapacity = 10000,
MaxDegreeOfParallelism = Environment.ProcessorCount,
CancellationToken = new CancellationTokenSource(TimeSpan.FromHours(1)).Token
});
await actionBlock.SendAsync(context.Request.UserHostAddress);
実際には、1つのスレッドでタスクを実行する必要はなく、シリアルで(次々と)FIFOで実行する必要があります。 TPLにはそのためのクラスはありませんが、ここにテスト付きの非常に軽量でノンブロッキングの実装があります。 https://github.com/Gentlee/SerialQueue
また、@ Servyの実装もあります。テストでは、それが私のものより2倍遅く、FIFOを保証していません。
例:
private readonly SerialQueue queue = new SerialQueue();
async Task SomeAsyncMethod()
{
var result = await queue.Enqueue(DoSomething);
}
つかいます BlockingCollection<Action>
1つのコンシューマ(必要に応じて一度に1つだけ実行される)と1つ以上のプロデューサでプロデューサ/コンシューマパターンを作成します。
最初にどこかに共有キューを定義します:
BlockingCollection<Action> queue = new BlockingCollection<Action>();
コンシューマーThread
またはTask
から取得します。
//This will block until there's an item available
Action itemToRun = queue.Take()
次に、他のスレッド上の任意の数のプロデューサーから、単にキューに追加します。
queue.Add(() => LocateAddress(context.Request.UserHostAddress));
ここに別のソリューションを投稿しています。正直に言うと、これが良い解決策かどうかはわかりません。
私は、BlockingCollectionを使用してプロデューサー/コンシューマーパターンを実装し、専用のスレッドがそれらのアイテムを消費するのに慣れています。常に入ってくるデータがあり、コンシューマスレッドがそこに座って何もしない場合は問題ありません。
アプリケーションの1つが別のスレッドでメールを送信したいというシナリオに遭遇しましたが、メールの総数はそれほど多くありません。私の最初の解決策は、専用のコンシューマスレッド(Task.Run()によって作成された)を使用することでしたが、多くの時間、そこに座って何もしません。
古いソリューション:
private readonly BlockingCollection<EmailData> _Emails =
new BlockingCollection<EmailData>(new ConcurrentQueue<EmailData>());
// producer can add data here
public void Add(EmailData emailData)
{
_Emails.Add(emailData);
}
public void Run()
{
// create a consumer thread
Task.Run(() =>
{
foreach (var emailData in _Emails.GetConsumingEnumerable())
{
SendEmail(emailData);
}
});
}
// sending email implementation
private void SendEmail(EmailData emailData)
{
throw new NotImplementedException();
}
ご覧のとおり、送信する電子メールが十分にない場合(そして私の場合)、コンシューマスレッドはそれらのほとんどをそこに座って何もしません。
実装を次のように変更しました。
// create an empty task
private Task _SendEmailTask = Task.Run(() => {});
// caller will dispatch the email to here
// continuewith will use a thread pool thread (different to
// _SendEmailTask thread) to send this email
private void Add(EmailData emailData)
{
_SendEmailTask = _SendEmailTask.ContinueWith((t) =>
{
SendEmail(emailData);
});
}
// actual implementation
private void SendEmail(EmailData emailData)
{
throw new NotImplementedException();
}
これはもはやプロデューサー/コンシューマーパターンではありませんが、スレッドが存在せず、何も行いません。代わりに、メールを送信するたびに、スレッドプールスレッドを使用して実行します。