您的位置:时时app平台注册网站 > 编程知识 > [翻译]在 .NET Core 中的并发编程时时app平台注册网

[翻译]在 .NET Core 中的并发编程时时app平台注册网

2019-11-28 03:17
  • 多段代码正在并发运行;
  • 这几段代码在访问(读或写)同一个数据;
  • 至少有一段代码在修改数据。

处理异常

将两个线程合并在一起的时候,任务抛出的任何异常将被传递到调用线程中:

  • 如果使用 Result 或 Wait() ,它们将被打包到 AggregateException 中。实际的异常将被抛出并存储在其 InnerException 属性中。
  • 如果您使用 await,原来的异常将不会被打包。

在这两种情况下,调用堆栈的信息将保持不变。

说明:

3、阻塞信号   ManualResetEventSlim

  需要从一个线程发送信号给另外一个线程

  

 public class MyClass
    {
        private readonly ManualResetEventSlim _resetEvent = new ManualResetEventSlim();

        private int _value;

        private int WaitForInitialization()
        {
            _resetEvent.Wait();
            return _value;
        }

        private void InitializeFromAnotherThread()
        {
            _value = 10;
            _resetEvent.Set();
        }

    }

ManualResetEventSlim 是功能强大、通用的线程间信号,但必须合理地使用

 

任务并行库

.NET Framework 4 引入了任务并行库 (TPL) 作为编写并发代码的首选 API。.NET Core采用相同的编程模式。
要在后台运行一段代码,需要将其包装成一个 任务

var backgroundTask = Task.Run(() => DoComplexCalculation(42));
// do other work
var result = backgroundTask.Result;

当需要返回结果时,Task.Run 方法接收一个 函数 (Func) ;当不需要返回结果时,方法 Task.Run 接收一个 动作 (Action) 。当然,所有的情况下都可以使用 lambda 表达式,就像我上面例子中调用带一个参数的长时间方法。
线程池中的某个线程将会处理任务。.NET Core 的运行时包含一个默认调度程序,使用线程池来处理队列并执行任务。您可以通过派生 TaskScheduler 类实现自己的调度算法,代替默认的,但这超过本文的讨论范围。
正如我们之前所见,我使用 Result 属性来合并被调用的后台线程。对于不需要返回结果的线程,我可以调用 Wait() 来代替。这两种方式都将被堵塞到后台任务完成。
为了避免堵塞调用线程 ( 如在ASP.NET Core应用程序中) ,可以使用 await 关键字:

var backgroundTask = Task.Run(() => DoComplexCalculation(42));
// do other work
var result = await backgroundTask;

这样被调用的线程将被释放以便处理其他传入请求。一旦任务完成,一个可用的工作线程将会继续处理请求。当然,控制器动作方法必须是异步的:

public async Task<iactionresult> Index() {     // method body }

1)可以将这个修饰符用于类和struct的字段,但不能声明使用volatile关键字的局部变量。

  如果程序中用到了并发技术,一段代码需要修改数据,同时其他代码需要访问同一数据。

协调多任务

如果你需要运行多个后台任务,这里有些方法可以帮助到你。
要同时运行多个任务,只需连续启动它们并收集它们的引用,例如在数组中:

var backgroundTasks = new []
{
    Task.Run(() => DoComplexCalculation(1)),
    Task.Run(() => DoComplexCalculation(2)),
    Task.Run(() => DoComplexCalculation(3))
};

现在你可以使用 Task 类的静态方法,等待他们被异步或者同步执行完毕。

// wait synchronously
Task.WaitAny(backgroundTasks);
Task.WaitAll(backgroundTasks);
// wait asynchronously
await Task.WhenAny(backgroundTasks);
await Task.WhenAll(backgroundTasks);

实际上,这两个方法最终都会返回所有自身的任务,可以像任何其他任务一样再次操作。为了获取对应任务的结果,你可以检查该任务的 Result 属性。
处理多任务的异常有点棘手。方法 WaitAll 和 WhenAll 不管哪个任务被收集到异常时都会抛出异常。不过,对于 WaitAll ,将会收集所有的异常到对应的 InnerExceptions 属性;对于 WhenAll ,只会抛出第一个异常。为了确认哪个任务抛出了哪个异常,您需要单独检查每个任务的 Status 和 Exception 属性。
在使用 WaitAny 和 WhenAny 时必须足够小心。他们会等到第一个任务完成 (成功或失败),即使某个任务出现异常时也不会抛出任何异常。他们只会返回已完成任务的索引或者分别返回已完成的任务。你必须等到任务完成或访问其 result 属性时捕获异常,例如:

var completedTask = await Task.WhenAny(backgroundTasks);
try
{
    var result = await completedTask;
}
catch (Exception e)
{
    // handle exception
}

如果你想连续运行多个任务,代替并发任务,可以使用延续 (continuations)的方式:

var compositeTask = Task.Run(() => DoComplexCalculation(42))
    .ContinueWith(previous => DoAnotherComplexCalculation(previous.Result), 
        TaskContinuationOptions.OnlyOnRanToCompletion)

ContinueWith() 方法允许你把多个任务一个接着一个执行。这个延续的任务将获取到前面任务的结果或状态的引用。 你仍然可以增加条件判断是否执行延续任务,例如只有在前面任务成功执行或者抛出异常时。对比连续等待多个任务,提高了灵活性。
当然,您可以将延续任务与之前讨论的所有功能相结合:异常处理、取消和并行运行任务。这就有了很大的表演空间,以不同的方式进行组合:

var multipleTasks = new[]
{
    Task.Run(() => DoComplexCalculation(1)),
    Task.Run(() => DoComplexCalculation(2)),
    Task.Run(() => DoComplexCalculation(3))
};
var combinedTask = Task.WhenAll(multipleTasks);

var successfulContinuation = combinedTask.ContinueWith(task =>
        CombineResults(task.Result), TaskContinuationOptions.OnlyOnRanToCompletion);
var failedContinuation = combinedTask.ContinueWith(task =>
        HandleError(task.Exception), TaskContinuationOptions.NotOnRanToCompletion);

await Task.WhenAny(successfulContinuation, failedContinuation);

5) public long CurrentPhaseNumber { get; internal set; }获取屏障的当前阶段编号。

  如果以下三个条件都满足,就需要使用同步来保护数据。

今天我们购买的每台电脑都有一个多核心的 CPU,允许它并行执行多个指令。操作系统通过将进程调度到不同的内核来发挥这个结构的优点。
然而,还可以通过异步 I/O 操作和并行处理来帮助我们提高单个应用程序的性能。
在.NET Core中,任务 (tasks) 是并发编程的主要抽象表述,但还有其他支撑类可以使我们的工作更容易。

1.简介

2、异步锁  SemaphoreSlim

  多个代码需要安全读写数据,并且这些代码块可能使用await语句。同步锁的规则同样适用于异步锁。

  

 public class MyClass
    {
        /// <summary>
        /// 次锁保护_value
        /// </summary>
        private readonly SemaphoreSlim _mutx = new SemaphoreSlim(1);

        private int _value;

        public async Task DelayAndIncrementAsync()
        {
            await _mutx.WaitAsync();
            try
            {
                await Task.Delay(TimeSpan.FromSeconds(_value));
                _value = _value   1;
            }
            finally
            {
                _mutx.Release();
            }
        }
    }

 

并发编程 - 异步 vs. 多线程代码

并行编程是一个广泛的术语,我们应该通过观察异步方法和实际的多线程之间的差异展开探讨。
尽管 .NET Core 使用了任务来表达同样的概念,一个关键的差异是内部处理的不同。
调用线程在做其他事情时,异步方法在后台运行。这意味着这些方法是 I/O 密集型的,即他们大部分时间用于输入和输出操作,例如文件或网络访问。
只要有可能,使用异步 I/O 方法代替同步操作很有意义。相同的时间,调用线程可以在处理桌面应用程序中的用户交互或处理服务器应用程序中的同时处理其他请求,而不仅仅是等待操作完成。

*你可以在我的文章 [Asynchronous Programming in C# using Async Await – Best Practices]() 中阅读更多关于使用 asyncawait 调用异步方法。该文章来自 [DNC Magazine]() (9月刊) 。*

计算密集型的方法要求 CPU 周期工作,并且只能运行在他们专用的后台线程中。CPU 的核心数限制了并行运行时的可用线程数量。操作系统负责在剩余的线程之间切换,使他们有机会执行代码。
这些方法仍然被并发地执行,却不必被并行地执行。尽管这意味着方法不是同时执行,却可以在其他方法暂停的时候执行。

并行 vs 并发

本文将在最后一段中重点介绍 在 .NET Core中多线程并发编程

5) public void Dispose();释放资源。

5、限流

  有一段高度并发的代码,由于它的并发程度实在太高了,需要有方法对并发性进行限流。可以避免数据项占用太多的内存。

  如果发现程序的CPU或者网络连接数太多了,或者内存占用太多,就需要进行限流。

  

  数据流和并行代码都自带了对并发性限流的方法:

    

 IEnumerable<int> ParallelMultiplyBy2(IEnumerable<int> values)
        {
            return values.AsParallel()
            .WithDegreeOfParallelism(10)
            .Select(item => item * 2);
        }

  

  并发性异步代码可以使用 SemaphoreSlim 来限流

  

  async Task<string[]> DownloadUrlsAsync(IEnumerable<string> urls)
        {
            var httpClient = new HttpClient();
            var semaphore = new SemaphoreSlim(10);
            var tasks = urls.Select(async url =>
            {
                await semaphore.WaitAsync();
                try
                {
                    return await httpClient.GetStringAsync(url);
                }
                finally
                {
                    semaphore.Release();
                }
            }).ToArray();
            return await Task.WhenAll(tasks);
        }

 

 

  

并发集合

当一个临界区需要确保对数据结构的原子访问时,用于并发访问的专用数据结构可能是更好和更有效的替代方案。例如,使用 ConcurrentDictionary 而不是 Dictionary,可以简化 lock 语句示例:

var counters = new ConcurrentDictionary< int, int >();

counters.TryAdd(key, 0);
lock (syncObject)
{
    counters[key]  ;
}

自然地,也有可能像下面一样:

counters.AddOrUpdate(key, 1, (oldKey, oldValue) => oldValue   1);

因为 update 的委托是临界区外面的方法,因此,第二个线程可能在第一个线程更新值之前,读取到同样的旧值,使用自己的值有效地覆盖了第一个线程的更新值,这就丢失了一个增量。错误使用并发集合也是无法避免多线程带来的问题。
并发集合的另一个替代方案是 不变的集合 (immutable collections)
类似于并发集合,同样是线程安全的,但是底层实现是不一样的。任何关改变数据结构的操作将不会改变原来的实例。相反,它们返回一个更改后的副本,并保持原始实例不变:

var original = new Dictionary< int, int >().ToImmutableDictionary();
var modified = original.Add(key, value);

因此在一个线程中对集合任何更改对于其他线程来说都是不可见的。因为它们仍然引用原来的未修改的集合,这就是不变的集合本质上是线程安全的原因。
当然,这使得它们对于解决不同集合的问题很有效。最好的情况是多个线程在同一个输入集合的情况下,独立地修改数据,在最后一步可能为所有线程合并变更。而使用常规集合,需要提前为每个线程创建集合的副本。

1)通过使用lock关键字可以获得一个对象的互斥锁。

1、阻塞锁    lock

  多个线程需要安全的读写共享数据。

  一个线程进入锁后,在锁被释放之前,其他线程是无法进入的。

  锁的使用,有四条重要的规则:

  • 限制锁的作用范围
  • 文档中写清锁保护的内容
  • 锁范围内的代码尽量少
  • 在控制锁的时候,绝不运行随意的代码

  首先,要尽量限制锁的作用范围。应该把 lock 语句使用的对象设为私有成员,并且永远不
要暴露给非本类的方法。每个类型通常最多只有一个锁。如果一个类型有多个锁,可考虑通
过重构把它分拆成多个独立的类型。可以锁定任何引用类型,但是我建议为 lock 语句定义
一个专用的成员,就像最后的例子那样。尤其是千万不要用 lock(this),也不要锁定 Type
或 string 类型的实例。因为这些对象是可以被其他代码访问的,这样锁定会产生死锁。
  第二,要在文档中描述锁定的内容。这种做法在最初编写代码时很容易被忽略,但是在代
码变得复杂后就会变得很重要。
  第三,在锁定时执行的代码要尽可能得少。要特别小心阻塞调用。在锁定时不要做任何阻
塞操作。
  最后,在锁定时绝不要调用随意的代码。随意的代码包括引发事件、调用虚拟方法、调用
委托。如果一定要运行随意的代码,就在释放锁之后运行

 

结论:

每当应用程序包含可以并行运行的 CPU 密集型代码时,利用并发编程来提高性能并提高硬件利用率是很有意义的。
.NET Core 中的 API 抽象了许多细节,使编写并发代码更容易。然而需要注意某些潜在的问题, 其中大部分涉及从多个线程访问共享数据。
如果可以的话,你应该完全避免这种情况。如果不行,请确保选择最合适的同步方法或数据结构。


本文采用 知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议
转载请注明:<strong>作者 张很水


  1. 原文为: "This allows it to synchronize access to a resource not only on thread boundaries, but even over process boundaries",为避免出现翻译后的谬误,这里参照 C# 高级编程 (第9版) 对 Mutex 的解释。还望有识之士可以提供翻译建议。 ↩

3)public int Release();退出 System.Threading.SemaphoreSlim 一次。

4、异步信号   

  需要在代码的各个部分间发送通知,并且要求接收方必须进行异步等待。

  

 public class MyClass
    {
        private readonly TaskCompletionSource<object> _initialized = new TaskCompletionSource<object>();

        private int _value1;
        private int _value2;

        public async Task<int> WaitForInitializationAsync()
        {
            await _initialized.Task;
            return _value1   -_value2;
        }

        public void Initialize()
        {
            _value1 = 10;
            _value2 = 5;
            _initialized.TrySetResult(null);
        }

    }

  在所有情况下都可以用 TaskCompletionSource<T> 来异步地等待:本例中,通知来自于另一
部分代码。如果只需要发送一次信号,这种方法很适合。但是如果要打开和关闭信号,这
种方法就不大合适了.。

 

任务同步

如果任务是完全独立的,那么我们刚才看到的协调方法就已足够。然而,一旦需要同时共享数据,为了防止数据损坏,就必须要有额外的同步。
两个以及更多的线程同时更新一个数据结构时,数据很快就会变得不一致。就好像下面这个示例代码一样:

var counters = new Dictionary< int, int >();

if (counters.ContainsKey(key))
{
    counters[key]   ;
}
else
{
    counters[key] = 1;
}

当多个线程同时执行上述代码时,不同线程中的特定顺序执行指令可能导致数据不正确,例如:

  • 所有线程将会检查集合中是否存在同一个 key
  • 结果,他们都会进入 else 分支,并将这个 key 的值设为1
  • 最后结果将会是1,而不是2。如果是接连着执行代码的话,将会是预期的结果。

上述代码中,**临界区 (critical section) ** 一次只允许一个线程可以进入。在C# 中,可以使用 lock 语句来实现:

var counters = new Dictionary< int, int >();

lock (syncObject)
{
    if (counters.ContainsKey(key))
    {
        counters[key]  ;
    }
    else
    {
        counters[key] = 1;
    }
}

在这个方法中,所有线程都必须共享相同的的 syncObject 。作为最佳做法,syncObject 应该是一个专用的 Object 实例,专门用于保护对一个独立的临界区的访问,避免从外部访问。
在 lock 语句中,只允许一个线程访问里面的代码块。它将阻止下一个尝试访问它的线程,直到前一个线程退出。这将确保线程完整执行临界区代码,而不会被另一个线程中断。当然,这将减少并行性并减慢代码的整体执行速度,因此您最好最小化临界区的数量并使其尽可能的短。

使用 Monitor 类来简化 lock 声明:

var lockWasTaken = false;
var temp = syncObject;
try
{
    Monitor.Enter(temp, ref lockWasTaken);
    // lock statement body
}
finally
{
    if (lockWasTaken)
    {
        Monitor.Exit(temp);
    }
}

尽管大部分时间您都希望使用 lock 语句,但 Monitor 类可以在需要时给予额外的控制。例如,您可以使用 TryEnter() 而不是 Enter(),并指定一个限定时间,避免无止境地等待锁释放。

主要成员:

  同步的类型:a.通信  b.数据保护。

原文地址:http://www.dotnetcurry.com/dotnet/1360/concurrent-programming-dotnet-core

2)内存开销非常小。其是一个结构体,不会产生不必要的内存开销。

在完整的 .NET Framework 中并发编程

由于 .NET Core 是完整的 .NET Framework 的简化实现,所以 .NET Framework 中所有并行编程方法也可以在.NET Core 中使用。唯一的例外是不变的集合,它们不是完整的 .NET Framework 的组成部分。它们作为单独的 NuGet 软件包(System.Collections.Immutable)分发,您需要在项目中安装使用。

注意:

并行LINQ (PLINQ)

并行LINQ (PLINQ) 是 Task Parallel Library 的替代方案。顾名思义,它很大程度上依赖于 LINQ(语言集成查询)功能。对于在大集合中执行相同的昂贵操作的场景是很有用的。与所有操作都是顺序执行的普通 LINQ to Objects 不同的是,PLINQ可以在多个CPU上并行执行这些操作。
发挥优势所需要的代码改动也是极小的:

// sequential execution
var sequential = Enumerable.Range(0, 40)
    .Select(n => ExpensiveOperation(n))
    .ToArray();

// parallel execution
var parallel = Enumerable.Range(0, 40)
    .AsParallel()
    .Select(n => ExpensiveOperation(n))
    .ToArray();

如你所见,这两个代码片段的不同仅仅是调用 AsParallel()。这将IEnumerable 转换为 ParallelQuery,导致查询的部分并行运行。要切换为回顺序执行,您可以调用 AsSequential(),它将再次返回一个IEnumerable。
默认情况下,PLINQ 不保留集合中的顺序,以便让进程更有效率。但是当顺序很重要时,可以调用 AsOrdered():

var parallel = Enumerable.Range(0, 40)
    .AsParallel()
    .AsOrdered()
    .Select(n => ExpensiveOperation(n))
    .ToArray();

同理,你可以通过调用 AsUnordered() 切换回来。

4.ManualResetEvent与ManualResetEventSlim

取消任务

由于任务是可以长时间运行的,所以你可能想要有一个可以提前取消任务的选项。实现这个选项,需要在任务创建的时候传入取消的令牌 (token),之后再使用令牌触发取消任务:

var tokenSource = new CancellationTokenSource();
var cancellableTask = Task.Run(() =>
{
    for (int i = 0; i < 100; i  )
    {
        if (tokenSource.Token.IsCancellationRequested)
        {
            // clean up before exiting
            tokenSource.Token.ThrowIfCancellationRequested();
        }
        // do long-running processing
    }
    return 42;
}, tokenSource.Token);
// cancel the task
tokenSource.Cancel();
try
{
    await cancellableTask;
}
catch (OperationCanceledException e)
{
    // handle the exception
}

实际上,为了提前取消任务,你需要检查任务中的取消令牌,并在需要取消的时候作出反应:在执行必要的清理操作后,调用 ThrowIfCancellationRequested() 退出任务。这个方法将会抛出 OperationCanceledException,以便在调用线程中执行相应的处理。

3)public static void TryEnter(object obj, int millisecondsTimeout, ref bool lockTaken);在指定的毫秒数中,尝试获取指定对象上的排他锁,并自动设置一个值,指示是否得到了该锁。

其他同步基元

Monitor 只是 .NET Core 中众多同步基元的一员。根据实际情况,其他基元可能更适合。

Mutex 是 Monitor 更重量级的版本,依赖于底层的操作系统,提供跨多个进程同步访问资源[[1]](https://www.jianshu.com/p/c6ba98367f5e#fn1), 是针对 Mutex 进行同步的推荐替代方案。

SemaphoreSlim 和 Semaphore 可以限制同时访问资源的最大线程数量,而不是像 Monitor 一样只能限制一个线程。 SemaphoreSlim 比 Semaphore 更轻量,但仅限于单个进程。如果可能,您最好使用 SemaphoreSlim 而不是 Semaphore。

ReaderWriterLockSlim 可以区分两种对访问资源的方式。它允许无限数量的读取器 (readers) 同时访问资源,并且限制同时只允许一个写入器 (writers) 访问锁定资源。读取时线程安全,但修改数据时需要独占资源,很好地保护了资源。

AutoResetEvent、ManualResetEvent 和 ManualResetEventSlim 将堵塞传入的线程,直到它们接收到一个信号 (即调用 Set() )。然后等待中的线程将继续执行。AutoResetEvent 在下一次调用 Set() 之前,将一直阻塞,并只允许一个线程继续执行。ManualResetEvent 和 ManualResetEventSlim 不会堵塞线程,除非 Reset() 被调用。ManualResetEventSlim 比前两者更轻量,更值得推荐。

Interlocked 提供一种选择——原子操作,这是替代 locking 和其他同步基元更好的选择(如果适用):

// non-atomic operation with a lock
lock (syncObject)
{
    counter  ;
}
// equivalent atomic operation that doesn't require a lock
Interlocked.Increment(ref counter);

1) public int InitialCount { get; } 获取设置事件时最初的信号数。

1)public Barrier(int participantCount, Action<Barrier> postPhaseAction);构造 函数,participantCount:参与的线程个数(参与者的个数), postPhaseAction每个阶段后执行的操作。

主要成员:

2)使用lock,这会调用System.Threading.Monitor.Enter(object obj, ref bool lockTaken)和System.Threading.Monitor.Exit(object obj)方法。

1)public static void SpinUntil(Func<bool> condition);在指定条件得到满足之前自旋。

主要成员:

新的轻量级同步原语:Barrier,CountdownEvent,ManualResetEventSlim,SemaphoreSlim,SpinLock,SpinWait。轻量级同步原语只能用在一个进程内。而相应的那些重量级版本支持跨进程的同步。

1) public CountdownEvent(int initialCount);

主要成员:

6)public int ParticipantsRemaining { get; }获取屏障中尚未在当前阶段发出信号的参与者的数量。每当新阶段开始时,这个值等于ParticipantCount ,每当有参与者调用这个属性时,其减一。

说明:

主要成员:

8.Look:互斥锁

 1 public static void SemaphoreSlimTest()
 2 {
 3             int initialCount = 10;//可以是其他值
 4             List<string> list = new List<string>();
 5             var tasks = new Task[initialCount];
 6        //如果使用Semaphore,实例化的时候,那么最少要传递两个参数,信号量的初始请求数和信号量的最大请求数
 7             using(SemaphoreSlim ssl = new SemaphoreSlim(initialCount))
 8             {
 9                 for (int i = 0; i < initialCount; i  )
10                 {
11                     int j = i;
12                     tasks[j] = Task.Factory.StartNew(() =>
13                     {
14                         try
15                         {
16                             //等待,直到进入SemaphoreSlim为止
17                  //如果使用Semaphore,应调用WaitOne
18                             ssl.Wait();
19                             //直到进入SemaphoreSlim才会执行下面的代码
20                             list.Add("" j);//可将这部分替换成真实的业务代码
21                         }
22                         finally
23                         {
24                             ssl.Release();
25                         }
26                     });
27                 }
28                 //注意一定要在using块的最后阻塞线程,直到所有的线程均处理完任务
29                 //如果没有等待任务全部完成的语句,会导致SemaphoreSlim资源被提前释放。
30                 Task.WaitAll(tasks);
31             }          
32 }

Semaphore:可实现跨进程或AppDomain的同步,可使用WaitHandle操作递减信号量的计数。

2)public int CurrentCount { get; } 获取将允许进入 System.Threading.SemaphoreSlim 的线程的数量。

 

3)public bool Set();将事件状态设置为终止状态,允许一个或多个等待线程继续,返回值指示操作是否成功。

6)public ManualResetEventSlim(bool initialState, int spinCount);

1)public static void Enter(object obj, ref bool lockTaken);获取指定对象上的排他锁,并自动设置一个值,指示是否获取了该锁。

12 使用模式

4)public static int Decrement(ref int location);以原子操作的形式递减指定变量的值并存储结果。

说明:

1)不要对值类型使用Monitor。

6)SpinWait

作用:

1)public static int Increment(ref int location);以原子操作的形式递增指定变量的值并存储结果。

2)public static int Add(ref int location1, int value);对两个 32 位整数进行求和并用和替换第一个整数,上述操作作为一个原子操作完成。

注意:

5)SpinLock

5)public WaitHandle AvailableWaitHandle { get; }返回一个可用于在信号量上等待的 System.Threading.WaitHandle。

3)如果自旋时间过长,SpinWait会让出底层线程的时间片并触发上下文切换。

主要成员:

3)ManualResetEvent与ManualResetEventSlim

2)public static void Exit(object obj);释放指定对象上的排他锁。

 1 下面两段代码是等价的。
 2 lock (Object)
 3 {
 4        //do something
 5 }
 6 
 7 //等价代码
 8 bool lockTaken = false;
 9 try
10 {
11     Monitor.Enter(object,lockTaken);
12      //do something
13 }
14 finally
15 {
16      if(lockTaken)
17      {
18             Monitor.Exit(object);
19        }
20 }

3)不要对值类型使用Lock

ManualResetEvent:可实现跨进程或AppDomain的同步。

3) public bool SignalAndWait(int millisecondsTimeout); 如果所有参与者都已在指定时间内达到屏障,则为 true;否则为 false。

2) public bool Signal();向 CountdownEvent 注册信号,同时减小CurrentCount的值。

2)public bool Set();将事件状态设置为终止状态,允许一个或多个等待线程继续,返回值指示操作是否成功。

2)避免锁定类的外部对象,避免跨成员或类的边界获得和释放一个锁,避免获得锁的时候调用未知代码。

11.Interlocked:为多任务或线程共享的变量提供原子操作

 

时间仓促,水平有限,如有不当之处,欢迎指正。

1)public SemaphoreSlim(int initialCount, int maxCount);

7) Look

一定要确保每个参与工作的线程都调用了Signal,如果有至少一个没有调用,那么任务会永久阻塞。所以一般在finally块中调用Signal是个好习惯。

 1 public static void SpinLockTest()
 2 {
 3             bool lockTaken = false;
 4             SpinLock sl = new SpinLock(true);
 5             try
 6             {
 7                 //获得锁
 8                 //如果不能获得锁,将会等待并不断检测锁是否可用
 9                 //获得锁后,lockTaken为true,此行代码之后的部分才会开始运行
10                 sl.Enter(ref lockTaken);
11 
12                 //或使用含有超时机制的TryEnter方法
13                 //sl.TryEnter(1000,ref lockTaken);
14                 //然后抛出超时异常
15                 //if (!lockTaken)
16                 //{
17                 //    throw new TimeoutException("超时异常");
18                 //}
19 
20                 //真正的业务。。。
21                 
22             }
23             finally
24             {
25                 if (lockTaken)
26                 {
27                     //释放锁
28                     //SpinLock没有使用内存屏障,因此设成false
29                     sl.Exit(false);
30                 }
31             }                        
32 }

8)Interlocked

2)确保每次任务结束后都释放锁。

ManualResetEventSlim:不可应用于跨进程的同步。

1)Barrier

7.SpinWait:基于自旋的等待

2.Barrier 

4)避免锁定类的外部对象,避免跨成员或类的边界获得和释放一个锁,避免获得锁的时候调用未知代码。

1)适用情形:等待某个条件满足需要的时间很短,并且不希望发生昂贵的上下文切换。

主要成员:

使用完Semaphore立即调用Dispose()方法释放资源。

2)public static bool SpinUntil(Func<bool> condition, int millisecondsTimeout);在指定条件得到满足或指定超时过期之前自旋。

主要成员:

1)public void Enter(ref bool lockTaken); 采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 lockTaken 以确定是否已获取锁。

2)public int Release();退出信号量并返回前一个计数。

2) public void Reset();将事件状态设置为非终止状态,从而导致线程受阻,返回值指示操作是否成功。

9.Monitor

SemaphoreSlim:不可实现跨进程或AppDomain的同步,不可使用WaitHandle操作递减信号量的计数。

说明:

2)CountdownEvent

2)Barrier使用完要调用Dispose()方法释放资源


3) public void Reset(int count);将 System.Threading.CountdownEvent.InitialCount 属性重新设置为指定值。

主要成员:

3)public static float CompareExchange(ref float location1, float value, float comparand); 比较两个单精度浮点数是否相等,如果相等,则替换其中一个值。

 1 public static void InterlockedTest()
 2 {
 3             Task[] tasks = new Task[10];
 4             long j = 0;
 5             for(int i=0;i<10;i  )
 6             {
 7                 int t = i;
 8                 tasks[t] = Task.Factory.StartNew(()=>
 9                 {
10                 //以安全的方式递增j
11                     Interlocked.Increment(ref j);
12                 });
13             }
14             Task.WaitAll(tasks);           
15 }

2) public void SignalAndWait();发出信号,表示参与者已达到屏障并等待所有其他参与者也达到屏障。

注意:

1) 每当屏障(Barrier实例)接收到来自所有参与者的信号之后,屏障就会递增其阶段数,运行构造函数中指定的动作,并且解除阻塞每一个参与者。

 1 public static void CountdownEventTest()
 2 {
 3             //注意初始化信号数等于并行的任务数
 4             int initialCount = N;
 5             using (CountdownEvent cd = new CountdownEvent(initialCount))
 6             {
 7                 //多个并行任务,完成一个减少一个信号
 8                 for (int i = 0; i < N; i  )
 9                 {
10                     Task.Factory.StartNew(() => 
11                     {
12                         try
13                         {
14                             //真正的业务
15                         }
16                         finally
17                         {
18                             //确保不论何种情况都能减少信号量,防止死循环
19                             cd.Signal();
20                         }
21                     });
22                 }
23 
24                 //等待上述多个任务执行完毕
25                 cd.Wait();
26             }
27 }

使用完SemaphoreSlim立即调用Dispose()方法释放资源。

注意:

 1 public static void BarrierTest1()
 2 {
 3             //构造函数的参数participantCount表示参与者的数量。
 4             //注意:父线程也是一个参与者,所以两个任务,但是Barrier的participantCount为3
 5             //注意:无法保证任务1和任务2完成的先后顺序。
 6             //Barrier(int participantCount, Action<Barrier> postPhaseAction);也可使 用此方法
 7             //当所有参与者都已到达屏障后,执行要处理的任务,即对两个任务产生的数据统一处理的过程可放在此处执行。
 8             using (Barrier bar = new Barrier(3))
 9             {
10                 Task.Factory.StartNew(() =>
11                 {
12 
13                     //具体业务
14 
15                     //当业务完成时,执行下面这行代码;发出信号,表明任务已完成,并等待其他参与者
16                     bar.SignalAndWait();
17 
18                 });
19 
20                 Task.Factory.StartNew(() =>
21                 {
22 
23                     //具体业务
24 
25                     //当业务完成时,执行下面这行代码;发出信号,表明任务已完成,并等待其他参与者
26                     bar.SignalAndWait();
27 
28                 });
29 
30                 //保证上面两个任务都能完成才执行bar.SignalAndWait();这一句之后的代码
31                 bar.SignalAndWait();
32                 //当上述两个任务完成后,对两个任务产生的数据进行统一处理。
33 
34             }
35 
36         }
37 
38         public static void BarrierTest2()
39         {
40             //构造函数的参数participantCount表示参与者的数量。
41             using (Barrier bar = new Barrier(3))
42             {
43                 Task.Factory.StartNew(() =>
44                 {
45 
46                     //具体业务
47 
48                     //当业务完成时,执行下面这行代码;移除一个参与者
49                     //注意:bar.SignalAndWait();与bar.RemoveParticipant();可以混用
50                     bar.RemoveParticipant();
51 
52                 });
53 
54                 Task.Factory.StartNew(() =>
55                 {
56 
57                     //具体业务
58 
59                     //当业务完成时,执行下面这行代码;移除一个参与者
60                     bar.RemoveParticipant();
61 
62                 });
63 
64                 bar.SignalAndWait();
65                 //当上述两个任务完成后,对两个任务产生的数据进行统一处理。
66             }
67 }

1) public bool IsSet { get; }获取是否设置了事件。

2)public void Exit(bool useMemoryBarrier);释放锁

 1 public static void ManualResetEventTest()
 2 {
 3             ManualResetEvent mre = new ManualResetEvent(false);
 4             ManualResetEvent mre1 = new ManualResetEvent(false);
 5 
 6             try
 7             {
 8                 Task.Factory.StartNew(() =>
 9                 {
10                     //业务
11                     mre.Set();
12                 });
13 
14                 Task.Factory.StartNew(() =>
15                 {
16                     mre.WaitOne();
17 
18                     //使用任务1的数据
19                     
20                     mre1.Set();
21 
22                 });
23 
24                 //等待任务全部执行完
25                 mre1.WaitOne();
26             }
27             finally
28             {
29                 mre.Dispose();
30                 mre1.Dispose();
31             }
32         }
33         //注意:本示例并不是一个最佳实践,目的在于演示ManualResetEventSlim
34         //当没有更好的协调机制时,可考虑使用本示例
35         public static void ManualResetEventSlimTest1()
36         {
37             ManualResetEventSlim mreslim = new ManualResetEventSlim();
38             ManualResetEventSlim mreslim1 = new ManualResetEventSlim();
39             try
40             {
41                 Task.Factory.StartNew(() =>
42                 {
43                     mreslim.Set();
44 
45                     //业务
46 
47                     mreslim.Reset();
48 
49                 });
50 
51                 Task.Factory.StartNew(() =>
52                 {
53                     //当mreslim.Set()被调用时,mreslim.Wait()立即返回,即解除阻塞。
54                     mreslim.Wait();
55                     //直到mreslim.Reset()被调用,循环才会结束
56                     while (mreslim.IsSet)
57                     {
58                         //业务
59                     }
60                     mreslim1.Set();
61                 });
62 
63                 //等待第二个任务完成
64                 mreslim1.Wait();
65             }
66             finally
67             {
68                 mreslim.Dispose();
69                 mreslim1.Dispose();
70             }
71 }

5.Semaphore与SemaphoreSlim

1)public bool Reset();将事件状态设置为非终止状态,导致线程阻止,返回值指示操作是否成功。

3.CountdownEvent

 1 public static void SpinWaitTest()
 2 {
 3             bool isTrue = false;
 4             //任务一,处理业务,成功将isTrue设置为true
 5             Task.Factory.StartNew(() => 
 6             {
 7                 //处理业务,返回结果result指示是否成功
 8                 bool result = ...;
 9                 if (result)
10                 {
11                     isTrue = true;
12                 }
13             });
14 
15             //可设定等待时间,如果超时,则向下执行
16             Task.Factory.StartNew(() => {
17                 SpinWait.SpinUntil(()=>isTrue,10000);
18                 //真正的业务
19             });
20 }

注意:

2)Volatile可修饰的类型为:整型,bool,带有整型的枚举,引用类型,推到为引用类型的泛型类型,不安全上下文中的指针类型以及表示指针或者句柄的平台相关类型。

主要成员

1)public Semaphore(int initialCount, int maximumCount);

最大的好处:开销低,效率高。

3)public virtual bool WaitOne(); 阻止当前线程,直到当前 System.Threading.WaitHandle 收到信号。 如果当前实例收到信号,则为 true。 如果当前实例永远收不到信号,则 System.Threading.WaitHandle.WaitOne(System.Int32,System.Boolean)永不返回。

4)public void Wait();阻止当前线程,直到设置了当前 ManualResetEventSlim 为止。

当共享变量被不同的线程访问和更新且没有锁和原子操作的时候,最新的值总能在共享变量中表现出来。

4)Semaphore与SemaphoreSlim

注意:

4) public int ParticipantCount { get; } 获取屏障中参与者的总数。

10.volatile修饰符

6.SpinLock:自旋锁,对SpinWait的包装

1)不要将SpinLock声明为只读字段。

转载与引用请注明出处。

4)public void Wait();阻止当前线程,直至它可进入 System.Threading.SemaphoreSlim 为止。

本文由时时app平台注册网站发布于编程知识,转载请注明出处:[翻译]在 .NET Core 中的并发编程时时app平台注册网

关键词: