多线程
线程的建立与启动
void fun() {
// .. 执行耗时操作
}
var start = new ThreadStart(fun); // 创建线程入口
var thread = new Thread(start); // 创建线程
thread.Start();
线程的常用方法
方法 | 作用 |
---|---|
Start | 启动线程,分配给线程除了CPU之外的所有系统资源,并执行各种安全检查。调用后线程处于Unstarted状态 |
Join | 当前线程阻塞直到调用这个方法的线程执行完毕 |
Sleep | 休眠指定毫秒数,醒来后变为就绪态 |
Suspend | 阻塞线程 |
Resume | 恢复挂起的线程,进入就绪状态 |
Abort | 永久删除线程的数据 |
Thread类的常用属性
属性 | 作用 |
---|---|
CurrentThread | 获取当前正在运行的线程 |
Name | 线程名称 |
Priority | 线程调度优先级 (Highest, AboveNormal, Normal, BelowNormal, Lowest |
IsBackground | 设置后台线程 |
IsAlive | 线程如果启动且尚未正常终止,为true |
ThreadState | 指示当前线程的状态 |
给线程传递数据
public class Data {
public string Message;
}
public void fun(object o) {
// 做耗时的任务
}
var d = new Data { Message = "数据" }
new Thread(fun).Start(d);
public class MyThread {
private string data;
public MyThread(string data) {
this.data = data;
}
public fun() {
// ... 耗时
}
}
var myThread = new MyThread("info");
new Thread(myThread.fun).Start();
后台线程
- 默认情况下,Thread类创建的是前台线程,线程池的线程是后台线程
- 只有一个前台线程在运行,进程就在运行,后台线程不能独立于前台线程运行
- 后台线程适合完成后台任务,例如word的拼写检查器
- 通过IsBackground属性可以设置线程为后台线程或者前台线程,也可以获取线程是后台线程还是前台线程
线程池
线程池主要特点:
- 如果线程池线程始终保持繁忙,队列中包含挂起的工作,线程池会在一段时间后创建另一个辅助线程
- 每个进程有且仅有一个线程池,第一次将回调方法排入队列时才会创建线程池
- 最大线程数是可以配置的,工作数量大于最大线程数,新入队的工作就要排队
- 线程池通常用于服务器应用程序,可以来一个请求分配一个线程执行,不会占用主线程。
public void fun(object state) {
// ... 耗时
}
ThreadPool.GetMaxThreads(); // 获得最大线程数量
// 传入线程池中的回调方法是一个委托 public delegate void WaitCallBack(object state);
ThreadPool.QueueUserWorkItem(fun);
线程池限制
- 不能把线程设置为前台线程
- 不能给线程设置优先级和名称
- 只能用于比较短的任务,如果要一直运行,用Thread类创建线程
- 不能阻塞线程池中的线程
线程同步
同步就是某一个时刻只有一个线程可以访问某个区域,这个区域也被称为临界区。
- System.Threading.Monitor
方法 | 作用 |
---|---|
Enter(object obj) | 在obj上获取排它锁 |
Exit(object obj) | 释放obj上的排它锁 |
Wait(object obj) | 释放obj上的锁并阻塞这个线程 |
Pluse(object obj) | 通知等待该锁的其中一个线程 |
PluseAll(object obj) | 通知所有等待该锁的线程 |
public void fun() {
Monitor.Enter (this);
// 临界区
Monitor.Exit (this);
}
当时上述代码如果在获取或者释放之间发生异常,可能导致锁永远得不到释放,导致死锁
public void fun() {
Monitor.Enter(this);
try {
}
catch {
...
}
finally {
Monitor.Exit(this);
}
}
有时候可能还会调用这个类的其他方法, 例如 TryEnter
可以尝试获得锁,如果500ms后没有获取的话,就不再等待,继续下面的操作
if (Monitor.TryEnter(obj, 500)) {
try {
...
}
finally {
Monitor.Exit();
}
} else {
...
}
上述方法也可以改写为
Bool lockToken = false;
Monitor.TryEnter(obj, 500, ref lockToken);
if (lockToken) {
try {
...
}
finally {
Monitor.Exit();
}
} else {
...
}
- Lock
上述代码其实每次都要获得锁和释放锁,需要两行代码,有没有更加简便的方式呢?
object obj = new Object();
public void fun() {
lock(obj) {
// do something
}
}
上述代码就可以保证在lock块内,最多只有一个进程
- Interlocked
为了实现进程的互斥
public int State {
get {
lock(this) {
return ++state;
}
}
}
在访问一个变量的时候我们可以加锁,上述语句是简单的语句
Interlocked`可以使变量的简单语句原子化,提供了以线程安全的方式递增,递减以及交换值的方法。
与其他同步技术相比,使用这个类会快的很多,但是只能解决简单的同步问题
上述代码可以替换为
public int State {
get {
// 这个方法可以使变量递增1
return Interlocaked.Increment(ref state);
}
}
- mutex
// 第一个参数表示是否为这个锁由调用线程所拥有
var mutex = new Mutex(false);
public void fun() {
if (mutex.WaitOne()) {
try {
// ... 临界区
} finally {
mutex.ReleaseMutex();
}
} else {
}
}
其实就是在临界区的前后调用Mutex
的两个方法
WaitOne
与 ReleaseMutex
public static void Main(string[] args) {
bool createNew;
var mutex = new Mutex(false, "AppMutex", out createNew);
if (!createNew) {
Message.Show("不可以同时启动两个一样的APP");
Application.Exit();
return;
}
...
}
信号量 Semaphore
// 第一个参数是指初始可以用的资源数为2
// 第二个参数是指最大可以用的资源数为5
// 第三个参数是信号量的名称
var sp = new Semaphore(2, 5, "sp");
public void fun() {
sp.WaitOne(); // 获得信号量
// do something
sp.Release(); // 释放信号量, 释放后资源数 + 1,但是如果超过了最大可用的资源数,将会抛出异常
}
// Release(int n = 1); 不指定的话,默认释放一个资源
句柄
int base, num1, num2;
AutoResetEvent[] eventNum = {
new AutoResetEvent(false),
new AutoResetEvent(false)
}
ManualResetEvent eventBase = new ManualResetEvent();
public void fun1(object state) {
base = 1 * 1;
eventBase.Set();
}
public void fun2(object state) {
// 等待 eventBase 被 Set
eventBase.WaitOne();
num1 = base * 2;
eventNum[0].Set();
}
public void fun3(object state) {
// 等待 eventBase 被 Set
eventBase.WaitOne();
num2 = base * 4;
eventNum[0].Set();
}
// 向进程池入队三个计算任务
ThreadPool.QueueUserWorkItem(computeBase);
ThreadPool.QueueUserWorkItem(computeNum1);
ThreadPool.QueueUserWorkItem(computeNum2);
// 等待数组里面的元素都被Set
waitHandle.WaitAll(eventNum);
Console.WriteLine(num1);
Console.WriteLine(num2);
Task
任务内部其实悄悄使用了线程池,任务有几个优点
- 可以定义连续的任务
- 可以在层次结构中安排任务,例如父任务可以创建新的子任务,可以创建依赖关系
启动任务
public void fun() {};
// 1.
new TaskFactory().StartNew(fun);
// 2.
Task.Factory.StartNew(fun);
// 3.
new Task(fun).RunSynchronously(); // 可以直接阻塞当前运行的线程,获得资源
new Task(fun).Start();
// 4.
new Task(fun, TaskCreationOptions.PreferFairness).Start();
TaskCreationOptions | 效果 |
---|---|
LongRunning | 表示任务可能需要很长时间运行,这样调度器可能会使用新线程运行这个任务 |
AttachToParent | 如果父任务取消了,这个任务也会取消 |
PreferFairness | 以公平的方式运行 |
None | 默认 |
对任务传参
public void fun(object o) {
COnsole.WriteLine(o); // 输出 "xxx"
}
new Task(fun, "xxx").Start();
连续任务
public void first() {}
public void second() {}
public void third() {}
var t1 = new Task(first);
var t2 = t1.ContinueWith(second);
var t3 = t1.ContinueWith(third);
t1.Start();
上面的任务只可以保证 second
和 third
在 first
之后运行,并不能保证second
和third
的运行关系,如果需要的话可以改成
var t1 = new Task(first);
var t2 = t1.ContinueWith(second);
var t3 = t2.ContinueWith(third);
t1.Start();
无论前面一个任务是完成了还是失败了,都会在前一个任务结束的时候启动下一个任务
可以通过枚举类型 TaskContinuationOptions
指定启动条件
Task t2 = t1.ContinueWith(DoOnError, TaskContinuationOptions.OnlyOnFaulted);
任务的结果
public string fun() {
return "xxx";
}
var t = new Task(fun).Start();
COnsole.WriteLine(t.Result);
父子任务
static void ParentAndChild() {
Task parent = new Task(ParentTask);
parent.Start();
}
static void ParentTask() {
Task child = new Task(ChildTask);
child.Start();
}
static void ChildTask() {
Thread.Sleep(3000);
COnsole.WriteLine("xxx");
}
ReaderWriterLockSlim
ReaderWriterLockSlim
是 ReaderWriterLock
的轻量版
ReaderWriterLockSlim
var readerWriterLockSlim = new ReaderWriterLockSlim();
public void Read() {
// TryEnterWriteLock(50) 也可以调用这个方法,返回值是bool,表示尝试等待50ms,要是没有资源的话,就返回 false
readerWriterLockSlim.EnterReadLock();
// read
readerWriterLockSlim.ExitReadLock();
}
public void Write() {
readerWriterLockSlim.EnterWriterLock();
// writer
readerWriterLockSlim.ExitWriterLock();
}
死锁的产生和避免
死锁的产生需要的条件是请求和保持,互斥,不可剥夺资源以及环路等待条件。
要避免死锁,可以让程序以相同的顺序申请对象
例如
a => 1
b => 2
c => 3
给资源编号,每次申请的时候都从小到大申请需要的资源
例如线程1需要ab资源,线程2需要ba资源。
都让他们先申请a再申请b,按顺序申请就不会出现死锁。
异步委托
- 轮询
public delegate int beginDelegate();
class Worker {
private event beginDelegate bd;
public void work() {
if (bd != null) {
foreach (beginDelegate e in bd.GetInvocationList()) {
IAsyncResult r = e.BeginInvoke(null, null);
while (!r.IsCompleted) {
Thread.Sleep(1); // 如果没有完成的话,就等待一段时间
}
Console.WriteLine(e.EndInvoke(r)); // 获得返回值
}
}
}
}
- 等待句柄
public delegate int beginDelegate();
class Worker {
private event beginDelegate bd;
public void work() {
if (bd != null) {
foreach (beginDelegate e in bd.GetInvocationList()) {
IAsyncResult r = e.BeginInvoke(null, null);
while (!r.AsyncWaitHandle.waitOne(50)) {
Thread.Sleep(1);
}
Console.WriteLine(e.EndInvoke(r)); // 获得返回值
}
}
}
}
- 异步回调
public delegate int beginDelegate();
class Worker{
private event beginDelegate bd;
public void Work() {
if (bd != null) {
foreach(beginDelegate e in bd.GetInvocationList()) {
IAsyncResult r = e.BeginInvoke(GetGrade, e);
}
}
}
public void GetGrade(IAsyncResult r) {
beginDelegate e = r.AsyncState as beginDelegate;
Console.WriteLine(e.EndInvoke(r));
}
}
屏障
static void Main(string[] args)
{
Barrier barrier = new Barrier(4, it => {
Console.WriteLine("再次集结,友谊万岁,再次开跑");
});
string[] names = { "张三", "李四", "王五", "赵六" };
Random random = new Random();
foreach(string name in names)
{
Task.Run(() => {
Console.WriteLine($"{name}开始跑");
int t = random.Next(1, 10);
Thread.Sleep(t * 1000);
Console.WriteLine($"{name}用时{t}秒,跑到友谊集结点");
barrier.SignalAndWait();
Console.WriteLine($"友谊万岁,{name}重新开始跑");
});
}
Console.ReadKey();
}