八、lock语句和线程安全

C#为多个线程的同步提供了自己的关键字:lock语句。lock语句是设置锁定和解除锁定的一种简单方式。在添加lock语句之前,先进入另一个争用条件。SharedState类说明了如何使用线程之间的共享状态,并共享一个整数值。

    class SharedState
    {
        public int State { get; set; }
    }

下面所有同步示例的代码(SingletonWPF除外)都使用如下名称空间:

System

System.Collections.Generic

System.Linq

System.Text

System.Threading

System.Threading.Tasks

Job类包含DoTheJob()方法,该方法是新任务的入口点。通过其实现代码,将SharedState对象的State递增50000次。SharedState变量在这个类的构造函数中初始化:

    class Job
    {
        public Job(SharedState sharedState)
        {
            _sharedState = sharedState;
        }
        private SharedState _sharedState;
        public void DoTheJob()
        {
            for (int i = 0; i < 5000; i++)
            {
                _sharedState.State += 1;
            }
        }
    }

在Main()方法中,创建一个SharedState对象,并把它传递给20个Task对象的构造函数。在启动所有的任务后,Main()方法进入另一个循环,等待20个任务都执行完毕。任务执行完毕后,把共享状态的合计值写入控制台中。因为执行了50000次循环,有20个任务,所以写入控制台的值应是1 000 000。但是,事实常常并非如此。

    class Program
    {
        static void Main(string[] args)
        {
            for (int num = 0; num < 5; num++) //定义运行5次
            {
                int numTasks = 20;
                var state = new SharedState();
                var tasks = new Task[numTasks];
                for (int i = 0; i < numTasks; i++)
                {
                    tasks[i] = Task.Run(() => new Job(state).DoTheJob());
                }
                Task.WaitAll(tasks);
                Console.WriteLine($"summarized {state.State}");
            }            
        }
    }

多次运行应用程序的结果如下所示:

summarized 256734
summarized 269939
summarized 271407
summarized 321613
summarized 465098

每次运行的结果都不同,但没有一个结果是正确的。如前所述,调试版本和发布版本的区别很大。根据使用的CPU类型,结果也不一样。如果将循环次数改为比较小的值,就会多次得到正确的值,但不是每次都正确。这个应用程序非常小,很容易看出问题,但该问题的原因在大型应用程序中就很难确定。

上一个示例用lock锁定后,代码块和运行结果: 

    class Job
    {
        public Job(SharedState sharedState)
        {
            _sharedState = sharedState;
        }
        private SharedState _sharedState;
        
        public void DoTheJob()
        {
            for (int i = 0; i < 50000; i++)
            {
                lock (_sharedState)
                {
                    _sharedState.State += 1;
                }
            }
        }
    }

 

summarized 1000000
summarized 1000000
summarized 1000000
summarized 1000000
summarized 1000000

必须在这个程序中添加同步功能,这可以用lock关键字实现。用lock语句定义的对象表示,要等待指定对象的锁定。只能传递引用类型。锁定值类型只是锁定了一个副本,这没有什么意义。如果对值类型使用了lock语句,C#编译器就会发出一个错误。进行了锁定后——只锁定了一个线程,就可以运行lock语句块。在lock语句块的最后,对象的锁定被解除,另一个等待锁定的线程就可以获得该锁定块了。

                lock (obj)
                {
                    // synchronized region
                }

要锁定静态成员,可以把锁放在object类型或静态成员上:

                lock (typeof(StaticClass))
                {
                }

使用lock关键字可以将类的实例成员设置为线程安全的。这样,一次只有一个线程能访问相同实例的DoThis()和DoThat()方法。

    class Demo
    {
        public void DoThis()
        {
            lock (this)
            {
                // only one thread at a time can access the DoThis and DoThat methods
            }
        }
        public void DoThat()
        {
            lock (this)
            {
            }
        }
    }

但是,因为实例的对象也可以用于外部的同步访问,而且我们不能在类自身中控制这种访问,所以应采用SyncRoot模式。通过SyncRoot模式,创建一个私有对象_syncRoot,将这个对象用于lock语句。

    class Demo
    {
        private object _syncRoot = new object();
        public void DoThis()
        {
            lock (_syncRoot)
            {
                // only one thread at a time can access the DoThis and DoThat methods
            }
        }
        public void DoThat()
        {
            lock (_syncRoot)
            {
            }
        }
    }

注意:在Job类中,使用SyncRoot模式或this,输出的仍是非预期的结果。对于SharedState类有效。如下代码片段所示:

    class Job
    {
        public Job(SharedState sharedState)
        {
            _shareState = sharedState;
        }
        private SharedState _shareState;
        private object _asyncRoot = new object();
        public void DoTheJob()
        {
            for (int i = 0; i < 5000; i++)
            {
                lock (_asyncRoot)
                {
                    _shareState.IncrementState();
                }
                //lock (this)
                //{

                //}
            }
        }
    }

使用锁定需要时间,且并不总是必要的。可以创建类的两个版本,一个同步版本,一个异步版本。下一个示例通过修改Demo类来说明。Demo类本身并不是同步的,这可以在DoThis()和DoThat()方法的实现中看出。该类还定义了IsSynchronized属性,客户可以从该属性中获得类的同步选项信息。为了获得该类的同步版本,可以使用静态方法Synchronized()传递一个非同步对象,这个方法会返回SynchronizedDemo类型的对象。SynchronizedDemo实现为派生自基类Demo的一个内部类,并重写基类的虚成员。重写的成员使用了SyncRoot模式。

    class Demo
    {
        private class SynchronizedDemo:Demo
        {
            private object _syncRoot = new object();
            private Demo _d;
            public SynchronizedDemo(Demo d)
            {
                _d = d;
            }
            public override bool IsSynchronized => true;
            public override void DoThis()
            {
                lock (_syncRoot)
                {
                    _d.DoThis();
                }                
            }
            public override void DoThat()
            {
                lock (_syncRoot)
                {
                    _d.DoThat();
                }               
            }
        }
        public virtual bool IsSynchronized => false;
        public static Demo Synchronized(Demo d)
        {
            if (!d.IsSynchronized)
            {
                return new SynchronizedDemo(d);
            }
            return d;
        }
        public virtual void DoThis()
        {

        }
        public virtual void DoThat()
        {

        }
    }

必须注意,在使用SynchronizedDemo类时,只有方法是同步的。对这个类的两个成员的调用并没有同步。

首先修改异步的SharedState类,以使用SyncRoot模式。如果试图用SyncRoot模式锁定对属性的访问,使SharedState类变成线程安全的,就仍会出现前面描述的争用条件。

    class SharedState
    {
        private int _state = 0;
        private object _syncRoot = new object();
        public int State //there's still a race condition,don't do this!
        {
            get
            {
                lock (_syncRoot)
                {
                    return _state;
                }
            }
            set
            {
                lock (_syncRoot)
                {
                    _state = value;
                }
            }
        }
    }

调用方法DoTheJob()的线程访问SharedState类的get存取器,以获得State的当前值,接着get存取器给State设置新值。在调用对象的get和set存取器期间,对象没有锁定,另一个线程可以获得临时值。

        public void DoTheJob()
        {
            for (int i = 0; i < 50000; i++)
            {
                _sharedState.State += 1;
            }
        }

运行结果:

summarized 346902
summarized 426230
summarized 299191
summarized 298090
summarized 251107

所以,最好不改变SharedState类,让它依旧没有线程安全性。

    class SharedState
    {
        public int State { get; set; }
    }

然后在DoTheJob方法中,将lock语句添加到合适的地方:

        public void DoTheJob()
        {
            for (int i = 0; i < 50000; i++)
            {
                lock (_sharedState)
                {
                    _sharedState.State += 1;
                }               
            }
        }

这样,应用程序的结果就总是正确的。

注意:

在一个地方使用lock语句并不意味着,访问对象的其他线程都正在等待。必须对每个访问共享状态的线程显示地使用同步功能。

当然,还必须修改SharedState类的设计,并作为一个原子操作提供递增方式。这是一个设计问题——把什么实现为类的原子功能?下面的代码片段锁定了递增操作。

    class SharedState
    {
        private int _state = 0;
        private object _syncRoot = new object();
        public int State => _state;
        public int IncrementState()
        {
            lock (_syncRoot)
            {
                return ++_state;
            }
        }
    }

改变相关代码,以及运行结果:

    class Job
    {
        public Job(SharedState sharedState)
        {
            _sharedState = sharedState;
        }
        private SharedState _sharedState;

        public void DoTheJob()
        {
            for (int i = 0; i < 50000; i++)
            {
                //lock (_sharedState)
                //{
                //    _sharedState.State += 1;
                //}  
                _sharedState.IncrementState();
            }
        }
    }
summarized 1000000
summarized 1000000
summarized 1000000
summarized 1000000
summarized 1000000

锁定状态的递增还有一种更快的方式,如下节所示。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值