RuntimeState and asynchronous tasks
Posted: Fri Jan 19, 2024 12:08 pm
Hi,
Currently, the RuntimeState class uses the ThreadLocal class to store values. This is fine for classic multi-threaded programming based on Thread classes, but causes problems in the more modern style based on asynchronous tasks (Task class, async, await). The problem is that asynchronous tasks are abstract from threads and use a pool of background threads under the hood. One task can start in one thread and end in another (resuming the task in a free thread from the pool). This leads to the fact that RuntimeState can return different values for the same task, which leads to errors in the application logic. To support asynchronous tasks, a good choice of value store for RuntimeState would be the AsyncLocal class. At the same time, the logic of its operation is also suitable for classical threads. Thus, there is a proposal to the X# development team to replace the storage class in RuntimeState with AsyncLocal instead of ThreadLocal. But not for all values. For example, it is better to leave the list of workareas as is on ThreadLocal (each thread has its own isolated list of workareas). Because existing user algorithms can be guided by this feature.
This is just my suggestion, so I wonder what other colleagues think about it.
One example where the problem described above may occur:
1. Thread #1 (the main application thread) set the working directory using the SetDefault function.
2. Thread #1 (the main application thread) starts the asynchronous task 'TaskA', which is executed in the background thread #2. This task gets the correct SetDefault value from RuntimeState.
3. The asynchronous task 'TaskA' has completed its work.
4. Thread #1 (the main application thread) changed the working directory using the SetDefault function.
5. Thread #1 (the main application thread) starts a new asynchronous task 'TaskB' (or even the same task TaskA, it doesn't matter), for which a free thread was automatically selected from the thread pool, and it turned out to be the same background thread #2. TaskB receives an incorrect SetDefault value from RuntimeState, which was set in step 1. And the expected value was set in step 4.
Currently, the RuntimeState class uses the ThreadLocal class to store values. This is fine for classic multi-threaded programming based on Thread classes, but causes problems in the more modern style based on asynchronous tasks (Task class, async, await). The problem is that asynchronous tasks are abstract from threads and use a pool of background threads under the hood. One task can start in one thread and end in another (resuming the task in a free thread from the pool). This leads to the fact that RuntimeState can return different values for the same task, which leads to errors in the application logic. To support asynchronous tasks, a good choice of value store for RuntimeState would be the AsyncLocal class. At the same time, the logic of its operation is also suitable for classical threads. Thus, there is a proposal to the X# development team to replace the storage class in RuntimeState with AsyncLocal instead of ThreadLocal. But not for all values. For example, it is better to leave the list of workareas as is on ThreadLocal (each thread has its own isolated list of workareas). Because existing user algorithms can be guided by this feature.
This is just my suggestion, so I wonder what other colleagues think about it.
One example where the problem described above may occur:
1. Thread #1 (the main application thread) set the working directory using the SetDefault function.
2. Thread #1 (the main application thread) starts the asynchronous task 'TaskA', which is executed in the background thread #2. This task gets the correct SetDefault value from RuntimeState.
3. The asynchronous task 'TaskA' has completed its work.
4. Thread #1 (the main application thread) changed the working directory using the SetDefault function.
5. Thread #1 (the main application thread) starts a new asynchronous task 'TaskB' (or even the same task TaskA, it doesn't matter), for which a free thread was automatically selected from the thread pool, and it turned out to be the same background thread #2. TaskB receives an incorrect SetDefault value from RuntimeState, which was set in step 1. And the expected value was set in step 4.