C# 多线程总结
- Thread#
- 1.1 Thread生命周期#
- 1.2 Thread本地存储#
- 1.3 认识MemoryBarrier#
- 1.4 ThreadPool 线程池#
- 1.5 TheadPool定时任务#
- Task#
1.Thread#
1.1 Thread生命周期#
1.2 Thread本地存储#
本地存储槽#
Local Store Slot(本地存储槽):存储的信息只对该线程有用,这叫做线程本地化存储
//1.给所有线程分配一个(未命名)数据槽。 存放数据
var slot = Thread.AllocateDataSlot();
Thread.SetData(slot, "hello world");
//2.给所有线程分配一个(aaa)数据槽。 存放数据
var slotaaa = Thread.AllocateNamedDataSlot("aaa");
Thread.SetData(slotaaa, "hello world aaa");
var slotObj = Thread.GetData(slot);
var slotObjaaa = Thread.GetData(slotaaa);
Console.WriteLine(slotObj);
Console.WriteLine(slotObjaaa);
//消除进程与槽位
Thread.FreeNamedDataSlot("aaa");
ThreadStatic#
性能提升:
[ThreadStatic]
static string username = string.Empty;
static void Main(string[] args)
{
username = "hello world!!!";
var t = new Thread(() =>
{
Console.WriteLine("当前工作线程:{0}", username);
});
t.Start();
Console.WriteLine("主线程:{0}", username);
Console.Read();
}
ThreadLocal#
ThreadLocal<string> local = new ThreadLocal<string>();
local.Value = "hello world!!!";
var t = new Thread(() =>
{
Console.WriteLine("当前工作线程:{0}", local.Value);
});
t.Start();
Console.WriteLine("主线程:{0}", local.Value);
1.3 认识MemoryBarrier#
2个线程同时操作一个变量时,这个变量可能加载到Cpu Cache中。这时,就使用到MemoryBarrier
。
例子:因为Release中做了一些代码和缓存的优化。。。 比如说将一些数据从memory中读取到cpu高速缓存中。
static void Main(string[] args)
{
var isStop = false;
var t = new Thread(() =>
{
var isSuccess = false;
while (!isStop)
{
Thread.MemoryBarrier(); //及时从cpu cache中更新到 memor
isSuccess = !isSuccess;
}
});
t.Start();
Thread.Sleep(1000);
isStop = true;
t.Join();
Console.WriteLine("主线程执行结束!");
Console.ReadLine();
}
在此方法之前的内存写入都要及时从cpu cache中更新到 memory。
在此方法之后的内存读取都要从memory中读取,而不是cpu cache。
类似的功能还有:
var isStop = 0;
var t = new Thread(() =>
{
var isSuccess = false;
while (isStop == 0)
{
Thread.VolatileRead(ref isStop);
isSuccess = !isSuccess;
}
});
1.4ThreadPool 线程池#
使用线程池的线程,要调用静态方法 ThreadPool.QueueUserWorkItem
,以指定线程要调用的方法。该静态方法有两种:
public static bool QueueUserWorkItem(WaitCallback callBack);
public static bool QueueUserWorkItem(WaitCallback callBack, object state);
这两个方法用于向线程池队列添加一个工作项(work item)以及一个可选的状态数据。
工作项是指一个由 callback 参数标识的委托对象,被委托对象包装的回调方法由线程池来执行。
传入的回调方法匹配 System.Threading.WaitCallback
委托类型
private void button2_Click(object sender, EventArgs e)
{
Console.WriteLine("主线程ID={0}", Thread.CurrentThread.ManagedThreadId);
ThreadPool.QueueUserWorkItem(CallbackWorkItem);
ThreadPool.QueueUserWorkItem(CallbackWorkItem, "work");
Thread.Sleep(3000);
Console.WriteLine("主线程退出");
//线程池线程ID = 5
//主线程ID = 1
//线程池开始执行
//线程池开始执行
//线程池线程ID = 6
//线程池线程ID = 3 传入的参数为 work
//主线程退出
}
private static void CallbackWorkItem(object state)
{
Console.WriteLine("线程池开始执行");
if (state != null)
{
Console.WriteLine("线程池线程ID = {0} 传入的参数为 {1}", Thread.CurrentThread.ManagedThreadId, state.ToString());
}
else
{
Console.WriteLine("线程池线程ID ={0}", Thread.CurrentThread.ManagedThreadId);
}
}
工作者线程与I/O线程#
CLR线程池分为工作者线程(workerThreads)与I/O线程 (completionPortThreads) 两种,
工作者线程是主要用作管理CLR内部对象的运作,I/O(Input/Output) 线程顾名思义是用于与外部系统交换信息.
I/O 线程是.NET专为访问外部资源所设置的一种线程,因为访问外部资源常常要受到外界因素的影响,为了防止让主线程受影响而长期处于阻塞状态,.NET为多个I/O操作都建立起了异步方法,例如:FileStream、TCP/IP、WebRequest、WebService等等,而且每个异步方法的使用方式都非常类似,都是以BeginXXX为开始,以EndXXX结束
1.5 TheadPool定时任务#
使用ThreadPool.RegisterWaitForSingleObject
最后一个false
表示,循环执行,直到注销等待。
下面2个例子:
等间隔时间再执行AutoResetEvent(false)
Console.WriteLine($"开始时间:{DateTime.Now}");
//AutoResetEvent(true),一起执行
//AutoResetEvent(false), 间隔时间后,再执行
ThreadPool.RegisterWaitForSingleObject(new AutoResetEvent(false), new WaitOrTimerCallback((obj, Timeout) =>
{
Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId},参数obj:{obj},时间:{DateTime.Now}");
}), "Hello", 3000, false);
Console.Read();
立即执行AutoResetEvent(true)
Console.WriteLine($"开始时间:{DateTime.Now}");
//AutoResetEvent(true),一起执行
//AutoResetEvent(false), 间隔时间后,再执行
ThreadPool.RegisterWaitForSingleObject(new AutoResetEvent(true), new WaitOrTimerCallback((obj, Timeout) =>
{
Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId},参数obj:{obj},时间:{DateTime.Now}");
}), "Hello", 3000, false);
Console.Read();
发现一个问题,这里的线程ID会改变的哦!
Task#
2.1.Task的开启#
Task的异步#
1.new Task
var task = new Task(() =>
{
Console.WriteLine("工作线程");
});
task.Start();
2.Factory
var task2 = Task.Factory.StartNew(() =>
{
Console.WriteLine("工作线程");
});
3.Run
var task3 = Task.Run(() =>
{
Console.WriteLine("工作线程");
});
Task的同步#
其他的没什么好说的,这里看看同步。
var task = new Task(() =>
{
Console.WriteLine("工作线程");
});
task.RunSynchronously();
Console.WriteLine("主线程");
2.2 Task等待和延续#
等待#
task.wait()
: 等待操作,相当于thread.join()
Task.WaitAll()
:必须其中所有的task执行完成才算完成,相当于并且&&
Task.WaitAny()
:只要其中一个task执行完成就算完成,相当于或||
延续#
(1)Task延续
ContinueWith
方法与下面2个方法的组合,
Task.WhenAll()
:必须其中所有的task执行完成才算完成,相当于并且&&
,再执行ContinueWith
里面的方法。
Task.WhenAny()
:只要其中一个task执行完成就算完成,相当于或||
,再执行ContinueWith
里面的方法。
Task.WhenAll(task1, task2).ContinueWith(t =>
{
//执行完1和2后,再执行这里的
Console.WriteLine("我是延续线程{0}", DateTime.Now);
});
(2)Task工厂延续,也可以采用这种方法。
ContinueWhenAll
ContinueWhenAny
Task.Factory.ContinueWhenAll(new Task[] { task1, task2 }, (t) =>
{
Console.WriteLine("我是工作线程3:{0}", DateTime.Now);
});
2.3 枚举TaskCreationOptions的一些方法#
1. AttachedToParent: 把子任务附加到父任务下#
指定将任务附加到任务层次结构中的某个父级
相当于建立了父子关系。父任务想要继续执行,必须等待子任务执行完毕。
相当于是一个WaitAll的一个操作。
Task task = new Task(() =>
{
Task task1 = Task.Factory.StartNew(() =>
{
Thread.Sleep(2000);
Console.WriteLine("我是工作线程1:{0}", DateTime.Now);
}, TaskCreationOptions.AttachedToParent);
Task task2 = Task.Factory.StartNew(() =>
{
Thread.Sleep(1000);
Console.WriteLine("我是工作线程2:{0}", DateTime.Now);
}, TaskCreationOptions.AttachedToParent);
});
task.Start();
task.Wait(); //task.WaitAll(task1,task2);
Console.WriteLine("我是主线程");
Console.ReadKey();
输出结果
我是工作线程2:2020/8/15 22:50 :53
我是工作线程1:2020/8/15 22:50:54
主线程
主线程
我是工作线程2:2020/8/15 22:50 :53
我是工作线程1:2020/8/15 22:50:54
2.DenyChildAttach: 不让子任务附加到父任务上去#
输出结果与没有添加AttachedToParent
一样
主线程
我是工作线程2:2020/8/15 22:50 :53
我是工作线程1:2020/8/15 22:50:54
3.LongRunning:长任务#
指定任务将是长时间运行的、粗粒度的操作,涉及比细化的系统更少、更大的组件。
相当于新开一个 Thread 任务执行,不在 ThreadPool 执行。如果长期租用不还给ThreadPool,ThreadPool会新开一些线程,这会导致ThreadPool的线程过多,销毁和调度都是一个很大的麻烦,对性能造成影响。
-
如果运行I/O密集任务,则可以使用
TaskCompletionSource
和异步函数
,通过回调函数(延续)实现并发性,而不通过线程实现。 -
如果是运行计算密集任务,则可以使用一个生产者/消费者队列,控制这些任务的并发数量,避免出现线程和进程阻塞的问题。
2.4 枚举TaskContinuationOptions#
1.LazyCancellation 延续被取消时,先完成先前的任务再判断Source.Token的状态#
使用 TaskContinuationOptions.LazyCancellation
,
TaskContinuationOptions.LazyCancellation
它的本质就是:
需要等待task1执行完成之后再判断source.token的状态
2.ExecuteSynchronously 强制使用同一个线程来同步#
Task task1 = new Task(() =>
{
Thread.Sleep(1000);
Console.WriteLine("task1 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId,
DateTime.Now);
});
var task2 = task1.ContinueWith(t =>
{
Console.WriteLine("task2 tid={0}, dt={1} {2}", Thread.CurrentThread.ManagedThreadId,
DateTime.Now, task1.Status);
}, TaskContinuationOptions.ExecuteSynchronously);
var task3 = task2.ContinueWith(t =>
{
Console.WriteLine("task3 tid={0}, dt={1} {2}", Thread.CurrentThread.ManagedThreadId,
DateTime.Now, task2.Status);
}, TaskContinuationOptions.ExecuteSynchronously);
task1.Start();
Console.ReadKey();
3.当前状态的几个判断 NotOnRanToCompletion,OnlyOnRanToCompletion#
-
NotOnRanToCompletion:前面的任务非完成状态,才执行延续任务
-
OnlyOnRanToCompletion: 只有前面的任务完成状态,才执行延续任务
-
NotOnFaulted: 前面的任务非失败状态,才执行延续任务
-
OnlyOnFaulted:前面的任务失败状态,才执行延续任务
-
NotOnCanceled: 前面的任务非取消状态,才执行延续任务
-
OnlyOnCanceled: 前面的任务取消状态,才执行延续任务
2.5 取消专用 CancellationTokenSource 替代变量方式#
1.共享变量方式(不推荐)#
不能让多个线程操作一个共享变量,否则会在release版本中有潜在bug。
static void Main(string[] args)
{
var isStop = false;
var thread = new Thread(() =>
{
while (!isStop)
{
Thread.Sleep(100);
Console.WriteLine("当前thread={0} 正在运行", Thread.CurrentThread.ManagedThreadId);
}
});
thread.Start();
Thread.Sleep(1000);
isStop = true;
Console.WriteLine("已经被取消了");
Console.ReadKey();
}
2.CancellationTokenSource方式 (推荐)#
下面结果是一样的
CancellationTokenSource source = new CancellationTokenSource();
//注册一个将在取消此 System.Threading.CancellationToken 时调用的委托。
source.Token.Register(() =>
{
//如果当前的token被取消,此函数将会被执行
Console.WriteLine("当前source已经被取消,现在可以做资源清理了。。。。");
});
var task = Task.Factory.StartNew(() =>
{
//未发送请求操作时,为false
while (!source.IsCancellationRequested)
{
Thread.Sleep(100);
Console.WriteLine("当前thread={0} 正在运行", Thread.CurrentThread.ManagedThreadId);
}
}, source.Token);
Thread.Sleep(1000);
source.Cancel();
Console.ReadKey();
下面还可以针对CancellationTokenSource
做功能增强。
2.1.当任务取消的时候,我希望有一个函数能够被触发
CancellationTokenSource source = new CancellationTokenSource();
//注册一个将在取消此 System.Threading.CancellationToken 时调用的委托。
source.Token.Register(() =>
{
//如果当前的token被取消,此函数将会被执行
Console.WriteLine("当前source已经被取消,现在可以做资源清理了。。。。");
});
var task = Task.Factory.StartNew(() =>
{
//未发送请求操作时,为false
while (!source.IsCancellationRequested)
{
Thread.Sleep(100);
Console.WriteLine("当前thread={0} 正在运行", Thread.CurrentThread.ManagedThreadId);
}
}, source.Token);
Thread.Sleep(1000);
source.Cancel();
Console.WriteLine("已经被取消了");
Console.ReadKey();
2.2 延时取消
CancellationTokenSource source = new CancellationTokenSource();
//注册一个将在取消此 System.Threading.CancellationToken 时调用的委托。
source.Token.Register(() =>
{
//如果当前的token被取消,此函数将会被执行
Console.WriteLine("当前source已经被取消,可以执行其他操作");
});
var task = Task.Factory.StartNew(() =>
{
//未发送请求操作时,为false
while (!source.IsCancellationRequested)
{
Thread.Sleep(100);
Console.WriteLine("当前thread={0} 正在运行", Thread.CurrentThread.ManagedThreadId);
}
}, source.Token);
Thread.Sleep(1000);
//source.Cancel();
source.CancelAfter(1000); //延时1秒取消
Console.WriteLine("已经被取消了");
Console.ReadKey();
2.3 组合取消
取消的组合 将CancellationTokenSource 组合成一个链表,其中任何一个CancellationTokenSource被取消,组合source也会被取消。
CancellationTokenSource source1 = new CancellationTokenSource();
//source1取消
//source1.Cancel();
CancellationTokenSource source2 = new CancellationTokenSource();
//source2取消
source2.Cancel();
var combineSource = CancellationTokenSource.CreateLinkedTokenSource(source1.Token, source2.Token);
Console.WriteLine("source1={0} source2={1} combineSource={2}", source1.IsCancellationRequested,
source2.IsCancellationRequested,
combineSource.IsCancellationRequested);
Console.Read();
输出:
source1=False
source2=True
combineSource=True
2.6 异常处理 AggregateException#
AggregateException
是一个集合,因为task中可能会抛出多个异常,所以我们需要一种新的类型,把这些异常都追加到一个集合中
抛出的时间#
与线程不同,Task可以随时抛出异常。
任务代码抛出一个未处理异常,那么这个异常会自动传递到调用Wait()
的任务上或者访问Task<TResult>
的Result
属性的代码上:
Task task = Task.Factory.StartNew(() =>
{
Console.WriteLine("task执行");
Task task1 = Task.Factory.StartNew(() =>
{
Console.WriteLine("task1执行");
throw new Exception("task1异常");
}, TaskCreationOptions.AttachedToParent);
Task task2 = Task.Factory.StartNew(() =>
{
Console.WriteLine("task2执行");
throw new Exception("task2异常");
}, TaskCreationOptions.AttachedToParent);
});
try
{
//Task.WhenAll(new Task[2] { task1, task2 });
task.Wait();
}
catch (AggregateException ex)
{
foreach (var item in ex.InnerExceptions)
{
Console.WriteLine($"meassage:{item.InnerException.Message},type:{item.GetType()}");
}
}
Console.Read();
输出
task执行
task1执行
task2执行
meassage: task2异常,type : System.AggregateException
meassage: task1异常,type : System.AggregateException
CLR会将异常封装在AggregateException
中,从而更适合并行编程场景;
使用Task的IsFaulted
和IsCanceled
属性,就可以不重新抛出异常而检测出错的任务。
如果都返回false,则没有出错;
IsCanceled为true,任务抛出 OperationCanceledOPeration;
IsFaulted为true,则任务抛出另一种异常,而Exception属性包含该错误。
Handle方法#
是处理当前的异常数组,判断上一层我当前哪些已经处理好了, 没有处理好的,还需要向上抛出
ex.Handle(x =>
{
if (x.InnerException.Message == "我是 childTask1 异常")
{
return true;
}
return false;
});
当前的Handle
就是来遍历 异常数组,如果有一个异常信息是这样的,我认为是已经处理的。
如果你觉得异常还需要往上抛,请返回false
。
2.7 Task核心调度器 TaskScheduler#
在Task底层有一个TaskScheduler,它决定了task该如何被调度,而在.net framework中有两种系统定义Scheduler,第一个是Task默认的ThreadPoolTaskScheduler
,还是一种就是 SynchronizationContextTaskScheduler
,以及这两种类型之外的如何自定义。
1.ThreadPoolTaskScheduler#
这种scheduler机制是task的默认机制,而且从名字上也可以看到它是一种委托到ThreadPool的机制,刚好也从侧面说明task是基于ThreadPool基础上的封装,如果想具体查看代码逻辑,你可以通过ILSpy反编译一下代码看看:
protected internal override void QueueTask(Task task)
{
if ((task.Options & TaskCreationOptions.LongRunning) != TaskCreationOptions.None)
{
new Thread(ThreadPoolTaskScheduler.s_longRunningThreadWork)
{
IsBackground = true
}.Start(task);
return;
}
bool forceGlobal = (task.Options & TaskCreationOptions.PreferFairness) > TaskCreationOptions.None;
ThreadPool.UnsafeQueueCustomWorkItem(task, forceGlobal);
}
2.SynchronizationContextTaskScheduler#
从这个名字中就可以看到,这是一个同步上下文的taskscheduler,原理就是把繁重的耗时工作丢给ThreadPool,然后将更新UI的操作丢给 UI线程的队列中,由UIThread来执行,具体的也可以在这种scheduler中窥得一二。
protected internal override void QueueTask(Task task)
{
this.m_synchronizationContext.Post(SynchronizationContextTaskScheduler.s_postCallback, task);
}
然后可以从s_postCallback上看到里面有一个Invoke函数:
public virtual void Post(SendOrPostCallback d, object state)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(d.Invoke), state);
}
例如,在winform,或者wpf中如果给一个控件赋值,都是调用invoke方法。
(1)不要再UIThread做非常耗时的任务,否则会出问题。
private void button1_Click(object sender, EventArgs e)
{
Task task = new Task(() =>
{
try
{
label1.Text = "你好";
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
});
task.Start(TaskScheduler.FromCurrentSynchronizationContext());
}
(2) 耗时的操作我们要放到threadpool,更新操作放到同步上下文中。
var task = Task.Factory.StartNew(() =>
{
//默认耗时操作
Thread.Sleep(1000 * 10);
});
task.ContinueWith(t =>
{
label1.Text = "你好";
}, TaskScheduler.FromCurrentSynchronizationContext());
3.自定义TaskScheduler#
我们知道在现有的.net framework中只有这么两种TaskScheduler,有些同学可能想问,这些Scheduler我用起来不爽,我想自定义一下,这个可以吗?当然!!!如果你想自定义,只要自定义一个类实现一下TaskScheduler就可以了,然后你可以将ThreadPoolTaskScheduler简化一下,即我要求所有的Task都需要走Thread,杜绝使用TheadPool,这样可以吗,当然了,不信你看。
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
var task = Task.Factory.StartNew(() =>
{
Console.WriteLine("hello world!!!");
}, new CancellationToken(), TaskCreationOptions.None, new PerThreadTaskScheduler());
Console.Read();
}
}
/// <summary>
/// 每个Task一个Thread
/// </summary>
public class PerThreadTaskScheduler : TaskScheduler
{
protected override IEnumerable<Task> GetScheduledTasks()
{
return null;
}
protected override void QueueTask(Task task)
{
var thread = new Thread(() =>
{
TryExecuteTask(task);
});
thread.Start();
}
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
throw new NotImplementedException();
}
}
}
作者:【唐】三三
出处:https://www.cnblogs.com/tangge/p/13472312.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
2012-08-10 分享一个不错的VS插件——CodeMap(转发)