ホームページ  >  記事  >  バックエンド開発  >  async 非同期、スレッド .NET のマルチスレッド

async 非同期、スレッド .NET のマルチスレッド

angryTom
angryTom転載
2019-11-27 14:26:132719ブラウズ

async 非同期、スレッド .NET のマルチスレッド

1. Task

System.Threading.Tasks は .NET4 で導入されましたが、API が多すぎます。前スレッドの場合 制御が不便であり、ThreadPool の制御能力が弱すぎるため、例えば、スレッドの継続、ブロック、キャンセル、タイムアウトなどの機能を実行するのが不便であるため、タスクはスレッドの機能を抽象化し、スレッドプールを使用します。バックグラウンド

#1. タスクを開始します##TaskFactory クラスまたは Task クラスのコンストラクターと Start() メソッドを使用できます。デリゲートは入力パラメーターを提供できます。 Object 型を使用しているため、任意のデータをタスクに渡すことができます。また、一般的に使用される Task.Run

TaskFactory taskFactory = new TaskFactory();
taskFactory.StartNew(() => 
{
    Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}");
});
Task.Factory.StartNew(() =>
{
    Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}");
});
Task task = new Task(() =>
{
    Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}");
});
task.Start();

タスク クラス インスタンス モードのみで、タスクを開始するために Start() が必要です。 RunSynchronously() を使用してタスクを同期的に実行できます。メイン スレッドは待機します。つまり、メイン スレッドを使用してこのタスクを実行します

#
Task task = new Task(() =>
{
    Thread.Sleep(10000);
    Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}");
});
task.RunSynchronously();

##2。継続のブロック

#Threadではjoinを使ってブロックして待機しているので、複数のThreadを制御するのは不便です。 Task では、インスタンス メソッド Wait を使用して単一のタスクをブロックするか、静的メソッド WaitAll および WaitAny を使用して複数のタスクをブロックします。

var task = new Task(() =>
{
    Thread.Sleep(5*1000);
    Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}");
});
var task2 = new Task(() =>
{
    Thread.Sleep(10 * 1000);
    Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}");
});
task.Start();
task2.Start();
//task.Wait();//单任务等待
//Task.WaitAny(task, task2);//任何一个任务完成就继续
Task.WaitAll(task, task2);//任务都完成才继续
メイン スレッドをブロックしたくない場合は、タスクの実行後に他のタスクを実行できます。タスクまたは複数のタスクが完了しました。Task の静的メソッド WhenAll および WhenAny を使用してください。これらは Task を返しますが、この Task では制御できません。WhenAll および WhenAny のタスクが完了すると自動的に完了します。その後、タスクの ContinueWith メソッドを使用して、タスクの完了後にタスクを完了します。すぐに別のタスクを開始します

Task.WhenAll(task, task2).ContinueWith((t) =>
{
    Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}");
});
Task.Factory工厂中也存在类似ContinueWhenAll和ContinueWhenAny

3。タスク階層

終了後に別のタスクを実行できるだけでなく、 1 つのタスクでは、タスク内でタスクを開始することもできます。タスク、これは親子階層を開始します

var parentTask = new Task(()=> 
{
    Console.WriteLine($"parentId={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}");
    Thread.Sleep(5*1000);
    var childTask = new Task(() =>
    {
        Thread.Sleep(10 * 1000);
        Console.WriteLine($"childId={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}")
    });
    childTask.Start();
});
parentTask.Start();
親タスクが子タスクより前に終了した場合、親タスクのステータスは WaitingForChildrenToComplete になります。子タスクも完了すると、親タスクのステータスは RanToCompletion になります。もちろん、タスクの作成時に TaskCreationOptions 列挙パラメータを指定します。これにより、タスクの作成と実行のオプションの動作を制御できます

4。列挙パラメータ

タスク作成の簡単な説明 TaskCreationOptions 列挙パラメータでは、タスクの作成時に、タスクの作成と実行のオプションの動作を制御するために使用されるフラグである TaskCreationOptions 列挙パラメータを指定できます AttachedToParent: タスクがタスク階層にアタッチされていることを指定します。特定の親とは、親子関係を確立することを意味します。親タスクは、実行を続行する前に、子タスクが完了するまで待機する必要があります。効果はWaitAllと同じです。上記の例では、サブタスクの作成時に TaskCreationOptions.AttachedToParent が指定されている場合、親タスクも待機中にサブタスクの終了を待ちます。

DenyChildAttach: サブタスクを親にアタッチすることを許可しません。 task

LongRunning: 長時間実行されるタスクを指定します。タスクに時間がかかることが事前にわかっている場合は、これを設定することをお勧めします。このようにして、タスク スケジューラは ThreadPool スレッドを使用する代わりに Thread スレッドを作成します。 ThreadPool スレッドを長期間占有して返さないため、必要に応じてスレッド プール内に新しいスレッドが開かれ、スケジューリング プレッシャーが発生する可能性があります。

PreferFairness: タスクをできるだけ公平に配置します。これは、次のことを意味します。より早くスケジュールされているタスクはより早く実行される可能性が高く、後で実行するようにスケジュールされているタスクはより遅く実行される可能性が高くなります。実際、タスクはスレッド プールのグローバル キューに配置され、ワー​​カー スレッドがそれをめぐって競合します (デフォルトではローカル キューにあります)。

もう 1 つの列挙パラメータは、ContinueWith メソッドの TaskContinuationOptions 列挙パラメータです。上記と同じ機能を持つ複数の列挙値を持つほか、タスクのキャンセルと継続を制御する機能などもあります。

# #Lazy cancel: 継続キャンセルの場合、前のタスクが完了するまで継続の完了を防ぎます。それはどういう意味ですか?

CancellationTokenSource source = new CancellationTokenSource();
source.Cancel();
var task1 = new Task(() => 
{
    Console.WriteLine($"task1 id={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}");
});
var task2 = task1.ContinueWith(t =>
{
    Console.WriteLine($"task2 id={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}");
},source.Token);
var task3 = task2.ContinueWith(t =>
{
    Console.WriteLine($"task3 id={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}");
});
task1.Start();

上の例では、task1->task2->task3 を順番に実行しようとし、その後、 cancelToken を通じて task2 の実行をキャンセルします。結果はどうなるでしょうか?その結果、task1 と task3 が並行して実行されます (task3 も task1 と並行して実行されます。これは、元のチェーンが 2 つのチェーンになることを意味します)。そして、

LazyCancellation,
var task2 = task1.ContinueWith(t =>
{
    Console.WriteLine($"task2 id={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}");
},source.Token,TaskContinuationOptions.LazyCancellation,TaskScheduler.Current);

In を使用しようとします。これでタスク1に入ります 実行完了後、タスク2がsource.Tokenを判定し、キャンセルの場合は実行されません その後、タスク3の実行で元の順序が確保されます

ExecuteSynchronously: 継続タスクを同期的に実行することを指定します。たとえば、上記の例では、継続タスク task2 でこのパラメータが指定されている場合、task2 は task1 を実行するスレッドを使用して実行されます。これにより、スレッドの切り替えが防止され、一部のタスクが許可されます。共有リソースへのアクセス。指定しない場合はランダムになりますが、task1のスレッドも使用できます

NotOnRanToCompletion: 継続タスクは前のタスクの未完了状態で実行する必要があります

OnlyOnRanToCompletion:継続タスクは、前のタスクの完了状態である必要があります。

NotOnFaulted、OnlyOnCanceled、OnlyOnFaulted などを実行するには、

5. タスクのキャンセル

前回の記事でThreadを使用する際、変数isStopマークを使用しました タスクをキャンセルするか否かに関わらず、このシェア変数へのアクセス方法は必ず問題が発生します。 CancelTokenSource クラスは、特にタスクのキャンセルを処理するためにタスクで提案されています。一般的な使用法については、

CancellationTokenSource source = new CancellationTokenSource();//构造函数中也可指定延迟取消
//注册一个取消时调用的委托
source.Token.Register(() =>
{
    Console.WriteLine("当前source已经取消,可以在这里做一些其他事情(比如资源清理)...");
});
var task1 = new Task(() => 
{
    while (!source.IsCancellationRequested)
    {
        Console.WriteLine($"task1 id={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}");
    }
},source.Token);
task1.Start();
//source.Cancel();//取消
source.CancelAfter(1000);//延时取消
6 の下のコード コメントを参照してください。タスクの結果

让子线程返回结果,可以将信息写入到线程安全的共享变量中去,或则使用可以返回结果的任务。使用Task的泛型版本Taskb54c2c292509147c0b54128f4eb90887,就可以定义返回结果的任务。Task是继承自Task的,Result获取结果时是要阻塞等待直到任务完成返回结果的,内部判断没有完成则wait。通过TaskStatus属性可获得此任务的状态是启动、运行、异常还是取消等

var task = new Task<string>(() =>
{
     return "hello ketty";
});
task.Start();
string result = task.Result;

7、异常

可以使用AggregateException来接受任务中的异常信息,这是一个聚合异常继承自Exception,可以遍历获取包含的所有异常,以及进行异常处理,决定是否继续往上抛异常等

var task = Task.Factory.StartNew(() =>
{
    var childTask1 = Task.Factory.StartNew(() =>
    {
        throw new Exception("childTask1异常...");
    },TaskCreationOptions.AttachedToParent);
    var childTask12= Task.Factory.StartNew(() =>
    {
        throw new Exception("childTask2异常...");
    }, TaskCreationOptions.AttachedToParent);
});
try
{
    try
    {
        task.Wait();
    }
    catch (AggregateException ex)
    {
        foreach (var item in ex.InnerExceptions)
        {
            Console.WriteLine($"message{item.InnerException.Message}");
        }
        ex.Handle(x =>
        {
            if (x.InnerException.Message == "childTask1异常...")
            {
                return true;//异常被处理,不继续往上抛了
            }
            return false;
        });
    }
}
catch (Exception ex)
{
    throw;
}

二、并行Parallel

1、Parallel.For()、Parallel.ForEach()

在.NET4中,另一个新增的抽象的线程时Parallel类。这个类定义了并行的for和foreach的静态方法。Parallel.For()和Parallel.ForEach()方法多次调用一个方法,而Parallel.Invoke()方法允许同时调用不同的方法。首先Parallel是会阻塞主线程的,它将让主线程也参与到任务中

Parallel.For()类似于for允许语句,并行迭代同一个方法,迭代顺序没有保证的

ParallelLoopResult result = Parallel.For(0, 10, i =>
{
    Console.WriteLine($"{i} task:{Task.CurrentId} thread:{Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine(result.IsCompleted);

也可以提前中断Parallel.For()方法。For()方法的一个重载版本接受Action0b577780dcee67a906216f9ca0bee521类型参数。一般不使用,像下面这样,本想大于5就停止,但实际也可能有大于5的任务已经在跑了。可以通过ParallelOptions传入允许最大线程数以及取消Token等

ParallelLoopResult result = Parallel.For(0, 10, new ParallelOptions() { MaxDegreeOfParallelism = 8 },(i,loop) =>
{
    Console.WriteLine($"{i} task:{Task.CurrentId} thread:{Thread.CurrentThread.ManagedThreadId}");
    if (i > 5)
    {
        loop.Break();
    }
});

2、Parallel.For484ebffe0129924a37882ea651634562

For还有一个高级泛型版本,相当于并行的聚合计算

ParallelLoopResult For<TLocal>(int fromInclusive, int toExclusive, Func<TLocal> localInit, Func<int, ParallelLoopState, TLocal, TLocal> body, Action<TLocal> localFinally);

像下面这样我们求0…100的和,第三个参数更定一个种子初始值,第四个参数迭代累计,最后聚合

int totalNum = 0;
Parallel.For<int>(0, 100, () => { return 0; }, (current, loop, total) =>
{
    total += current;
    return total;
}, (total) =>
{
    Interlocked.Add(ref totalNum, total);
});

上面For用来处理数组数据,ForEach()方法用来处理非数组的数据任务,比如字典数据继承自IEnumerable的集合等

3、Parallel.Invoke()

Parallel.Invoke()则可以并行调用不同的方法,参数传递一个Action的委托数组

Parallel.Invoke(() => { Console.WriteLine($"方法1 thread:{Thread.CurrentThread.ManagedThreadId}"); }
    , () => { Console.WriteLine($"方法2 thread:{Thread.CurrentThread.ManagedThreadId}"); }
    , () => { Console.WriteLine($"方法3 thread:{Thread.CurrentThread.ManagedThreadId}"); });

4、PLinq

Plinq,为了能够达到最大的灵活度,linq有了并行版本。使用也很简单,只需要将原始集合AsParallel就转换为支持并行化的查询。也可以AsOrdered来顺序执行,取消Token,强制并行等

var nums = Enumerable.Range(0, 100);
var query = from n in nums.AsParallel()
            select new
            {
                thread=$"tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}"
            };

三、异步等待AsyncAwait

异步编程模型,可能还需要大篇幅来学习,这里先介绍下基本用法,内在本质需要用ILSpy反编译来看,以后可能要分专题总结。文末先给几个参考资料,有兴趣自己阔以先琢磨琢磨鸭

1、简单使用

这是.NET4.5开始提供的一对语法糖,使得可以较简便的使用异步编程。async用在方法定义前面,await只能写在带有async标记的方法中,任何方法都可以增加async,一般成对出现,只有async没有意义,只有await会报错,请先看下面的示例

private static async void AsyncTest()
{
    //主线程执行
    Console.WriteLine($"before await ThreadId={Thread.CurrentThread.ManagedThreadId}");
    TaskFactory taskFactory = new TaskFactory();
    Task task = taskFactory.StartNew(() =>
    {
        Thread.Sleep(3000);
        Console.WriteLine($"task ThreadId={Thread.CurrentThread.ManagedThreadId}");
    });
    await task;//主线程到这里就返回了,执行主线程任务
    //子线程执行,其实是封装成委托,在task之后成为回调(编译器功能  状态机实现) 后面相当于task.ContinueWith()
    //这个回调的线程是不确定的:可能是主线程  可能是子线程  也可能是其他线程,在winform中是主线程
    Console.WriteLine($"after await ThreadId={Thread.CurrentThread.ManagedThreadId}");
}

一般使用async都会让方法返回一个Task的,像下面这样复杂一点的

private static async Task<string> AsyncTest2()
{
    Console.WriteLine($"before await ThreadId={Thread.CurrentThread.ManagedThreadId}");
    TaskFactory taskFactory = new TaskFactory();
    string x = await taskFactory.StartNew(() =>
      {
          Thread.Sleep(3000);
          Console.WriteLine($"task ThreadId={Thread.CurrentThread.ManagedThreadId}");
          return "task over";
      });
    Console.WriteLine($"after await ThreadId={Thread.CurrentThread.ManagedThreadId}");
    return x;
}

通过var reslult = AsyncTest2().Result;调用即可。但注意如果调用Wait或Result的代码位于UI线程,Task的实际执行在其他线程,其需要返回UI线程则会造成死锁,所以应该Async all the way

2、优雅

从上面简单示例中可以看出异步编程的执行逻辑:主线程A逻辑->异步任务线程B逻辑->主线程C逻辑。

异步方法的返回类型只能是void、Task、Task。示例中异步方法的返回值类型是Task,通常void也不推荐使用,没有返回值直接用Task就是

上一篇也大概了解到如果我们要在任务中更新UI,需要调用Invoke通知UI线程来更新,代码看起来像下面这样,在一个任务后去更新UI

private void button1_Click(object sender, EventArgs e)
{
    var ResultTask = Task.Run(() => {
        Thread.Sleep(5000);
        return "任务完成";
    });
    ResultTask.ContinueWith((r)=> 
    {
        textBox1.Invoke(() => {
            textBox1.Text = r.Result;
        });
    });
}

如果使用async/await会看起来像这样,是不是优雅了许多。以看似同步编程的方式实现异步

private async void button1_Click(object sender, EventArgs e)
{
    var t = Task.Run(() => {
        Thread.Sleep(5000);
        return "任务完成";
    });
    textBox1.Text = await t;
}

3、最后

在.NET 4.5中引入的Async和Await两个新的关键字后,用户能以一种简洁直观的方式实现异步编程。甚至都不需要改变代码的逻辑结构,就能将原来的同步函数改造为异步函数。

在内部实现上,Async和Await这两个关键字由编译器转换为状态机,通过System.Threading.Tasks中的并行类实现代码的异步执行。

本文来自 C#.Net教程 栏目,欢迎学习!  

以上がasync 非同期、スレッド .NET のマルチスレッドの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はcnblogs.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。