Update:
See it. There is a simple rule: if a variable is accessed from several threads (it is all the same, reading or writing), then this reading needs to be surrounded by lock 'ohm. Non-observance of this rule can lead to an immediately visible wrong result, but (which is much worse!), The appearance of an incorrect result can very rarely occur! Thus, you may have hard-to-replicable program crashes.
This rule is even valid for the case when the variable is “small” (for example, int ), and can be written “in one go”. For exact reasons, see this answer below and other answers, as well as a discussion in the comments.
In your case, callbacks from the timer (modifying) do not come in the same stream as the read code, so there is a need for blocking.
This rule is not absolute: there are cases where locks can actually be avoided. But for this you need to know the subtleties of the work of modern processors, optimizers and specific equipment, and it is very easy to make a mistake. Therefore, my advice - do not try to avoid blocking at all costs, the game is often not worth the candle. Worse, testing often will not show you problems with the code.
An important case where locks are not needed is the use of TPL and async / await for competitive access without unloading work into an additional stream.
Example:
int x = 0; async Task RunVariableChange(int howManyTimes) { for (int i = 0; i < howManyTimes; i++) { await Task.Delay(500); x++; } } async Task ReadVariable(CancellationToken ct) { try { while (true) { await Task.Delay(750, ct); Console.WriteLine(x); } } catch (TaskCanceledException) { // ничего не делать, это ожидаемое исключение } } // использование (*) var cts = new CancellationTokenSource(); var readTask = ReadVariable(cts.Token); await RunVariableChange(20); cts.Cancel(); await readTask;
If the code (*) runs in the UI thread, then all cases of access to the variable occur in it, and there is no need for blocking. In fact, we don’t have a multithreaded code here! (However, locks are still needed if Task is not launched in the UI thread, for example, via Task.Run .)
In addition, using a local variable in the async method is safe even if this method “jumps” from stream to stream, since every await is a synchronization point.
(Therefore, try to use the “functional” approach: work not with shared, but with local variables, get input as parameters, and return the result as a return value of the function: this will reduce the need for locks.)
Old answer:
Yes.
If the code modifies a variable, then multithreaded access requires the protection of the memory barrier, for example, lock .
Otherwise, both the optimizer and the processor's cache are entitled to assume that the variable does not change.
Example with processor cache: let's say stream # 1 runs on processor # 1, and stream # 2 runs on processor # 2. Stream # 1 writes the new value of the variable. This value enters the cache of processor # 1, but does not “fail” immediately into memory, since synchronization with memory is a very slow process. Now, stream # 2 reads the value from processor cache # 2, and there it is old!
Forcing a "reset" of the processor cache into memory is one of the effects that lock causes.
Why is this effect rarely seen in practice? The fact is that the cache reset can sometimes occur for external reasons, for example, when switching context.
I hope I understood the purpose of your code correctly. I had to guess, because neither Thread nor Timer contain an event backFunc .