并发技术中同步

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

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

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

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

1、阻塞锁    lock

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

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

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

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

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

 

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();
            }
        }
    }

 

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 是功能强大、通用的线程间信号,但必须合理地使用

 

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> 来异步地等待:本例中,通知来自于另一
部分代码。如果只需要发送一次信号,这种方法很适合。但是如果要打开和关闭信号,这
种方法就不大合适了.。

 

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);
        }

 

 

  

posted @ 2018-06-14 13:59  _York  阅读(...)  评论(...编辑  收藏