Это на самом деле потокобезопасно (чисто как деталь реализации Count
), но:
Потокобезопасные фрагменты кода не делают поточно-ориентированное приложение. Вы можете комбинировать различные поточно-ориентированные операции в не поточно-ориентированные. Действительно, многие не поточнобезопасные коды можно разбить на более мелкие кусочки, каждый из которых сам по себе является поточно-ориентированным.
Он не является поточно-ориентированным по той причине, на которую вы надеялись, а это означает, что дальнейшее его расширение не будет поточно-ориентированным.
Этот код будет поточно-ориентированным:
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.* Две важные вещи:
Когда что-то описывается как «поточно-ориентированное», знайте только то, что оно обещает, так как существуют разные виды обещаний, которые попадают под «поток»-safe "и само по себе это просто означает" я не буду переводить этот объект в бессмысленное состояние ", который, хотя и является важным строительным блоком, сам по себе не так уж много.
Блокировка на групп операций с одинаковой блокировкой для каждой группы, которые будут влиять на одни и те же данные, и защищать доступ к объектам, чтобы не могло быть другого потока, не играющего в мяч с этим.
* Это очень ограниченное определение многопоточности.Вызов list[93]
для List<T>
, где T
- это тип, который будет записан и прочитан атомарно, и мы не знаем, содержит ли он хотя бы 94 элемента, одинаково безопасно, независимо от того, работают ли на нем другие потоки.,Конечно, тот факт, что он может генерировать ArgumentOutOfRangeException
в любом случае, не является тем, что большинство людей считает «безопасным», но гарантия, которую мы имеем с несколькими потоками, остается такой же, как и с одной.Это то, что мы получаем более надежную гарантию, проверяя Count
в одном потоке, но не в многопоточной ситуации, что заставляет меня описать это как поточно-ориентированное;хотя это комбо все еще не испортит состояние, оно может привести к исключению, которое, как мы уверены, не может произойти.