一台服务器能运行多少个线程,大致取决于CPU的管理能力。CPU负责线程的创建、协调、切换、销毁、暂停、唤醒、运行等。
一个应用程序中,必须有一个进程,一个进程可同时多个线程协作处理。
同步:单线程,每一步都执行结束并返回结果,下一步处于等待,阻塞程序流
异步:多线程,不需要等待执行结束,可继续执行下一步,形成并行处理,无序的不可预测的执行顺序
前台线程:主线程退出后,子线程直至完成计算。
后台线程:主线程退出后,子线程也会停止退出。
线程的应用
常用的线程应用方式
- new Thread
- ThreadPool.QueueUserWorkItem 后台线程
- delegate 委托 invoke
- Task.Run / Task.Factory.StartNewd
- Parallel
- await / async
ThreadPool 线程池
线程池线程是后台线程。 每个线程均使用默认的堆栈大小,以默认的优先级运行,并且位于多线程单元中。 一旦线程池中的线程完成任务,它将返回到等待线程队列中。 这时开始即可重用它。 通过这种重复使用,应用程序可以避免产生为每个任务创建新线程的开销。
每个进程只有一个线程池。由线程池统一管理每个线程的创建/分配/销毁。
// 设置可同时并行运行的线程数
ThreadPool.SetMinThreads
ThreadPool.SetMaxThreads
// 用线程池中的后台线程执行方法
ThreadPool.QueueUserWorkItem(方法1);
Task
Task所用到的线程同样来自于ThreadPool。
Action<object> action = (object obj) =>
{
Console.WriteLine("Task={0}, obj={1}, Thread={2}",
Task.CurrentId, obj,
Thread.CurrentThread.ManagedThreadId);
};
// Create a task but do not start it.
Task t1 = new Task(action, "alpha");
// Construct a started task
Task t2 = Task.Factory.StartNew(action, "beta");
// Block the main thread to demonstrate that t2 is executing
t2.Wait();
// Launch t1
t1.Start();
Console.WriteLine("t1 has been launched. (Main Thread={0})",
Thread.CurrentThread.ManagedThreadId);
// Wait for the task to finish.
t1.Wait();
// Construct a started task using Task.Run.
String taskData = "delta";
Task t3 = Task.Run( () => {
Console.WriteLine("Task={0}, obj={1}, Thread={2}",
Task.CurrentId, taskData,
Thread.CurrentThread.ManagedThreadId);
});
// Wait for the task to finish.
t3.Wait();
// Construct an unstarted task
Task t4 = new Task(action, "gamma");
// Run it synchronously
t4.RunSynchronously();
// Although the task was run synchronously, it is a good practice
// to wait for it in the event exceptions were thrown by the task.
t4.Wait();
// Task 的返回值(等待返回结果)
Task<int> task = new Task<int>(() =>
{
//...
return 0;
});
task.Start();
int result = task.Result;
// 仅仅启动一个Task
Task.Run(() =>
{
//...
});
// 批量启动 Task
// 同时并行运行至少30个线程的方式
ThreadPool.SetMinThreads(30, 50);
ThreadPool.SetMaxThreads(50, 70);
for (int i = 0; i < 30; i++)
{
Task.Run(() =>
{
//...
});
}
// 取消线程运行,更多取消方式参考 https://learn.microsoft.com/zh-cn/dotnet/standard/threading/cancellation-in-managed-threads
CancellationTokenSource cts = new CancellationTokenSource();
Task.Run(() =>
{
if (Console.ReadKey(true).KeyChar.ToString().ToUpperInvariant() == "C")
{
// 取消
cts.Cancel();
}
});
Parallel
多线程并行处理的 Parallel.Invoke,Parallel.For,Parallel.ForEach;无法确保顺序处理。
// 示例:并行运行几个方法
Parallel.Invoke(方法1, 方法2, () => 方法3, () => 方法4)
// ParallelOptions 参数设定
// 先指定启用30个线程同时处理的设置
ParallelOptions paroptions = new ParallelOptions();
paroptions.MaxDegreeOfParallelism = 30;
// 指定数量的线程,并行执行200遍
List<int> datas = new List<int>();
Parallel.For(0, 200, paroptions, index =>
{
datas.add(index);
});
// 指定数量的线程,并行读取数据
Parallel.ForEach(datas, paroptions, (item) =>
{
Console.WriteLine(item);
});
await / async
创建后台进程并行处理,并取得处理结果。
// 执行 async 方法(后台线程执行)
Task<string> _task_result_1 = t1.Func1();
// 主线程不等返回结果,继续往下执行
// 执行方法
string _result_2 = t1.Func2();
// 以上两个方法并行执行
// 取 Func1 的运行结果
string _result_1 = await _task_result_1;
// 整合两个方法的运行结果
int _total_ = _result_1.length + _result_2.length;
常用的线程锁
lock
通用的标准锁,封装自应用级锁Monitor类,需要有线程共享的锁标识,告知其它线程是否等待。
Monitor
程序级锁,于单个应用范围内,通过获取或释放用于标识资源的对象上的 lock 来授予对共享资源的相互独占访问权限。
// 给要操作的对象加把锁,排他锁
Monitor.Enter(T);
// 释放当前锁,允许其它线程使用了
Monitor.Exit(T);
SpinLock
快速的、低级别的简易锁。不同于标准锁,适合应用于简单的、非耗时的逻辑处理,此场景下更多时候性能优于标准锁。
如果应用场景并非足够简单或存在不确定性的可能,SpinLock 将比标准锁开销更大。
static SpinLock _spinlock = new SpinLock();
bool lockTaken = false;
try
{
// 加锁
_spinlock.Enter(ref lockTaken);
// ...
}
finally
{
// 释放
if (lockTaken) _spinlock.Exit(false);
}
Interlocked
更细化的锁,针对性的场景时用来代替lock
,包括非空、递增等针对场景的应用,性能优于lock
。
// 递增场景案例
// lock 方式
lock(lockObject)
{
myField++;
}
// Interlocked 的替代方式
System.Threading.Interlocked.Increment(myField);
Semaphore
进程间同步,跨应用限制可同时访问某一资源或资源池的线程数,允许同时共享的线程数
// 创建时,定义同时的默认线程数和最多线程数(起始线程数,最多线程数)
Semaphore sem = new Semaphore(10, 20);
// 上锁
sem.WaitOne();
// 释放锁(一次释放线程数)
sem.Release(3);
Mutex
// 阻挡锁住当前块,其它线程处于等待
// 超时后返回false,不允许其它线程进入
Mutex.WaitOne(timeout);
// 释放当前,一个,允许其它线程进入
Mutex.ReleaseMutex();
线程安全
多个线程可以调用单个对象的属性和方法时,对这些调用进行同步处理是非常重要的。 否则,一个线程可能会中断另一个线程正在执行的任务,可能使该对象处于无效状态。 其成员不受这类中断影响的类叫做线程安全类。
在早期的.NET版本中,提供了常用的线程安全类,通过 Synchronized 属性和 lock(xxx.SyncRoot) 实现线程同步。
如:ArrayList,Hashtable
// Synchronized 与 SyncRoot 协作,实现线程安全
// 线程安全创建对象,并实现多线程同步
Hashtable ht = Hashtable.Synchronized(new Hashtable());
lock (ht.SyncRoot)
{
ht.Add("key1", true);
}
线程安全的高性能集合类
.NET Framework 4 引入了几种专为支持多线程添加和删除操作而设计的集合类型。 为了实现线程安全,这些类型使用多种高效的锁定和免锁定同步机制。 同步会增加操作的开销。 开销数取决于所用的同步类型、执行的操作类型和其他因素,例如尝试并行访问该集合的线程数。
在 System.Collections.Concurrent
命名空间,其中包含多个线程安全且可缩放的集合类。 多个线程可以安全高效地从这些集合添加或删除项,而无需在用户代码中进行其他同步。 编写新代码时,只要将多个线程同时写入到集合时,就使用并发集合类。
直接在其它线程中对集合操作,不需要手动加锁:
ConcurrentBag
无序元素集合的线程安全实现
ConcurrentStack
LIFO(后进先出)堆栈的线程安全实现
ConcurrentQueue
无序元素集合的线程安全实现
ConcurrentDictionary
键值对字典的线程安全实现