1

To ensure thread-safety, I'm trying to find a generic cross-platform approach to

  1. execute all delegates asynchronously in the main thread or ...
  2. execute delegete in a background thread and pass result to the main one

Considering that console apps do not have synchronization context, I create new context when app is loading and then use one of the following methods.

  1. Set and restore custom SC as described in Await, SynchronizationContext, and Console Apps article by Stephen Toub
  2. Marshall all delegates to main thread using context.Post call as described in the article ExecutionContext vs SynchronizationContext by Stephen Toub
  3. Using background thread with producer-consumer collection as described in Basic synchronization by Joe Albahari

Question

Ideas #1 and #2 set context correctly only if it's done synchronously. If they're called from inside Parallel.For(0, 100) then synchronization context starts using all threads available in a thread pool. Idea #3 always performs tasks within dedicated thread as expected, unfortunately, not in the main thread. Combining idea #3 with IOCompletionPortTaskScheduler, I can achieve asynchrony and single-threading, unfortunately, this approach will work only in Windows. Is there a way to combine these solutions to achieve requirements at the top of the post, including cross-platform.

Scheduler

public class SomeScheduler 
{
  public Task<T> RunInTheMainThread<T>(Func<T> action, SynchronizationContext sc)
  {
    var res = new TaskCompletionSource<T>();

    SynchronizationContext.SetSynchronizationContext(sc); // Idea #1
    sc.Post(o => res.SetResult(action()), null); // Idea #2 
    ThreadPool.QueueUserWorkItem(state => res.SetResult(action())); // Idea #3
    
    return res.Task;
  }
}

Main

var scheduler = new SomeScheduler();
var sc = SynchronizationContext.Current ?? new SynchronizationContext();

new Thread(async () =>
{
  var res = await scheduler.ExecuteAsync(() => 5, sc);
});
Kit
  • 20,354
  • 4
  • 60
  • 103
Anonymous
  • 1,823
  • 2
  • 35
  • 74
  • 1
    The question looks like an [XY problem](https://en.wikipedia.org/wiki/XY_problem) where you are asking about your solution rather than explaining the original problem. Most likely your problem can be solved in a simple way without a SynchronizationContext, like for example firing all tasks then using Task.WhenAll, or something similar. It is also not clear what do you mean by "execute all delegates asynchronously in the main thread" or "I can achieve asynchrony and single-threading". – Sherif Elmetainy May 31 '22 at 09:13
  • @SherifElmetainy Trying to rephrase, I'm looking for a way to make console app work similar to WPF, so it could execute some heavy tasks in the background but can always refresh UI in the thread that would be considered main. Something like window [message loop](https://stackoverflow.com/a/51944920/437393) in C++ and Windows but cross-platform. I think there must be some common design pattern to solve problems like this and `Synchronizationcontext` sounds like a part of it, but it feels like I may lack some knowledge and thus can't implement it correctly. – Anonymous May 31 '22 at 17:19

1 Answers1

2

You can use the lock/Monitor.Pulse/Monitor.Wait and a Queue

I know the title says lock-free. But my guess is that you want the UI updates to occur outside the locks or worker threads should be able to continue working without having to wait for main thread to update the UI (at least this is how I understand the requirement).

Here the locks are never during the producing of items, or updating the UI. They are held only during the short duration it takes to enqueue/dequeue item in the queue.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using static System.Threading.Thread;

namespace ConsoleApp1
{
    internal static class Program
    {
        private class WorkItem
        {
            public string SomeData { get; init; }
        }

        private static readonly Queue<WorkItem> s_workQueue = new Queue<WorkItem>();

        private static void Worker()
        {
            var random = new Random();
            // Simulate some work
            Sleep(random.Next(1000));
            // Produce work item outside the lock
            var workItem = new WorkItem
            {
                SomeData = $"data produced from thread {CurrentThread.ManagedThreadId}"
            };
            // Acquire lock only for the short time needed to add the work item to the stack
            lock (s_workQueue)
            {
                s_workQueue.Enqueue(workItem);
                // Notify the main thread that a new item is added to the queue causing it to wakeup
                Monitor.Pulse(s_workQueue);
            }
            // work item is now queued, no need to wait for main thread to finish updating the UI
            // Continue work here
        }

        private static WorkItem GetWorkItem()
        {
            // Acquire lock only for the duration needed to get the item from the queue
            lock (s_workQueue)
            {
                WorkItem result;
                // Try to get the item from the queue
                while (!s_workQueue.TryDequeue(out result))
                {
                    // Lock is released during Wait call
                    Monitor.Wait(s_workQueue);
                    // Lock is acquired again after Wait call
                }

                return result;
            }
        }

        private static void Main(string[] args)
        {
            const int totalTasks = 10;
            for (var i = 0; i < totalTasks; i++)
            {
                _ = Task.Run(Worker);
            }

            var remainingTasks = totalTasks;
            // Main loop (similar to message loop)
            while (remainingTasks > 0)
            {
                var item = GetWorkItem();
                // Update UI
                Console.WriteLine("Got {0} and updated UI on thread {1}.", item.SomeData, CurrentThread.ManagedThreadId);
                remainingTasks--;
            }

            Console.WriteLine("Done");
        }
    }
}

Update

Since you don't want to have the main thread Wait for an event, you can change the code as follows:

private static WorkItem? GetWorkItem()
{
    // Acquire lock only for the duration needed to get the item from the queue
    lock (s_workQueue)
    {
        // Try to get the item from the queue
        s_workQueue.TryDequeue(out var result);
        return result;
    }
}

private static void Main(string[] args)
{
    const int totalTasks = 10;
    for (var i = 0; i < totalTasks; i++)
    {
        _ = Task.Run(Worker);
    }

    var remainingTasks = totalTasks;
    // Main look (similar to message loop)
    while (remainingTasks > 0)
    {
        var item = GetWorkItem();
        if (item != null)
        {
            // Update UI
            Console.WriteLine("Got {0} and updated UI on thread {1}.", item.SomeData, CurrentThread.ManagedThreadId);
            remainingTasks--;
        }
        else
        {
            // Queue is empty, so do some other work here then try again after the work is done
            // Do some other work here
            // Sleep to simulate some work being done by main thread 
            Thread.Sleep(100);
        }
    }

    Console.WriteLine("Done");
}

The problem in the above solution is that the Main thread should do only part of the work it is supposed to do, then call GetWorkItem to check if the queue has something, before resuming whatever it was doing again. It is doable if you can divide that work into small pieces that don't take too long.

I don't know if my answer here is what you want. What do you imagine the main thread would be doing when there are no work items in the queue?

if you think it should be doing nothing (i.e. waiting) then the Wait solution should be fine.

If you think it should be doing something, then may be that work it should be doing can be queued as a Work item as well.

Sherif Elmetainy
  • 4,034
  • 1
  • 13
  • 22
  • This is pretty close to how I imagined it. The only thing that I would like to have is the ability not to block the main thread with `while` and `Monitor.Wait`. Is there a way to replace `Wait` with something non-blocking like `observable.ObserveOn(mainThread).Subscribe()` or `Event` handler? – Anonymous Jun 03 '22 at 10:31
  • You can remove the Wait call entirely and change the while to an if and return null if there is nothing in the queue. Then the main thread can do whatever it needs to do, and then call GetWorkItem whenever it wants to check for the existence of a work item. See my updated answer. – Sherif Elmetainy Jun 03 '22 at 14:53
  • To elaborate more on this. I don't know of a way where you can send task to a thread such that the thread stops doing whatever it is doing, saves its state, then execute the task, then restore the state and resume whatever it was doing before. When comparing to GUI applications, the UI thread has a message loop where it does wait for messages which are added to a queue. The message can be a keyboard event, a mouse event, etc. Methods that Invoke on main thread, actually convert the call to a message and add the message to the queue in a similar manner to the Wait solution I suggested. – Sherif Elmetainy Jun 03 '22 at 16:49
  • After looking at the reactive `RX` library I believe you're right because they do execute a [loop](https://github.com/dotnet/reactive/blob/main/Rx.NET/Source/src/System.Reactive/Concurrency/CurrentThreadScheduler.cs#L155) and this loop is actually blocking the thread. In order to make it look like non-blocking, the tasks in the queue are executed in batches, each batch executes all queued actions, then the main thread continues, then new loop with queued actions is scheduled. Accepted. – Anonymous Jun 04 '22 at 06:13