Потоки и небезопасные переменные - PullRequest
0 голосов
/ 18 ноября 2009

У меня есть код, указанный здесь: Потоки и сокеты .

Ответом на этот вопрос было изменение isListening с volatile. Как я заметил, этот модификатор позволил мне получить доступ к переменной из другого потока. После прочтения MSDN я понял, что читаю isListening из следующего вновь созданного процесса потока.

Итак, мои вопросы сейчас:

  • Является ли volatile предпочтительным методом, так как я в основном делаю не потокобезопасный запрос к переменной? Я прочитал о классе Interlocked и подумал, не лучше ли это использовать в моем коде. Interlocked выглядит аналогично тому, что делает lock(myObj), но с немного большим «талантом» и контролем. Я знаю, что простое применение кодового блока lock(myObj) вокруг isListening не сработало.

  • Должен ли я реализовать класс блокировки?

Спасибо за ваше время и ответы.

Ответы [ 3 ]

2 голосов
/ 18 ноября 2009

Если все, что вы делаете, это чтение и запись переменной в нескольких потоках в C #, то вам не нужно беспокоиться о синхронизации доступа к (блокировке) этой переменной , если ее тип - bool, char, byte, sbyte , short, ushort, int, uint, float и reference. Подробнее см. здесь .

В примере из вашего другого поста причина, по которой вы должны пометить поле как volatile, состоит в том, чтобы гарантировать, что оно не подлежит оптимизации компилятора и что в поле всегда присутствует самое последнее значение. См. здесь для получения подробной информации о ключевом слове volatile. Это позволяет читать и записывать это поле в потоках, не блокируя (не синхронизируя доступ) его. Но имейте в виду, ключевое слово volatile может использоваться только для вашего поля, поскольку оно имеет тип bool. Например, если бы оно было двойным, ключевое слово volatile не сработало бы, и вам пришлось бы использовать блокировку.

Класс Interlocked используется для специальной цели, а именно для увеличения, уменьшения и обмена значениями (обычно) числовых типов. Эти операции не являются атомарными. Например, если вы увеличиваете значение в одном потоке и пытаетесь прочитать полученное значение в другом потоке, вам, как правило, придется заблокировать переменную, чтобы предотвратить чтение промежуточных результатов. Класс Interlocked просто предоставляет некоторые вспомогательные функции, поэтому вам не нужно блокировать переменную самостоятельно, пока выполняется операция приращения.

То, что вы делаете с флагом isListening, не требует использования класса Interlocked. Достаточно пометить поле как изменчивое.

1 голос
/ 18 ноября 2009

Изменить в связи с поспешным ответом в обеденное время ..

Оператор блокировки, использованный в предыдущем коде, блокирует экземпляр объекта, созданный в области действия метода, поэтому он не будет влиять на другой поток, вызывающий тот же метод. Каждый поток должен иметь возможность блокировать один и тот же экземпляр объекта, чтобы синхронизировать доступ к данному блоку кода. Один из способов сделать это (в зависимости от требуемой семантики) - сделать объект блокировки частной статической переменной класса, в котором он используется. Это позволит нескольким экземплярам данного объекта синхронизировать доступ к блоку кода или один общий ресурс. Если синхронизация требуется для отдельных экземпляров объекта или ресурса, который специфичен для конкретного экземпляра, то должна генерироваться статическая.

Volatile не гарантирует, что чтение или запись в данную переменную будут атомарными среди разных потоков. Это подсказка компилятора для сохранения порядка команд и предотвращения кэширования переменной внутри регистра. В общем, если вы не работаете над чем-то чрезвычайно чувствительным к производительности (алгоритмы с низкой блокировкой / без блокировок, структуры данных и т. Д.) Или действительно знаете, что делаете, я бы выбрал Interlocked. Разница в производительности между использованием volatile / interlocked / lock в большинстве приложений будет незначительной, поэтому, если вы не уверены, что лучше всего использовать то, что дает вам самую безопасную гарантию (читайте блог Джо Даффи и book ).

Например, использование volatile в приведенном ниже примере не является потокобезопасным, и приращенный счетчик не достигает 10 000 000 (когда я запустил тест, он достиг 8848450). Это потому, что volatile гарантирует только чтение последнего значения (например, не кешируется из регистра, например). При использовании блокировок операция является поточно-ориентированной, и счетчик достигает 10 000 000.

public class Incrementor
{
    private volatile int count;

    public int Count
    {
        get { return count; }   
    }

    public void UnsafeIncrement()
    {
        count++;
    }

    public void SafeIncrement()
    {
        Interlocked.Increment(ref count);
    }
}

[TestFixture]
public class ThreadingTest
{
    private const int fiveMillion = 5000000;
    private const int tenMillion = 10000000;

    [Test]
    public void UnsafeCountShouldNotCountToTenMillion()
    {
        const int iterations = fiveMillion;
        Incrementor incrementor = new Incrementor();
        Thread thread1 = new Thread(() => UnsafeIncrement(incrementor, iterations));
        Thread thread2 = new Thread(() => UnsafeIncrement(incrementor, iterations));

        thread1.Start();
        thread2.Start();

        thread1.Join();
        thread2.Join();

        Assert.AreEqual(tenMillion, incrementor.Count);
    }

    [Test]
    public void SafeIncrementShouldCountToTenMillion()
    {
        const int iterations = fiveMillion;
        Incrementor incrementor = new Incrementor();
        Thread thread1 = new Thread(() => SafeIncrement(incrementor, iterations));
        Thread thread2 = new Thread(() => SafeIncrement(incrementor, iterations));

        thread1.Start();
        thread2.Start();

        thread1.Join();
        thread2.Join();

        Assert.AreEqual(tenMillion, incrementor.Count);
    }

    private void UnsafeIncrement(Incrementor incrementor, int times)
    {
        for (int i =0; i < times; ++i)
            incrementor.UnsafeIncrement();
    }

    private void SafeIncrement(Incrementor incrementor, int times)
    {
        for (int i = 0; i < times; ++i)
            incrementor.SafeIncrement();
    }
}

Если вы ищете «изменчивая блокировка», вы найдете несколько ответов на свой вопрос. Например, приведенный ниже ответ на ваш вопрос:

Простой пример ниже показывает

Летучий против блокировки и против блокировки

0 голосов
/ 18 ноября 2009

"Один из способов сделать это - сделать объект блокировки частной статической переменной класса, в котором он используется." Почему это должно быть статичным? Вы можете получить доступ к одной и той же функции из нескольких потоков, если они работают с другим объектом. Я не говорю, что это не будет работать, но серьезно замедлит скорость приложения без каких-либо преимуществ. Или я что-то упустил?

А вот что MSDN говорит о летучих веществах: «Кроме того, при оптимизации компилятор должен поддерживать порядок среди ссылок на изменчивые объекты, а также ссылок на другие глобальные объекты. В частности,

Запись в энергозависимый объект (volatile write) имеет семантику Release; ссылка на глобальный или статический объект, который происходит перед записью в энергозависимый объект в последовательности команд, произойдет до того, как энергозависимая запись в скомпилированном двоичном файле.

Чтение летучего объекта (volatile read) имеет семантику Acquire; ссылка на глобальный или статический объект, который происходит после чтения энергозависимой памяти в последовательности команд, будет происходить после этого энергозависимого чтения в скомпилированном двоичном файле.

Это позволяет использовать энергозависимые объекты для блокировок и выпусков памяти в многопоточных приложениях. "

...