.Net 多线程与锁全在这里

一台服务器能运行多少个线程,大致取决于CPU的管理能力。CPU负责线程的创建、协调、切换、销毁、暂停、唤醒、运行等。

一个应用程序中,必须有一个进程,一个进程可同时多个线程协作处理。

同步:单线程,每一步都执行结束并返回结果,下一步处于等待,阻塞程序流

异步:多线程,不需要等待执行结束,可继续执行下一步,形成并行处理,无序的不可预测的执行顺序

前台线程:主线程退出后,子线程直至完成计算。.

后台线程:主线程退出后,子线程也会停止退出。

线程的应用

常见的线程应用方式

  • new Thread
  • ThreadPool.QueueUserWorkItem 后台线程
  • Task.Run / Task.Factory.StartNewd
  • Parallel
  • await / async

ThreadPool 线程池

线程池线程是后台线程。 每个线程均使用默认的堆栈大小,以默认的优先级运行,并且位于多线程单元中。 一旦线程池中的线程完成任务,它将返回到等待线程队列中。 这时开始即可重用它。 通过这种重复使用,应用程序可以避免产生为每个任务创建新线程的开销。

每个进程只有一个线程池。由线程池统一管理每个线程的创建/分配/销毁。

// 设置可同时并行运行的线程数
ThreadPool.SetMinThreads
ThreadPool.SetMaxThreads
// 用线程池中的后台线程执行方法
ThreadPool.QueueUserWorkItem(new WaitCallback(方法名), 参数);

Task

Task所用到的线程同样来自于ThreadPool。所以通过ThreadPool线程数量的设置有助于Task的线程管理。

// Action:有参    (Action 与 Func 的区别:Action不能有返回值,Func必须有返回值)
Action<string> action = (string a) =>
{
    Console.WriteLine($"有参Action\tparams {a}\tTId {Thread.CurrentThread.ManagedThreadId}");
};

// Create a task
Task t1 = new Task(action, "alpha");
t1.Start();

// Task.Factory
Task t2 = Task.Factory.StartNew(action, "beta");
t2.Start();



// 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);
List<Task> tks = new List<Task>();
for (int i = 0; i < 30; i++)
{
    tks.Add(Task.Run(() =>
    {
        //...
    }));
}
// 等待线程全部运行结束
Task.WaitAll(tks.ToArray());



// 取消线程运行
// 更多取消方式参考 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.InvokeParallel.ForParallel.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()、Func2() 并行执行

// 取 Func1 的运行结果
string _result_1 = await _task_result_1;

// 整合两个方法的运行结果
int _total_ = _result_1.length + _result_2.length;

常用的线程锁

lock

通用的标准锁,封装自应用级锁Monitor类,需要有线程共享的锁标识,告知其它线程是否等待。

Monitor

程序级锁,于单个应用范围内,通过获取或释放用于标识资源 T 来授予对共享资源的相互独占访问权限。

// 给要操作的对象加把锁,排他锁
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方法创建的实例实现线程同步。
如:ArrayList,Hashtable

// Synchronized 方法案例

// 线程安全创建对象,并实现多线程同步
Hashtable ht = Hashtable.Synchronized(new Hashtable());
// 多线程调用 不用锁
ht.Add("key1", true);

线程安全的高性能集合类

.NET Framework 4 引入了几种专为支持多线程添加和删除操作而设计的集合类型。 为了实现线程安全,这些类型使用多种高效的锁定和免锁定同步机制。 同步会增加操作的开销。 开销数取决于所用的同步类型、执行的操作类型和其他因素,例如尝试并行访问该集合的线程数。

在 System.Collections.Concurrent 命名空间,其中包含多个线程安全且可缩放的集合类。 多个线程可以安全高效地从这些集合添加或删除项,而无需在用户代码中进行其他同步。 编写新代码时,只要将多个线程同时写入到集合时,就使用并发集合类。

直接在其它线程中对并发集合操作,不需要手动加锁:

ConcurrentBag  无序元素集合的线程安全实现

ConcurrentStack  LIFO(后进先出)堆栈的线程安全实现

ConcurrentQueue  无序元素集合的线程安全实现

ConcurrentDictionary  键值对字典的线程安全实现