为什么一开始就有线程竞争

本文关键字:线程 竞争 一开始 为什么 | 更新日期: 2023-09-27 18:01:59

我最近试着写一个锁语句的例子。考虑以下代码:

public partial class Form1 : Form
    {
        private class Concurrency
        {
            private int _myValue;
            private object _locker = new object();
            public int Value
            {
                set
                {
                    lock (_locker)
                    {
                        _myValue = value;
                        Thread.Sleep(new Random().Next(5, 25));
                    }
                }
                get
                {
                    return _myValue;
                }
            }
        }
        private Random _random;
        private Concurrency _concurrency;
        public Form1()
        {
            InitializeComponent();
            _random = new Random();
            _concurrency = new Concurrency();
        }
        private void button1_Click(object sender, EventArgs e)
        {
            CreateTask(1);
            CreateTask(2);
        }
        private void CreateTask(int taskId)
        {
            Task.Factory.StartNew(() =>
                {
                    for (int i = 0; i < 10; ++i)
                    {
                        int randomNumber = _random.Next(0, 50);
                        Console.WriteLine("Thread {0}, setting value {1}", taskId, randomNumber);
                        _concurrency.Value = randomNumber;
                        Console.WriteLine("Thread {0}, getting value {1}", taskId, _concurrency.Value);
                        Thread.Sleep(_random.Next(5, 15));
                    }
                });
        }
    }

结果是:

Thread 2, setting value 4
Thread 1, setting value 22
Thread 2, getting value 22
Thread 1, getting value 22
Thread 2, setting value 11
Thread 2, getting value 11
Thread 1, setting value 8
Thread 2, setting value 41
Thread 1, getting value 8
Thread 1, setting value 30
Thread 2, getting value 41
Thread 1, getting value 30
Thread 2, setting value 18
Thread 1, setting value 42
Thread 2, getting value 18
Thread 2, setting value 30
Thread 1, getting value 42
Thread 1, setting value 24
Thread 2, getting value 30
Thread 1, getting value 24
Thread 2, setting value 13
Thread 1, setting value 7
Thread 2, getting value 13
Thread 2, setting value 13
Thread 1, getting value 7
Thread 2, getting value 13
Thread 1, setting value 38
Thread 2, setting value 19
Thread 1, getting value 38
Thread 1, setting value 4
Thread 2, getting value 19
Thread 2, setting value 44
Thread 1, getting value 4
Thread 2, getting value 44
Thread 1, setting value 48
Thread 2, setting value 12
Thread 1, getting value 48
Thread 1, setting value 47
Thread 2, getting value 12
Thread 1, getting value 47

正如你所看到的,一切都很好,除了第一次设置/获取情况:线程2设置值2,但得到22。这不是单一情况,每次都会发生。我知道设置和获取不是原子的,锁应该围绕任务中的指令设置,但为什么第一次尝试总是失败,而其他工作很好?

编辑:

我更新并发类如下:

private class Concurrency
        {
            private static Random _random = new Random();
            private int _myValue;
            private object _locker = new object();
            public int Value
            {
                set
                {
                    lock (_locker)
                    {
                        _myValue = value;
                        Thread.Sleep(_random.Next(5, 250));
                    }
                }
                get
                {
                    return _myValue;
                }
            }
        }

注意,我还扩展了Thread.Sleep中的时间范围。结果是:

Thread 2, setting value 3
Thread 1, setting value 9
Thread 2, getting value 9
Thread 2, setting value 44
Thread 1, getting value 9
Thread 1, setting value 35
Thread 2, getting value 44
Thread 2, setting value 32
Thread 1, getting value 35
Thread 1, setting value 25
Thread 2, getting value 32
Thread 2, setting value 15
Thread 1, getting value 25
Thread 1, setting value 5
Thread 2, getting value 15
Thread 2, setting value 34
Thread 1, getting value 5
Thread 1, setting value 42
Thread 2, getting value 34
Thread 2, setting value 36
Thread 1, getting value 42
Thread 1, setting value 8
Thread 2, getting value 36
Thread 2, setting value 42
Thread 1, getting value 8
Thread 1, setting value 16
Thread 2, getting value 42
Thread 2, setting value 0
Thread 1, getting value 16
Thread 1, setting value 43
Thread 2, getting value 0
Thread 2, setting value 20
Thread 1, getting value 43
Thread 1, setting value 30
Thread 2, getting value 20
Thread 2, setting value 38
Thread 1, getting value 30
Thread 1, setting value 0
Thread 2, getting value 38
Thread 1, getting value 0

什么都没变。我猜这不是随机的问题,而是别的什么。

为什么一开始就有线程竞争

这种情况不止发生在第一次

你只"看到"它一次,实际上就是程序中的bug。可能每次你看到两个"设定",你就会读到最后一个。想象一下这种情况:

<>之前主线程1主线程2值= 0int x1 = Value值= 2int x2 = ValueWriteLine (x1)WriteLine (x2)之前

输出正确(线程1为0,线程2为2)。现在想象一下,如果调度是这样的:

<>之前主线程1主线程2值= 0值= 2int x1 = ValueWriteLine (x1)int x2 = ValueWriteLine (x2)之前

你会得到一个错误的结果,因为对于两个线程你将读取值2。实际上,没有错,因为锁定的唯一操作是set,不能保证线程1的读操作(属性值的get)会在线程2的写操作(属性值的set)之前执行。

最后看一下这篇文章,你会发现如果你这样写的话,类似的代码可能会失败(完全出于同样的原因):

++_concurrency.Value;

正如您指出的那样,锁定是不正确的。所以这更像是一个"为什么它似乎有效,除了一开始?"的问题。(我只是在重复你的问题。)

[编辑]

既然你修改了代码以消除我所说的问题,这里有另一个想法-我认为这确实是答案。

你的代码,在线程退出锁和读取值之间有一个极短的时间。

检查你的setter:

set
{
    lock (_locker)
    {
        _myValue = value;
        Thread.Sleep(_random.Next(5, 25));
    }
}

现在如果thread1在锁内,它将设置_myValue,然后休眠。在此期间,Thread2将等待进入锁。

当thread1退出休眠时,它立即退出锁并继续执行下一行代码,在本例中是打印当前值的那行:

Console.WriteLine("Thread {0}, getting value {1}", taskId, _concurrency.Value);

除非thread1在退出锁和取消引用_concurrency.Value之间被重调度,否则它将收到正确的值。因为时间很短,所以不太可能在这段时间内被重新安排。

如果thread1 被取消调度,那么thread2可以在thread1取消引用之前进入锁并修改_myValue

做任何事情来增加线程设置和获取值之间的时间,将更有可能观察到"不正确"的值。

尝试下面的程序,然后取消用// Try with this sleep uncommented.指示的行注释。您将看到更多行打印"Number Mismatch"。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Demo
{
    class Program
    {
        private static void Main(string[] args)
        {
            Console.WriteLine("Starting");
            CreateTask(1);
            CreateTask(2);
            Console.ReadKey();
        }
        private static void CreateTask(int taskId)
        {
            Task.Factory.StartNew(() =>
            {
                for (int i = 0; i < 10; ++i)
                {
                    int randomNumber = _random.Next(0, 50);
                    Console.WriteLine("Thread {0}, setting value {1}", taskId, randomNumber);
                    _concurrency.Value = randomNumber;
                    // Thread.Sleep(10); // Try with this sleep uncommented.
                    int test = _concurrency.Value;
                    Console.WriteLine("Thread {0}, getting value {1}", taskId, test);
                    if (test != randomNumber)
                    {
                        Console.WriteLine("Number mismatch.");
                    }
                    Thread.Sleep(_random.Next(5, 15));
                }
            });
        }
        private static Random _random = new Random();
        private static Concurrency _concurrency = new Concurrency();
    }
    class Concurrency
    {
        private int _myValue;
        private object _locker = new object();
        public int Value
        {
            set
            {
                lock (_locker)
                {
                    _myValue = value;
                    Thread.Sleep(_random.Next(5, 25));
                }
            }
            get
            {
                return _myValue;
            }
        }
        static Random _random = new Random();
    }
}

那么为什么一开始就失败了呢?嗯,我认为这只是系统启动线程方式的一个工件。