【发布时间】:2018-10-05 13:34:27
【问题描述】:
我有线程池实现,每当我尝试停止/加入池时,池中总会有一个随机线程在我在池上调用 Stop() 时不会停止 (state == Running)。
我不明白为什么,我只有一把锁,我用Stop 中的Monitor.PulseAll 通知可能被阻止等待Dequeue 的人。调试器清楚地显示他们中的大多数人都收到了消息,它总是 N 中的 1 个仍在运行...
这是池的最小实现
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MultiThreading
{
public class WorkerHub
{
private readonly object _listMutex = new object();
private readonly Queue<TaskWrapper> _taskQueue;
private readonly List<Thread> _threads;
private int _runCondition;
private readonly Dictionary<string, int> _statistics;
public WorkerHub(int count = 4)
{
_statistics = new Dictionary<string, int>();
_taskQueue = new Queue<TaskWrapper>();
_threads = new List<Thread>();
InitializeThreads(count);
}
private bool ShouldRun
{
get => Interlocked.CompareExchange(ref _runCondition, 1, 1) == 1;
set
{
if (value)
Interlocked.CompareExchange(ref _runCondition, 1, 0);
else
Interlocked.CompareExchange(ref _runCondition, 0, 1);
}
}
private void InitializeThreads(int count)
{
Action threadHandler = () =>
{
while (ShouldRun)
{
var wrapper = Dequeue();
if (wrapper != null)
{
wrapper.FunctionBinding.Invoke();
_statistics[Thread.CurrentThread.Name] += 1;
}
}
};
for (var i = 0; i < count; ++i)
{
var t = new Thread(() => { threadHandler.Invoke(); });
t.Name = $"WorkerHub Thread#{i}";
_statistics[t.Name] = 0;
_threads.Add(t);
}
}
public Task Enqueue(Action work)
{
var tcs = new TaskCompletionSource<bool>();
var wrapper = new TaskWrapper();
Action workInvoker = () =>
{
try
{
work.Invoke();
tcs.TrySetResult(true);
}
catch (Exception e)
{
tcs.TrySetException(e);
}
};
Action workCanceler = () => { tcs.TrySetCanceled(); };
wrapper.FunctionBinding = workInvoker;
wrapper.CancelBinding = workCanceler;
lock (_taskQueue)
{
_taskQueue.Enqueue(wrapper);
Monitor.PulseAll(_taskQueue);
}
return tcs.Task;
}
private TaskWrapper Dequeue()
{
lock (_listMutex)
{
while (_taskQueue.Count == 0)
{
if (!ShouldRun)
return null;
Monitor.Wait(_listMutex);
}
_taskQueue.TryDequeue(out var wrapper);
return wrapper;
}
}
public void Stop()
{
ShouldRun = false;
//Wake up whoever is waiting for dequeue
lock (_listMutex)
{
Monitor.PulseAll(_listMutex);
}
foreach (var thread in _threads)
{
thread.Join();
}
var sum = _statistics.Sum(pair => pair.Value) * 1.0;
foreach (var stat in _statistics)
{
Console.WriteLine($"{stat.Key} ran {stat.Value} functions, {stat.Value/sum * 100} percent of the total.");
}
}
public void Start()
{
ShouldRun = true;
foreach (var thread in _threads) thread.Start();
}
}
}
通过测试运行
public static async Task Main(string[] args)
{
var hub = new WorkerHub();
var tasks = Enumerable.Range(0, (int) 100).Select(x => hub.Enqueue(() => Sum(x)))
.ToArray();
var sw = new Stopwatch();
sw.Start();
hub.Start();
await Task.WhenAll(tasks);
hub.Stop();
sw.Start();
Console.WriteLine($"Work took: {sw.ElapsedMilliseconds}ms.");
}
public static int Sum(int n)
{
var sum = 0;
for (var i = 0; i <= n; ++i) sum += i;
Console.WriteLine($"Sum of numbers up to {n} is {sum}");
return sum;
}
我错过了一些基本的东西吗?请注意,这不是生产代码 (phew),而是我缺少的东西,所以您可能会发现不止 1 个问题 :)
【问题讨论】:
-
您真的应该尽可能避免使用非常低级的机制来处理线程。您应该更喜欢任务和更高级别的锁定机制,而不是互锁的显式线程等。这将帮助您大大降低代码的复杂性,从而减少出错的机会。看起来您所做的任何事情都需要使用那些较低级别的机制。
-
你说得对,我不敢在生产中这样做。但我是这门语言的新手,想学习它的结构。
-
然后学会正确地做事,使用正确的工具完成正确的工作,而不是尝试使用不正确的工具完成工作,并重新养成习惯。
-
关于了解您的工具的工作原理,有话要说。至少对我个人而言,这很重要,我必须看到具体细节才能知道房子是如何建造的。也许我应该坚持使用 cpp。
-
Brr,这是一个必须调试的真正可怕的死锁。每个使线程看起来简单的 C# 新特性都增加了 5 个新的难以调试的问题。使 Main 方法异步以及使用 Task 导致了问题。您无法预测异步方法将在哪个线程上运行。碰巧的是,在这个节目中有一个非常微妙的比赛。在等待调用之后,Main 中的任务继续可以在完成后立即在“工作集线器”线程之一上运行。在 hub.Stop() 上设置断点然后查看 Debug > Windows > Threads 时很容易看到。很难修复。
标签: c# multithreading