C
C#16mo ago
br4kejet

❔ Executing a method every n seconds but only during certain conditions

What would be the best way to do this? The easiest way I can think of would be:
Task.Run(MainAsync);
...
public async Task MainAsync()
{
while (true)
{
if (SomeCondition)
{
// do stuff
SomeCondition = false;
}

await Task.Delay(1000);
}
}
Task.Run(MainAsync);
...
public async Task MainAsync()
{
while (true)
{
if (SomeCondition)
{
// do stuff
SomeCondition = false;
}

await Task.Delay(1000);
}
}
But this feels like it might be wasteful, especially if I end up having 100s of these tasks running. Is there a better way to do this?
57 Replies
cap5lut
cap5lut16mo ago
i think thats the best u can do, its simple and has next to no overhead. the question is more about if thats really what u want to do. lets assume for a moment that the condition is always met: so if u want it to start doing stuff every second, then this approach is wrong, because the time to do stuff is not accounted for. basically if doing stuff would take 500ms u would still wait 1s afterwards, if its desired to be kicked off every 1s then u would need to measure the execution time of doing stuff and substract that from 1s. or simplier: use a PeriodicTimer (https://learn.microsoft.com/en-us/dotnet/api/system.threading.periodictimer?view=net-7.0) if u want to indeed have 1sec delay between executions, the code example is fine. another thing is what u do with that condition, this looks a bit like u write a polling method instead of using proper event constructs, but to suggest something on this matter u would have to explain more about the real use case
br4kejet
br4kejetOP16mo ago
I'm using it for a file explorer app, I would use the build in file change listener thing to listen to when the current folder changes in some way, and that would update the app But if the folder changes extremely quickly, it could freeze the app, so I just want updates to happen every 500 ms~ maybe, and just calculate the folder changes in that tick well not every 500 ms, but no less than 500ms
ero
ero16mo ago
have you actually checked that the filechangelistener is not good enough?
cap5lut
cap5lut16mo ago
hmm i wouldnt write that via polling
br4kejet
br4kejetOP16mo ago
It not really polling, it's more like delay event firing i guess Events from the file changes, and delaying them until a certain amount of time has passed, ignoring any other events during that wait time The only other way I can think of doing it would be polling which I guess could work, I could work but I'd have to track the size of every file in that folder possibly, and do that every tick
cap5lut
cap5lut16mo ago
i wouldnt ingore the other events
br4kejet
br4kejetOP16mo ago
why?
cap5lut
cap5lut16mo ago
i would simply kick off the gui update task with an initial delay
br4kejet
br4kejetOP16mo ago
That's kinda what i was thinking too
cap5lut
cap5lut16mo ago
that way the event listener can update the state asap and once the gui update kicks in the stuff is applied gui update might need some additional delaying inbetween tho
br4kejet
br4kejetOP16mo ago
That's what the 500ms delay time would be for
cap5lut
cap5lut16mo ago
but at least u have 2 seperate small issues then instead of one fat complex
br4kejet
br4kejetOP16mo ago
to prevent it freezing from updating too much
cap5lut
cap5lut16mo ago
hmmm a loop might be not too bad
br4kejet
br4kejetOP16mo ago
I'm kinda assuming the case where the app is currently viewing a folder and another application is creating/deleting files every few microseconds Pretty much like the temp folder
cap5lut
cap5lut16mo ago
but use a reset event to await changes hmmm i need to think about it a bit basically u need something like an awaitable auto reset event. then ur long running update task would be
while (true) {
await updateTriggeredEvent.WaitAsync(); // the async auto reset event
await Task.Delay(500);
// do gui updates
}
while (true) {
await updateTriggeredEvent.WaitAsync(); // the async auto reset event
await Task.Delay(500);
// do gui updates
}
and the file change events would update the internal state and set the async auto reset event to be triggered iirc the BCL doesnt have that tho but instead u could use a task completion source as workaround so await updateTriggeredEvent.WaitAsync(); would become something like
await cts.Task;
cts = new TaskCompletionSource();
await cts.Task;
cts = new TaskCompletionSource();
and the file change event listeners would then trigger the update via cts.TrySetResult(); there is probably a lib out there that has an async task completen event tho
br4kejet
br4kejetOP16mo ago
I guess that could work but it still requires sitting there waiting, which means it needs its own thread or task
cap5lut
cap5lut16mo ago
an alternative would be to use channels to pump the updates through, i guess https://learn.microsoft.com/en-us/dotnet/core/extensions/channels
Channels - .NET
Learn the official synchronization data structures in System.Threading.Channels for producers and consumers with .NET.
br4kejet
br4kejetOP16mo ago
I wrote this in the mean time, no clue if it will work but oh well
cap5lut
cap5lut16mo ago
async waiting doesnt need a thread $nothread
MODiX
MODiX16mo ago
There Is No Thread
This is an essential truth of async in its purest form: There is no thread.
cap5lut
cap5lut16mo ago
but u will have a long running task somewhere, yes
br4kejet
br4kejetOP16mo ago
Yeah that what I want to avoid is long running tasks Just in case i end up using this behaviour all the time which I hope i don't...
cap5lut
cap5lut16mo ago
well, the alternative would be to fire and forget the gui update task (which still has the delay in it), only if one isnt already running but having that in sync would be relatively complex the long running task will most likely not have much of an performance inpact because most of the time its waiting so its just couple bytes (the async context) somewhere in the memory
br4kejet
br4kejetOP16mo ago
I managed to get it without without using a thread or long running tasks: https://gist.github.com/AngryCarrot789/5867c78d1bdc75e8decd7e8627c20a2f
Gist
Executes user code no less than a certain minimum interval, every t...
Executes user code no less than a certain minimum interval, every time an input function is called - ConditionMonitor.cs
cap5lut
cap5lut16mo ago
on briefly reading lgtm
br4kejet
br4kejetOP16mo ago
I feel like it might get into a locked state in that while loop; locks, puts the condition into the state, then outside of the lock it processes the state... but then OnInput() could modify the condition just after the task leaves the lock
br4kejet
br4kejetOP16mo ago
Either the debugger is helping me, or the race condition has a tiny window where this coud happen.. this test code has been running for about 2 mins
cap5lut
cap5lut16mo ago
Gist
Executes user code no less than a certain minimum interval, every t...
Executes user code no less than a certain minimum interval, every time an input function is called - ConditionMonitor.cs
br4kejet
br4kejetOP16mo ago
Think i might have fixed it now
cap5lut
cap5lut16mo ago
tbh, just keep it simple, asked for what actually is a long running task and something like the referred code isnt so keep it simple and probably use a channel and let that update task stay alive for the application life time https://discord.com/channels/143867839282020352/663803973119115264/1139129758953783336 is the start of the verification
MODiX
MODiX16mo ago
cap5lut
about long running tasks... whats actually "long running" is it about its life time, or how long the synchronous execution is?
Quoted by
<@233139962277658624> from #advanced (click here)
React with ❌ to remove this embed.
br4kejet
br4kejetOP16mo ago
I just didn't want to clog up the task scheduler with 100s of tasks Even if it can handle it performance wise, still feels weird to have so many long running tasks
Florian Voß
Florian Voß16mo ago
@cap5lut I've always found that article to be very contradicting: Stephen Curry starts with the premisse that there is no thread blocked and waiting for the Task to finish executing, no OS Thread, no ThreadPool Thread no nothing. But later he proceeds to say that a threadpool thread is borrowed to execute the APC. If you ask me thats what most people would describe as a threadpool thread being blocked and waiting for something, something being the APC from the OS
cap5lut
cap5lut16mo ago
but its not, the os does its stuff in its own threads yes, but the signaling is just a minor thing within that thread, which causes the await to finish that os thread will run anyway, no matter if you await something or not
Florian Voß
Florian Voß16mo ago
a threadpool thread needs to get notified of the APC. That should be blocking said threadpool thread
cap5lut
cap5lut16mo ago
but that os thread wouldnt be per application, but system wide so its running anyway if thats blocking or polling i dunno and it doesnt really matter in this context
Florian Voß
Florian Voß16mo ago
Sorry I meant a threadpool thread created by the Task, not some OS thread outside of our scope. there is a Threadpool thread that waits for this APC and then sets the Task to Completed and runs any ocntinuations on the UI context. This Threadpool thread is blocked for the time until the APC is done, that makes the initial premisse false
cap5lut
cap5lut16mo ago
thats not how the synchronization context is implemented afaik
Florian Voß
Florian Voß16mo ago
The task has captured the UI context, so it does not resume the async method directly on the thread pool thread. Instead, it queues the continuation of that method onto the UI context, and the UI thread will resume executing that method when it gets around to it.
cap5lut
cap5lut16mo ago
and even if it would, it would be an "one for all"
Florian Voß
Florian Voß16mo ago
I just don't see what Stephen Curry wants to tell me here. he says there is no Thread waiting, but there is a threadpool thread waiting for the APC he contradicts himjself
cap5lut
cap5lut16mo ago
well, the threads will ofc blockingly wait for something to execute, but that doesnt mean that an await causes a thread to block
Florian Voß
Florian Voß16mo ago
but await does cause a thread to block! just not the Calling thread instead it blocks a threadpool thread thats my point
cap5lut
cap5lut16mo ago
and that isnt the case is my point
Florian Voß
Florian Voß16mo ago
so then I must be missunderstanding the point that - Threadpool thread waiting for an APC blocks said threadpool thread, how tho? explain to me
cap5lut
cap5lut16mo ago
this would never cause 100000 thread spawns+blocks
for (int i = 0; i < 100000; i++) {
Task.run(async () => await Task.Delay(10000));
]
for (int i = 0; i < 100000; i++) {
Task.run(async () => await Task.Delay(10000));
]
Florian Voß
Florian Voß16mo ago
Thats due to optimizations of TaskScheduler, outside of our topic
cap5lut
cap5lut16mo ago
u cant talk about them separately
Florian Voß
Florian Voß16mo ago
ok then lets bring in the TaskScheduler too... The TaskScheduler reuses threadpool threads, but that doesn't change my point that some threadpool thread is blocked
cap5lut
cap5lut16mo ago
the thread pool threads are basically (more pseudo code than anything else)
while (true) {
var task = getScheduledTaskFromQueue(); // this is blocking
RestoreAsyncContext(task);
Execute(task);
}
while (true) {
var task = getScheduledTaskFromQueue(); // this is blocking
RestoreAsyncContext(task);
Execute(task);
}
[12:12 PM]cap5lut: and even if it would, it would be an "one for all"
it might be one, but not one for each
Florian Voß
Florian Voß16mo ago
right but then again, Stephen Curry's article is false
cap5lut
cap5lut16mo ago
and im pretty sure that the runtime has some optimization to use the os scheduler instead of its own thread for such stuff on the software level something deep down close to the bare metal has to do the polling, yes, but thats taken care of by the OS. not by dotnet runtime it will register a trigger + a callback to the task scheduler at the os, unless the synchronization context is poorly written so yeah, there is a thread for that, but no, its not a thread of ur application
Florian Voß
Florian Voß16mo ago
eehhhm from my understanding, The TaskScheduler just spawns Threads, Threads being OS Threads here. Whenever any process spawns an OS THread the OS Scheduler always comes into play. You cannot "use" it or not use it both is involved, jsut on different layers not to the Task Scheduler directly but to any OS Thread that got previously spawned by the Task Scheduler
cap5lut
cap5lut16mo ago
the os is handling the polling (thus not dependent on any application runng( -> callbacks can be registered at the os to execute stuff on event -> these events/callbacks are used for task scheduling. if a TaskScheduler would only be used for thread handling they wouldve been called ThreadScheduler, there is more to it than just threads... im not sure where exactly the polling/callback invocation happens, its probably OS dependent, but for sure i can say its outside of the dotnet process and somewhere in kernel space (or mode?)
Since the library/BCL is using the standard P/Invoke overlapped I/O system, it has already registered the handle with the I/O Completion Port (IOCP), which is part of the thread pool. So an I/O thread pool thread is borrowed briefly to execute the APC, which notifies the task that it’s complete.
its basically throwing in a task to schedule a task
Florian Voß
Florian Voß16mo ago
no it wouldn't be called ThreadScheduler, as it doesn't schedule Threads, it schedules Tasks on Threads. Not sure what "more" there is to it, what do you mean?
Accord
Accord16mo ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
Want results from more Discord servers?
Add your server