Hoe een taak afbreken zoals het afbreken van een thread (Thread.Abort-methode)?

We kunnen een Threadals volgt afbreken:

Thread thread = new Thread(SomeMethod);
.
.
.
thread.Abort();

Maar kan ik een Task(in .Net 4.0) op dezelfde manier afbreken, niet door een annuleringsmechanisme. Ik wil de taak onmiddellijk beëindigen.


Antwoord 1, autoriteit 100%

De richtlijnen voor het niet gebruiken van een draadafbreking zijn controversieel. Ik denk dat er nog steeds een plaats voor is, maar in uitzonderlijke omstandigheden. Probeer er echter altijd omheen te ontwerpen en het als een laatste redmiddel te zien.

Voorbeeld;

U hebt een eenvoudige Windows Form-toepassing die verbinding maakt met een blokkerende synchrone webservice. Waarbinnen het een functie op de webservice uitvoert binnen een parallelle lus.

CancellationTokenSource cts = new CancellationTokenSource();
ParallelOptions po = new ParallelOptions();
po.CancellationToken = cts.Token;
po.MaxDegreeOfParallelism = System.Environment.ProcessorCount;
Parallel.ForEach(iListOfItems, po, (item, loopState) =>
{
    Thread.Sleep(120000); // pretend web service call
});

Stel in dit voorbeeld dat het blokkeren van de oproep 2 minuten duurt. Nu heb ik mijn MaxDegreeOfParallelism ingesteld om ProcessorCount te zeggen. iListOfItems heeft 1000 items om te verwerken.

De gebruiker klikt op de procesknop en de lus begint, we hebben ‘tot’ 20 threads die worden uitgevoerd tegen 1000 items in de iListOfItems-verzameling. Elke iteratie wordt uitgevoerd op zijn eigen thread. Elke thread zal een voorgrondthread gebruiken wanneer deze wordt gemaakt door Parallel.ForEach. Dit betekent dat ongeacht het afsluiten van de hoofdtoepassing, het app-domein in leven wordt gehouden totdat alle threads zijn voltooid.

De gebruiker moet de toepassing echter om de een of andere reden sluiten, bijvoorbeeld door het formulier te sluiten.
Deze 20 threads worden uitgevoerd totdat alle 1000 items zijn verwerkt. Dit is niet ideaal in dit scenario, omdat de applicatie niet wordt afgesloten zoals de gebruiker verwacht en achter de schermen blijft draaien, zoals te zien is in taakbeheer.

Stel dat de gebruiker probeert de app opnieuw op te bouwen (VS 2010), hij meldt dat de exe is vergrendeld, dan moeten ze naar Taakbeheer gaan om de app te doden of gewoon wachten tot alle 1000 items zijn verwerkt.

Ik zou het je niet kwalijk nemen dat je het zegt, maar natuurlijk! Ik zou deze threads moeten annuleren met behulp van de CancellationTokenSourceobject en aanroepen van Cancel … maar er zijn enkele problemen hiermee vanaf .net 4.0. Ten eerste zal dit nog steeds nooit resulteren in het afbreken van een thread, wat een abort-uitzondering zou bieden, gevolgd door threadbeëindiging, dus het app-domein zal in plaats daarvan moeten wachten tot de threads normaal zijn voltooid, en dit betekent wachten op de laatste blokkeeroproep, wat de allerlaatste iteratie (thread) zou zijn die uiteindelijk po.CancellationToken.ThrowIfCancellationRequestedmag aanroepen.
In het voorbeeld zou dit betekenen dat het app-domein nog maximaal 2 minuten in leven kan blijven, ook al is het formulier gesloten en is de oproep geannuleerd.

Houd er rekening mee dat het aanroepen van Annuleren op CancellationTokenSource geen uitzondering op de verwerkingsthread(s) genereert, wat inderdaad zou werken om de blokkerende oproep te onderbreken, vergelijkbaar met een thread-abort, en de uitvoering te stoppen. Een uitzondering wordt in de cache opgeslagen, klaar voor wanneer alle andere threads (gelijktijdige iteraties) uiteindelijk eindigen en terugkeren, de uitzondering wordt gegooid in de initiërende thread (waar de lus wordt gedeclareerd).

Ik heb nietgekozen om de Annulerenoptie op een CancellationTokenSource-object. Dit is verspilling en schendt aantoonbaar het bekende anti-patten van het controleren van de stroom van de code door Exceptions.

In plaats daarvan is het aantoonbaar ‘beter’ om een ​​eenvoudige thread-safe-eigenschap te implementeren, d.w.z. Bool stopExecuting. Controleer vervolgens binnen de lus de waarde van stopExecuting en als de waarde is ingesteld op true door de externe invloed, kunnen we een alternatief pad nemen om gracieus af te sluiten. Aangezien we niet moeten annuleren, sluit dit het controleren van CancellationTokenSource.IsCancellationRequestedwat anders een andere optie zou zijn.

Zoiets als het volgende als de voorwaarde geschikt zou zijn binnen de lus;

if (loopState.ShouldExitCurrentIteration || loopState.IsExceptional || stopExecuting) {loopState.Stop(); terug;}

De iteratie zal nu op een ‘gecontroleerde’ manier worden afgesloten en verdere iteraties worden beëindigd, maar zoals ik al zei, doet dit weinig voor ons probleem van moeten wachten op de langlopende en blokkerende oproep(en) die binnen worden gedaan elke iteratie (parallelle loop-thread), omdat deze moeten worden voltooid voordat elke thread de mogelijkheid krijgt om te controleren of deze moet stoppen.

Samengevat, als de gebruiker het formulier sluit, zullen de 20 threads worden gesignaleerd om te stoppen via stopExecuting, maar ze zullen pas stoppen wanneer ze klaar zijn met het uitvoeren van hun langlopende functieaanroep.

We kunnen niets doen aan het feit dat het applicatiedomein altijd in leven blijft en pas wordt vrijgegeven als alle voorgrondthreads zijn voltooid. En dit betekent dat er een vertraging zal optreden bij het wachten tot alle blokkerende oproepen binnen de lus zijn voltooid.

Alleen een echte draadafbreking kan de blokkeeroproep onderbreken, en u moet het systeem zo goed mogelijk in een onstabiele/ongedefinieerde toestand beperken in de uitzonderingshandler van de afgebroken draad, wat zonder twijfel gaat. Of dat gepast is, is een kwestie voor de programmeur om te beslissen, op basis van de resourcehandles die ze hebben gekozen om te onderhouden en hoe gemakkelijk het is om ze te sluiten in het definitieve blok van een thread. Je zou je kunnen registreren met een token om te beëindigen bij annuleren als een semi-oplossing, d.w.z.

CancellationTokenSource cts = new CancellationTokenSource();
ParallelOptions po = new ParallelOptions();
po.CancellationToken = cts.Token;
po.MaxDegreeOfParallelism = System.Environment.ProcessorCount;
Parallel.ForEach(iListOfItems, po, (item, loopState) =>
{
    using (cts.Token.Register(Thread.CurrentThread.Abort))
    {
        Try
        {
           Thread.Sleep(120000); // pretend web service call          
        }
        Catch(ThreadAbortException ex)
        {
           // log etc.
        }
        Finally
        {
          // clean up here
        }
    }
});

maar dit zal nog steeds resulteren in een uitzondering in de declarerende thread.

Al met al had het blokkeren van oproepen met behulp van de parallel.loop-constructies een methode voor de opties kunnen zijn, waarbij het gebruik van meer obscure delen van de bibliotheek werd vermeden. Maar waarom er geen optie is om te annuleren en te voorkomen dat er een uitzondering in de declarerende methode wordt gegooid, lijkt me een mogelijke vergissing.


Antwoord 2, autoriteit 82%

Maar kan ik een taak afbreken (in .Net 4.0) op dezelfde manier, niet door
annuleringsmechanisme. Ik wil de taak onmiddellijk beëindigen.

Andere beantwoorders hebben je gezegd het niet te doen. Maar ja, u kunthet doen. U kunt Thread.Abort()opgeven als de gemachtigde die moet worden aangeroepen door het annuleringsmechanisme van de taak. U kunt dit als volgt configureren:

class HardAborter
{
  public bool WasAborted { get; private set; }
  private CancellationTokenSource Canceller { get; set; }
  private Task<object> Worker { get; set; }
  public void Start(Func<object> DoFunc)
  {
    WasAborted = false;
    // start a task with a means to do a hard abort (unsafe!)
    Canceller = new CancellationTokenSource();
    Worker = Task.Factory.StartNew(() => 
      {
        try
        {
          // specify this thread's Abort() as the cancel delegate
          using (Canceller.Token.Register(Thread.CurrentThread.Abort))
          {
            return DoFunc();
          }
        }
        catch (ThreadAbortException)
        {
          WasAborted = true;
          return false;
        }
      }, Canceller.Token);
  }
  public void Abort()
  {
    Canceller.Cancel();
  }
}

disclaimer: doe dit niet.

Hier is een voorbeeld van wat u niet moet doen:

var doNotDoThis = new HardAborter();
 // start a thread writing to the console
 doNotDoThis.Start(() =>
    {
       while (true)
       {
          Thread.Sleep(100);
          Console.Write(".");
       }
       return null;
    });
 // wait a second to see some output and show the WasAborted value as false
 Thread.Sleep(1000);
 Console.WriteLine("WasAborted: " + doNotDoThis.WasAborted);
 // wait another second, abort, and print the time
 Thread.Sleep(1000);
 doNotDoThis.Abort();
 Console.WriteLine("Abort triggered at " + DateTime.Now);
 // wait until the abort finishes and print the time
 while (!doNotDoThis.WasAborted) { Thread.CurrentThread.Join(0); }
 Console.WriteLine("WasAborted: " + doNotDoThis.WasAborted + " at " + DateTime.Now);
 Console.ReadKey();

uitvoer van voorbeeldcode


Antwoord 3, autoriteit 71%

  1. Je moet Thread.Abort() niet gebruiken
  2. Taken kunnen worden geannuleerd, maar niet worden afgebroken.

De Thread.Abort()methode is (ernstig) verouderd.

Zowel threads als taken moeten samenwerken wanneer ze worden gestopt, anders loopt u het risico het systeem in een onstabiele/ongedefinieerde toestand achter te laten.

Als je een proces moet uitvoeren en het van buitenaf moet doden, is de enige veilige optie om het in een apart AppDomain uit te voeren.


Dit antwoord gaat over .net 3.5 en eerder.

De verwerking van het afbreken van threads is sindsdien verbeterd, o.a. door de manier te veranderen waarop het werk uiteindelijk wordt geblokkeerd.

Maar Thread.Abort is nog steeds een verdachte oplossing die je altijd moet proberen te vermijden.


En in .net Core (.net 5+) zal Thread.Abort() nu een PlatformNotSupportedException.

Een soort van onderstreping van het ‘verouderde’ punt.


Antwoord 4, autoriteit 29%

Iedereen weet (hopelijk) dat het slecht is om een ​​thread te beëindigen. Het probleem is wanneer je niet in het bezit bent van een stukje code dat je belt. Als deze code draait in een do/while oneindige lus , zelf enkele native functies aanroept, enz. zit je eigenlijk vast. Wanneer dit gebeurt in je eigen codebeëindiging, stop of Dispose-oproep, is het best oké om de slechteriken neer te schieten (zodat je zelf geen slechterik wordt).

Dus, voor wat het waard is, ik heb die twee blokkeerfuncties geschreven die hun eigen native thread gebruiken, niet een thread uit de pool of een thread gemaakt door de CLR. Ze zullen de thread stoppen als er een time-out optreedt:

// returns true if the call went to completion successfully, false otherwise
public static bool RunWithAbort(this Action action, int milliseconds) => RunWithAbort(action, new TimeSpan(0, 0, 0, 0, milliseconds));
public static bool RunWithAbort(this Action action, TimeSpan delay)
{
    if (action == null)
        throw new ArgumentNullException(nameof(action));
    var source = new CancellationTokenSource(delay);
    var success = false;
    var handle = IntPtr.Zero;
    var fn = new Action(() =>
    {
        using (source.Token.Register(() => TerminateThread(handle, 0)))
        {
            action();
            success = true;
        }
    });
    handle = CreateThread(IntPtr.Zero, IntPtr.Zero, fn, IntPtr.Zero, 0, out var id);
    WaitForSingleObject(handle, 100 + (int)delay.TotalMilliseconds);
    CloseHandle(handle);
    return success;
}
// returns what's the function should return if the call went to completion successfully, default(T) otherwise
public static T RunWithAbort<T>(this Func<T> func, int milliseconds) => RunWithAbort(func, new TimeSpan(0, 0, 0, 0, milliseconds));
public static T RunWithAbort<T>(this Func<T> func, TimeSpan delay)
{
    if (func == null)
        throw new ArgumentNullException(nameof(func));
    var source = new CancellationTokenSource(delay);
    var item = default(T);
    var handle = IntPtr.Zero;
    var fn = new Action(() =>
    {
        using (source.Token.Register(() => TerminateThread(handle, 0)))
        {
            item = func();
        }
    });
    handle = CreateThread(IntPtr.Zero, IntPtr.Zero, fn, IntPtr.Zero, 0, out var id);
    WaitForSingleObject(handle, 100 + (int)delay.TotalMilliseconds);
    CloseHandle(handle);
    return item;
}
[DllImport("kernel32")]
private static extern bool TerminateThread(IntPtr hThread, int dwExitCode);
[DllImport("kernel32")]
private static extern IntPtr CreateThread(IntPtr lpThreadAttributes, IntPtr dwStackSize, Delegate lpStartAddress, IntPtr lpParameter, int dwCreationFlags, out int lpThreadId);
[DllImport("kernel32")]
private static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32")]
private static extern int WaitForSingleObject(IntPtr hHandle, int dwMilliseconds);

Antwoord 5, autoriteit 4%

Hoewel het mogelijk is om een ​​thread af te breken, is het in de praktijk bijna altijd een heel slecht idee om dit te doen. Het afbreken van een thread betekent dat de thread geen kans krijgt om zichzelf op te ruimen, waardoor bronnen niet worden verwijderd en dingen in onbekende toestanden blijven.

In de praktijk, als je een thread afbreekt, zou je dit alleen moeten doen in combinatie met het beëindigen van het proces. Helaas denken maar al te veel mensen dat ThreadAbort een haalbare manier is om iets te stoppen en door te gaan, maar dat is niet zo.

Aangezien taken als threads worden uitgevoerd, kunt u er ThreadAbort op aanroepen, maar net als bij generieke threads wilt u dit bijna nooit doen, behalve als laatste redmiddel.


Antwoord 6

U kunt een taak “afbreken” door deze uit te voeren op een thread die u beheert en die thread af te breken. Dit zorgt ervoor dat de taak wordt voltooid in een defecte staat met een ThreadAbortException. U kunt het maken van threads beheren met een aangepaste taakplanner, zoals beschreven in dit antwoord. Merk op dat de waarschuwing bij het afbreken van een discussieis van toepassing.

(Als u er niet zeker van bent dat de taak op zijn eigen thread wordt gemaakt, zou het afbreken ervan een thread-pool-thread of de thread die de taak initieert afbreken, wat u normaal gesproken geen van beide wilt doen.)


Antwoord 7

using System;
using System.Threading;
using System.Threading.Tasks;
...
var cts = new CancellationTokenSource();
var task = Task.Run(() => { while (true) { } });
Parallel.Invoke(() =>
{
    task.Wait(cts.Token);
}, () =>
{
    Thread.Sleep(1000);
    cts.Cancel();
});

Dit is een eenvoudig fragment om een ​​oneindige taak af te breken met CancellationTokenSource.

Other episodes