Bir Görevde eşzamanlı sürekliliği nasıl engelleyebilirim?


83

Taskİsteklere yönelik bekleyen yanıtlar için tabanlı bir API sağlayan bazı kitaplık (soket ağı) kodum var TaskCompletionSource<T>. Bununla birlikte, eşzamanlı sürekliliği önlemenin imkansız görünmesi nedeniyle TPL'de bir sıkıntı var. Ne olur gibi yapabilmek için aşağıdakilerden biri geçerlidir:

  • TaskCompletionSource<T>Arayanların bağlanmasına izin vermemesi gerektiğini söyleyin TaskContinuationOptions.ExecuteSynchronouslyveya
  • sonucu ( SetResult/ TrySetResult) TaskContinuationOptions.ExecuteSynchronouslygöz ardı edilmesi gerektiğini belirten bir şekilde, bunun yerine havuzu kullanarak ayarlayın

Spesifik olarak, sahip olduğum sorun, gelen verilerin özel bir okuyucu tarafından işleniyor olması ve bir arayanın ekleyebilmesi TaskContinuationOptions.ExecuteSynchronouslydurumunda okuyucuyu oyalayabilmesi (bu sadece onlardan daha fazlasını etkiliyor). Daha önce, herhangi bir sürekliliğin mevcut olup olmadığını tespit eden bazı bilgisayar korsanları tarafından bu sorunu çözmüştüm ve eğer varsa, tamamlamayı üzerine iter ThreadPool, ancak bu, arayanın iş kuyruğunu doldurması durumunda önemli bir etkiye sahiptir, çünkü tamamlama işlenmeyecektir. zamanında. Eğer kullanıyorlarsa Task.Wait()(veya benzerlerse), o zaman esasen kendilerini kilitleyeceklerdir. Aynı şekilde, okuyucunun işçi kullanmaktan ziyade özel bir ileti dizisinde olmasının nedeni de budur.

Yani; TPL ekibini denemeden ve dırdır etmeden önce: bir seçeneği kaçırıyor muyum?

Anahtar noktaları:

  • Dışarıdan arayanların ileti dizimi ele geçirmesini istemiyorum
  • ThreadPoolHavuz doyduğunda çalışması gerektiğinden uygulama olarak kullanamıyorum

Aşağıdaki örnek çıktı üretir (sıralama zamanlamaya göre değişebilir):

Continuation on: Main thread
Press [return]
Continuation on: Thread pool

Sorun, rastgele arayanın "Ana ileti dizisi" üzerinde bir devam ettirmeyi başardığı gerçeğidir. Gerçek kodda, bu birincil okuyucuyu kesintiye uğratır; kötü şeyler!

Kod:

using System;
using System.Threading;
using System.Threading.Tasks;

static class Program
{
    static void Identify()
    {
        var thread = Thread.CurrentThread;
        string name = thread.IsThreadPoolThread
            ? "Thread pool" : thread.Name;
        if (string.IsNullOrEmpty(name))
            name = "#" + thread.ManagedThreadId;
        Console.WriteLine("Continuation on: " + name);
    }
    static void Main()
    {
        Thread.CurrentThread.Name = "Main thread";
        var source = new TaskCompletionSource<int>();
        var task = source.Task;
        task.ContinueWith(delegate {
            Identify();
        });
        task.ContinueWith(delegate {
            Identify();
        }, TaskContinuationOptions.ExecuteSynchronously);
        source.TrySetResult(123);
        Console.WriteLine("Press [return]");
        Console.ReadLine();
    }
}

2
TaskCompletionSourceDoğrudan çağrıyı önlemek için kendi API'mi sarmaya çalışıyorum ContinueWith, çünkü ne ne TaskCompletionSourcede Taskonlardan miras almaya uygun değil.
Dennis

1
@Dennis açık olmak gerekirse, aslında Taskmaruz kalınan şeydir TaskCompletionSource,. Bu (farklı bir API açığa çıkarmak) teknik olarak bir seçenek, ancak sadece bunun için yapılması oldukça aşırı bir şey ... Bunu haklı çıkardığından emin değilim
Marc Gravell

2
Ya kullanın: - @MattH gerçekten sadece soruyu rephrases ThreadPoolveya iplik "devamlılık bekleyen" özel var, sonra onlar (ile continations - (Bu sorunlara neden Zaten bahsedilen olan) Bunun için ExecuteSynchronouslybelirtilen) kaçırmak olabilir o bunun yerine - bu tam olarak aynı soruna neden oluyor, çünkü diğer mesajlar için devamların durabileceği anlamına geliyor ve bu da birden fazla arayan kişiyi etkiliyor
Marc Gravell

3
@Andrey (tüm arayanlar exec-sync olmadan ContinueWith kullanıyormuş gibi çalışıyor) tam olarak elde etmek istediğim şey. Sorun şu ki, kütüphanem birine bir Görev verirse, çok istenmeyen bir şey yapabilir: okuyucumu exec-sync kullanarak (kaçınılmaz olarak) kesebilirler. Bu çok tehlikelidir, bu yüzden kütüphanenin içinde olmasını engellemek istiyorum .
Marc Gravell

2
@Andrey çünkü a: birçok görev ilk etapta asla devam etmez (özellikle toplu iş yaparken) - bu, her görevi bir tane yapmaya zorlar ve b: devamı olanların bile artık çok daha karmaşık olması, genel gider ve işçi operasyonları. Bu önemlidir.
Marc Gravell

Yanıtlar:


50

.NET 4.6'daki yenilikler:

.NET 4.6 yeni içeriyor TaskCreationOptions: RunContinuationsAsynchronously.


Özel alanlara erişmek için Reflection'ı kullanmak istediğiniz için ...

TCS'nin Görevini TASK_STATE_THREAD_WAS_ABORTEDbayrakla işaretleyebilirsiniz, bu da tüm devamların satır içi olmamasına neden olur.

const int TASK_STATE_THREAD_WAS_ABORTED = 134217728;

var stateField = typeof(Task).GetField("m_stateFlags", BindingFlags.NonPublic | BindingFlags.Instance);
stateField.SetValue(task, (int) stateField.GetValue(task) | TASK_STATE_THREAD_WAS_ABORTED);

Düzenle:

Reflection emit kullanmak yerine ifadeler kullanmanızı öneririm. Bu çok daha okunabilir ve PCL uyumlu olma avantajına sahiptir:

var taskParameter = Expression.Parameter(typeof (Task));
const string stateFlagsFieldName = "m_stateFlags";
var setter =
    Expression.Lambda<Action<Task>>(
        Expression.Assign(Expression.Field(taskParameter, stateFlagsFieldName),
            Expression.Or(Expression.Field(taskParameter, stateFlagsFieldName),
                Expression.Constant(TASK_STATE_THREAD_WAS_ABORTED))), taskParameter).Compile();

Yansıma kullanmadan:

İlgilenen varsa, bunu Yansıma olmadan yapmanın bir yolunu buldum, ama bu biraz da "kirli" ve elbette göz ardı edilemeyecek bir mükemmel ceza getiriyor:

try
{
    Thread.CurrentThread.Abort();
}
catch (ThreadAbortException)
{
    source.TrySetResult(123);
    Thread.ResetAbort();
}

3
@MarcGravell Bunu, TPL ekibi için bazı sözde örnek oluşturmak ve bunu yapıcı seçenekleri veya başka bir şey yoluyla yapabilmek için bir değişiklik talebinde bulunmak için kullanın.
Adam Houldsworth

1
@Adam evet, bu bayrağa "neden olan" yerine "ne yapıyor" demek zorunda olsaydınız, böyle bir şey TaskCreationOptions.DoNotInlineolurdu - ve ctor imzasının değiştirilmesine bile gerek kalmazdıTaskCompletionSource
Marc Gravell

2
@AdamHouldsworth ve endişelenme, zaten onlara aynı şekilde e-posta gönderiyorum; p
Marc Gravell


1
@Noseratio evet, kontrol ettim - teşekkürler; hepsi tamam IMO; Bunun tamamen geçici bir çözüm olduğuna katılıyorum, ancak tam olarak doğru sonuçlara sahip.
Marc Gravell

9

TPL'de süreklilikler üzerinde açık API kontrolü sağlayacak bir şey olduğunu sanmıyorum TaskCompletionSource.SetResult. Senaryolar için bu davranışı kontrol etmek için ilk cevabımı tutmaya karar verdim async/await.

İşte -tetiklenmiş devam ettirme çağrıldığı aynı iş parçacığı üzerinde gerçekleşirse ContinueWith, asenkronize uygulayan başka bir çözüm :tcs.SetResultSetResult

public static class TaskExt
{
    static readonly ConcurrentDictionary<Task, Thread> s_tcsTasks =
        new ConcurrentDictionary<Task, Thread>();

    // SetResultAsync
    static public void SetResultAsync<TResult>(
        this TaskCompletionSource<TResult> @this,
        TResult result)
    {
        s_tcsTasks.TryAdd(@this.Task, Thread.CurrentThread);
        try
        {
            @this.SetResult(result);
        }
        finally
        {
            Thread thread;
            s_tcsTasks.TryRemove(@this.Task, out thread);
        }
    }

    // ContinueWithAsync, TODO: more overrides
    static public Task ContinueWithAsync<TResult>(
        this Task<TResult> @this,
        Action<Task<TResult>> action,
        TaskContinuationOptions continuationOptions = TaskContinuationOptions.None)
    {
        return @this.ContinueWith((Func<Task<TResult>, Task>)(t =>
        {
            Thread thread = null;
            s_tcsTasks.TryGetValue(t, out thread);
            if (Thread.CurrentThread == thread)
            {
                // same thread which called SetResultAsync, avoid potential deadlocks

                // using thread pool
                return Task.Run(() => action(t));

                // not using thread pool (TaskCreationOptions.LongRunning creates a normal thread)
                // return Task.Factory.StartNew(() => action(t), TaskCreationOptions.LongRunning);
            }
            else
            {
                // continue on the same thread
                var task = new Task(() => action(t));
                task.RunSynchronously();
                return Task.FromResult(task);
            }
        }), continuationOptions).Unwrap();
    }
}

Yorumu ele almak için güncellendi:

Arayan kişiyi kontrol etmiyorum - belirli bir devam etme varyantını kullanmalarını sağlayamıyorum: Yapabilseydim, sorun ilk etapta mevcut olmazdı

Arayan kişiyi kontrol etmediğini bilmiyordum. Yine de, onu kontrol etmezseniz, muhtemelen TaskCompletionSourcenesneyi doğrudan arayana da iletmiyorsunuzdur. Mantıksal olarak, token kısmını, yani tcs.Task. Bu durumda, yukarıdakine başka bir uzatma yöntemi ekleyerek çözüm daha da kolay olabilir:

// ImposeAsync, TODO: more overrides
static public Task<TResult> ImposeAsync<TResult>(this Task<TResult> @this)
{
    return @this.ContinueWith(new Func<Task<TResult>, Task<TResult>>(antecedent =>
    {
        Thread thread = null;
        s_tcsTasks.TryGetValue(antecedent, out thread);
        if (Thread.CurrentThread == thread)
        {
            // continue on a pool thread
            return antecedent.ContinueWith(t => t, 
                TaskContinuationOptions.None).Unwrap();
        }
        else
        {
            return antecedent;
        }
    }), TaskContinuationOptions.ExecuteSynchronously).Unwrap();
}

Kullanım:

// library code
var source = new TaskCompletionSource<int>();
var task = source.Task.ImposeAsync();
// ... 

// client code
task.ContinueWith(delegate
{
    Identify();
}, TaskContinuationOptions.ExecuteSynchronously);

// ...
// library code
source.SetResultAsync(123);

Bu aslında her ikisi için çalışır awaitveContinueWith ( keman ) ve yansıma kesmek serbesttir.


1
Arayan kişiyi kontrol etmiyorum - onlara belirli bir devam etme varyantını kullanmalarını sağlayamıyorum: eğer yapabilseydim, sorun ilk etapta mevcut olmazdı
Marc Gravell

@MarcGravell, arayan kişiyi kontrol edemeyeceğinin farkında değildim. Bununla nasıl başa çıkacağıma dair bir güncelleme yayınladım.
noseratio

kütüphane yazarının ikilemi; p Birinin istenen sonucu elde etmenin çok daha basit ve daha doğrudan bir yolunu bulduğuna dikkat edin
Marc Gravell

4

Ya yapmak yerine

var task = source.Task;

bunun yerine sen yap

var task = source.Task.ContinueWith<Int32>( x => x.Result );

Bu nedenle, her zaman eşzamansız olarak yürütülecek bir devam ekliyorsunuz ve ardından abonelerin aynı bağlamda bir devamı isteyip istemediklerinin önemi yok. Görevi biraz zorlaştırıyor, değil mi?


1
Yorumlarda ortaya çıktı (bkz. Andrey); Sorun orada tam o zorlar de o şeydir, başka türlü olmazdı zaman devamı mı tüm görevleri, ContinueWithve awaitbu zorlamak olacağından ve - normalde (vs zaten tamamlama kontrol ederek) kaçınmak için çok çalışıyorum her şeyi üzerine işçiler, durumu daha da kötüleştirecektir. Bu olumlu bir fikir ve bunun için size teşekkür ederim: ancak bu senaryoda yardımcı olmayacak.
Marc Gravell

3

Eğer düşünebiliyorsan ve kullanmaya hazırsan, bunu yapmalı;

public static class MakeItAsync
{
    static public void TrySetAsync<T>(this TaskCompletionSource<T> source, T result)
    {
        var continuation = typeof(Task).GetField("m_continuationObject", BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance);
        var continuations = (List<object>)continuation.GetValue(source.Task);

        foreach (object c in continuations)
        {
            var option = c.GetType().GetField("m_options", BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance);
            var options = (TaskContinuationOptions)option.GetValue(c);

            options &= ~TaskContinuationOptions.ExecuteSynchronously;
            option.SetValue(c, options);
        }

        source.TrySetResult(result);
    }        
}

Bu hack, Framework'ün bir sonraki sürümünde çalışmayı durdurabilir.
noseratio

@Noseratio, doğru ama şimdi işe yarıyor ve bir sonraki sürümde bunu yapmanın uygun bir yolunu da uygulayabilirler
Fredou

Ama basitçe yapabiliyorsan neden buna ihtiyacın olsun ki Task.Run(() => tcs.SetResult(result))?
noseratio

@Noseratio, bilmiyorum, bu soruyu Marc'a sorun :-), TaskContinuationOptions.ExecuteSynchronously bayrağını kaldırıyorum bir TaskCompletionSource'a bağlı tüm görevlerde ana iş parçacığı yerine threadpool'u kullandıklarından emin olun
Fredou

M_continuationObject hack, aslında potansiyel olarak sorunlu görevleri tanımlamak için zaten kullandığım hiledir - bu yüzden bu dikkate alınmanın ötesinde değildir. İlginç, teşekkürler. Bu şimdiye kadarki en kullanışlı görünen seçenek.
Marc Gravell

3

Güncelleme , ben yayınlanmıştır ayrı cevap başa ContinueWithaksine await(çünkü ContinueWithmevcut senkronizasyon bağlamda önemsemiyor).

Sen devam arayarak tetiklenen üzerine asenkroniye empoze etmek dilsiz senkronizasyon bağlamını kullanabilirsiniz SetResult/SetCancelled/SetExceptionüzerinde TaskCompletionSource. Mevcut senkronizasyon bağlamının (noktasında await tcs.Task), TPL'nin bu tür bir sürekliliğin senkron veya asenkronize olup olmadığına karar vermek için kullandığı kriter olduğuna inanıyorum .

Aşağıdakiler benim için çalışıyor:

if (notifyAsync)
{
    tcs.SetResultAsync(null);
}
else
{
    tcs.SetResult(null);
}

SetResultAsync şu şekilde uygulanır:

public static class TaskExt
{
    static public void SetResultAsync<T>(this TaskCompletionSource<T> tcs, T result)
    {
        FakeSynchronizationContext.Execute(() => tcs.SetResult(result));
    }

    // FakeSynchronizationContext
    class FakeSynchronizationContext : SynchronizationContext
    {
        private static readonly ThreadLocal<FakeSynchronizationContext> s_context =
            new ThreadLocal<FakeSynchronizationContext>(() => new FakeSynchronizationContext());

        private FakeSynchronizationContext() { }

        public static FakeSynchronizationContext Instance { get { return s_context.Value; } }

        public static void Execute(Action action)
        {
            var savedContext = SynchronizationContext.Current;
            SynchronizationContext.SetSynchronizationContext(FakeSynchronizationContext.Instance);
            try
            {
                action();
            }
            finally
            {
                SynchronizationContext.SetSynchronizationContext(savedContext);
            }
        }

        // SynchronizationContext methods

        public override SynchronizationContext CreateCopy()
        {
            return this;
        }

        public override void OperationStarted()
        {
            throw new NotImplementedException("OperationStarted");
        }

        public override void OperationCompleted()
        {
            throw new NotImplementedException("OperationCompleted");
        }

        public override void Post(SendOrPostCallback d, object state)
        {
            throw new NotImplementedException("Post");
        }

        public override void Send(SendOrPostCallback d, object state)
        {
            throw new NotImplementedException("Send");
        }
    }
}

SynchronizationContext.SetSynchronizationContext eklediği ek yük açısından çok ucuz . Aslında, WPF'nin uygulanmasıylaDispatcher.BeginInvoke çok benzer bir yaklaşım benimsenmiştir .

TPL, noktasındaki hedef senkronizasyon içeriğini await, noktasınınki ile karşılaştırır tcs.SetResult. Senkronizasyon içeriği aynıysa (veya her iki yerde de senkronizasyon içeriği yoksa), devam doğrudan, senkronize olarak çağrılır. Aksi takdirde, SynchronizationContext.Posthedef senkronizasyon bağlamında, yani normal awaitdavranış kullanılarak sıraya alınır . Bu yaklaşımın yaptığı her zaman SynchronizationContext.Postdavranışı (veya hedef senkronizasyon bağlamı yoksa bir havuz iş parçacığı sürekliliğini) uygulamaktır.

Güncellenen , bu işe yaramayacak task.ContinueWithçünkü ContinueWithmevcut senkronizasyon bağlamı umurunda değil. Ancak await task( keman ) için çalışır . Bunun için de çalışıyor await task.ConfigureAwait(false).

OTOH, bu yaklaşım işe yarıyor ContinueWith.


Cazip, ancak senkronizasyon bağlamını değiştirmek neredeyse kesinlikle çağıran uygulamayı etkileyecektir - örneğin, kitaplığımı kullanan bir web veya Windows uygulaması senkronizasyon bağlamının saniyede yüzlerce kez değiştiğini görmemelidir.
Marc Gravell

@MarcGravell, sadece tcs.SetResultaramanın kapsamı için değiştiriyorum . Devamı kendisinde olacağını çünkü tür, atomik ve evreli bu şekilde olur ya başka havuz iplik veya orijinal olarak senkronizasyonu. bağlam yakalandı await tcs.Task. Ve SynchronizationContext.SetSynchronizationContextkendisi çok ucuz, bir iplik anahtarının kendisinden çok daha ucuz.
noseratio

Ancak bu, ikinci gereksiniminizi karşılamayabilir: kullanmamak ThreadPool. Bu çözümle, ThreadPooleğer senkronizasyon yoksa , TPL gerçekten kullanacaktır . bağlam (veya temel varsayılan olandı) await tcs.Task. Ancak bu standart TPL davranışıdır.
noseratio

Hmmm ... eşitleme bağlamı iş parçacığı başına olduğundan, bu aslında uygulanabilir olabilir - ve ctx'i değiştirmeye devam etmem gerekmeyecek - yalnızca bir kez çalışan iş parçacığı için ayarlayın. Onunla oynamam gerekecek
Marc Gravell

1
@Hayır ah, doğru: anahtar noktanın farklı olmaları olduğu açık değildi . Bakacak. Teşekkürler.
Marc Gravell

3

İptal etmek benzetmek yaklaşımı gerçekten iyi görünüyordu ama TPL kaçırma parçacığı yol açtı bazı senaryolarda .

Daha sonra , devam nesnesini kontrol etmeye benzer bir uygulamaya sahiptim , ancak verilen kodun iyi çalışması için aslında çok fazla senaryo olduğundan, herhangi bir devamlılığı kontrol ettim, ancak bu gibi şeylerin bile Task.Waitbir iş parçacığı havuzu aramasına neden olduğu anlamına geliyordu .

Nihayetinde, çok sayıda IL'yi inceledikten sonra, güvenli ve kullanışlı tek senaryo SetOnInvokeMressenaryodur (manuel sıfırlama-olay-ince devam). Pek çok başka senaryo var:

  • bazıları güvenli değildir ve iş parçacığı kaçırılmasına neden olur
  • geri kalanı, sonuçta iş parçacığı havuzuna yol açtıkları için yararlı değildir.

Sonunda, boş olmayan bir devam nesnesini kontrol etmeyi seçtim; boşsa, iyi (devam yok); boş değilse, özel durum kontrol edin SetOnInvokeMres- eğer öyleyse: fine (güvenli çağırmak); aksi takdirde, iş parçacığı havuzunun TrySetComplete, görevi aldatma iptali gibi özel bir şey yapmasını söylemeden gerçekleştirmesine izin verin . kilitlenmemek için gerçekten çok denemek istediğimiz özel senaryo olan yaklaşımı Task.Waitkullanır .SetOnInvokeMres

Type taskType = typeof(Task);
FieldInfo continuationField = taskType.GetField("m_continuationObject", BindingFlags.Instance | BindingFlags.NonPublic);
Type safeScenario = taskType.GetNestedType("SetOnInvokeMres", BindingFlags.NonPublic);
if (continuationField != null && continuationField.FieldType == typeof(object) && safeScenario != null)
{
    var method = new DynamicMethod("IsSyncSafe", typeof(bool), new[] { typeof(Task) }, typeof(Task), true);
    var il = method.GetILGenerator();
    var hasContinuation = il.DefineLabel();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldfld, continuationField);
    Label nonNull = il.DefineLabel(), goodReturn = il.DefineLabel();
    // check if null
    il.Emit(OpCodes.Brtrue_S, nonNull);
    il.MarkLabel(goodReturn);
    il.Emit(OpCodes.Ldc_I4_1);
    il.Emit(OpCodes.Ret);

    // check if is a SetOnInvokeMres - if so, we're OK
    il.MarkLabel(nonNull);
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldfld, continuationField);
    il.Emit(OpCodes.Isinst, safeScenario);
    il.Emit(OpCodes.Brtrue_S, goodReturn);

    il.Emit(OpCodes.Ldc_I4_0);
    il.Emit(OpCodes.Ret);

    IsSyncSafe = (Func<Task, bool>)method.CreateDelegate(typeof(Func<Task, bool>));
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.