说到线程相信很多开发人员都会认为只要使用了多线程技术服务性能就会提高很多,但涉及过渡使用问题就很少人去了解。在使用上更多是了解是创建,使用,销毁或使用线程池之类的。但这些资料更多是如何使用线程,但对于应用怎样针对性规划线程让应用发挥出更好的性能则很难有更详细的资料来讲述。
在硬件层面相信大家应该知道一个CPU核心只能运行一个线程,对于一个8逻辑核心的CPU同时工作线程是8个。但在软件服务应用中很多时候进程都开启远超过CPU核心数工作线程,这主要原因是在应用中往往涉及到IO这样低效率工作,为了确保CPU可以更好地继续其他的工作,系统会把等待IO完成线程加入到调度环节等待,然后由其它需要的任务线程补上。所以为了满足服务应用需要系统的逻辑线程远高于工作线程数。
线程工作饱和度
线程工作饱和度相信很少人去了解它,只知道多线程可以更多地使用CPU资源,达到一个更高效的处理。在进程中线程有创建时间和CPU工作时间,不同的代码对线程使用的CPU时间也有所不同; 在服务应用中最常见的代码可以划分两种:一种是高速的内存运算,别一种则是IO操作(数据库,文件或其他网络服务等)。两种代码引起线程的工作饱和度都有着很大的差别,接下来通过简单的代码来看一下不同代码引起的线程工作量问题。
static void Memory()
{
while (true)
{
System.Threading.Interlocked.Increment(ref mCount);
}
}
以上是一个简单内存累加方法,由于没有IO操作所以类似于CPU自悬状态,这一操作会一个占用比较多的CPU资源
static void IO()
{
while (true)
{
Console.WriteLine(mCount);
System.Threading.Interlocked.Increment(ref mCount);
}
}
同样一个代码增加了一个控制台输出,由于Console存在输出IO操作这类操作比内存操作要慢,所以该操作会占用比较少的CPU
接下来两个代码使用4和8个线程运行情况又怎样呢?
Memory(4线程)
Memory(8线程)
IO(4线程)
IO(8线程)
测试可以发现纯内存操作随着线程的增加CPU使用率也成正比增加,而涉及到IO的操作的测试中4线程已经达到的IO操作的饱和量了,在往上加线程也无法达到更高的操作。所以当在创建多线程的时候,要确保创建的线程是否过量。就拿Memory函数来说,当CPU是8逻辑核的情况,再往上压到10线程已经没有提交效率的意义了,反而增加了线程调度的损耗。同样在IO操作也是,受IO限制开启再多的线程也无法提高IO操作效率(所以这种操作一般异步配合线程池回调来更好利用线程资源)。
规划准则
从上面的测试可以反映出普遍使用线程场景的需求,当存在大量内存运算的时候总线程数尽量不要超过CPU的逻辑核数;而针对IO场景的应用规划线程数据超过IO负载能力。CPU有自己的处理想极限,不同IO也同样有处理极限;当到达相关资源极限的时候创建再多的线程意义不大,不但不能增加处理的效率,反而会引起线程过多影响性能的情况出现。在规划中要以线程最大饱和度作为规划的依据,最少线程资源最大化的目标让使用到的线程都得到最大化运行饱和度。
高效使用多线程
提高性能不应该以线程多少来进行规划使用,首要目标是提升线程的工作饱和度。提高线程工作饱和度最主要方式就是线程复用,而线程池正是达到这种方式的最佳途径。但系统任务多样性,一个全局的线程池是无法达到更的分配,所以想在多线程上更用得更灵活,需要针对不同的业务场和资源来制定任务队列来控制线程的开销。
以下是aspcore针对socket io封装的任务处理队列,在execute方法实现可以看到尽可以让线程处于工作状态,不要轻易回到收到全局的线程池中。
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
{
internal class IOQueue : PipeScheduler, IThreadPoolWorkItem
{
private readonly ConcurrentQueue<Work> _workItems = new ConcurrentQueue<Work>();
private int _doingWork;
public override void Schedule(Action<object?> action, object? state)
{
_workItems.Enqueue(new Work(action, state));
// Set working if it wasn't (via atomic Interlocked).
if (Interlocked.CompareExchange(ref _doingWork, 1, 0) == 0)
{
// Wasn't working, schedule.
System.Threading.ThreadPool.UnsafeQueueUserWorkItem(this, preferLocal: false);
}
}
void IThreadPoolWorkItem.Execute()
{
while (true)
{
while (_workItems.TryDequeue(out Work item))
{
item.Callback(item.State);
}
// All work done.
// Set _doingWork (0 == false) prior to checking IsEmpty to catch any missed work in interim.
// This doesn't need to be volatile due to the following barrier (i.e. it is volatile).
_doingWork = 0;
// Ensure _doingWork is written before IsEmpty is read.
// As they are two different memory locations, we insert a barrier to guarantee ordering.
Thread.MemoryBarrier();
// Check if there is work to do
if (_workItems.IsEmpty)
{
// Nothing to do, exit.
break;
}
// Is work, can we set it as active again (via atomic Interlocked), prior to scheduling?
if (Interlocked.Exchange(ref _doingWork, 1) == 1)
{
// Execute has been rescheduled already, exit.
break;
}
// Is work, wasn't already scheduled so continue loop.
}
}
private readonly struct Work
{
public readonly Action<object?> Callback;
public readonly object? State;
public Work(Action<object?> callback, object? state)
{
Callback = callback;
State = state;
}
}
}
}
以上是aspcore Kestrel模块的网络接收任务队列实现,在techempower的plaintext测试中配置了8个任务队列来完成相关操作,性能达到这项测试的最高。测试硬件资源是40线程,而aspcore Kestrel使用了8个线程来处理这一块达到最高的性能状态。
同样BeetleX也实现类似的任务队列,只是为了使用方便通过队列组的方式进行定义和处理。
public class DispatchCenter<T> : IDisposable
{
List<SingleThreadDispatcher<T>> mDispatchers = new List<SingleThreadDispatcher<T>>();
long mIndex = 1;
public DispatchCenter(Action<T> process) : this(process, Math.Min(Environment.ProcessorCount, 16))
{
}
public DispatchCenter(Action<T> process, int count)
{
for (int i = 0; i < count; i++)
{
mDispatchers.Add(new SingleThreadDispatcher<T>(process));
}
}
public void SetErrorHaneler(Action<T, Exception> handler)
{
if (handler != null)
{
foreach (var item in mDispatchers)
{
item.ProcessError = handler;
}
}
}
public void Enqueue(T data, int waitLength = 5)
{
if (waitLength < 2)
{
Next().Enqueue(data);
}
else
{
for (int i = 0; i < mDispatchers.Count; i++)
{
var item = mDispatchers[i];
if (item.Count < waitLength)
{
item.Enqueue(data);
return;
}
}
Next().Enqueue(data);
}
}
public int Count
{
get
{
int count = 0;
foreach (var item in mDispatchers)
count += item.Count;
return count;
}
}
public SingleThreadDispatcher<T> Get(object data)
{
int id = Math.Abs(data.GetHashCode());
return mDispatchers[id % mDispatchers.Count];
}
public SingleThreadDispatcher<T> Next()
{
return mDispatchers[(int)(System.Threading.Interlocked.Increment(ref mIndex) % mDispatchers.Count)];
}
public void Dispose()
{
foreach (SingleThreadDispatcher<T> item in mDispatchers)
{
item.Dispose();
}
mDispatchers.Clear();
}
}
总结
其实针对多线程的使用并不能简单地创建线程即可,实际应用中需要考虑代码占用的CPU资源情况和具体CPU资源做一个规则调整才能更好的发挥出更高效的作用。所以在设计应用时一般都依据不同业务定义不同的任务队列,并定义相关配置参数,确保服务应用在不同硬件环境配置出更优化的处理性能状态。