控制台应用程序中的无锁线程安全

Lock-free thread safety in console apps

为了确保线程安全,我正在尝试寻找一种通用的跨平台方法来

  1. 在主线程中异步执行所有委托或...
  2. 在后台线程中执行 delegete 并将结果传递给主线程

考虑到控制台应用 not 具有同步上下文,我在加载应用时创建了新的上下文,然后使用以下方法之一。

  1. 按照 Stephen ToubAwait, SynchronizationContext, and Console Apps 文章中所述设置和恢复自定义 SC
  2. 使用 context.Post 调用将所有委托编组到主线程,如 Stephen Toub ExecutionContext vs SynchronizationContext 文章中所述
  3. 如 Joe Albahari Basic synchronization 所述
  4. 将后台线程与生产者-消费者集合结合使用

问题

想法 #1 和 #2 只有在同步完成时才能正确设置上下文。如果它们是从 Parallel.For(0, 100) 内部调用的,那么同步上下文将开始使用线程池中可用的所有线程。想法 #3 总是按预期在专用线程中执行任务,不幸的是,不是在主线程中。结合idea #3 with IOCompletionPortTaskScheduler,我可以实现异步和单线程,不幸的是,这种方法只适用于Windows。 有没有办法结合这些解决方案来实现上面的需求post,包括跨平台

调度程序

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;
  }
}

主要

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

new Thread(async () =>
{
  var res = await scheduler.ExecuteAsync(() => 5, sc);
});

您可以使用 lock/Monitor.Pulse/Monitor.Wait and a Queue

我知道标题说 lock-free。但我的猜测是你希望 UI 更新发生在锁之外,或者工作线程应该能够继续工作而不必等待主线程更新 UI (至少我是这样的了解要求)。

这里的锁永远不会在生成项目或更新 UI 期间。它们仅在 queue.

中的 enqueue/dequeue 项所需的短暂时间内保留
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");
        }
    }
}

更新

由于不想让主线程等待一个事件,可以改代码如下:

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");
}

上述解决方案中的问题是主线程应该只做它应该做的部分工作,然后调用 GetWorkItem 检查 queue 是否有东西,然后再恢复它正在做的事情再次。如果你能把这项工作分成不会花太长时间的小块,这是可行的。

不知道我这里的回答是不是你想要的。当 queue 中没有工作项时,您认为主线程会做什么?

如果您认为它应该什么也不做(即等待),那么 Wait 解决方案应该没问题。

如果您认为它应该做某事,那么可能它应该做的工作也可以 queued 作为工作项。