Почему к локальной переменной можно получить доступ в другом потоке, созданном в том же классе?
Я не мог найти что-либо по этой точной теме, поэтому, пожалуйста, отведите меня в правильном направлении, если вопрос уже существует.
Из того, что я узнал о .NET, невозможно получить доступ к переменным в разных потоках (пожалуйста, исправьте меня, если это утверждение неверно, это то, что я где-то читал).
Теперь в этом коде, однако, кажется, что он не должен работать:
class MyClass
{
public int variable;
internal MyClass()
{
Thread thread = new Thread(new ThreadStart(DoSomething));
thread.IsBackground = true;
thread.Start();
}
public void DoSomething()
{
variable = 0;
for (int i = 0; i < 10; i++)
variable++;
MessageBox.Show(variable.ToString());
}
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void SomeMethod();
{
MyClass mc = new MyClass();
}
}
Когда я запускаю SomeMethod()
,.NET не должен создавать исключение, потому что созданный объект mc
работает в другом потоке, чем поток, созданный в mc
-initializer, и этот новый поток пытается получить доступ локальная переменная mc
?
MessageBox
показывает 10
как (не) ожидаемый, но я не уверен, почему это должно сработать.
Возможно, я не знал, что искать, но ни одна тема нитей, которую я мог бы найти, рассмотрит эту проблему, но, возможно, моя идея о переменных и потоках неверна.
Ответы
Ответ 1
Из того, что я узнал о .NET, невозможно получить доступ к переменным в разных потоках. Пожалуйста, исправьте меня, если это утверждение неверно, это то, что я где-то читал.
Это утверждение полностью неверно, поэтому рассмотрите эту коррекцию.
Вероятно, вы читали, что локальные переменные недоступны для разных потоков. Это утверждение также неверно, но обычно указывается. Правильное утверждение состоит в том, что локальные переменные, которые не являются
- в асинхронном методе
- в блоке итератора (то есть метод с
yield return
или yield break
)
- закрытые внешние переменные анонимной функции
невозможно получить доступ несколькими потоками. И даже это требование немного изворотливое; есть способы сделать это с помощью указателей и блоков кода unsafe
, но это очень плохая идея, чтобы сделать это.
Я также отмечаю, что ваш вопрос задает вопрос о локальных переменных, но затем дает пример поля. Поле по определению является не локальной переменной. Локальная переменная по определению является локальной для тела метода. (Или тело конструктора, тело индексатора и т.д.) Убедитесь, что вы поняли это. Определяющей характеристикой локального не является то, что это "в стеке" или какая-то такая вещь; "локальная" часть локального - это то, что его имя не имеет смысла вне тела метода.
В общем случае: переменная - это место хранения, которое относится к памяти. Поток - это точка управления процессом, и все потоки процесса имеют одну и ту же память; что делает их потоками, а не процессами. Таким образом, в целом все переменные могут быть доступны несколькими потоками во все времена и во всех заказах, если не будет установлен какой-либо механизм для предотвращения этого.
Позвольте мне сказать, что снова, чтобы убедиться, что в вашем сознании абсолютно кристально ясно: правильный способ думать о однопоточной программе состоит в том, что все переменные стабильны, если только что-то не изменит их. Правильный способ думать о многопоточной программе состоит в том, что все переменные постоянно изменяются без особого порядка, если только что-то не сохраняет их или упорядоченно. Это основная причина, по которой модель многопоточности с общей памятью настолько сложна, и поэтому почему вы должны ее избегать.
В вашем конкретном примере оба потока имеют доступ к this
, и поэтому оба потока могут видеть переменную this.variable
. Вы не внедрили никаких механизмов для предотвращения этого, и поэтому оба потока могут писать и читать эту переменную в любом порядке, с учетом очень немногих ограничений. Некоторые механизмы, которые вы могли бы реализовать, чтобы приручить это поведение:
- Отметьте переменную как
ThreadStatic
. Это приводит к созданию новой переменной для каждого потока.
- Отметьте переменную как
volatile
. Это налагает определенные ограничения на то, как можно наблюдать наблюдения и записи, а также накладывает определенные ограничения на оптимизации, сделанные компилятором или процессором, которые могут вызвать неожиданные результаты.
- Поместите оператор
lock
вокруг каждого использования переменной.
- Не разделяйте эту переменную в первую очередь.
Если у вас нет глубокого понимания многопоточности и оптимизации процессоров, я рекомендую против любой опции, кроме последней.
Теперь предположим, что вы действительно хотели убедиться, что доступ к переменной завершился неудачно в другом потоке. У вас может быть конструктор, который захватит идентификатор потока создающего потока и отложит его. Затем вы можете получить доступ к переменной через свойство getter/setter, где getter и setter проверяют текущий идентификатор потока и генерируют исключение, если оно не совпадает с исходным идентификатором потока.
По существу, это делает рулон вашей собственной однопоточной моделью поточной квартиры. Объект с однопоточной квартирой - это объект, доступ к которому можно получить только на законной основе в потоке, который его создал. (Вы покупаете телевизор, вы кладете его в свою квартиру, только люди в вашей квартире могут смотреть ваш телевизор.) Детали однопоточных квартир и многопоточных квартир и бесплатная резьба становятся довольно сложными; см. этот вопрос для получения дополнительной информации.
Не могли бы вы объяснить STA и MTA?
Вот почему, например, вы никогда не должны обращаться к элементу пользовательского интерфейса, создаваемому в потоке пользовательского интерфейса, из рабочего потока; элементы пользовательского интерфейса являются объектами STA.
Ответ 2
Из того, что я узнал о .NET, невозможно получить доступ к переменным в разных потоках (пожалуйста, исправьте меня, если это утверждение неверно, это то, что я где-то читал).
Это неверно. Доступ к переменной можно получить из любого места, где она находится в области видимости.
Вам нужно проявлять осторожность при доступе к одной и той же переменной из нескольких потоков, потому что каждый поток может воздействовать на переменную в недетерминированное время, что приводит к тонким, трудно разрешаемым ошибкам.
Существует выдающийся веб-сайт, который охватывает потоки в .NET от основ до продвинутых концепций.
http://www.albahari.com/threading/
Ответ 3
Я немного опоздал, и ответ @Eric J. дал замечательно и по существу.
Я просто хочу добавить немного ясности к другой проблеме в вашем восприятии потоков и переменных.
Вы сказали, что в заголовке своего вопроса "переменная доступна в другом потоке".
Добавим к этому тот факт, что в вашем коде вы получаете доступ к своей переменной только из 1 потока, который является созданным здесь потоком:
Thread thread = new Thread(new ThreadStart(DoSomething));
thread.IsBackground = true;
thread.Start();
Все это заставило меня понять, что вы испугались, что поток, отличный от того, который фактически создает экземпляр MyClass
, будет использовать что-то изнутри этого экземпляра.
Следующие факты важны для более четкого представления о том, что такое многопоточность (это проще, чем вы думали):
- потоки не являются собственными переменными, у них есть стеки, а стек может содержать некоторые переменные, но это не моя точка.
- не существует внутренней связи между потоком, на котором создается экземпляр класса и этот поток. Он принадлежит всем нити таким же образом, каким он не принадлежит ни одному из них.
- когда я говорю это, я не говорю о стеках потоков, но можно сказать, что потоки и экземпляры - это два набора независимых объектов, которые просто взаимодействуют для большего блага:)
EDIT
Я вижу, что в этой теме ответов появилось слово "безопасность потока".
Если вы, возможно, задаетесь вопросом, что означают эти слова, я рекомендую эту замечательную статью @Eric Lippert:
http://blogs.msdn.com/b/ericlippert/archive/2009/10/19/what-is-this-thing-you-call-thread-safe.aspx
Ответ 4
Нет, у вас есть это в обратном порядке, данные доступны до тех пор, пока они все еще находятся в области видимости.
Вам нужно защититься от противоположной проблемы, причем два потока обращаются к тем же данным одновременно, что называется условием гонки. Вы можете использовать метод синхронизации, например lock
, чтобы это не происходило, но если оно используется неправильно, это может привести к тупиковой ситуации.
Прочитайте С# Threading в .NET для учебника.
Ответ 5
Места памяти не изолированы от одного потока. Было бы неудобно, если бы они были. Память в CLR изолирована только на границе домена приложения. Вот почему существует отдельный экземпляр каждой статической переменной для AppDomain. Тем не менее, потоки не привязаны ни к одному конкретному домену приложения. Они могут выполнять код в нескольких доменах приложения или нет (неуправляемый код). То, что они не могут сделать, это выполнить код из нескольких доменов приложения одновременно. Это означает, что поток не может одновременно иметь доступ к структурам данных из двух разных доменов приложений. Вот почему вы должны использовать методы маршалинга (например, через MarshalByRefObject
) или использовать протоколы связи, такие как .NET Remoting или WCF, чтобы получить доступ к структурам данных из другого домена приложения.
Рассмотрим следующую диаграмму в стиле юникода процесса, в котором размещена CLR.
┌Process───────────────────────────────┐
│ │
│ ┌AppDomain───┐ ┌AppDomain───┐ │
│ │ │ │ │ │
│ │ ┌──────Thread──────┐ │ │
│ │ │ │ │ │
│ │ └──────────────────┘ │ │
│ │ │ │ │ │
│ └────────────┘ └────────────┘ │
└──────────────────────────────────────┘
Вы можете видеть, что каждый процесс может иметь более одного домена приложения и что поток может выполнять код из более чем одного из них. Я также попытался проиллюстрировать тот факт, что поток также может выполнять неуправляемый код, показывая его существование вне левого и правого блоков AppDomain.
Таким образом, в основном поток имеет тривиальный и нетривиальный доступ к любым структурам данных в том же домене приложения, в котором он выполняется. Я использую термин "тривиальный" здесь, чтобы включить обращения к памяти (структуры данных, переменные и т.д.)..) через публичных, защищенных или внутренних членов из одного класса в другой. Нить никоим образом не предотвратит это. Однако, используя отражение, вы все равно можете получить доступ даже к частным членам другого класса. Это то, что я называю нетривиальным доступом. Да, это требует немного больше работы с вашей стороны, но в противном случае ничего необычного не происходит, как только вы завершили вызовы отражения (которые, кстати, должны быть разрешены безопасностью доступа к коду, но это другая тема). Дело в том, что поток имеет доступ практически ко всей памяти в том же домене приложения, в котором он выполняется.
Причина, по которой поток имеет доступ практически ко всем приложениям в том же домене приложения, потому что это было бы безумно ограничительным, если бы это не так. Разработчики должны были бы приложить дополнительные усилия для совместного использования структур данных между классами при работе в многопоточной среде.
Итак, суммируем основные моменты:
- Между структурой данных (классом/структурой) или ее составными элементами и потоком нет отношения "один-к-одному".
- Между потоком и доменом приложения нет взаимно однозначного отношения.
- И технически нет никакой связи между потоком ОС и потоком CLR (хотя на самом деле я не знаю об основных реализациях CLI, которые отклоняются от этого подхода 1).
- Очевидно, что поток CLR все еще ограничен процессом, в котором он был создан.
1 Даже операционная система Singularity, как представляется, напрямую сопоставляет потоки .NET с операционной системой и оборудованием.