C# 非阻塞(无锁)一次性初始化 原始问题:

C# 非阻塞(无锁)一次性初始化 原始问题:,c#,multithreading,asynchronous,lock-free,C#,Multithreading,Asynchronous,Lock Free,我只需要在多线程应用程序中初始化一次(当第一个线程进入块时)。后续线程应该跳过初始化,而不等待它完成 我找到了这个博客条目,但它并没有完全满足我的要求,因为它会让其他线程等待初始化完成(如果我理解正确的话) 下面的示例说明了该问题,尽管由于缺乏同步而无法正常工作: using System; using System.Collections.Concurrent; using System.Threading.Tasks; namespace LockFreeInitialization {

我只需要在多线程应用程序中初始化一次(当第一个线程进入块时)。后续线程应该跳过初始化,而不等待它完成

我找到了这个博客条目,但它并没有完全满足我的要求,因为它会让其他线程等待初始化完成(如果我理解正确的话)

下面的示例说明了该问题,尽管由于缺乏同步而无法正常工作:

using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;

namespace LockFreeInitialization
{
    public class Program
    {
        private readonly ConcurrentQueue<int> _jobsQueue = new ConcurrentQueue<int>();
        private volatile bool _initialized;

        private async Task EnqueueAndProcessJobsAsync(int taskId, int jobId)
        {
            Enqueue(taskId, jobId);

            /* "Critical section"? Only the first thread to arrive should
             * execute OneTimeInitAsync. Subsequent threads should always
             * skip this part. This is where things go wrong as all the
             * tasks execute this section due to lack of synchronization. */
            if (!_initialized)
            {
                await OneTimeInitAsync(taskId);
            }

            /* Before and during initialization, all threads should skip
             * the ProcessQueueAsync. After initialization is completed,
             * it does not matter which thread will execute it (since the
             * _jobsQueue is thread-safe). */
            if (_initialized)
            {
                await ProcessQueueAsync(taskId);
            }
            Console.WriteLine($"Task {taskId} completed.");
        }

        private void Enqueue(int taskId, int jobId)
        {
            Console.WriteLine($"Task {taskId} enqueues job {jobId}.");
            _jobsQueue.Enqueue(jobId);
        }

        private async Task OneTimeInitAsync(int taskId)
        {
            Console.WriteLine($"Task {taskId} is performing initialization");

            /* Do some lengthy initialization */
            await Task.Delay(TimeSpan.FromSeconds(3));
            _initialized = true;

            Console.WriteLine($"Task {taskId} completed initialization");
        }

        private async Task ProcessQueueAsync(int taskId)
        {
            while (_jobsQueue.TryDequeue(out int jobId))
            {
                /* Do something lengthy with the jobId */
                await Task.Delay(TimeSpan.FromSeconds(1));

                Console.WriteLine($"Task {taskId} completed job {jobId}.");
            }
        }

        private static void Main(string[] args)
        {
            var p = new Program();
            var rand = new Random();

            /* Start 4 tasks in parallel */
            for (var threadId = 1; threadId < 5; threadId++)
            {
                p.EnqueueAndProcessJobsAsync(threadId, rand.Next(10));
            }

            /* Give tasks chance to finish */
            Console.ReadLine();
        }
    }
}
输出示例:

InitializeConnectionAsync started
InitializeJobAsync(1) started
InitializeJobAsync(2) started
InitializeJobAsync(3) started
InitializeJobAsync(4) started
InitializeJobAsync(5) started
InitializeJobAsync(3) completed
InitializeJobAsync(2) completed
InitializeConnectionAsync completed
Processed 3, 2,
InitializeJobAsync(1) completed
Processed 1,
InitializeJobAsync(5) completed
Processed 5,
InitializeJobAsync(4) completed
Processed 4,

感谢所有的反馈

正如OP评论中所指出的,更好的解决方案可能是在单线程模式下进行初始化,然后启动执行实际工作的线程

如果这对您不起作用,您将需要某种类型的锁,但您可以使用该锁进行调度,以减少阻塞。我将实现如下内容:

private bool _initializationIsScheduled = false;
private object _initializationIsScheduledLock = new object();
private bool _isInitialized = false;
private object _isInitializedLock = new object();

private async Task EnqueueAndProcessJobs(int taskId, int jobId)
{
    var shouldDoHeavyWork = false;

    lock(_initializationIsScheduledLock)
    {
        if (!_initializationIsScheduled)
        {
            shouldDoHeavyWork = true;
            _initializationIsScheduled= true;
        }
    }

    if (shouldDoHeavyWork)
    {
        await OneTimeInitAsync(taskId);
        lock (_isInitializedLock)
        {
            _isInitialized = true;
        }
    }

    lock (_isInitializedLock)
    {
        if (_isInitialized)
        {
            shouldDoHeavyWork = true;
        }
    }

    if (shouldDoHeavyWork)
    {
        await ProcessQueueAsync(taskId);
    }
    Console.WriteLine($"Task {taskId} completed.");
}
请注意,线程锁定其他线程的唯一时间是在它即将检查或设置控制其工作的标志之一时。换句话说,在实际执行繁重的工作时,线程不必彼此等待,只需在调度时等待(即在设置布尔标志时等待两个CPU周期)


代码并不十分漂亮,但您应该能够将上面的示例重构为合理易读的东西…:)

正如OP评论中所指出的,更好的解决方案可能是在单线程模式下进行初始化,然后启动执行实际工作的线程

如果这对您不起作用,您将需要某种类型的锁,但您可以使用该锁进行调度,以减少阻塞。我将实现如下内容:

private bool _initializationIsScheduled = false;
private object _initializationIsScheduledLock = new object();
private bool _isInitialized = false;
private object _isInitializedLock = new object();

private async Task EnqueueAndProcessJobs(int taskId, int jobId)
{
    var shouldDoHeavyWork = false;

    lock(_initializationIsScheduledLock)
    {
        if (!_initializationIsScheduled)
        {
            shouldDoHeavyWork = true;
            _initializationIsScheduled= true;
        }
    }

    if (shouldDoHeavyWork)
    {
        await OneTimeInitAsync(taskId);
        lock (_isInitializedLock)
        {
            _isInitialized = true;
        }
    }

    lock (_isInitializedLock)
    {
        if (_isInitialized)
        {
            shouldDoHeavyWork = true;
        }
    }

    if (shouldDoHeavyWork)
    {
        await ProcessQueueAsync(taskId);
    }
    Console.WriteLine($"Task {taskId} completed.");
}
请注意,线程锁定其他线程的唯一时间是在它即将检查或设置控制其工作的标志之一时。换句话说,在实际执行繁重的工作时,线程不必彼此等待,只需在调度时等待(即在设置布尔标志时等待两个CPU周期)


代码并不十分漂亮,但您应该能够将上面的示例重构为合理易读的东西…:)

也许你可以试试这样的东西:

static bool IsInitializing = false;
static int FirstThreadId = -1;

// check if initialised
// return if initialised

// somewhere in init mehtod
lock (lockObject)
{
   // first method start initializing
   IsInitializing = true;

   // set some id to the thread that start initializtion
   FirstThreadId = THIS_THREAD_OR_TASK_ID;
}

if (IsInitializing && FristThread != THIS_THREAD_OR_TASK_ID)
   return; // skip initializing

lock
必须快速工作,是的

也许你可以试试这样的方法:

static bool IsInitializing = false;
static int FirstThreadId = -1;

// check if initialised
// return if initialised

// somewhere in init mehtod
lock (lockObject)
{
   // first method start initializing
   IsInitializing = true;

   // set some id to the thread that start initializtion
   FirstThreadId = THIS_THREAD_OR_TASK_ID;
}

if (IsInitializing && FristThread != THIS_THREAD_OR_TASK_ID)
   return; // skip initializing

lock
必须快速工作,是的,老实说,
EnqueueAndProcessJobsAsync
的语义对于您的代码来说根本不是一个好主意,因为您描述了您实际在做什么以及您实际需要什么

当前,如果初始化不是由其他人启动的,则从
EnqueueAndProcessJobsAsync
返回的
任务
将等待初始化,然后每当队列为空时,或当此逻辑调用上下文处理出错的项时,任务完成。那…就是说不通

显然,您希望在作业传入完成时完成该任务(当然需要初始化才能完成),或者在该作业出错时出错,并且不受任何其他作业错误的影响。幸运的是,除了更有用之外,它也更容易实现

就实际初始化而言,您只需使用
惰性
,以确保异步初始化的正确同步,并将
任务
公开给任何将来的调用,这些调用可以在初始化完成时通知它们

public class MyAsyncQueueRequireingInitialization
{
    private readonly Lazy<Task> whenInitialized;
    public MyAsyncQueueRequireingInitialization()
    {
        whenInitialized = new Lazy<Task>(OneTimeInitAsync);
    }
    //as noted in comments, the taskID isn't actually needed for initialization
    private async Task OneTimeInitAsync() 
    {
        Console.WriteLine($"Performing initialization");

        /* Do some lengthy initialization */
        await Task.Delay(TimeSpan.FromSeconds(3));

        Console.WriteLine($"Completed initialization");
    }

    public async Task ProcessJobAsync(int taskID, int jobId)
    {
        await whenInitialized.Value;

        /* Do something lengthy with the jobId */
        await Task.Delay(TimeSpan.FromSeconds(1));

        Console.WriteLine($"Completed job {jobId}.");
    }
}
公共类MyAsyncQueueRequireInInitialization
{
初始化时为私有只读;
公共MyAsyncQueueRequireInInitialization()
{
whenInitialized=新延迟(OneTimeInitAsync);
}
//如注释中所述,初始化实际上不需要taskID
专用异步任务OneTimeInitAsync()
{
Console.WriteLine($“正在执行初始化”);
/*做一些冗长的初始化*/
等待任务延迟(时间跨度从秒(3));
Console.WriteLine($“已完成初始化”);
}
公共异步任务ProcessJobAsync(int-taskID,int-jobId)
{
在初始化时等待。值;
/*对jobId做些长时间的处理*/
等待任务延迟(时间跨度从秒(1));
WriteLine($“已完成作业{jobId}”);
}
}

老实说,
EnqueueAndProcessJobsAsync
的语义对于您的代码来说根本不是一个好主意,因为您描述了您实际在做什么以及您实际需要什么

当前,如果初始化不是由其他人启动的,则从
EnqueueAndProcessJobsAsync
返回的
任务
将等待初始化,然后每当队列为空时,或当此逻辑调用上下文处理出错的项时,任务完成。那…就是说不通

显然,您希望在作业传入完成时完成该任务(当然需要初始化才能完成),或者在该作业出错时出错,并且不受任何其他作业错误的影响。幸运的是,除了更有用之外,它也更容易实现

就实际初始化而言,您只需使用
惰性
,以确保异步初始化的正确同步,并将
任务
公开给任何将来的调用,这些调用可以在初始化完成时通知它们

public class MyAsyncQueueRequireingInitialization
{
    private readonly Lazy<Task> whenInitialized;
    public MyAsyncQueueRequireingInitialization()
    {
        whenInitialized = new Lazy<Task>(OneTimeInitAsync);
    }
    //as noted in comments, the taskID isn't actually needed for initialization
    private async Task OneTimeInitAsync() 
    {
        Console.WriteLine($"Performing initialization");

        /* Do some lengthy initialization */
        await Task.Delay(TimeSpan.FromSeconds(3));

        Console.WriteLine($"Completed initialization");
    }

    public async Task ProcessJobAsync(int taskID, int jobId)
    {
        await whenInitialized.Value;

        /* Do something lengthy with the jobId */
        await Task.Delay(TimeSpan.FromSeconds(1));

        Console.WriteLine($"Completed job {jobId}.");
    }
}
公共类MyAsyncQueueRequireInInitialization
{
初始化时为私有只读;
公共MyAsyncQueueRequireInInitialization()
{
whenInitialized=新延迟(OneTimeInitAsync);
}
//如注释中所述,初始化实际上不需要taskID
专用异步任务OneTimeInitAsync()
{
Console.WriteLine($“正在执行初始化”);
/*做一些冗长的初始化*/
等待任务。延迟