非阻塞(无锁)一次性初始化

时间:2017-08-01 13:57:23

标签: c# multithreading asynchronous lock-free

原始问题:

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

我发现此博客条目Lock-free Thread Safe Initialisation in C#但它并没有完全符合我的要求,因为它会让其他线程等待初始化完成(如果我理解正确的话)。

这是一个提出问题的示例,虽然由于缺乏同步而无效:

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

OneTimeInitAsyncProcessQueueAsync都是冗长的操作,在现实场景中会与某些远程服务进行通信。使用lock会阻止其他线程,而我希望他们将工作堆积到_jobsQueue并继续前进。我尝试使用ManualResetEvent无济于事。

有谁知道我将如何使这项工作?提前谢谢。

更新(解决方案)

根据下面的讨论,我了解所提出的情景并不完整,无法描述我的问题。但是,由于答案和评论,我想到了重新设计解决方案,按照我的意愿工作。

假设客户端有两个远程服务 ServiceA 作业处理器)和 ServiceB 作业存储库)应用程序必须与之通信。我们需要建立与ServiceA的连接,同时我们从ServiceB获取多个作业的数据。当作业数据可用时,我们使用ServiceA处理作业(分批)(实际示例涉及到ServiceA的Signal-R连接和ServiceB中需要发送到ServiceA的一些作业ID)。这是代码示例:

public class StackOverflowSolution
{
    private readonly ConcurrentQueue<int> _jobsQueue = new ConcurrentQueue<int>();

    /* Just to randomize waiting times */
    private readonly Random _random = new Random();

    /* Instance-scoped one-time initialization of a remote ServiceA connection */
    private async Task<string> InitializeConnectionAsync()
    {
        Console.WriteLine($"{nameof(InitializeConnectionAsync)} started");

        await Task.Delay(TimeSpan.FromSeconds(_random.Next(5) + 1));

        Console.WriteLine($"{nameof(InitializeConnectionAsync)} completed");

        return "Connection";
    }

    /* Preparation of a job (assume it requires communication with remote ServiceB) */
    private async Task InitializeJobAsync(int id)
    {
        Console.WriteLine($"{nameof(InitializeJobAsync)}({id}) started");

        await Task.Delay(TimeSpan.FromSeconds(_random.Next(10) + 1));
        _jobsQueue.Enqueue(id);

        Console.WriteLine($"{nameof(InitializeJobAsync)}({id}) completed");
    }

    /* Does something to the ready jobs in the _jobsQueue using connection to
     * ServiceA */
    private async Task ProcessQueueAsync(string connection)
    {
        var sb = new StringBuilder("Processed ");
        bool any = false;
        while (_jobsQueue.TryDequeue(out int idResult))
        {
            any = true;
            sb.Append($"{idResult}, ");
        }
        if (any)
        {
            await Task.Delay(TimeSpan.FromMilliseconds(_random.Next(500)));
            Console.WriteLine(sb.ToString());
        }
    }

    /* Orchestrates the processing */
    public async Task RunAsync()
    {
        /* Start initializing the conection to ServiceA */
        Task<string> connectionTask = InitializeConnectionAsync();
        /* Start initializing jobs */
        var jobTasks = new List<Task>();
        foreach (int id in new[] {1, 2, 3, 4})
        {
            jobTasks.Add(InitializeJobAsync(id));
        }
        /* Wait for initialization to complete */
        string connection = await connectionTask;

        /* Trigger processing of jobs as they become ready */
        var queueProcessingTasks = new List<Task>();
        while (jobTasks.Any())
        {
            jobTasks.Remove(await Task.WhenAny(jobTasks));
            queueProcessingTasks.Add(ProcessQueueAsync(connection));
        }

        await Task.WhenAll(queueProcessingTasks);
    }

    public static void Main()
    {
        new StackOverflowSolution().RunAsync().Wait();
    }
}

输出示例:

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,

感谢所有反馈!

3 个答案:

答案 0 :(得分:3)

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

目前,从Task返回的EnqueueAndProcessJobsAsync等待初始化,如果其他人没有启动初始化,那么只要队列为空,或者只要这个逻辑调用上下文碰巧处理了一个错误的项目。那...只是没有意义。

你明确想要的是,只要完成作业传递就完成了Task(当然需要初始化完成),或者如果那个工作错误就会出错,并且不受任何影响其他工作的错误。幸运的是,除了更有用之外,它还更容易做到。

就实际初始化而言,您可以使用Lazy<Task>来确保异步初始化的正确同步,并将Task暴露给任何将来可以在初始化时告诉它们的调用结束。

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

答案 1 :(得分:2)

正如对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周期)。

代码并不完美,但您应该能够将上述示例重构为合理易读的内容...... :)

答案 2 :(得分:-1)

也许你可以尝试这样的事情:

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必须快速工作,是的