Этот поток использования блокировки безопасен? - PullRequest
5 голосов
/ 19 января 2012

Я знаю, что неправильно использовать lock(this) или любой общий объект.

Интересно, нормально ли это использование?

public class A
{
  private readonly object locker = new object();
  private List<int> myList;
  public A()
  {
    myList = new List<int>()
  }

  private void MethodeA()
  {
    lock(locker)
    {
      myList.Add(10);
    }
  }

  public void MethodeB()
  {
    CallToMethodInOtherClass(myList);
  }
}

public class OtherClass
{
  private readonly object locker = new object();
  public CallToMethodInOtherClass(List<int> list)
  {
   lock(locker)
   {
     int i = list.Count;
   }
  }
}

Безопасен ли этот поток?В OtherClass мы блокируем с помощью закрытого объекта, поэтому, если блокировка class A с его закрытой блокировкой может изменить список в блоке блокировки в OtherClass?

Ответы [ 9 ]

10 голосов
/ 19 января 2012

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

Всегда блокируйте свой собственный объект блокировки при передаче списка:

  public void MethodeB()
  {
    lock(locker)
    {
      CallToMethodInOtherClass(myList);
    }
  }
3 голосов
/ 19 января 2012

Нет, это не потокобезопасно.Чтобы обеспечить многопоточность, вы можете использовать блокировку для static объектов, потому что они совместно используются потоками, это может привести к взаимоблокировкам в коде, но это может быть обработано путем поддержания правильного порядка блокировки.С lock связаны затраты на производительность, поэтому используйте их с умом.

Надеюсь, это поможет

3 голосов
/ 19 января 2012

Нет, это не потокобезопасно. A.MethodeA и OtherClass.CallToMethodInOtherClass блокируют разные объекты, поэтому они не являются взаимоисключающими. Если вам нужно защитить доступ к списку, не передавайте его внешнему коду, оставьте его закрытым.

2 голосов
/ 19 января 2012

Нет, они должны заблокировать один и тот же объект.С вашим кодом они оба блокируются по-разному, и каждый вызов может выполняться одновременно.

Чтобы обеспечить безопасность потока кода, установите блокировку в MethodeB или используйте сам список в качестве объекта блокировки.

2 голосов
/ 19 января 2012

Нет, это не потокобезопасно.

Ваши 2 метода блокируют 2 разных объекта, они не будут блокировать друг друга.

Поскольку CallToMethodInOtherClass() извлекает только значение Count, ничего не будет ужасно неправильно.Но lock() вокруг него бесполезен и вводит в заблуждение.

Если метод внесет изменения в список, у вас возникнет неприятная проблема.Чтобы решить эту проблему, измените MethodeB:

  public void MethodeB()
  {
    lock(locker)  // same instance as MethodA is using
    {
      CallToMethodInOtherClass(myList);
    }
  }
1 голос
/ 19 января 2012

Это на самом деле потокобезопасно (чисто как деталь реализации Count), но:

  1. Потокобезопасные фрагменты кода не делают поточно-ориентированное приложение. Вы можете комбинировать различные поточно-ориентированные операции в не поточно-ориентированные. Действительно, многие не поточнобезопасные коды можно разбить на более мелкие кусочки, каждый из которых сам по себе является поточно-ориентированным.

  2. Он не является поточно-ориентированным по той причине, на которую вы надеялись, а это означает, что дальнейшее его расширение не будет поточно-ориентированным.

Этот код будет поточно-ориентированным:

public void CallToMethodInOtherClass(List<int> list)
{
   //note we've no locks!
   int i = list.Count;
   //do something with i but don't touch list again.
}

Назовите его с любым списком, и он выдаст i значение, основанное на состоянии этого списка, независимо от того, что делают другие потоки. Это не повредит list. Это не даст i недопустимое значение.

Так что, хотя этот код также является поточно-ориентированным:

public void CallToMethodInOtherClass(List<int> list)
{
  Console.WriteLine(list[93]); // obviously only works if there's at least 94 items
                            // but that's nothing to do with thread-safety
}

Этот код не будет поточно-ориентированным:

public void CallToMethodInOtherClass(List<int> list)
{
   lock(locker)//same as in the question, different locker to that used elsewhere.
   {
     int i = list.Count;
     if(i > 93)
       Console.WriteLine(list[93]);
   }
}

Прежде чем идти дальше, два бита, которые я описал как поточно-ориентированные, не обещаются спецификацией для List . Консервативное кодирование предполагает, что они не являются поточно-ориентированными, а не зависят от деталей реализации, но я собираюсь зависеть от деталей реализации, потому что это затрагивает вопрос о том, как использовать блокировки важным способом:

Поскольку существует код, работающий на list, который сначала не получает блокировку на locker, этот код не может быть запущен одновременно с CallToMethodInOtherClass. Теперь, в то время как list.Count является поточно-ориентированным, а list[93] является протекторно-безопасным, * комбинация двух, где мы зависим от первого, чтобы гарантировать, что второе работает, не является поточно-ориентированным. Поскольку код вне блокировки может влиять на list, код может вызывать Remove или Clear в промежутке между Count, гарантируя, что list[93] будет работать и list[93] вызывается.

Теперь, если мы знаем, что list добавляется только когда-либо, это нормально, даже если изменение размера происходит одновременно, в итоге мы получим значение list[93] в любом случае. Если что-то пишет в list[93] и это тип, который .NET будет писать атомарно (а int - один из таких типов), мы получим либо старый, либо новый, как если бы мы ' Если бы мы были заблокированы правильно, мы бы получили старое или новое, в зависимости от того, какой поток сначала блокируется. Опять же, это деталь реализации, а не указанное обещание , я констатирую это просто для того, чтобы указать, как данная безопасность потока все еще приводит к не поточно-безопасному коду.

Перемещение к реальному коду. Мы не должны предполагать, что list.Count и list[93] являются потокобезопасными, потому что мы не обещали, что они будут, и это может измениться, но даже если бы у нас было это обещание, эти два обещания не составят обещание, которое они были бы поточнобезопасны вместе.

Важно использовать одну и ту же блокировку для защиты блоков кода, которые могут мешать друг другу. Следовательно, рассмотрим вариант ниже, который гарантированно безопасен для потоков:

public class ThreadSafeList
{
  private readonly object locker = new object();
  private List<int> myList = new List<int>();

  public void Add(int item)
  {
    lock(locker)
      myList.Add(item);
  }
  public void Clear()
  {
    lock(locker)
      myList.Clear();
  }
  public int Count
  {
    lock(locker)
      return myList.Count;
  }
  public int Item(int index)
  {
    lock(locker)
      return myList[index];
  }
}

Этот класс гарантированно является потокобезопасным во всем, что он делает. Вне зависимости от каких-либо подробностей реализации, здесь нет метода, который испортил бы состояние или дал бы неверные результаты из-за того, что другой поток делает с тем же экземпляром. Следующий код все еще не работает, хотя:

// (l is a ThreadSafeList visible to multiple threads.
if(l.Count > 0)
  Console.WriteLine(l[0]);

Мы гарантировали поточную безопасность каждого вызова на 100%, но мы не гарантировали комбинацию, и мы не можем гарантировать комбинацию.

Есть две вещи, которые мы можем сделать. Мы можем добавить метод для комбинации. Нечто похожее на следующее будет распространено для многих классов, специально разработанных для многопоточного использования:

public bool TryGetItem(int index, out int value)
{
  lock(locker)
  {
    if(l.Count > index)
    {
      value = l[index];
      return true;
    }
    value = 0;
    return false;
  }
}

Это делает проверку количества и получение элемента частью одной операции, которая гарантированно является поточно-ориентированной.

В качестве альтернативы, и чаще всего то, что нам нужно сделать, мы имеем блокировку в том месте, где сгруппированы операции:

lock(lockerOnL)//used by every other piece of code operating on l
  if(l.Count > 0)
    Console.WriteLine(l[0]);

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

Чтобы вернуться к коду в вашем вопросе:

Блокировка в CallToMethodInOtherClass должна быть удалена, если у OtherClass нет собственной причины дляблокировка внутри.Он не может дать многообещающего обещания, что он не будет комбинироваться без поточной защиты, а добавление дополнительных блокировок в программу просто усложняет анализ, чтобы убедиться в отсутствии тупиков.

вызов CallToMethodInOtherClass должен быть защищен той же блокировкой, что и другие операции в этом классе:

public void MethodeB()
{
  lock(locker)
    CallToMethodInOtherClass(myList);
}

Тогда, пока CallToMethodInOtherClass не хранит myList где-то, его могут видеть другие потоки.позже, не имеет значения, что CallToMethodInOtherClass не является поточно-ориентированным, потому что единственный код, который может получить доступ к myList, дает собственную гарантию не вызывать его одновременно с другими операциями на myList.

* 1096.* Две важные вещи:
  1. Когда что-то описывается как «поточно-ориентированное», знайте только то, что оно обещает, так как существуют разные виды обещаний, которые попадают под «поток»-safe "и само по себе это просто означает" я не буду переводить этот объект в бессмысленное состояние ", который, хотя и является важным строительным блоком, сам по себе не так уж много.

  2. Блокировка на групп операций с одинаковой блокировкой для каждой группы, которые будут влиять на одни и те же данные, и защищать доступ к объектам, чтобы не могло быть другого потока, не играющего в мяч с этим.

* Это очень ограниченное определение многопоточности.Вызов list[93] для List<T>, где T - это тип, который будет записан и прочитан атомарно, и мы не знаем, содержит ли он хотя бы 94 элемента, одинаково безопасно, независимо от того, работают ли на нем другие потоки.,Конечно, тот факт, что он может генерировать ArgumentOutOfRangeException в любом случае, не является тем, что большинство людей считает «безопасным», но гарантия, которую мы имеем с несколькими потоками, остается такой же, как и с одной.Это то, что мы получаем более надежную гарантию, проверяя Count в одном потоке, но не в многопоточной ситуации, что заставляет меня описать это как поточно-ориентированное;хотя это комбо все еще не испортит состояние, оно может привести к исключению, которое, как мы уверены, не может произойти.

1 голос
/ 19 января 2012

Задавайте, что во всех ответах говорится, что это разные объекты блокировки.

простой способ - создать объект статической блокировки f.ex:

publc class A
{
    public static readonly object lockObj = new object();
}

и в обоих классах используйте блокировку как:

lock(A.lockObj)
{
}
0 голосов
/ 19 января 2012

Во многих ответах упоминается использование статической блокировки только для чтения.

Однако вам действительно следует избегать этой статической блокировки.Было бы легко создать тупик, когда несколько потоков используют статическую блокировку.

Вместо этого вы можете использовать одну из параллельных коллекций .net 4, они обеспечивают некоторую синхронизацию потоков от вашего имени, таквам не нужно использовать блокировку.

Взгляните на пространство имен System.collections.Concurrent.В этом примере вы можете использовать класс ConcurrentBag<T>.

0 голосов
/ 19 января 2012

Наверное, самый простой способ сделать трюк

public class A
{
  private List<int> myList;
  public A()
  {
    myList = new List<int>()
  }

  private void MethodeA()
  {
    lock(myList)
    {
      myList.Add(10);
    }
  }

  public void MethodeB()
  {
    CallToMethodInOtherClass(myList);
  }
}

public class OtherClass
{
  public CallToMethodInOtherClass(List<int> list)
  {
   lock(list)
   {
     int i = list.Count;
   }
  }
}
...