При многопоточной работе исключена вероятность одновременнего доступа к ресурсам?
Заранее извиняюсь за большое количество текста. Я изучил довольно много информации на тему потоков. Разобрался с lock, мониторами, семафорами и мьютексами. Читал про SynchronizationAttribute, волатильность типов и класс Interlocked. Всё это мне более-менее понятно, но есть фундаментальный вопрос, который делает все остальные знания по теме бесполезными.
Как я понял, когда я запускаю несколько потоков, то они не работают одновременно. На деле процессор сначала n секунд занимается одним потоком, потом n секунд - другим и т. д. Это называется переключение контекста. Но тогда выходит, что одновременное обращение к переменной класса из разных потоков не возможно ? Ведь на деле мы просто сначала n секунд будем работать с переменной из одного потока, потом n секунд - из другого и т.д. По этой же логике выходит, что мы можем спокойно добавлять данные в список и удалять данные из того же списка из разных потоков? Но тогда не очень понятно, зачем нужны классы из неймспейса System.Collections.Concurrent.
Изучив информацию, я запомнил (но не понял), что атомарные операции безопасны при работе в многопоточном коде (это так?). Именно поэтому, например, инкрементация переменной с помощью Interlocked.Add безопасна и не приводит к проблемам. Текст получился несколько сумбурным, поэтому постараюсь подытожить:
1. Если никакой одновременности работы потоков нет, то как возможно одновременное изменение переменной из разных потоков?
2. Почему атомарные операции всегда безопасны в отношении потоков?
freeExec, то есть в случае одноядерного процессора не будет никаких проблем при работе с многопоточностью, потому что не будет одновременной работы потоков?
Михаил, может быть. Представим следующую ситуацию:
var a = 1;
var b = 1;
Console.WriteLine(a);
Console.WriteLine(b);
Console.WriteLine(a + b);
Что будет напечатано? В однопоточной среде точно 1,1,2. Во многопоточной может быть 1,1,100500. Это случилось потому что после вывода первых двух чисел произошло переключение на другой поток, который и изменил значения переменых, а изначальный поток об этом никак не узнает и продолжить роботу как обычно.
Станислав Силин, хорошо, тогда последний вопрос (надеюсь). Допустим, есть два ядра и два потока. То есть переключения контекста нет. Может ли быть такое, что случится одновременная попытка изменить переменную? Или всегда будет разница в несколько миллисекунд, и какой-то из потоков всегда будет опережать другой?
Михаил, переменная это просто участок памяти. Любой поток может писать туда в любое время(если не использовать синхронизацию). В случае когда два потока работаю одновременно, то время записи может совпадать.
Станислав Силин, но если у меня в один и тот же момент времени один поток попытается записать в участок памяти число 10, а другой поток - число 5, то какое число будет записано? И если какое-то из этих чисел будет записано, то выходит, что попытка записи не была одновременной и какой-то поток всё же опередил другой. Или я опять чего-то не понимаю?
Станислав Силин, то есть на уровне кода можно заставить 2 потока попытаться изменить значение одного участка памяти, но на уровне железа всё равно какой-то из потоков полоучит доступ первый?
Ты не совсем понял проблему многопоточности. Да, чтение неизменяемого значения из множества потоков безопасно, но вот изменение - нет.
На примере:
Лежит в памяти значение x = 1. Пока его только читают, всё ок, но предположим, что это у нас какой-то счётчик и два потока хотят его увеличивать на 1.
Первый поток считывает значение, кладёт его в стек, добавляет 1 и после записывает результат (2) обратно. Сам видишь, что тут отнюдь не одна операция, и между моментом считывания и записью проходит время. В это время второй поток может спокойно успеть считать пока ещё старое значение. Выходит, что оба потока возьмут 1, увеличать её на 1 и запишут в память, не важно в каком порядке. В результате вроде как оба потока отработало код, но значение в памяти увеличено только на 1. В этом и проблема.
Списки, к примеру, внутри хранят тот же счётчик для количества элементов, поэтому одновременная запись может неправильно отработать.
Кажется, я начал понимать свою ошибку. Я правильно понимаю, что, если у меня в методе, например, 1000 строк, то переключения контекста может случиться в любой строчке этого метода? Мне почему-то казалось, что весь метод полностью отработает сначала в одном потоке, потом - в другом
Михаил, нет, вы ошиблись. Более того, потоки могут в одно и то же время выполнять один и тот же метод. Метод - всего лишь абстракция, для процессора это всё сплошной поток машинных кодов.
Переключение контекста сохраняет все регистры процессора для потока и востанавливает для активного.
Если у вас идет обращение к переменной через регистр (см. volatile), а не напрямую к адресу памяти, то сохранность актуального значения не гарантируется. Примерный сценарий:
1. Первый поток поместил значение переменной в регистр (mov ax, [4C00h])
2. Второй поток сохранил его в регистр (mov ax, [4C00h])
3. Первый сделал какие-то операции над значением в регистре
4. Второй сделал.
5. Первый сохраняет значение по адресу памяти (mov [4C00h], ax)
6. Второй.
И вот какое значение сохранилось? Ведь каждый поток оперировал своими копиями регистров. И четкая последовательность вызова потоков тоже не гарантируется.
Если я запущу этот метод в двух потоках на одноядерном процессоре, то разве не получится, что сначала один поток полностью выполнит все 202 строчки, потом - второй поток и т. д.? Или переключение контекста может случиться даже в середине метода? То есть первый поток спокойно выполнит 101 строчку, а потом запустит другой поток, который выполнит, например, 103 строчки, а потом первый поток довыполнит метод Foo?
Как я понял, когда я запускаю несколько потоков, то они не работают одновременно. На деле процессор сначала n секунд занимается одним потоком, потом n секунд - другим и т. д. Это называется переключение контекста.
Процессоров и ядер может быть несколько. Соотвествтвенно эти рассуждения только в случае одного процессора и одного ядра. Но и в случае одного ядра атомарность операций не гарантированна см комментарий Meloman19