控制台应用程序中的无锁线程安全
Lock-free thread safety in console apps
为了确保线程安全,我正在尝试寻找一种通用的跨平台方法来
- 在主线程中异步执行所有委托或...
- 在后台线程中执行 delegete 并将结果传递给主线程
考虑到控制台应用 not 具有同步上下文,我在加载应用时创建了新的上下文,然后使用以下方法之一。
- 按照 Stephen ToubAwait, SynchronizationContext, and Console Apps 文章中所述设置和恢复自定义 SC
- 使用
context.Post
调用将所有委托编组到主线程,如 Stephen Toub ExecutionContext vs SynchronizationContext 文章中所述
- 如 Joe Albahari Basic synchronization 所述
将后台线程与生产者-消费者集合结合使用
问题
想法 #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 作为工作项。
为了确保线程安全,我正在尝试寻找一种通用的跨平台方法来
- 在主线程中异步执行所有委托或...
- 在后台线程中执行 delegete 并将结果传递给主线程
考虑到控制台应用 not 具有同步上下文,我在加载应用时创建了新的上下文,然后使用以下方法之一。
- 按照 Stephen ToubAwait, SynchronizationContext, and Console Apps 文章中所述设置和恢复自定义 SC
- 使用
context.Post
调用将所有委托编组到主线程,如 Stephen Toub ExecutionContext vs SynchronizationContext 文章中所述 - 如 Joe Albahari Basic synchronization 所述 将后台线程与生产者-消费者集合结合使用
问题
想法 #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 作为工作项。