Jadi, aplikasi saya perlu melakukan tindakan hampir terus menerus (dengan jeda 10 detik atau lebih di antara setiap proses) selama aplikasi berjalan atau diminta pembatalan. Pekerjaan yang perlu dilakukan memiliki kemungkinan memakan waktu hingga 30 detik.
Apakah lebih baik menggunakan System.Timers.Timer dan menggunakan AutoReset untuk memastikan tidak melakukan tindakan sebelum "centang" sebelumnya selesai.
Atau haruskah saya menggunakan Tugas umum dalam mode LongRunning dengan token pembatalan, dan memiliki loop sementara tak terbatas di dalamnya yang memanggil tindakan yang melakukan pekerjaan dengan Thread.Sleep 10 detik di antara panggilan? Adapun model async / await, saya tidak yakin itu akan sesuai di sini karena saya tidak memiliki nilai yang dikembalikan dari pekerjaan.
CancellationTokenSource wtoken;
Task task;
void StopWork()
{
wtoken.Cancel();
try
{
task.Wait();
} catch(AggregateException) { }
}
void StartWork()
{
wtoken = new CancellationTokenSource();
task = Task.Factory.StartNew(() =>
{
while (true)
{
wtoken.Token.ThrowIfCancellationRequested();
DoWork();
Thread.Sleep(10000);
}
}, wtoken, TaskCreationOptions.LongRunning);
}
void DoWork()
{
// Some work that takes up to 30 seconds but isn't returning anything.
}
atau cukup gunakan timer sederhana saat menggunakan properti AutoReset, dan panggil .Stop () untuk membatalkannya?
Jawaban:
Saya akan menggunakan TPL Dataflow untuk ini (karena Anda menggunakan .NET 4.5 dan digunakan secara
Task
internal). Anda dapat dengan mudah membuatActionBlock<TInput>
item yang memposting ke dirinya sendiri setelah diproses tindakannya dan menunggu waktu yang sesuai.Pertama, buat pabrik yang akan membuat tugas tanpa akhir Anda:
ITargetBlock<DateTimeOffset> CreateNeverEndingTask( Action<DateTimeOffset> action, CancellationToken cancellationToken) { // Validate parameters. if (action == null) throw new ArgumentNullException("action"); // Declare the block variable, it needs to be captured. ActionBlock<DateTimeOffset> block = null; // Create the block, it will call itself, so // you need to separate the declaration and // the assignment. // Async so you can wait easily when the // delay comes. block = new ActionBlock<DateTimeOffset>(async now => { // Perform the action. action(now); // Wait. await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken). // Doing this here because synchronization context more than // likely *doesn't* need to be captured for the continuation // here. As a matter of fact, that would be downright // dangerous. ConfigureAwait(false); // Post the action back to the block. block.Post(DateTimeOffset.Now); }, new ExecutionDataflowBlockOptions { CancellationToken = cancellationToken }); // Return the block. return block; }
Saya telah memilih
ActionBlock<TInput>
untuk mengambilDateTimeOffset
struktur ; Anda harus meneruskan parameter type, dan mungkin juga meneruskan beberapa status yang berguna (Anda dapat mengubah sifat status, jika Anda mau).Selain itu, perhatikan bahwa
ActionBlock<TInput>
secara default hanya memproses satu item dalam satu waktu, jadi Anda dijamin bahwa hanya satu tindakan yang akan diproses (artinya, Anda tidak perlu berurusan dengan reentrancy saat memanggil kembaliPost
metode ekstensi itu sendiri).Saya juga telah melewati
CancellationToken
struktur ke konstruktorActionBlock<TInput>
dan ke pemanggilanTask.Delay
metode ; jika proses dibatalkan, pembatalan akan dilakukan pada kesempatan pertama yang memungkinkan.Dari sana, pemfaktoran ulang kode Anda mudah dilakukan untuk menyimpan
ITargetBlock<DateTimeoffset>
antarmuka yang diterapkan olehActionBlock<TInput>
(ini adalah abstraksi tingkat yang lebih tinggi yang mewakili blok yang merupakan konsumen, dan Anda ingin dapat memicu konsumsi melalui panggilan kePost
metode ekstensi):StartWork
Metode Anda :void StartWork() { // Create the token source. wtoken = new CancellationTokenSource(); // Set the task. task = CreateNeverEndingTask(now => DoWork(), wtoken.Token); // Start the task. Post the time. task.Post(DateTimeOffset.Now); }
Dan kemudian
StopWork
metode Anda :void StopWork() { // CancellationTokenSource implements IDisposable. using (wtoken) { // Cancel. This will cancel the task. wtoken.Cancel(); } // Set everything to null, since the references // are on the class level and keeping them around // is holding onto invalid state. wtoken = null; task = null; }
Mengapa Anda ingin menggunakan TPL Dataflow di sini? Beberapa alasan:
Pemisahan kekhawatiran
The
CreateNeverEndingTask
Metode sekarang menjadi pabrik yang menciptakan "layanan" Anda sehingga untuk berbicara. Anda mengontrol kapan itu mulai dan berhenti, dan itu benar-benar mandiri. Anda tidak perlu menjalin kontrol status pengatur waktu dengan aspek lain dari kode Anda. Anda cukup membuat blok, memulainya, dan menghentikannya setelah selesai.Penggunaan utas / tugas / sumber daya yang lebih efisien
Penjadwal default untuk blok dalam aliran data TPL adalah sama untuk a
Task
, yang merupakan kumpulan utas. Dengan menggunakanActionBlock<TInput>
to memproses tindakan Anda, serta panggilan keTask.Delay
, Anda menghasilkan kendali atas utas yang Anda gunakan saat Anda tidak benar-benar melakukan apa pun. Memang, ini sebenarnya mengarah ke beberapa overhead saat Anda menelurkan yang baruTask
yang akan memproses kelanjutan, tetapi itu harus kecil, mengingat Anda tidak memproses ini dalam loop ketat (Anda menunggu sepuluh detik di antara pemanggilan).Jika
DoWork
fungsi benar-benar dapat dibuat menjadi menunggu (yaitu, dalam mengembalikan aTask
), maka Anda (mungkin) dapat mengoptimalkan ini lebih banyak lagi dengan mengubah metode pabrik di atas untuk mengambilFunc<DateTimeOffset, CancellationToken, Task>
alih - alihAction<DateTimeOffset>
, seperti:ITargetBlock<DateTimeOffset> CreateNeverEndingTask( Func<DateTimeOffset, CancellationToken, Task> action, CancellationToken cancellationToken) { // Validate parameters. if (action == null) throw new ArgumentNullException("action"); // Declare the block variable, it needs to be captured. ActionBlock<DateTimeOffset> block = null; // Create the block, it will call itself, so // you need to separate the declaration and // the assignment. // Async so you can wait easily when the // delay comes. block = new ActionBlock<DateTimeOffset>(async now => { // Perform the action. Wait on the result. await action(now, cancellationToken). // Doing this here because synchronization context more than // likely *doesn't* need to be captured for the continuation // here. As a matter of fact, that would be downright // dangerous. ConfigureAwait(false); // Wait. await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken). // Same as above. ConfigureAwait(false); // Post the action back to the block. block.Post(DateTimeOffset.Now); }, new ExecutionDataflowBlockOptions { CancellationToken = cancellationToken }); // Return the block. return block; }
Tentu saja, akan menjadi praktik yang baik untuk merangkai
CancellationToken
metode Anda (jika menerimanya), yang dilakukan di sini.Itu berarti Anda kemudian akan memiliki
DoWorkAsync
metode dengan tanda tangan berikut:Task DoWorkAsync(CancellationToken cancellationToken);
Anda harus mengubah (hanya sedikit, dan Anda tidak mengeluarkan pemisahan masalah di sini)
StartWork
metode untuk memperhitungkan tanda tangan baru yang diteruskan keCreateNeverEndingTask
metode, seperti:void StartWork() { // Create the token source. wtoken = new CancellationTokenSource(); // Set the task. task = CreateNeverEndingTask((now, ct) => DoWorkAsync(ct), wtoken.Token); // Start the task. Post the time. task.Post(DateTimeOffset.Now, wtoken.Token); }
sumber
Saya menemukan antarmuka berbasis Tugas baru menjadi sangat sederhana untuk melakukan hal-hal seperti ini - bahkan lebih mudah daripada menggunakan kelas Timer.
Ada beberapa penyesuaian kecil yang dapat Anda lakukan untuk contoh Anda. Dari pada:
task = Task.Factory.StartNew(() => { while (true) { wtoken.Token.ThrowIfCancellationRequested(); DoWork(); Thread.Sleep(10000); } }, wtoken, TaskCreationOptions.LongRunning);
Kamu bisa melakukan ini:
task = Task.Run(async () => // <- marked async { while (true) { DoWork(); await Task.Delay(10000, wtoken.Token); // <- await with cancellation } }, wtoken.Token);
Dengan cara ini pembatalan akan terjadi seketika jika di dalam
Task.Delay
, daripada harus menungguThread.Sleep
sampai selesai.Selain itu, menggunakan
Task.Delay
lebihThread.Sleep
berarti Anda tidak mengikat utas tanpa melakukan apa pun selama tidur.Jika Anda bisa, Anda juga dapat
DoWork()
menerima token pembatalan, dan pembatalan akan jauh lebih responsif.sumber
Task.Run
menggunakan kumpulan utas, jadi contoh Anda yang menggunakanTask.Run
alih-alihTask.Factory.StartNew
withTaskCreationOptions.LongRunning
tidak melakukan hal yang persis sama - jika saya memerlukan tugas untuk menggunakanLongRunning
opsi, apakah saya tidak dapat menggunakanTask.Run
seperti yang Anda tunjukkan, atau saya kehilangan sesuatu?LongRunning
agak tidak sesuai dengan tujuan tidak mengikat utas. Jika Anda ingin menjamin berjalan pada utasnya sendiri, Anda dapat menggunakannya, tetapi di sini Anda akan memulai utas yang tertidur hampir sepanjang waktu. Apa kasus penggunaannya?LongRunning
menggunakanTask.Run
sintaks. Dari dokumentasi, sepertinyaTask.Run
sintaks yang lebih bersih, selama Anda senang dengan pengaturan default yang digunakannya. Tampaknya tidak ada kelebihan beban yang membutuhkanTaskCreationOptions
argumen.Inilah yang saya dapatkan:
NeverEndingTask
dan menggantiExecutionCore
metode dengan pekerjaan yang ingin Anda lakukan.ExecutionLoopDelayMs
memungkinkan Anda untuk mengatur waktu antara loop, misalnya jika Anda ingin menggunakan algoritma backoff.Start/Stop
menyediakan antarmuka sinkron untuk memulai / menghentikan tugas.LongRunning
berarti Anda akan mendapatkan satu utas khusus perNeverEndingTask
.ActionBlock
solusi berbasis di atas.:
public abstract class NeverEndingTask { // Using a CTS allows NeverEndingTask to "cancel itself" private readonly CancellationTokenSource _cts = new CancellationTokenSource(); protected NeverEndingTask() { TheNeverEndingTask = new Task( () => { // Wait to see if we get cancelled... while (!_cts.Token.WaitHandle.WaitOne(ExecutionLoopDelayMs)) { // Otherwise execute our code... ExecutionCore(_cts.Token); } // If we were cancelled, use the idiomatic way to terminate task _cts.Token.ThrowIfCancellationRequested(); }, _cts.Token, TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning); // Do not forget to observe faulted tasks - for NeverEndingTask faults are probably never desirable TheNeverEndingTask.ContinueWith(x => { Trace.TraceError(x.Exception.InnerException.Message); // Log/Fire Events etc. }, TaskContinuationOptions.OnlyOnFaulted); } protected readonly int ExecutionLoopDelayMs = 0; protected Task TheNeverEndingTask; public void Start() { // Should throw if you try to start twice... TheNeverEndingTask.Start(); } protected abstract void ExecutionCore(CancellationToken cancellationToken); public void Stop() { // This code should be reentrant... _cts.Cancel(); TheNeverEndingTask.Wait(); } }
sumber