tp最新下载|tpl
总奖金12万5!TPL总决赛分组出炉,不是冤家不聚首_腾讯新闻
总奖金12万5!TPL总决赛分组出炉,不是冤家不聚首_腾讯新闻
总奖金12万5!TPL总决赛分组出炉,不是冤家不聚首
由War3蛋塔飞主办,斗鱼TV、OBSBOT寻影、SCBOY、朕宅赞助,魔坛情报局媒体支持的普力艾|ProIron TPL联赛,将于7月10日开启S1总决赛的争夺。本届总决赛总奖金高达125000元,是2023年度奖金第二高的赛事(目前奖金最高的比赛是1月份进行的TP联赛总决赛),120、Happy、Moon、Lyn等16位当今魔坛最强的选手再次齐聚一堂,为了冠军荣誉和丰厚奖金展开激烈厮杀。
目前,2023普力艾|ProIron TPL联赛S1总决赛的分组已经出炉,彩色&120、Fly&Happy、浪漫&Sok等多对冤家再度聚首。
A组:强敌环伺,Fly恐难逃出局命运
Fly最近半年的比赛状态并不理想,面对Happy和Lawliet两个实力明显强于自己的对手,恐怕很难从该小组脱颖而出。Happy和Lawliet携手晋级应该是大概率事件。
当然,历史的经验告诉我们,任何时候都不要低估Soin。
B组:赛制更新,120不会再“坑兄弟”了吧
当看到120、彩色、Laby分在一个小组时,你会想起什么?没错,自然是之前120意外不敌Laby再淘汰彩色的故事,彩色着实被120“坑”了一把。现在,两人又被分在了一起,这波着实是“冤家路窄”。
不过本次TPL小组赛赛制由双败改为循环+冒泡,每个选手都至少要交手一遍,所以理论上不会再出现之前被同一个选手淘汰的场景。这一回,DOTA兄弟总该携手出线了吧。
C组:两克星同组,卡号出线岌岌可危
2023年,卡号对FoCuS的战绩是1-6,其中最近6场全败。对Lyn的战绩也是1-3处于下风,看得出来NvO的是卡号的明显弱项,面对两位兽族王者,卡号和Foggy的日子着实不好过。
D组:第一人族之战,浪漫能否雄起?
Sok和浪漫目前谁是“第一人族”,相信通过最近的比赛,大家心中已经有了答案,无论是比赛成绩还是相互交手战绩,Sok都要更胜一筹。不过,双方的差距并没有大到不可逾越,对于浪漫而言,依然有追赶的机会。本届TPL总决赛就是他最好的翻身之战,如果他能在总决赛中力压毛皇小组出线,或许能一举扭转之前的颓势。
本组另一大看点莫过于Moon的表演,两位HUM选手和实力较弱的冰兽都不太能对他造成威胁,月神届时可以放心大胆的施展各路绝活。
总决赛赛事信息
比赛时间
小组赛:7月10日-7月17日,每日19点开始
季后赛:7月18日-7月23日,每日19点开始
比赛赛制
小组赛
小组赛分为循环赛和冒泡赛两个阶段。
循环赛
16位选手分为A、B、C、D四个小组,每组4位选手先进行一轮单循环BO3比赛,第一名直接晋级季后赛。
冒泡赛
剩下三名选手将进行冒泡赛(第四打第三、胜者打第二),冒泡赛胜者获得第二个晋级名额。
小组积分规则
胜者积1分,败者积0分,当出现不同选手积分相同时,将通过净胜分决定名次。
若两个选手积分与净胜分均相同时,将比较胜负关系,根据胜负关系决定名次。
若多个选手积分与净胜分均相同,且互为胜负,将通过加赛确定最终名次。
季后赛
八强双败淘汰,除了第一轮为BO3、决赛为BO7,其余场次均为BO5
第一轮对阵顺序为:A1 VS B2,A2 VS B1,C1 VS D2,C2 VS D1
比赛奖金
总奖金:125000元
冠军:50000元
亚军:30000元
季军:20000元
殿军:10000元
5-6名:5000元
7-8名:2500元
种族规则
同一个BO3、BO5、BO7只能使用同一种族(包括随机)
每场比赛BP开始前需提前提交种族
比赛地图
EI2.0、TM、TH、NI、LR、AZ、CH、TR、TS
深入理解.NET中的并行编程(TPL)——多线程、异步、任务和并行计算 - 知乎
深入理解.NET中的并行编程(TPL)——多线程、异步、任务和并行计算 - 知乎切换模式写文章登录/注册深入理解.NET中的并行编程(TPL)——多线程、异步、任务和并行计算懒得勤快在开发过程中,有很多工作我们都需要去开线程来解决,但是多线程往往会带来更多棘手的问题,但又不得不使用多线程,由多线程带来的传值、取值、资源同步、线程取消或暂停、异常的捕获等都会困扰着我们每一个编写这类代码的开发者。微软也在这方面做了巨大的努力,以至于到现在的.Net Framework和.NetCore都有非常丰富的多线程API可以选择,方便去编写多线程代码,同时又带来了一个问题:线程、异步、任务、并行计算等太多了,我该选择哪个?接下来就让我们一起来由浅入深的去熟悉线程、异步、任务,从是什么到为什么,追溯事物的本质,以及任务为什么还衍生出了并行计算(Parallel),同时还告诉大家如何优雅的去控制线程,以及处理异步、任务和并行计算中的异常。多线程编程(TPL)是我们所有开发人员职业生涯中曾经或是现在的一道坎,所以,我们必须战胜它!不过,在阅读本文章之前,你还是必须得有基本的TPL编程基础,最起码,线程、异步、任务的基本使用还是要会的。多线程和异步相信很多初学者学过多线程和异步之后,都会把异步和多线程混为一谈,如果对它们之间的区别不是很清楚的话,就很容易写出下面的代码:private void button1_Click(object sender, EventArgs e)
{
new Thread(() =>
{
var client = new WebClient();
var s = client.DownloadString("https://www.google.com");
MessageBox.Show(s);
}).Start();
}
以上代码模拟在一个WinForm程序中,单击按钮获取某个网页的html代码并弹窗显示出来,可以预见,如果网页的内容特别多,或者因为特殊的网络原因,获取网页内容时间比较长,所以我们开线程去完成这项工作,防止阻塞UI线程。确实,这样解决了UI线程被阻塞的问题吗,但是,它高效么?答案是否定的,要理解这一点,需要从计算机组成原理说起,其实我们的电脑主机的硬件里面,有很多零件是具备“IO操作的DMA(Direct Memory Access)模式”,DMA即直接内存访问,顾名思义,就是一种不经过CPU就可以直接访问内存数据的一种数据交换模式。通过DMA模式的数据交互几乎不耗CPU资源,比如我们电脑机箱里面的硬盘、声显网卡等都具有DMA功能,而我们.NET中CLR所提供的异步编程模型就是让我们充分利用硬件DMA功能来转移CPU的压力。知道了这一点,我们再来分析下上面的这个例子,我们可以画图来阐述下:为了下载网页内容,CPU新起了一个线程,然后在下载网页的整个过程中,该线程会始终占用着CPU资源,直到网页被下载完成。这就意味着CPU的资源一直被消耗,浪费,等待着。如果我们改用异步去实现,代码如下:private void button1_Click(object sender, EventArgs e)
{
var client = new WebClient();
client.DownloadStringCompleted+=(ssender, ee) =>
{
MessageBox.Show(ee.Result);
};
client.DownloadStringAsync(new Uri("https://www.google.com"));
}
上面的代码工作机制就可以这样描述了:经过改造后的代码采用了异步模式,它的底层使用线程池进行管理,异步操作启动时,CLR会将下载网页操作这部分工作丢给线程池中的某个线程去执行。当开始IO操作时,异步会把工作线程还给线程池,这时候就相当于下载网页的这个工作不会再占用CPU资源了。直到异步完成,即网页的html下载完成,WebClient会通知下载完成事件,让CLR响应异步操作完成,由此可见,异步模式借助线程池,极大的节约了CPU资源。所以,异步和多线程的执行流程图可以这样表示:明白了多线程和异步的区别后,我们来确定下二者的具体使用场景:CPU密集型采用多线程;I/O密集型采用异步。如果你区分不来什么是CPU密集型还是I/O密集型的,你就记住一点:涉及到任何读写操作和数据传输有关的,就属于I/O密集型,否则就是CPU密集型,也叫计算密集型。关于线程同步所谓线程同步,就是多线程访问共享资源时的一种等待(也可以理解为锁定某个对象),直到该共享资源被解除锁定,面向对象语言中的数据类型都分为值类型和引用类型。所以多线程在这两种数据类型上的等待是不一样的,有编程基础的都知道值类型不能被锁定,即不能在值类型上做等待操作。而在引用类型上的等待机制,又分为了锁定和同步。在C#里面,锁定我们使用微软提供的关键字语法糖lock或者使用Monitor对象,其实前者就是后者的语法糖,两者没有什么实质差别,这就是我们最常用的锁技术。不过,我们主要来讨论信号同步,而信号同步机制中涉及的类型都继承自抽象类WaitHandle,这些类型有Semaphore、Mutex以及EventWaitHandle,而EventWaitHandle又分为AutoResetEvent和ManualResetEvent,关系图如下:所以它们的底层原理都是一样的,维护的都是一个系统内核句柄。不过还是要简单的区别三者的关系。EventWaitHandle所维护的是一个由操作系统内核产生的bool值(称之为“阻塞状态”),如果为false,则表示线程被阻塞,可以通过调用Set方法将其置为true而解除线程阻塞。而它的子类AutoResetEvent和ManualResetEvent区别也不大,接下来会针对二者讲述下如何正确地使用信号量。Semaphore维护的是一个由系统内核产生的整型变量,如果其值为0,则表示等待,如果大于0,则解除阻塞,同时,每解除一个线程阻塞,其值就减1。初始化时就限制了最多能等待几个线程。上面两个提供的都是单应用程序域的线程同步,而Mutex则解决的是跨应用程序域线程阻塞和解锁的能力。使用线程同步的一个简单例子:public AutoResetEvent AutoResetEvent { get; set; } = new AutoResetEvent(false);
private void button2_Click(object sender, EventArgs e)
{
new Thread(() =>
{
label1.Text = "线程开启,等待信号...";
AutoResetEvent.WaitOne();
//todo:处理一些复杂工作
label1.Text = "继续工作...";
}).Start();
}
private void button3_Click(object sender, EventArgs e)
{
AutoResetEvent.Set();
}
同样在一个WinForm程序里面,一个按钮开启线程,另一个按钮给这个线程发送信号,这期间发生了什么?首先创建了一个AutoResetEvent同步类型对象,初始状态为阻塞状态false,这意味着所有的在它上面的的等待都会被阻塞,即线程中应用:AutoResetEvent.WaitOne();
这说明线程到这里就被阻塞,直到有人给它发信号才会继续执行,否则就一直等,而UI线程中的:AutoResetEvent.Set();
相对于其他线程来说,就是“另一个线程”,UI线程通过Set方法将阻塞状态置为true,等待的线程才继续执行,虽然例子很简单,但已经完全的解释了信号机制的工作原理。而AutoResetEvent和ManualResetEvent的区别在于,前者在发送完信号后会立即置为false,而后者需要手动指定,我们来看下面的例子:public AutoResetEvent AutoResetEvent { get; set; } = new AutoResetEvent(false);
private void button2_Click(object sender, EventArgs e)
{
new Thread(() =>
{
label1.Text = "线程1开启,等待信号...";
AutoResetEvent.WaitOne();
//todo:处理一些复杂工作
label1.Text = "继续1工作...";
}).Start();
new Thread(() =>
{
label2.Text = "线程2开启,等待信号...";
AutoResetEvent.WaitOne();
//todo:处理一些复杂工作
label2.Text = "继续2工作...";
}).Start();
}
private void button3_Click(object sender, EventArgs e)
{
AutoResetEvent.Set();
}
按钮2同时开启2个线程,按钮3发送信号,运行时我们发现,在线程都被阻塞的时候,点按钮3之后,只有一个线程被唤醒了,另一个线程仍然还在等待,根本没收到信号,要再唤醒另一个线程,那就再点一下按钮3再发一个信号,所以AutoResetEvent在发送完信号后立马把阻塞状态置为了false,要想多个线程同时被唤醒,那就是ManualResetEvent了。只要是引用类型,就可以随便加锁吗?加锁我相信大家都很熟悉了,这也是让线程同步的一种方式,其原理就是锁住一个共享资源,使得程序在多线程访问这个共享资源的时候只能有一个线程占用,通俗的讲就是让多线程变成单线程,但是,只要是对象,就可以加锁吗?既然加锁的是个对象,那我们不妨思考一下,到底要什么样的对象才能被锁,我在这儿整理了一下,选择锁对象的时候我们应该注意什么:锁对象应该是在多个线程中可见的同一对象;在非静态方法中,静态变量不应该作为锁对象;值类型不能作为锁对象;避免将字符串作为锁对象;降低锁对象的可见性。下面就分别详细的解释下这几点。首先第一个,锁对象必须在多线程中是可见的,且必须是同一对象。前半句很好理解,如果不可见那肯定也不能锁啊,至于“同一对象”,也很好理解,如果锁的不是同一对象,那加锁还有什么意义呢,但是,这却是我们经常会犯的一个错误,为了好理解,举个我们一定会遇到的场景:在遍历集合的时候,同时另一个线程又在修改这个集合,就像下面的代码,如果没有lock,会抛异常InvalidOperationException:集合已被修改,可能无法执行枚举。static void Main(string[] args)
{
object lockObj = new object();
AutoResetEvent are = new AutoResetEvent(false);
List
new Thread(() =>
{
are.WaitOne();
lock (lockObj)
{
foreach (var i in list)
{
Thread.Sleep(100);
}
}
}).Start();
new Thread(() =>
{
are.Set();//保证这个线程已经开始了才能执行上面的线程
Thread.Sleep(200);
lock (lockObj)
{
list.RemoveAt(0);
}
}).Start();
}
上面的代码锁定的是同一对象,肯定没有问题,如果将代码改造成这样:class Program
{
static void Main(string[] args)
{
var m1 = new MyClass();
var m2 = new MyClass();
m1.T1();
m2.T2();
Console.ReadKey();
}
}
public class MyClass
{
object lockObj = new object();
AutoResetEvent are = new AutoResetEvent(false);
static List
public void T1()
{
new Thread(() =>
{
are.WaitOne();
lock (lockObj)
{
foreach (var i in list)
{
Thread.Sleep(100);
}
}
}).Start();
}
public void T2()
{
new Thread(() =>
{
are.Set();//保证这个线程已经开始了才能执行上面的线程
Thread.Sleep(200);
lock (lockObj)
{
list.RemoveAt(0);
}
}).Start();
}
}
很显然,MyClass被实例化了两次,也就是锁对象lockObj也被实例化了两次,而多线程操作的却是MyClass的静态字段,运行则会抛InvalidOperationException:集合已被修改,可能无法执行枚举。也就是说,上面的代码锁定的是两个不同的对象,如果要改掉这个bug,那么把lockObj也改成静态的就OK了,另外,也思考下能不能lock(this),试想刚才我们用lockObj,同理lock(this)在多实例的时候也不是锁的同一对象,也不能达到锁同步的目的。那刚才的把lockObj改成了静态的之后,确实达到了锁的目的,但是,有读者可能发现了,非静态方法中使用了静态变量作为锁对象,这不矛盾了么,那好,接下来就说第二点了。针对第二点,事实上,刚才的代码也是出于演示的目的,其次,实际项目中强烈建议不要这么写,如果要,必须遵守这个原则:类型的静态方法应当保证线程安全,非静态方法不需要保证线程安全。.NetFramework和.NetCore底层绝大部分类都遵循了这个原则,上一个示例中,如果将lockObj改为静态的,Name就相当于让非静态方法具备了线程安全性,带来的问题就是:如果应用程序中该类型存在多实例,在遇到这个锁的时候,就会产生同步,试想,如果高并发使用这个类的时候,你愿意看到你的应用程序或者网站被卡死在这里吗?!第三点,值类型不能作为锁对象,这很好理解,值类型肯定不能作为锁对象啊,学基础的时候也是三令五申过的,但是为什么值类型不能作为锁对象你真的能解释清楚么?因为值类型都是在栈内存,当值类型被传递到另一个线程时,会创建一个装箱副本对象,相当于每个线程锁定的都是不同的对象,因此值类型不能作为锁对象。第四点,不能锁字符串,其实基础够扎实的也应该知道,字符串在所有的面向对象语言中都是一种特殊的引用类型,如果把字符串作为锁对象,是相当危险的。这似乎看上去和值类型正好相反,但字符串在内存中作为常量存在,如果有两个变量被赋值了相同的字符串,它们引用的将是同一块内存空间,所以,如果把字符串作为锁对象,那就相当于锁定了一个全局的对象,这可能造成整个应用程序被阻塞掉。如果非有一定要用字符串作为锁对象的,也不是不可以,但是,这样做之前,一定得考虑清楚。最后一点,降低锁对象的可见性。其实上面的第四点提到的锁字符串,字符串就相当于是一种可见范围最广的锁对象,其次还有typeof(class),typeof返回的结果是class的所有实例共有的,也就是说:所有实例的Type都指向typeof返回的结果。这样一来,如果我们也lock了typeof(class),其结果也可能就像刚才第四点一样了。这样的编码没有必要存在。一般来说,锁对象也不应该是一个公共变量或属性。在.NET的早期版本中,一些常用的集合类型提供了公有属性SyncRoot,让我们可以实现线程安全的集合操作,所以你可能会认为我们刚才的结论可能不对,然而,集合操作的大部分应用场景都不是多线程的,更多的是单线程操作,而且线程同步本身是一种耗时的操作,如果集合的所有的非静态方法都需要考虑线程安全,那么完全没有必要整个公开的SyncRoot,私有即可啊,而现在把它公开是为了让调用者去决定它操作时是否需要线程安全。除非你有这样的需求,否则就应该考虑锁对象的可见性,况且现在.NET较高版本的都已经提供了线程安全的集合了,如:ConcurrentBag、ConcurrentDictionary等。线程的IsBackground的坑在.NET中线程分为前台线程和后台线程,每个线程都有IsBackground属性,如果通过该属性将线程标记为后台线程,那么应用程序在退出的时候就会连线程一并退出;如果为前台线程,那么就只有等到所有线程都结束了,应用程序才算是真正的退出了。WinForm中有如下代码:private void button4_Click(object sender, EventArgs e)
{
var t = new Thread(() =>
{
while(true)
{
Thread.Sleep(1000);
}
});
t.IsBackground=false;
t.Start();
}
,在VS中启动调试,在单击按钮开启这个前台线程,VS进入调试模式,这时如果叉掉应用程序,你会发现VS仍然还在调试,如果你在上面的while循环里打个断点,你仍然可以看到它命中断点,这就意味着应用程序并没有退出。所以如果我们使用线程的话,我们要注意应该更多地将线程标记为后台线程,如果是需要执行事务或者占有某些非托管资源需要释放时,才使用前台线程。线程并不会立即开始市面上绝大部分的操作系统都不是一个实时操作系统,Windows也是如此,所以我们期望不了线程开启后能立刻执行,Windows系统有它自己调度线程的算法,什么时候该执行哪个线程,从操作系统的角度讲,就是每个线程都被分配了一定的CPU时间,可以执行一小段的工作,由于被分配的时间都非常短,所以即使你的系统现在有几千个线程再运行,你感觉到的也是他们都同时在运行,系统会在适当的时机根据自己的算法决定下一个时间点去调度哪个线程。线程本身就不是编程语言自身就有的东西,它的调度也是一个非常复杂的过程,但我们需要理解的就是:线程之间的切换一定需要花时间和空间,而且,它不实时。不妨我们用代码检验一下:for(int i = 0; i < 10; i++)
{
new Thread(() =>
{
Console.WriteLine(i);
}).Start();
}
我们期望的结果是0-9依次输出,但是结果却是如此:这就印证了刚才所说的线程不是立即启动的,也许后开的线程会先于先开的线程,而for循环传入线程的值,比如当前循环到5,可能线程真正执行的时候,早已到8了。要让刚才的代码按我们预想的结果输出,我们把开启线程的代码提取到方法:static void Main(string[] args)
{
for(int i = 0; i < 10; i++)
{
NewMethod(i);
}
Console.ReadKey();
}
private static void NewMethod(int i)
{
new Thread(() => { Console.WriteLine(i); }).Start();
}
由于在for循环外部启动线程,这就是我们预想的结果了:关于线程的优先级线程在C#中有5个优先级:Lowest,BelowNormal,Normal,AboveNormal,Highest,优先级就涉及到操作系统对线程的调度,Windows系统是一个基于线程优先级的抢占式调度模式,线程优先级高的总是比优先级低的获取得更多的CPU时间,如果有一个优先级高的线程,并且已经就绪,系统总是会优先执行。我们启动的所有线程,包括ThreadPool和Task,线程的优先级默认都是Normal级别,虽然可以去修改线程的优先级,但是我们不建议这么做,如果是一些非常关键的线程,我们还是可以考虑提升线程优先级的。这些高优先级的线程应该具备运行时间短,能立刻进入等待状态的特征。取消线程的正确姿势有时候我们总是想更大程度的去控制线程,比如,我想在线程还在执行的某个时候,把它取消了,最典型的场景就是,开线程发起http请求,有时网络很差就会导致线程执行时间过长,所以我们就想等待一定时间,回不来就取消了吧,然而,这并不是我们想怎样就怎样的,这涉及到两个问题:1.正如线程不能立即启动,当然线程也不能立即停止,不是你想停就能停的。无论采用哪种方式通知线程停止,线程都会忙完最紧要的事情之后在它觉得合适的时候退出。以传统的Thread.Abort,如果线程执行的是一段非托管代码,就不会抛线程取消异常,只有当代码回到CLR中,才会引发线程取消异常,当然,异常也不是立即引发的。2.取消线程不在于采用何种手段,更多的是依赖于线程能否主动响应发起者的停止请求。也就是说:如果线程需要被停止,那么线程需要给调用者开放Canceled的接口,线程在工作的同时还要去检测Canceled的状态,如被检测到Canceled,线程才会负责退出。.NET给我们提供了标准的取消模式:协作式取消(Cooperative Cancellation)。机制就是上面提到的这种机制。直接上代码:var cts = new CancellationTokenSource();
var t = new Thread(() =>
{
while(true)
{
if(cts.IsCancellationRequested)
{
Console.WriteLine("线程被取消");
break;
}
Thread.Sleep(100);
}
});
t.Start();
Console.ReadKey();
cts.Cancel();
主线程通过CancellationTokenSource的Cancel方法通知工作线程退出,工作线程以100ms的频率一边工作一边检测外界是否有Cancel的信号传入,若有,则退出,可以看出正确停止工作线程的机制中,真正起到主要作用的是线程自身,虽然上面的代码简单,但也阐述清楚了问题。更复杂的计算式工作,也应该是这样的方式,去妥善正确的退出线程。其实CancellationTokenSource还有一个方法值得注意,就是Register方法,它负责传递一个Action委托,线程被停止时会执行回调:cts.Token.Register(() =>
{
Console.WriteLine("线程已经停止");
});
虽然是用Thread在演示,但如果是ThreadPool,也是一样的模式,后面还会讲到Task的取消,它依赖于CancellationTokenSource和CancellationToken完成取消控制。控制好线程数量!这是一个很严肃的事情,如果线程过多,这意味着我们项目的架构设计存在缺陷。那到底一个应用程序应该使用多少个线程合适,我们打开计算机的任务管理器,切到性能界面,我们来算一下:现在博主的电脑运行着98个进程,1436个线程,使用1.9GB内存,除一下,一个应用程序平均也就14个线程左右,每个线程大约占1.5MB内存,所以每个应用程序的线程不会太多。错误创建过多线程的一个场景:就是我们当初学习编程的时候,网络编程我们都写socket聊天室,相信绝大部分朋友都写过,那时我们会为每个socket开一个线程去监听请求,假设这个聊天室我们要对外开放用户,那就意味着随着用户数的增多,线程就会变多,如果达到一定数量,就意味着计算机管理不过来了,而开线程也需要内存来支持的,CLR默认会给每个线程分配差不多1MB的内存空间,如果你的电脑又恰好是32位的,那就意味着当你电脑里面线程数达到4096的时候,内存就被耗尽了,这都是理想情况,而且32位系统往往只能支持2.xGB-3.xGB的内存,再加之每种型号的CPU其实都有线程数在多少合适这种说法的,比如i5处理器在1000个线程左右是最高效的,i7处理器在2000线程左右。过多的线程会造成CPU在线程之间的切换到开销过大,相当的损耗CPU时间,像Socket这类I/O密集型应该使用异步去完成。其实过多的线程带来的问题不仅仅如此,还会有另外的问题,就是:新开的线程可能需要等待相当长的时间才会开始执行,我们很无奈,我相信这也是你们无法忍受的结果,我们可以来实测一下,下面的代码,第501个线程会等待好几分钟才会开始执行:for (int i = 0; i < 500; i++)
{
new Thread(() =>
{
int j = 0;
while (true)
{
j++;
Thread.Sleep(1);
}
}).Start();
}
Thread.Sleep(5000);
new Thread(() =>
{
while (true)
{
Console.WriteLine("第501个线程正在运行...");
Thread.Sleep(1000);
}
}).Start();
其实除了启动问题外,还有线程切换的问题,也就是说上面的第501个线程被切换走了之后,也需要相当长的时间才会再次切换回来。所以,不要滥用线程,不要滥用过多的线程,当有工作需要新开线程去解决的时候,要仔细考虑这项工作是否真的需要开线程去解决,即使需要使用线程,也推荐大家使用线程池技术,比如之前的连接socket那样的I/O密集型场景,使用异步去管理,异步其实底层也是使用的线程池技术,成百上千个线程使用异步或者线程池技术后,实际上在工作的只有几个线程。继续讨论线程池使用线程池能极大地提升我们的打码体验和用户体验,但是我们作为开发者也应该要注意,线程是要产生开销的。线程的空间开销主要来自:1)线程内核对象(Thread Kernel Object)。每个线程都会创建一个这样的对象,它主要包含线程上下文信息,占用的内存在700字节左右。2)线程环境块(Thread Environment Block)。占用4KB内存。3)用户模式栈(User Mode Stack),即线程栈。线程栈用于保存方法的参数、局部变量和返回值。每个线程栈占用1MB的内存。要用完这些内存很简单,写一个不能结束的递归方法,让方法参数和返回值不停地消耗内存,很快就会发生OutOfMemoryException。4)内核模式栈(Kernel Mode Stack)。当调用操作系统的内核模式函数时,系统会将函数参数从用户模式栈复制到内核模式栈。会占用12KB内存。线程的时间开销来自:1)线程创建的时候,系统相继初始化以上这些内存空间。2)接着CLR会调用所有加载DLL的DLLMain方法,并传递连接标志(线程终止的时候,也会调用DLL的DLLMain方法,并传递分离标志)。3)线程上下文切换。一个系统中会加载很多的进程,而一个进程又包含若干个线程。但是一个CPU在任何时候都只能有一个线程在执行。为了让每个线程看上去都在运行,系统会不断地切换“线程上下文”:每个线程大概得到几十毫秒的执行时间片,然后就会切换到下一个线程了。这个过程大概又分为以下5个步骤:步骤1 进入内核模式。步骤2 将上下文信息(主要是一些CPU 寄存器信息)保存到正在执行的线程内核对象上。步骤3 系统获取一个 Spinlock,并确定下一个要执行的线程,然后释放 Spinlock。如果下一个线程不在同一个进程内,则需要进行虚拟地址交换。步骤4 从将被执行的线程内核对象上载入上下文信息。步骤5 离开内核模式。所以线程的创建和销毁是需要付出时间和空间的代价的,而微软为了防止我们开发者无节制的使用线程,就封装了线程池这种技术,简单说就是帮助我们开发者来管理线程,随着工作的完成,线程不会被销毁,而是回到线程池中,看别的工作会不会继续使用线程,而具体何时被销毁或者创建,由CLR自己的算法来决定,所以真实项目中,我们更多的应该考虑使用线程池来替代Thread,线程池主要有ThreadPool和BackgroundWorker这两个类,使用也蛮简单的:ThreadPool.QueueUserWorkItem(state =>
{
//todo
});
var bw = new BackgroundWorker();
bw.DoWork += (sender, e) =>
{
//todo
};
bw.RunWorkerAsync();
而ThreadPool和BackgroundWorker的区别在于:BackgroundWorker在WinForm和WPF中还提供了和UI线程交互的能力,而ThreadPool没有这种能力,BackgroundWorker的能力还包括:通知进度、完成回调、取消任务、暂停任务等功能。久等了的Task终于登场前面做了这么多的铺垫,其实就是为了给Task登场做准备的,Task是.NET4.5之后提供的线程的更高级的一种技术,虽然前面刚说了ThreadPool和BackgroundWorker比Thread更有优势,那么Task更是超越ThreadPool和BackgroundWorker更强大的概念。为线程池提供了更多的API可以调用,管理一个线程简直颠覆传统了:Task.Run(() =>
{
Console.WriteLine("我是异步线程...");
}).ContinueWith(t =>
{
if (t.IsCanceled)
{
Console.WriteLine("线程被取消了");
}
if (t.IsFaulted)
{
Console.WriteLine("发生异常而被取消了");
}
if (t.IsCompleted)
{
Console.WriteLine("线程成功执行完成了");
}
});
我们可以看出,Task具有以下属性:IsCanceled:线程被取消而完成;IsFailed:线程因发生未捕获的异常而完成;IsCompleted:成功完成。需要注意的是:Task并没有提供成功回调的事件功能,它是启动一个新的task来实现BackgroundWorker类似的事件回调功能,而ContinueWith正是这样的功能,这种方式天然就支持了任务的状态检查,而且还能在新任务中获得原任务返回的值。下面来个稍微复杂的例子,同时支持任务完成的通知,数据返回,任务被取消,异常的发生等情况:using (HttpClient client = new HttpClient() { BaseAddress = new Uri("https://www.baidu.com") })
{
var result = client.GetStringAsync("/").ContinueWith(t =>
{
if (t.IsCanceled)
{
Console.WriteLine("线程被取消了");
}
if (t.IsFaulted)
{
Console.WriteLine("发生异常而被取消了");
}
if (t.IsCompleted)
{
return t.Result;
}
return null;
}).Result;
Console.WriteLine(result);
}
Task的Result属性可以拿到线程执行完返回的值,同时阻塞线程直到拿到返回的结果,上面的代码调用HttpClient的GetStringAsync方法,即创建了一个Task来等待http响应,当IsCompleted属性为true,就可以拿到http请求返回的html代码,最后存到上一层的Task的Result属性中。如果我们把http请求的地址改成Google,在我们现在这样的网络环境下,HttpClient请求不了,那肯定就只有抛异常咯,所以当HttpClient请求的地址是Google的时候,Task的IsFailed则为true。如果要模拟线程被取消,上面的代码把最后的Result去掉,就让Task不阻塞,这段代码很快就结束,而HttpClient内部却认为请求被取消了,所以会触发Task的取消行为。如果要真实模拟Task的取消,可以这样做:var cts = new CancellationTokenSource();
Task.Run(() =>
{
Console.WriteLine("我是异步线程...");
}, cts.Token).ContinueWith(t =>
{
if (t.IsCanceled)
{
Console.WriteLine("线程被取消了");
}
if (t.IsFaulted)
{
Console.WriteLine("发生异常而被取消了");
}
if (t.IsCompleted)
{
Console.WriteLine("线程成功执行完成了");
}
});
cts.Cancel();
我们声明一个CancellationTokenSource传入Task,在调用CancellationTokenSource的Cancel方法即可提前终止掉线程。Task还支持工厂的概念,且支持多个任务之间共享相同的状态,也就是说,如果刚才的想取消任务,可以同时取消一组任务:var cts = new CancellationTokenSource(1000);
Task[] tasks = {Task.Factory.StartNew(() =>
{
Console.WriteLine("我是异步线程...");
Thread.Sleep(1000);
}, cts.Token),Task.Factory.StartNew(() =>
{
Console.WriteLine("我是异步线程...");
Thread.Sleep(1000);
}, cts.Token),Task.Factory.StartNew(() =>
{
Console.WriteLine("我是异步线程...");
Thread.Sleep(1000);
}, cts.Token)};
cts.Cancel();
Task.Factory.ContinueWhenAll(tasks, ts =>
{
Console.WriteLine($"线程被取消{ts.Count(t => t.IsCanceled) }次");
});
Task的工厂方法进一步优化了线程池的调度,所以我们更应该用Task来开启线程。如果想让Task异步变为同步,只需要接着调用Task的Wait方法即可。async和await关键字这是.NET4.5和C#6为我们提供的Task的两个关键字,它们几乎是成对存在的,使用这两个关键字,能够将我们定义的方法变成异步方法去执行。如下代码:static void Main(string[] args)
{
MyMethod();
Console.WriteLine("world");
Console.ReadKey();
}
public static void MyMethod()
{
Console.WriteLine("hello");
Thread.Sleep(5000);
}
我们看到的结果是先输出hello,等待5秒后才输出world,那么我不想改代码,又不想等待呢?这时我们把MyMethod改造成异步方法即可:static void Main(string[] args)
{
MyMethod();
Console.WriteLine("world");
Console.ReadKey();
}
public static async void MyMethod()
{
Console.WriteLine("hello");
await Task.Delay(5000);
}
运行之后就会看到先输出hello然后立马数出了world,并没有等待5秒了,来看一下我们做了哪些改动,首先方法签名加了async进行修饰,Thread.Sleep换成了Task.Delay,并且在前面加了await关键字,这表示异步等待,而我们常用的Thread.Sleep是同步等待,当然这里也只是为了模拟一个耗时操作。现在来说下异步方法的声明,异步方法必须被async关键字修饰,且方法体里有存在await关键字才能算是异步方法,否则仅被async修饰的方法也是同步方法,其次,方法的返回值只能是void、Task或者Task<>,三者的区别在于:void:执行一个任务,不需要返回值,不需要与任务进行任何的交互;Task:执行一个任务,不需要返回值,但需要与任务进行交互,比如取消、执行状态的检测等;Task<>:同上,但需要返回值,返回值被包含在Task的Result属性里。改造刚才的代码,我们对MyMethod方法不需要返回值,但需要与Task进行交互,直接将void改成Task即可,不需要在方法体里加return,因为await已经代替我们return了:static void Main(string[] args)
{
var task = MyMethod();
Console.WriteLine("world");
Console.WriteLine(task.IsCompleted);
Console.ReadKey();
}
public static async Task MyMethod()
{
Console.WriteLine("hello");
await Task.Delay(5000);
}
输出结果:为什么是false?因为输出world之后还没有5秒钟,而MyMethod需要执行5秒钟才完成,所以在此刻的执行完成状态是false。接下来我们再以有返回值的异步方法检验一下:static void Main(string[] args)
{
var task = MyMethod();
Console.WriteLine("world");
Console.WriteLine(task.Result);
Console.ReadKey();
}
public static async Task
{
Console.WriteLine("hello");
await Task.Delay(5000);
return "aaa";
}
输出结果:需要等到5秒之后才会输出aaa。使用异步方法需要注意的是:await后面跟的的必须是一个返回值为Task的方法;main方法不能被修饰为异步方法;方法体里有lock关键字不能作为异步方法;方法参数不能带ref和out关键字;异步方法不是在同一个线程中执行的。异步方法的执行过程如下:同步等待和异步等待下面的代码,不考虑Console输出的CPU时间消耗,输出的结果多少?大家用自己的大脑运行一下这段代码:static void Main(string[] args)
{
var sw = Stopwatch.StartNew();
var task = MyMethod();
Thread.Sleep(4000);
var result = task.Result;
Console.WriteLine(sw.ElapsedMilliseconds);
}
public static async Task
{
await Task.Delay(5000);
return "aaa";
}
其实是一道面试题,有的人回答4000,有人回答9000,有人回答5000,还有的人肯定说不出个答案,那么正确答案到底是多少?我们我们理一下代码的执行过程:首先调用了MyMethod异步方法,即开启了一个线程,而这个线程里面,异步等待了5000ms,然后主线程中同步等待了4000ms,在主线程等待4000ms之后,又有个变量在等着异步线程返回一个值,到这里之前,也就是主线程和一个工作线程都在同时进行等待,而主线程等待了4000ms,那么另一个工作线程也已经等待了4000ms,那么在等待MyMethod返回“aaa”给result的时候,就还需要1000ms的等待,最后,MyMethod返回后,输出了程序执行的时间:5000。我们来运行代码检验一下:5054,跟我们预想的5000很接近,我们的想法是正确的,多出来的54ms自然就是Console输出的CPU消耗咯。那么,如果MyMethod返回void或者Task,结果又是多少?static void Main(string[] args)
{
var sw = Stopwatch.StartNew();
var task = MyMethod();
Thread.Sleep(4000);
Console.WriteLine(sw.ElapsedMilliseconds);
}
public static async Task MyMethod()
{
await Task.Delay(5000);
}
我想大家已经知道答案了:4000。没错,就是4000,因为现在MyMethod没有与主线程进行交互了,即使刚才的MyMethod如果没有与主线程进行交互,那么结果仍然是4000,因为线程开启了就再也跟主线程没有关系了。刚才的代码也是模拟我们在同步线程和异步线程同时都在执行耗时操作,并且需要线程间进行通信的一个场景,最典型的一个场景就是我们在代码里面使用HttpClient发送http请求,有时我们需要在前一个请求完成后才能进行发下一个请求,并且保证两次请求都成功这样的一个场景。所以,如果异步线程执行之后,如果和主线程没有交互,是不会阻塞主线程执行的,只有当主线程需要等异步方法返回结果,而异步线程还没执行完的情况下,会造成主线程阻塞。并行计算Parallel在和Task的同命名空间下,有一个叫Parallel的静态类简化了Task在同步状态下的操作,主要提供了Invoke、For和ForEach三个方法的多个重载。Invoke传入可变长度参数的Action委托,For主要用于做类似于传统for循环时对数组元素的并行操作,ForEach主要用于做类似于传统foreach循环时对集合迭代的并行操作。Parallel.Invoke(() =>
{
Console.WriteLine("线程1");
Thread.Sleep(1000);
}, () =>
{
Console.WriteLine("线程2");
Thread.Sleep(1000);
}, () =>
{
Console.WriteLine("线程3");
Thread.Sleep(1000);
});
Console.WriteLine("-------------");
Parallel.For(0, 4, i =>
{
Console.WriteLine(i);
});
Console.WriteLine("-------------");
var list = new List
Parallel.ForEach(list, item =>
{
Console.WriteLine(item);
});
我们看出,不管哪种方式,其调用顺序是无序的,这说明如果我们对集合元素顺序输出的情况下,并行计算显然不合适了。而且,Parallel启动后是阻塞状态的,所以Parallel它是同步的。也就是说:Parallel简化了Task的同步操作,但不等同于Task的默认行为不知道大家刚才注意仔细阅读没有,上文中提到的Parallel简化Task的使用,特别的加个了“同步”来做定语修饰。所以说Parallel调用的线程是被阻塞的,虽然说Parallel把任务交给了Task去处理,但会等到所有的Task把任务执行完成了才会继续后面的操作,而且Parallel只提供了Invoke方法,并没有提供一个叫BeginInvoke的方法,这也说明了一定的问题。使用Task的时候,我们习惯于直接调Run方法开启一个任务,这个任务是异步的,如果要同步,则继续调用Wait方法,而Parallel所包装的,也就是这么一个过程。既然叫并行计算,也就意味着运行时在后台将任务尽可能地分配在CPU上,虽然是基于Task实现的,但这并不表示它等同于异步!Parallel踩坑之旅Parallel的循环操作还支持一些复杂的操作,比如它可以在每个任务启动时做一些初始化操作,结束时做一些扫尾操作。还允许监控任务状态,请注意,刚才这个说法是错误的,应该把“任务”改成“线程”,这就是坑所在。所以我们必须深刻地去理解Parallel的操作和应用,不然你跳到坑里去了还没人能把你拉出来,体会一下这段代码输出什么:var list = new List
int sum=0;
Parallel.For(0,list.Count,() => 1,(i, state, total) =>
{
total+=i;
return total;
},i => Interlocked.Add(ref sum,i));
Console.WriteLine(sum);
代码可能输出16,也可能输出17,理论上也可能是18、19,但概率比较小了,为什么?要究其原因,我们看看Parallel的for方法的声明:对于前两个参数很容易理解,分别是开始和结束的索引。body参数也很容易理解,即循环体。而localinit和localFinally就比较难理解了,并且坑就在这里,要理解这两个参数,就必须先理解Parallel.For的运作模式,该方法采用并发的方式启动for循环的循环体,就意味着循环的循环体是交给线程池去处理的,而上面的代码循环了6次,实际调度的线程可能没有6个,这就是并发的优势,也是线程池的优势,Parallel通过内部的调度算法,节约了线程的开销,localinit的作用就是如果Parallel新开线程,那么就会在localinit里面对线程开启进行一些初始化操作,比如上面的代码就是为新线程启动时为sum返回为1。LocalFinally的作用就是线程结束的时候做一些扫尾操作,比如刚才的代码:i => Interlocked.Add(ref sum,i)
代表的就是线程结束的时候为sum进行原子性的加i操作,相当于:sum=sum+i,而原子操作就是为了避免并发带来的问题。所以现在我们能很好理解上面的代码为什么输出会不确定,Parallel启动了6个任务,但是不确定启动了多少个线程,这是由运行时根据自己算法来决定的,如果只有一个线程,那么结果是16,如果两个线程,那就是17了,三个就是18……,如果我们把初始值localinit返回0的话,你可能永远也不知道你在坑里面。为了更清晰的理解这个坑,我们不妨再来一段代码试下:var list = new List
string str = String.Empty;
Parallel.For(0, list.Count, () => "-", (i, state, total) => total += list[i], s =>
{
str += s;
Console.WriteLine(“end:”+s);
});
Console.WriteLine(str);
可能的输出结果:并行计算一定比串行快?现在我们都知道了并行计算其实也是基于线程的,也知道线程的创建和销毁都需要时间和空间的开销,那么你还会想并行它就一定比串行代码更快?如果循环的循环体很小,那么并行的速度也许比串行更慢,下面这个例子,我们测试一下在并行和串行的情况下的时间消耗:var sw = Stopwatch.StartNew();
for (int i = 0; i < 2000; i++)
{
for (int j = 0; j < 10; j++)
{
var sum = i + j;
}
}
Console.WriteLine("串行循环耗时:" + sw.ElapsedMilliseconds);
sw.Restart();
Parallel.For(0, 2000, i =>
{
for (int j = 0; j < 10; j++)
{
var sum = i + j;
}
});
Console.WriteLine("并行循环耗时:" + sw.ElapsedMilliseconds);
输出结果如下:我们发现,当循环体很小的时候,串行循环几乎不耗时,而并行循环耗时123毫秒,并行循环所消耗的性能是串行循环的好几倍。如果我们把循环体里面的10改到更大的时候:var sw = Stopwatch.StartNew();
for (int i = 0; i < 2000; i++)
{
for (int j = 0; j < 100000; j++)
{
var sum = i + j;
}
}
Console.WriteLine("串行循环耗时:" + sw.ElapsedMilliseconds);
sw.Restart();
Parallel.For(0, 2000, i =>
{
for (int j = 0; j < 100000; j++)
{
var sum = i + j;
}
});
Console.WriteLine("并行循环耗时:" + sw.ElapsedMilliseconds);
输出结果显然并行循环优于串行循环了:所以只有当我们要在循环体里做更多的工作的时候,才能考虑使用并行计算。并行加锁需谨慎!既然是多线程,那么我们有时候不得不考虑加锁来解决共享资源的原子性操作,以保证数据的一致性。但在并行里面加锁是需要谨慎的,这包括:操作本身就需要使用同步代码去完成;或者需要长时间占用共享资源的场景。在对整数变量进行原子操作的时候,.NET提供了Interlocked的Add方法,这就极大的避免了我们需要对整数进行原子操作时所带来的同步性能损耗,回想我们刚才的Parallel踩坑之旅:var list = new List
int sum=0;
Parallel.For(0,list.Count,() => 1,(i, state, total) =>
{
total+=i;
return total;
},i => Interlocked.Add(ref sum,i));
Console.WriteLine(sum);
如果扫尾的时候我们不进行原子操作,那么最后导致的结果可能就是汇编语言在最后mov操作时内存地址的对齐问题,而Interlocked解决了这样的问题,同时.NET还提供了volatile关键字来解决变量的原子性操作问题,当然这也不是我们要深入研究的重点,.NET提供的原生的对变量进行原子性操作,带来了性能上的提升,但有些场景,我们不得不加锁来实现同步:var mc = new MyClass();
Parallel.For(0,100000,i => mc.AddCount());
Console.WriteLine(mc.Count);
public class MyClass
{
public int Count { get; set; }
public void AddCount()
{
Count++;
}
}
输出的结果如下:显然和我们预想的100000还有一定的差距,为了保证输出的正确性,我们就必须在并行里面加锁了(假设MyClass的AddCount是第三方提供的API,不允许修改源码):var mc = new MyClass();
Parallel.For(0, 100000, i =>
{
lock (mc)
{
mc.AddCount();
}
});
Console.WriteLine(mc.Count);
这样就能得到我们预想的结果:但是,带来了另外的问题,由于同步锁的存在,系统的开销也增加了,线程在上下文的切换也增加了,牺牲了更多的CPU时间和内存,也就是说,这段代码因为同步锁,其实已经是串行代码了,并且还不如串行代码的性能,所以,如果我们要考虑数据的一致性,选择并行还需谨慎,如果循环体里面的全部代码都需要加锁,那么完全不能考虑使用并行。又一个好东西:并行linq(plinq)这可是所有编程语言中C#的一大神器,linq可谓是无所不能,对我们的编程体验确实很爽。Linq最基本的功能就是对集合的各种复杂操作,其实仔细想想你会发现,并行编程简直就是专门为这一类应用而准备的。所以,微软不仅有linq,还有plinq,也就是微软专门为linq扩展了一个叫ParallelEnumerable的类,该类也在System.Linq命名空间下,所提供的功能就是让linq支持并行计算,这就是plinq。我们传统的linq是单线程的,而plinq是并发的、多线程的,通过下面的例子我们可以看出区别:var list = new List
var query = from i in list select i;
foreach(var i in query)
{
Console.WriteLine(i);
}
Console.WriteLine("----------");
var query2 = from i in list.AsParallel() select i;
foreach(var i in query2)
{
Console.WriteLine(i);
}
输出结果如下:可以看出我们传统的linq是顺序输出的,而plinq是无序的。其实并行输出还有另一种方式可以,那就是ForAll:query2.ForAll(Console.WriteLine);
但是如果以这种方式进行循环输出的话,ForAll会忽略掉查询的AsOrdered请求:var query2 = from i in list.AsParallel().AsOrdered() select i;
query2.ForAll(Console.WriteLine);
AsOrdered可以对并行之后的结果进行重新排序,以保证数据的顺序,而在ForAll方法中,仍然是无序的,如果要保证排序,我们还是只有老老实实的用普通的for循环了,即:var query2 = from i in list.AsParallel().AsOrdered() select i;
foreach (var i in query2)
{
Console.WriteLine(i);
}
然而在并行之后再排序,会牺牲掉一定性能的,排序操作包括:OrderBy(Descding)、ThenBy(Descding),所以如果我们要考虑使用并行linq,我们必须考虑集合元素的顺序性是否是重要的,以便程序按照我们预想的结果运行。还有一些其他的操作,比如Take,如果我们有类似于“随机推荐”的功能,我们可以这么干:foreach(var i in list.AsParallel().Take(5))
{
Console.WriteLine(i);
}
如果是顺序的,那就会取出前5个元素,而这儿因为Parallel是无序的,所以取前5个相对于源数据也是随机的5个。虽然使用plinq来迭代集合元素比linq更高效,但一定要注意,不是所有的并行都比串行更快,linq的某些方法串行比并行快,比如ElementAt等。所以实际开发中我们应该根据我们的使用场景去衡量和决定使用串行linq还是并行linq。找到最佳的解决方案。并行编程的异常处理关于异常处理,这是并行编程中最头疼的一个问题,异常的处理也是非常重要的一个环节,而且,由于并行的存在,使得我们在并行编程的时候,调试起来也是比较难的,程序出问题了找到问题所在也是非常苦恼的一件事情,所以,在并行编程的时候,我们就必须攻下它们。从一道面试题说起这是小编我们公司的一道面试题,也是各大公司喜欢考察的一道面试题,其实很简单,但可能很多人会迷糊,下面这个方法,调用会不会抛异常?public static async void MyMethod()
{
await Task.CompletedTask;
throw new Exception();
}
很多人只有猜会或者不会,但说不出理由,答案肯定是不会抛异常,那为什么不会抛异常?因为这是个异步方法,发生异常是在另一个线程里面发生的,并没有抛给调用者,因为它没有与调用线程进行交互,调用者只是调用了它,但有没有发生异常调用者不知道。好了,其实上面这段代码就反映了在我们实际项目中,有些异步方法发生了异常,但并没有被调用者捕获,导致项目出现一些bug,这样的bug其实很难找出来,所以在异步线程中必须正确地去处理异常。Task中的异常处理如果我们的Task是可以进行交互的,比如可以调用Task的Wait、WaitAny、WaitAll等阻塞方法,或者拿Task的Result属性,发生异常是可以被调用者捕获的,能捕获到AggregateException异常,而AggregateException可以看作是并行编程中的最顶层的异常,所以当异步线程中发生异常时,我们都应该把异常包装到AggregateException中,一个异步线程异常的简单处理如下:var t = Task.Run(() => throw new Exception("我在异步线程中抛出的异常"));
try
{
t.Wait();
}
catch (AggregateException e)
{
foreach (var ex in e.InnerExceptions)
{
Console.WriteLine($"异常类型:{ex.GetType()},异常源:{ex.Source},异常信息:{ex.Message}");
}
}
输出结果:上面的代码以及运行效果大家可以看出,虽然调用Wait等方法以及获取Result属性能得到任务的异常信息,但是会阻塞调用线程,这不是我们想要的结果,凭什么就为了去捕获一个异常而去阻塞调用者线程,所以Task的ContinueWith也解决了这个问题,新开一个后续Task去捕获异常:Task.Run(() => throw new Exception("我在异步线程中抛出的异常")).ContinueWith(t =>
{
if (t.IsFaulted)
{
foreach (var ex in t.Exception?.InnerExceptions)
{
Console.WriteLine($"异常类型:{ex.GetType()},异常源:{ex.Source},异常信息:{ex.Message}");
}
}
});
输出结果同上。这样的方式解决了调用者线程等待任务阻塞的问题,但仔细研究我们会发现,异常的处理没有往外抛,它还是在线程池里面,在某些场景下,比如在业务逻辑上特定的异常处理,我们就需要才去这种方式,而且也鼓励使用这种做法,但很明显,我们更多的时候需要更进一步的将异常封装。而Task没有提供将任务中的异常抛到主线程。不过有一个可行的办法,仍然使用Wait方法来达到此目的,不过刚才我们说过这样的方式不可取,因为会阻塞,降低我们的编码体验,后来建议使用ContinueWith新开一个后续Task来处理异常,这样很好,所以,我们可以不妨将前两者结合一下得到下面这种方式将异步任务中的异常抛到主线程:var task = Task.Run(() => throw new Exception("我在异步线程中抛出的异常")).ContinueWith(t =>
{
if (t.IsFaulted)
{
throw t.Exception;
}
});
try
{
task.Wait();
}
catch (AggregateException e)
{
foreach (var ex in e.InnerExceptions)
{
Console.WriteLine($"异常类型:{ex.GetType()},异常源:{ex.Source},异常信息:{ex.Message}");
}
}
但到这里一切还没有结束,因为调用Wait会阻塞,而且CLR在后台会新开线程池线程来完成额外的工作,如果要把任务的异常抛给主线程,不妨试试时间通知的方式:static event EventHandler
public class AggregateExceptionArgs : EventArgs
{
public AggregateException AggregateException { get; set; }
}
static void Main(string[] args)
{
CatchedAggregateExceptionEventHandler += (sender, e) =>
{
foreach (var ex in e.AggregateException.InnerExceptions)
{
Console.WriteLine($"异常类型:{ex.GetType()},异常源:{ex.Source},异常信息:{ex.Message}");
}
};
var task = Task.Run(() =>
{
try
{
throw new Exception("我在异步线程中抛出的异常");
}
catch (Exception e)
{
CatchedAggregateExceptionEventHandler(null, new AggregateExceptionArgs() { AggregateException = new AggregateException(e) });
}
});
Console.ReadKey();
}
这样的方式,我们先定义了一个事件CatchedAggregateExceptionEventHandler,它接收标准事件的两个参数,而AggregateExceptionArgs则是包装异常的一个包装类型,所以当异常发生的时候,我们就将异常交给了CatchedAggregateExceptionEventHandler这个事件处理器去处理。事件的调用完全没有阻塞主线程,如果是在Windows窗体程序里面,也可以将异常信息交给窗体程序的线程模型去处理,所以最终建议大家使用这种方式去处理异步任务的异常,这种方式不管任务能否与调用者进行交互,都能捕获到异常。另外一个冷知识:虽然TaskScheduler静态类有一个类似的功能,但一般不建议这样做,因为它的事件回调只有在进行垃圾回收的时候才会被触发,如下:TaskScheduler.UnobservedTaskException += (sender, e) =>
{
Console.WriteLine($"异常类型:{e.Exception.GetType()},异常源:{e.Exception.Source},异常信息:{e.Exception.Message}");
};
var task = Task.Run(() =>
{
throw new Exception("我在异步线程中抛出的异常");
});
Console.ReadKey();
task.Dispose();
task = null;
GC.Collect(0);
上面的代码,如果把GC.Collect去掉,那么异常信息就不会被输出,所以这种方式看似更简单,其实存在着局限性。Parallel中的异常处理相对于Task,Parallel的异常处理要简单很多,因为Parallel是会阻塞主线程的,它会等到所有的task执行完成了才会继续接下来的其他工作,而Task要采用非阻塞的方式去捕获异常,所以下面这样的代码就能把Parallel的异常抛给主线程:var exs = new ConcurrentQueue
try
{
Parallel.For(0, 2, i =>
{
try
{
throw new ArgumentException();
}
catch (Exception e)
{
exs.Enqueue(e);
}
if (exs.Any())
{
throw new AggregateException(exs);
}
});
}
catch (AggregateException e)
{
foreach (var ex in e.InnerExceptions)
{
Console.WriteLine($"异常类型:{ex.GetType()},异常源:{ex.Source},异常信息:{ex.Message}");
}
}
输出结果为:我们专门声明了一个线程安全的集合ConcurrentQueue队列来存储异常信息,最终将这个异常集合包装给AggregateException再往外抛。总结其实并行编程没什么难的,主要是并行编程会给我们带来各种各样的坑,有时候我们踩坑了,可能永远不知道程序为什么出bug,可能为了一个bug,加了好几天班熬了好几天夜也不能解决。只要我们在并行编程的时候避掉这些坑,我们都是大神!其实大家眼中的大神跟自己差不多,也就是人家踩的坑多一点,而你踩的坑少一点而已,最后,祝大家踩坑快乐!转载自原文:编辑于 2020-09-14 12:40多线程高并发C#赞同 509 条评论分享喜欢收藏申请
TPL十二月赛分组出炉:Fly遭遇120,浪漫再战Happy_腾讯新闻
TPL十二月赛分组出炉:Fly遭遇120,浪漫再战Happy_腾讯新闻
TPL十二月赛分组出炉:Fly遭遇120,浪漫再战Happy
由War3蛋塔飞主办,斗鱼TV、OBSBOT寻影、SCBOY、朕宅赞助,魔坛情报局媒体支持的普力艾|ProIron TPL联赛,将于12月20日19点开启十二月赛正赛的争夺。目前,正赛分组已经出炉,下面让我们一起看下。
首日进行的是A组的比赛,Moon和ReMinD两位韩国NE宗师狭路相逢,又是一番剑宗与气宗的较量,不管胜负如何,场面一定会很刺激。只是,他们会不会都被Laby制裁呢?
B组,浪漫再战Happy,又到了坦克流大展身手的时候了。同组卡号、FoCuS实力也很强大,这真是个不折不扣的死亡之组。
C组的重头戏莫过于120和Fly的比赛,Fly最近状态恢复得不错,期待他能够给B王上上课。
D组,Infi拿下Hawk问题不大,对战Sok和Lawliet也有机会战而胜之,塔魔这次有望重返八强。
十二月赛赛事信息
比赛赛制
小组赛
16名选手(12位邀请选手+4位海选出线选手)分为4个组进行BO3双败制淘汰赛,每个小组前两名晋级8强
季后赛
八强双败淘汰,除了第一轮为BO3、决赛为BO7,其余场次均为BO5
第一轮对阵顺序为:A1 VS B2,A2 VS B1,C1 VS D2,C2 VS D1
比赛时间
12月20日-12月23日 小组赛
12月26日-12月31日 季后赛
*每个比赛日均为19点开始
比赛奖励
第一名:15000元+150分
第二名:8000元+100分
第三名:4000元+65分
第四名:3000元+45分
5-6名:1500元+30分
7-8名:1000元+20分
9-16名:10分
*所有月赛结束后,积分前16的选手将晋级总决赛
种族规则
同一个BO3、BO5、BO7只能使用同一种族(包括随机)
每场比赛BP开始前需提前提交种族
比赛地图
EI2.0、TM、TH、NI、LT、HF、CH、TR、TS
直播规则
1. 主播可以无偿使用官方提供的延迟流进行直播,直播延迟为五分钟。
2. 进入正赛的选手可以无偿使用官方提供的延迟流进行直播,直播延迟为三分钟,同时参赛选手可以直播自己的第一视角游戏画面,不受任何限制。
3. 所有使用直播流进行直播的主播和选手不得跳过或遮挡比赛画面中出现的赛事logo以及广告,第一次发现将警告通报,第二次发现取消直播资格。
4. 所有选手和主播不得私自违规盗播他人游戏画面
5. TPL组委会作为赛事主办方对所有规则有最终解释权
.data_color_scheme_dark{--weui-ORANGERED: #ff6146;--weui-BG-0: #111;--weui-BG-1: #1e1e1e;--weui-BG-2: #191919;--weui-BG-3: #202020;--weui-BG-4: #404040;--weui-BG-5: #2c2c2c;--weui-FG-0: rgba(255, 255, 255, .8);--weui-FG-HALF: rgba(255, 255, 255, .6);--weui-FG-1: rgba(255, 255, 255, .5);--weui-FG-2: rgba(255, 255, 255, .3);--weui-FG-3: rgba(255, 255, 255, .1);--weui-FG-4: rgba(255, 255, 255, .15);--weui-FG-5: rgba(255, 255, 255, .1);--weui-RED: #fa5151;--weui-REDORANGE: #ff6146;--weui-ORANGE: #c87d2f;--weui-YELLOW: #cc9c00;--weui-GREEN: #74a800;--weui-LIGHTGREEN: #3eb575;--weui-BRAND: #07c160;--weui-BLUE: #10aeff;--weui-INDIGO: #1196ff;--weui-PURPLE: #8183ff;--weui-WHITE: rgba(255, 255, 255, .8);--weui-LINK: #7d90a9;--weui-TEXTGREEN: #259c5c;--weui-FG: #fff;--weui-BG: #000;--weui-TAG-TEXT-RED: rgba(250, 81, 81, .6);--weui-TAG-BACKGROUND-RED: rgba(250, 81, 81, .1);--weui-TAG-TEXT-ORANGE: rgba(250, 157, 59, .6);--weui-TAG-BACKGROUND-ORANGE: rgba(250, 157, 59, .1);--weui-TAG-TEXT-GREEN: rgba(6, 174, 86, .6);--weui-TAG-BACKGROUND-GREEN: rgba(6, 174, 86, .1);--weui-TAG-TEXT-BLUE: rgba(16, 174, 255, .6);--weui-TAG-BACKGROUND-BLUE: rgba(16, 174, 255, .1);--weui-TAG-TEXT-BLACK: rgba(255, 255, 255, .5);--weui-TAG-BACKGROUND-BLACK: rgba(255, 255, 255, .05)}.data_color_scheme_dark{--weui-BTN-ACTIVE-MASK: rgba(255, 255, 255, .1)}.data_color_scheme_dark{--weui-BTN-DEFAULT-ACTIVE-BG: rgba(255, 255, 255, .126)}.data_color_scheme_dark{--weui-DIALOG-LINE-COLOR: rgba(255, 255, 255, .1)}.data_color_scheme_dark{--weui-BG-COLOR-ACTIVE: #373737}.data_color_scheme_dark{--weui-BG-6: rgba(255, 255, 255, .1);--weui-ACTIVE-MASK: rgba(255, 255, 255, .1)}.rich_media_content{color:#000000e5;font-size:17px;font-size:var(--articleFontsize);overflow:hidden;text-align:justify}.rich_media_content{color:#ffffffa6;color:var(--weui-FG-HALF)}.rich_media_content{position:relative;z-index:0}.wxw-img{vertical-align:bottom}@media screen and (min-width:1024px){body:not(.pages_skin_pc) :root{--appmsgPageGap: 20px}}:root{--articleFontsize: 17px}:root{--sab: env(safe-area-inset-bottom)}:root{--wxBorderAvatarRatio: 3}:root{--discussPageGap: 20px}:root{--appmsgPageGap: 20px}body,.wx-root,page{--weui-BTN-HEIGHT: 48;--weui-BTN-HEIGHT-MEDIUM: 40;--weui-BTN-HEIGHT-SMALL: 32}body,.wx-root{--weui-FG-1: rgba(0, 0, 0, .55);--weui-ORANGERED: #ff6146;--weui-BG-0: #ededed;--weui-BG-1: #f7f7f7;--weui-BG-2: #fff;--weui-BG-3: #f7f7f7;--weui-BG-4: #4c4c4c;--weui-BG-5: #fff;--weui-FG-0: rgba(0, 0, 0, .9);--weui-FG-HALF: rgba(0, 0, 0, .9);--weui-FG-1: rgba(0, 0, 0, .5);--weui-FG-2: rgba(0, 0, 0, .3);--weui-FG-3: rgba(0, 0, 0, .1);--weui-FG-4: rgba(0, 0, 0, .15);--weui-FG-5: rgba(0, 0, 0, .05);--weui-RED: #fa5151;--weui-REDORANGE: #ff6146;--weui-ORANGE: #fa9d3b;--weui-YELLOW: #ffc300;--weui-GREEN: #91d300;--weui-LIGHTGREEN: #95ec69;--weui-BRAND: #07c160;--weui-BLUE: #10aeff;--weui-INDIGO: #1485ee;--weui-PURPLE: #6467f0;--weui-WHITE: #fff;--weui-LINK: #576b95;--weui-TEXTGREEN: #06ae56;--weui-FG: #000;--weui-BG: #fff;--weui-TAG-TEXT-RED: rgba(250, 81, 81, .6);--weui-TAG-BACKGROUND-RED: rgba(250, 81, 81, .1);--weui-TAG-TEXT-ORANGE: #fa9d3b;--weui-TAG-BACKGROUND-ORANGE: rgba(250, 157, 59, .1);--weui-TAG-TEXT-GREEN: #06ae56;--weui-TAG-BACKGROUND-GREEN: rgba(6, 174, 86, .1);--weui-TAG-TEXT-BLUE: #10aeff;--weui-TAG-BACKGROUND-BLUE: rgba(16, 174, 255, .1);--weui-TAG-TEXT-BLACK: rgba(0, 0, 0, .5);--weui-TAG-BACKGROUND-BLACK: rgba(0, 0, 0, .05)}body,.wx-root{--weui-BG-6: rgba(0, 0, 0, .05);--weui-ACTIVE-MASK: rgba(0, 0, 0, .05)}@media screen and (min-width:1024px){body:not(.pages_skin_pc){background:#191919;background:var(--weui-BG-2)}}@media(prefers-color-scheme:dark){body:not([data-weui-theme=light]).my_comment_empty_data{background-color:#111}}.wx-root,body{--weui-BTN-ACTIVE-MASK: rgba(0, 0, 0, .1)}.wx-root,body{--weui-BTN-DEFAULT-ACTIVE-BG: #e6e6e6}.wx-root,body{--weui-DIALOG-LINE-COLOR: rgba(0, 0, 0, .1)}.wx-root,body{--weui-BG-COLOR-ACTIVE: #ececec}@media(prefers-color-scheme:dark){.wx-root:not([data-weui-theme=light]),body:not([data-weui-theme=light]){--appmsgExtra-BG: #121212}}@media(prefers-color-scheme:dark){.wx-root:not([data-weui-theme=light]),body:not([data-weui-theme=light]){--weui-ORANGERED: #ff6146;--weui-BG-0: #111;--weui-BG-1: #1e1e1e;--weui-BG-2: #191919;--weui-BG-3: #202020;--weui-BG-4: #404040;--weui-BG-5: #2c2c2c;--weui-FG-0: rgba(255, 255, 255, .8);--weui-FG-HALF: rgba(255, 255, 255, .6);--weui-FG-1: rgba(255, 255, 255, .5);--weui-FG-2: rgba(255, 255, 255, .3);--weui-FG-3: rgba(255, 255, 255, .1);--weui-FG-4: rgba(255, 255, 255, .15);--weui-FG-5: rgba(255, 255, 255, .1);--weui-RED: #fa5151;--weui-REDORANGE: #ff6146;--weui-ORANGE: #c87d2f;--weui-YELLOW: #cc9c00;--weui-GREEN: #74a800;--weui-LIGHTGREEN: #3eb575;--weui-BRAND: #07c160;--weui-BLUE: #10aeff;--weui-INDIGO: #1196ff;--weui-PURPLE: #8183ff;--weui-WHITE: rgba(255, 255, 255, .8);--weui-LINK: #7d90a9;--weui-TEXTGREEN: #259c5c;--weui-FG: #fff;--weui-BG: #000;--weui-TAG-TEXT-RED: rgba(250, 81, 81, .6);--weui-TAG-BACKGROUND-RED: rgba(250, 81, 81, .1);--weui-TAG-TEXT-ORANGE: rgba(250, 157, 59, .6);--weui-TAG-BACKGROUND-ORANGE: rgba(250, 157, 59, .1);--weui-TAG-TEXT-GREEN: rgba(6, 174, 86, .6);--weui-TAG-BACKGROUND-GREEN: rgba(6, 174, 86, .1);--weui-TAG-TEXT-BLUE: rgba(16, 174, 255, .6);--weui-TAG-BACKGROUND-BLUE: rgba(16, 174, 255, .1);--weui-TAG-TEXT-BLACK: rgba(255, 255, 255, .5);--weui-TAG-BACKGROUND-BLACK: rgba(255, 255, 255, .05)}}@media(prefers-color-scheme:dark){.wx-root:not([data-weui-theme=light]),body:not([data-weui-theme=light]){--weui-BTN-ACTIVE-MASK: rgba(255, 255, 255, .1)}}@media(prefers-color-scheme:dark){.wx-root:not([data-weui-theme=light]),body:not([data-weui-theme=light]){--weui-BTN-DEFAULT-ACTIVE-BG: rgba(255, 255, 255, .126)}}@media(prefers-color-scheme:dark){.wx-root:not([data-weui-theme=light]),body:not([data-weui-theme=light]){--weui-DIALOG-LINE-COLOR: rgba(255, 255, 255, .1)}}@media(prefers-color-scheme:dark){.wx-root:not([data-weui-theme=light]),body:not([data-weui-theme=light]){--APPMSGCARD-BG: #1E1E1E}}@media(prefers-color-scheme:dark){.wx-root:not([data-weui-theme=light]),body:not([data-weui-theme=light]){--APPMSGCARD-LINE-BG: rgba(255, 255, 255, .07)}}@media(prefers-color-scheme:dark){.wx-root:not([data-weui-theme=light]),body:not([data-weui-theme=light]){--weui-BG-COLOR-ACTIVE: #373737}}@media(prefers-color-scheme:dark){.wx-root:not([data-weui-theme=light]),body:not([data-weui-theme=light]){--weui-BG-6: rgba(255, 255, 255, .1);--weui-ACTIVE-MASK: rgba(255, 255, 255, .1)}}@media(prefers-color-scheme:dark){.wx-root:not([data-weui-theme=light]),body:not([data-weui-theme=light]){--discussInput-BG: rgba(255, 255, 255, .03)}}@media(prefers-color-scheme:dark){.wx-root:not([data-weui-theme=light]),body:not([data-weui-theme=light]){--nickName-FG: #959595}}.rich_media_content p{clear:both;min-height:1em}td p{margin:0;padding:0}@media(prefers-color-scheme:dark){body:not([data-weui-theme=light]) .rich_media_content img:not(.wx_img_placeholder){filter:brightness(.8)}}*{margin:0;padding:0}.rich_media_content *{max-width:100%!important;box-sizing:border-box!important;-webkit-box-sizing:border-box!important;word-wrap:break-word!important}
PHP 开发中的.TPL 文件 - 知乎
PHP 开发中的.TPL 文件 - 知乎首发于体育数字化运营管理切换模式写文章登录/注册PHP 开发中的.TPL 文件王双忠数字体育研究人员/原上海体职院兼职教授.tpl文件是一个包含 PHP 和/或 HTML 源代码的纯文本文件,通常具有 PHP 驱动的网站设计模板的功能。 TPL文件被大多数支持PHP的网络服务器特殊处理。.tpl扩展名经常被用来表示PHP模板,特别是在使用某种内容管理系统(CMS)时。PHP(PHP HTML Preprocessor)是一种非常流行的解释型服务器端编程语言和运行时框架,在全球的网络服务器上广泛使用。[1].TPL 文件中的模板语言由HTML和简单的Phorum模板语句组成。这些语句支持四种数据类型,可能是整数、字符串、PHP常量或模板变量。由于模板语言以纯文本形式存储在.TPL 个文件中,因此可以使用任何文本编辑器打开和编辑.TPL 文件。Phorum通常使用.TPL 文件来存储模板代码,但是如果将.PHP 文件放在模板目录中并使用适当的基名命名,则也可以使用.PHP 文件。例如,如果Phorum显示页脚模板,它首先搜索页脚.php如果文件不存在,则会查找页脚.tpl。如果用户不想使用.TPL 模板所需的Phorum模板语言,则PHP文件的包含允许用户仅将PHP/HTML代码用于模板。[2]如果自己的计算机上没有与TPL文件关联的程序,则该文件将无法打开。 要打开文件,可下载与TPL文件相关的最流行程序如 Document Template,Access Workflow Designer或SignIQ Template等。Windows、 Mac和 Linux 操作系统可用于查看 TPL 文件。Smarty是一个PHP的模板引擎,提供让程序逻辑与页面显示(HTML/CSS)代码分离的功能。 也就是PHP代码是程序逻辑,与页面显示分开。Smarty在应用程序中,当作V层(视图层)的组件来使用, Smarty可以很轻易连接到其他的视图引擎中。 TPL 文件用写字板就可以打开并编辑,也可以用编辑器进行编辑。参考^https://www.filetypeadvisor.com/zh-cn/extension/tpl^https://www.dounaite.com/article/6284616cac359fc9133ae986.html发布于 2022-08-13 10:14文件管理视频文件文件整理赞同 1添加评论分享喜欢收藏申请转载文章被以下专栏收录体育数字化运营管理体育数字化运
百度地图
百度地图
任务并行库 (TPL) - .NET | Microsoft Learn
任务并行库 (TPL) - .NET | Microsoft Learn
跳转至主内容
此浏览器不再受支持。
请升级到 Microsoft Edge 以使用最新的功能、安全更新和技术支持。
下载 Microsoft Edge
有关 Internet Explorer 和 Microsoft Edge 的详细信息
目录
退出焦点模式
使用英语阅读
保存
目录
使用英语阅读
保存
打印
电子邮件
目录
任务并行库 (TPL)
项目
05/10/2023
13 个参与者
反馈
本文内容
任务并行库 (TPL) 是 System.Threading 和 System.Threading.Tasks 空间中的一组公共类型和 API。 TPL 的目的是通过简化将并行和并发添加到应用程序的过程来提高开发人员的工作效率。 TPL 动态缩放并发的程度以最有效地使用所有可用的处理器。 此外,TPL 还处理工作分区、ThreadPool 上的线程调度、取消支持、状态管理以及其他低级别的细节操作。 通过使用 TPL,你可以在将精力集中于程序要完成的工作,同时最大程度地提高代码的性能。
在 .NET Framework 4 中,首选 TPL 编写多线程代码和并行代码。 但是,并不是所有代码都适合并行化。 例如,如果某个循环在每次迭代时只执行少量工作,或它在很多次迭代时都不运行,那么并行化的开销可能导致代码运行更慢。 此外,像任何多线程代码一样,并行化会增加程序执行的复杂性。 尽管 TPL 简化了多线程方案,但我们建议你对线程处理概念(例如,锁、死锁和争用条件)进行基本的了解,以便能够有效地使用 TPL。
相关文章
Title
描述
数据并行
描述如何创建并行的 for 和 foreach 循环(在 Visual Basic 中为 For 和 For Each)。
基于任务的异步编程
描述如何通过使用 Parallel.Invoke 隐式创建和运行任务,或通过直接使用 Task 对象显式创建和运行任务。
数据流
描述如何使用 TPL 数据流库中的数据流组件处理多项运算。 这些运算必须彼此通信,并在数据可用时处理数据。
数据和任务并行的潜在问题
描述一些常见缺陷以及如何避免它们。
并行 LINQ (PLINQ)
描述如何使用 LINQ 查询实现数据并行化。
并行编程
.NET 并行编程的顶级节点。
另请参阅
使用 .NET Core 和 .NET Standard 并行编程的示例
在 GitHub 上与我们协作
可以在 GitHub 上找到此内容的源,还可以在其中创建和查看问题和拉取请求。 有关详细信息,请参阅参与者指南。
.NET
提出文档问题
提供产品反馈
反馈
Coming soon: Throughout 2024 we will be phasing out GitHub Issues as the feedback mechanism for content and replacing it with a new feedback system. For more information see: https://aka.ms/ContentUserFeedback.
提交和查看相关反馈
此产品
此页面
查看所有页面反馈
其他资源
加州消费者隐私法案 (CCPA) 禁用图标
你的隐私选择
主题
亮
暗
高对比度
早期版本
博客
参与
隐私
使用条款
商标
© Microsoft 2024
其他资源
本文内容
加州消费者隐私法案 (CCPA) 禁用图标
你的隐私选择
主题
亮
暗
高对比度
早期版本
博客
参与
隐私
使用条款
商标
© Microsoft 2024
php tpl模板引擎定义与使用示例-腾讯云开发者社区-腾讯云
tpl模板引擎定义与使用示例-腾讯云开发者社区-腾讯云砸漏php tpl模板引擎定义与使用示例关注作者腾讯云开发者社区文档建议反馈控制台首页学习活动专区工具TVP最新优惠活动文章/答案/技术大牛搜索搜索关闭发布登录/注册首页学习活动专区工具TVP最新优惠活动返回腾讯云官网砸漏首页学习活动专区工具TVP最新优惠活动返回腾讯云官网社区首页 >专栏 >php tpl模板引擎定义与使用示例php tpl模板引擎定义与使用示例砸漏关注发布于 2020-10-20 15:53:282.3K0发布于 2020-10-20 15:53:28举报文章被收录于专栏:恩蓝脚本恩蓝脚本本文实例讲述了php tpl模板引擎定义与使用。分享给大家供大家参考,具体如下:tpl.phpnamespace tpl;
/**
* Class Tpl
*/
class Tpl
{
protected $view_dir;//模板文件
protected $cache_dir;//缓存文件
protected $lifetime;//过期时间
protected $vars = [];//存放显示变量的数组
/**
* Tpl constructor.
* @param string $view_dir
* @param string $cache_dir
* @param string $lifetime
*/
public function __construct($view_dir='', $cache_dir='', $lifetime='')
{
//如果模板文件不为空,则设置,为空则为默认值
if (!empty($view_dir)) {
if ($this- check_dir($view_dir)) {
$this- view_dir = $view_dir;
}
}
//如果缓存文件不为空,则设置,为空时为默认值
if (!empty($cache_dir)) {
if ($this- check_dir($cache_dir)) {
$this- cache_dir = $cache_dir;
}
}
//如果过期时间不为空,则设置,为空时为默认值
if (!empty($lifetime)) {
$this- lifetime = $lifetime;
}
}
/**
* 对外公开的方法
* @param string $name
* @param string $value
*/
public function assign($name, $value)
{
$this- vars[$name] = $value;//将传入的参数以键值对存入数组中
}
/**
* 测试文件
* @param $dir_path
* @return bool
*/
protected function check_dir($dir_path)
{
//如果文件不存在或不是文件夹,则创建
if (!file_exists($dir_path) || !is_dir($dir_path)) {
return mkdir($dir_path, 0777, true);
}
//如果文件不可读或不可写,则设置模式
if (!is_writable($dir_path) || !is_readable($dir_path)) {
return chmod($dir_path, 0777);
}
return true;
}
/**
* 展示方法
* @param $view_name
* @param bool $isInclude
* @param null $uri
*/
public function display($view_name, $isInclude=true, $uri=null)
{
//通过传入的文件名,得到模板文件路径
$view_path = rtrim($this- view_dir, '/') . '/' . $view_name;
//判断路径是否存在
if (!file_exists($view_path)) {
die('文件不存在');
}
//通过传入的文件名得到缓存文件名
$cache_name = md5($view_name . $uri) . '.php';
//缓过缓存文件名得到缓存路径
$cache_path = rtrim($this- cache_dir, '/') . '/' .$cache_name;
//判断缓存文件是否存在,如果不存在,重新生成
if (!file_exists($cache_path)) {
$php = $this- compile($view_path);//解析模板文件
file_put_contents($cache_path, $php);//缓存文件重新生成
} else {
//如果缓存文件存在,判断是否过期,判断模板文件是否被修改
$is_time_out = (filectime($cache_path) + $this- lifetime) time() ? false : true;
$is_change = filemtime($view_path) filemtime($cache_path) ? true : false;
//如果缓存文件过期或模板文件被修改,重新生成缓存文件
if ($is_time_out || $is_change) {
$php = $this- compile($view_path);
file_put_contents($cache_path, $php);
}
}
if ($isInclude) {
extract($this- vars);//解析传入变量的数组
include $cache_path;//展示缓存
}
}
/**
* 正则解析模板文件
* @param string $file_name
* @return mixed|string
*/
protected function compile($file_name)
{
$html = file_get_contents($file_name);//获取模板文件
//正则转换数组
$array = [
'{$%%}' = '=$\1? ',
'{foreach %%}' = '
'{/foreach}' = '
'{include %%}' = '',
'{if %%}' = '
'{/if}' = '
'{for %%}' = '
'{/for}' = '
'{switch %%}' = '
'{/switch}' = '
];
//遍历数组,生成正则表达式
foreach ($array AS $key= $value) {
//正则表达式,
$pattern = '#' . str_replace('%%', '(.+?)' , preg_quote($key, '#')) . '#';
if (strstr($pattern, 'include')) {
$html = preg_replace_callback($pattern, [$this, 'parseInclude'], $html);
} else {
$html = preg_replace($pattern, $value, $html);
}
}
return $html;
}
/**
* 处理include表达式
* @param array $data
* @return string
*/
protected function parseInclude($data)
{
$file_name = trim($data[1], '\'"');
$this- display($file_name, false);
$cache_name = md5($file_name) . '.php';
$cache_path = rtrim($this- cache_dir, '/') . '/' . $cache_name;
return '
}
}复制user_tpl,,,,从数据库中取值,作为参数传到模板文件,再解析模板文件
include './sql/pdo.sql.php';
include 'tpl.php';
$tpl = new tpl\Tpl('./view/', './cache/', 3000);
$link = new pdo_sql();
$dat = ['menu_name', 'menu_url'];
$res = $link- table('blog_menu')- field($dat)- order('id ASC')- select();
$tpl- assign('menu', $res);
$tpl- display('index.html');复制更多关于PHP相关内容感兴趣的读者可查看本站专题:《PHP模板技术总结》、《PHP基于pdo操作数据库技巧总结》、《PHP运算与运算符用法总结》、《PHP网络编程技巧总结》、《PHP基本语法入门教程》、《php面向对象程序设计入门教程》、《php字符串(string)用法总结》、《php+mysql数据库操作入门教程》及《php常见数据库操作技巧汇总》希望本文所述对大家PHP程序设计有所帮助。本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。 原始发表:2020-09-11
,如有侵权请联系 cloudcommunity@tencent.com 删除前往查看php数据库sql本文分享自 作者个人站点/博客 前往查看如有侵权,请联系 cloudcommunity@tencent.com 删除。本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!php数据库sql评论登录后参与评论0 条评论热度最新登录 后参与评论推荐阅读LV.关注文章0获赞0相关产品与服务数据库云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!产品介绍2024新春采购节领券社区专栏文章阅读清单互动问答技术沙龙技术视频团队主页腾讯云TI平台活动自媒体分享计划邀请作者入驻自荐上首页技术竞赛资源技术周刊社区标签开发者手册开发者实验室关于社区规范免责声明联系我们友情链接腾讯云开发者扫码关注腾讯云开发者领取腾讯云代金券热门产品域名注册云服务器区块链服务消息队列网络加速云数据库域名解析云存储视频直播热门推荐人脸识别腾讯会议企业云CDN加速视频通话图像分析MySQL 数据库SSL 证书语音识别更多推荐数据安全负载均衡短信文字识别云点播商标注册小程序开发网站监控数据迁移Copyright © 2013 - 2024 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有 深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档Copyright © 2013 - 2024 Tencent Cloud.All Rights Reserved. 腾讯云 版权所有登录 后参与评论00
TPL 和传统 .NET 异步编程 - .NET | Microsoft Learn
TPL 和传统 .NET 异步编程 - .NET | Microsoft Learn
跳转至主内容
此浏览器不再受支持。
请升级到 Microsoft Edge 以使用最新的功能、安全更新和技术支持。
下载 Microsoft Edge
有关 Internet Explorer 和 Microsoft Edge 的详细信息
目录
退出焦点模式
语言
使用英语阅读
保存
目录
使用英语阅读
保存
打印
电子邮件
目录
TPL 和传统 .NET 异步编程
项目
04/06/2023
13 个参与者
反馈
本文内容
.NET 提供了以下两种标准模式,用于执行 I/O 密集型和计算密集型异步操作:
异步编程模型 (APM),在该模式中异步操作由一对 Begin/End 方法表示。 例如 FileStream.BeginRead 和 Stream.EndRead。
基于事件的异步模式 (EAP),在该模式中异步操作由名为
任务并行库 (TPL) 可采用各种方法与任一异步模式协同使用。 可将 APM 和 EAP 操作作为 Task 对象向库使用者公开,也可以公开 APM 模式但用 Task 对象在内部实现它们。 在这两种情况下,可使用 Task 对象简化代码以及利用以下有用的功能:
在任务开始后随时以任务延续形式注册回调。
使用 ContinueWhenAll 和 ContinueWhenAny 方法,或者 WaitAll 和 WaitAny 方法并列为响应 Begin_ 方法而执行的多个操作。
封装同一 Task 对象中的异步 I/O 密集型和计算密集型操作。
监视 Task 对象的状态。
使用 TaskCompletionSource
在 Task 中包装 APM 操作
System.Threading.Tasks.TaskFactory 和 System.Threading.Tasks.TaskFactory
对于具有返回值(在 Visual Basic 中为 Function)的 End 方法的对,使用 TaskFactory
在极少情况下,如果 Begin 方法具有三个以上参数或包含 ref 或 out 参数,则提供仅封装 End 方法的其他 FromAsync 重载。
下面的示例显示了匹配 FileStream.BeginRead 和 FileStream.EndRead 方法的 FromAsync 重载的签名。
public Task
Func
Func
TArg1 arg1, // the byte[] buffer
TArg2 arg2, // the offset in arg1 at which to start writing data
TArg3 arg3, // the maximum number of bytes to read
object state // optional state information
)
Public Function FromAsync(Of TArg1, TArg2, TArg3)(
ByVal beginMethod As Func(Of TArg1, TArg2, TArg3, AsyncCallback, Object, IAsyncResult),
ByVal endMethod As Func(Of IAsyncResult, TResult),
ByVal dataBuffer As TArg1,
ByVal byteOffsetToStartAt As TArg2,
ByVal maxBytesToRead As TArg3,
ByVal stateInfo As Object)
此重载采用三个输入参数,如下所示。 第一个参数是匹配 FileStream.BeginRead 方法签名的 Func
存储文件数据的缓冲区。
开始写入数据的缓冲区的偏移量。
要从文件中读取的最大数据量。
存储要传递至回调的用户定义状态数据的可选对象。
使用 ContinueWith 执行回调功能
如果需要访问文件中的数据,而不仅仅访问字节数,则 FromAsync 方法不能满足此操作。 请改用 Task,其 Result 属性包含文件数据。 可以通过向原始任务添加延续来实现这种操作。 延续执行通常由 AsyncCallback 委托执行的任务。 先前任务完成且填充了数据缓冲区后调用此操作。 (FileStream 对象应在返回前关闭。)
下面的示例演示如何返回封装 FileStream 类的 BeginRead/EndRead 对的 Task。
const int MAX_FILE_SIZE = 14000000;
public static Task
{
FileInfo fi = new FileInfo(path);
byte[] data = null;
data = new byte[fi.Length];
FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, data.Length, true);
//Task
Task
fs.BeginRead, fs.EndRead, data, 0, data.Length, null);
// It is possible to do other work here while waiting
// for the antecedent task to complete.
// ...
// Add the continuation, which returns a Task
return task.ContinueWith((antecedent) =>
{
fs.Close();
// Result = "number of bytes read" (if we need it.)
if (antecedent.Result < 100)
{
return "Data is too small to bother with.";
}
else
{
// If we did not receive the entire file, the end of the
// data buffer will contain garbage.
if (antecedent.Result < data.Length)
Array.Resize(ref data, antecedent.Result);
// Will be returned in the Result property of the Task
// at some future point after the asynchronous file I/O operation completes.
return new UTF8Encoding().GetString(data);
}
});
}
Const MAX_FILE_SIZE As Integer = 14000000
Shared Function GetFileStringAsync(ByVal path As String) As Task(Of String)
Dim fi As New FileInfo(path)
Dim data(fi.Length - 1) As Byte
Dim fs As FileStream = New FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, data.Length, True)
' Task(Of Integer) returns the number of bytes read
Dim myTask As Task(Of Integer) = Task(Of Integer).Factory.FromAsync(
AddressOf fs.BeginRead, AddressOf fs.EndRead, data, 0, data.Length, Nothing)
' It is possible to do other work here while waiting
' for the antecedent task to complete.
' ...
' Add the continuation, which returns a Task
Return myTask.ContinueWith(Function(antecedent)
fs.Close()
If (antecedent.Result < 100) Then
Return "Data is too small to bother with."
End If
' If we did not receive the entire file, the end of the
' data buffer will contain garbage.
If (antecedent.Result < data.Length) Then
Array.Resize(data, antecedent.Result)
End If
' Will be returned in the Result property of the Task
' at some future point after the asynchronous file I/O operation completes.
Return New UTF8Encoding().GetString(data)
End Function)
End Function
然后可调用此方法,如下所示。
Task
// Do some other work:
// ...
try
{
Console.WriteLine(t.Result.Substring(0, 500));
}
catch (AggregateException ae)
{
Console.WriteLine(ae.InnerException.Message);
}
Dim myTask As Task(Of String) = GetFileStringAsync(path)
' Do some other work
' ...
Try
Console.WriteLine(myTask.Result.Substring(0, 500))
Catch ex As AggregateException
Console.WriteLine(ex.InnerException.Message)
End Try
提供自定义状态数据
在通常的 IAsyncResult 操作中,如果AsyncCallback 委托需要一些自定义状态数据,则必须通过 Begin 方法中的最后一个参数将它传入,以便可将数据打包到最终要传递至回调方法的 IAsyncResult 对象中。 当使用 FromAsync 方法时,通常无需此操作。 如果延续知道自定义数据,可直接在延续委托中捕获它。 下面的示例与以前的示例类似,但延续检查此延续的用户委托可直接访问的自定义状态数据,而不是检查历史任务的 Result 属性。
public Task
{
FileInfo fi = new FileInfo(path);
byte[] data = new byte[fi.Length];
MyCustomState state = GetCustomState();
FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, data.Length, true);
// We still pass null for the last parameter because
// the state variable is visible to the continuation delegate.
Task
fs.BeginRead, fs.EndRead, data, 0, data.Length, null);
return task.ContinueWith((antecedent) =>
{
// It is safe to close the filestream now.
fs.Close();
// Capture custom state data directly in the user delegate.
// No need to pass it through the FromAsync method.
if (state.StateData.Contains("New York, New York"))
{
return "Start spreading the news!";
}
else
{
// If we did not receive the entire file, the end of the
// data buffer will contain garbage.
if (antecedent.Result < data.Length)
Array.Resize(ref data, antecedent.Result);
// Will be returned in the Result property of the Task
// at some future point after the asynchronous file I/O operation completes.
return new UTF8Encoding().GetString(data);
}
});
}
Public Function GetFileStringAsync2(ByVal path As String) As Task(Of String)
Dim fi = New FileInfo(path)
Dim data(fi.Length - 1) As Byte
Dim state As New MyCustomState()
Dim fs As New FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, data.Length, True)
' We still pass null for the last parameter because
' the state variable is visible to the continuation delegate.
Dim myTask As Task(Of Integer) = Task(Of Integer).Factory.FromAsync(
AddressOf fs.BeginRead, AddressOf fs.EndRead, data, 0, data.Length, Nothing)
Return myTask.ContinueWith(Function(antecedent)
fs.Close()
' Capture custom state data directly in the user delegate.
' No need to pass it through the FromAsync method.
If (state.StateData.Contains("New York, New York")) Then
Return "Start spreading the news!"
End If
' If we did not receive the entire file, the end of the
' data buffer will contain garbage.
If (antecedent.Result < data.Length) Then
Array.Resize(data, antecedent.Result)
End If
'/ Will be returned in the Result property of the Task
'/ at some future point after the asynchronous file I/O operation completes.
Return New UTF8Encoding().GetString(data)
End Function)
End Function
同步多个 FromAsync 任务
当结合使用 FromAsync 方法时,静态 ContinueWhenAll 和 ContinueWhenAny 方法具有更大的灵活性。 下面的示例显示如何启动多个异步 I/O 操作,然后等待所有这些操作都完成后再执行延续。
public Task
{
FileStream fs;
Task
byte[] fileData = null;
for (int i = 0; i < filesToRead.Length; i++)
{
fileData = new byte[0x1000];
fs = new FileStream(filesToRead[i], FileMode.Open, FileAccess.Read, FileShare.Read, fileData.Length, true);
// By adding the continuation here, the
// Result of each task will be a string.
tasks[i] = Task
fs.BeginRead, fs.EndRead, fileData, 0, fileData.Length, null)
.ContinueWith((antecedent) =>
{
fs.Close();
// If we did not receive the entire file, the end of the
// data buffer will contain garbage.
if (antecedent.Result < fileData.Length)
Array.Resize(ref fileData, antecedent.Result);
// Will be returned in the Result property of the Task
// at some future point after the asynchronous file I/O operation completes.
return new UTF8Encoding().GetString(fileData);
});
}
// Wait for all tasks to complete.
return Task
{
// Propagate all exceptions and mark all faulted tasks as observed.
Task.WaitAll(data);
// Combine the results from all tasks.
StringBuilder sb = new StringBuilder();
foreach (var t in data)
{
sb.Append(t.Result);
}
// Final result to be returned eventually on the calling thread.
return sb.ToString();
});
}
Public Function GetMultiFileData(ByVal filesToRead As String()) As Task(Of String)
Dim fs As FileStream
Dim tasks(filesToRead.Length - 1) As Task(Of String)
Dim fileData() As Byte = Nothing
For i As Integer = 0 To filesToRead.Length
fileData(&H1000) = New Byte()
fs = New FileStream(filesToRead(i), FileMode.Open, FileAccess.Read, FileShare.Read, fileData.Length, True)
' By adding the continuation here, the
' Result of each task will be a string.
tasks(i) = Task(Of Integer).Factory.FromAsync(AddressOf fs.BeginRead,
AddressOf fs.EndRead,
fileData,
0,
fileData.Length,
Nothing).
ContinueWith(Function(antecedent)
fs.Close()
'If we did not receive the entire file, the end of the
' data buffer will contain garbage.
If (antecedent.Result < fileData.Length) Then
ReDim Preserve fileData(antecedent.Result)
End If
'Will be returned in the Result property of the Task
' at some future point after the asynchronous file I/O operation completes.
Return New UTF8Encoding().GetString(fileData)
End Function)
Next
Return Task(Of String).Factory.ContinueWhenAll(tasks, Function(data)
' Propagate all exceptions and mark all faulted tasks as observed.
Task.WaitAll(data)
' Combine the results from all tasks.
Dim sb As New StringBuilder()
For Each t As Task(Of String) In data
sb.Append(t.Result)
Next
' Final result to be returned eventually on the calling thread.
Return sb.ToString()
End Function)
End Function
仅用于 End 方法的 FromAsync 任务
在极少情况下,如果 Begin 方法需要三个以上的输入参数,或具有 ref 或 out 参数,可以使用仅表示 End 方法的 FromAsync 重载,例如,TaskFactory
static Task
{
IAsyncResult ar = DoSomethingAsynchronously();
Task
{
return (string)ar.AsyncState;
});
return t;
}
Shared Function ReturnTaskFromAsyncResult() As Task(Of String)
Dim ar As IAsyncResult = DoSomethingAsynchronously()
Dim t As Task(Of String) = Task(Of String).Factory.FromAsync(ar, Function(res) CStr(res.AsyncState))
Return t
End Function
开始和取消 FromAsync 任务
FromAsync 方法返回的任务具有 WaitingForActivation 状态,并在创建任务后在某个时刻由操作系统启动。 如果尝试调用此类任务上的“启动”,将引发异常。
无法取消 FromAsync 任务,因为基础 .NET API 目前不支持取消正在进行中的文件或网络 I/O。 可以将取消功能添加到封装 FromAsync 调用的方法中,但只能在调用 FromAsync 之前或在调用完成之后响应取消(例如,在延续任务中)。
一些支持 EAP 的类(如 WebClient)不支持取消,但可以通过使用取消标记集成该本机取消功能。
将复杂的 EAP 操作公开为任务
TPL 不提供任何专用于以 FromAsync 系列方法包装 IAsyncResult 模式相同的方式封装基于事件的异步操作的方法。 但是,TPL 会提供 System.Threading.Tasks.TaskCompletionSource
下面的示例显示如何使用 TaskCompletionSource
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
public class SimpleWebExample
{
public Task
CancellationToken token)
{
TaskCompletionSource
WebClient[] webClients = new WebClient[urls.Length];
object m_lock = new object();
int count = 0;
List
// If the user cancels the CancellationToken, then we can use the
// WebClient's ability to cancel its own async operations.
token.Register(() =>
{
foreach (var wc in webClients)
{
if (wc != null)
wc.CancelAsync();
}
});
for (int i = 0; i < urls.Length; i++)
{
webClients[i] = new WebClient();
#region callback
// Specify the callback for the DownloadStringCompleted
// event that will be raised by this WebClient instance.
webClients[i].DownloadStringCompleted += (obj, args) =>
{
// Argument validation and exception handling omitted for brevity.
// Split the string into an array of words,
// then count the number of elements that match
// the search term.
string[] words = args.Result.Split(' ');
string NAME = name.ToUpper();
int nameCount = (from word in words.AsParallel()
where word.ToUpper().Contains(NAME)
select word)
.Count();
// Associate the results with the url, and add new string to the array that
// the underlying Task object will return in its Result property.
lock (m_lock)
{
results.Add(String.Format("{0} has {1} instances of {2}", args.UserState, nameCount, name));
// If this is the last async operation to complete,
// then set the Result property on the underlying Task.
count++;
if (count == urls.Length)
{
tcs.TrySetResult(results.ToArray());
}
}
};
#endregion
// Call DownloadStringAsync for each URL.
Uri address = null;
address = new Uri(urls[i]);
webClients[i].DownloadStringAsync(address, address);
} // end for
// Return the underlying Task. The client code
// waits on the Result property, and handles exceptions
// in the try-catch block there.
return tcs.Task;
}
}
Imports System.Collections.Generic
Imports System.Net
Imports System.Threading
Imports System.Threading.Tasks
Public Class SimpleWebExample
Dim tcs As New TaskCompletionSource(Of String())
Dim token As CancellationToken
Dim results As New List(Of String)
Dim m_lock As New Object()
Dim count As Integer
Dim addresses() As String
Dim nameToSearch As String
Public Function GetWordCountsSimplified(ByVal urls() As String, ByVal str As String,
ByVal token As CancellationToken) As Task(Of String())
addresses = urls
nameToSearch = str
Dim webClients(urls.Length - 1) As WebClient
' If the user cancels the CancellationToken, then we can use the
' WebClient's ability to cancel its own async operations.
token.Register(Sub()
For Each wc As WebClient In webClients
If wc IsNot Nothing Then
wc.CancelAsync()
End If
Next
End Sub)
For i As Integer = 0 To urls.Length - 1
webClients(i) = New WebClient()
' Specify the callback for the DownloadStringCompleted
' event that will be raised by this WebClient instance.
AddHandler webClients(i).DownloadStringCompleted, AddressOf WebEventHandler
Dim address As New Uri(urls(i))
' Pass the address, and also use it for the userToken
' to identify the page when the delegate is invoked.
webClients(i).DownloadStringAsync(address, address)
Next
' Return the underlying Task. The client code
' waits on the Result property, and handles exceptions
' in the try-catch block there.
Return tcs.Task
End Function
Public Sub WebEventHandler(ByVal sender As Object, ByVal args As DownloadStringCompletedEventArgs)
If args.Cancelled = True Then
tcs.TrySetCanceled()
Return
ElseIf args.Error IsNot Nothing Then
tcs.TrySetException(args.Error)
Return
Else
' Split the string into an array of words,
' then count the number of elements that match
' the search term.
Dim words() As String = args.Result.Split(" "c)
Dim name As String = nameToSearch.ToUpper()
Dim nameCount = (From word In words.AsParallel()
Where word.ToUpper().Contains(name)
Select word).Count()
' Associate the results with the url, and add new string to the array that
' the underlying Task object will return in its Result property.
SyncLock (m_lock)
results.Add(String.Format("{0} has {1} instances of {2}", args.UserState, nameCount, nameToSearch))
count = count + 1
If (count = addresses.Length) Then
tcs.TrySetResult(results.ToArray())
End If
End SyncLock
End If
End Sub
End Class
有关包括其他异常处理且展示了如何通过客户端代码调用方法的更完整示例,请参阅如何:在任务中包装 EAP 模式。
请记住,通过 TaskCompletionSource
使用任务实现 APM 模式
在某些情况下,可能需要通过使用 API 中 begin/end 方法对直接公开 IAsyncResult 模式。 例如,可能想要与现有的 API 保持一致,或者可能具有需要这种模式的自动化工具。 在这种情况下,可使用 Task 对象来简化在内部实现 APM 模式的方式。
下面的示例显示如何使用任务实现长时间运行计算密集型方法的 APM begin/end 方法对。
class Calculator
{
public IAsyncResult BeginCalculate(int decimalPlaces, AsyncCallback ac, object state)
{
Console.WriteLine("Calling BeginCalculate on thread {0}", Thread.CurrentThread.ManagedThreadId);
Task
if (ac != null) f.ContinueWith((res) => ac(f));
return f;
}
public string Compute(int numPlaces)
{
Console.WriteLine("Calling compute on thread {0}", Thread.CurrentThread.ManagedThreadId);
// Simulating some heavy work.
Thread.SpinWait(500000000);
// Actual implementation left as exercise for the reader.
// Several examples are available on the Web.
return "3.14159265358979323846264338327950288";
}
public string EndCalculate(IAsyncResult ar)
{
Console.WriteLine("Calling EndCalculate on thread {0}", Thread.CurrentThread.ManagedThreadId);
return ((Task
}
}
public class CalculatorClient
{
static int decimalPlaces = 12;
public static void Main()
{
Calculator calc = new Calculator();
int places = 35;
AsyncCallback callBack = new AsyncCallback(PrintResult);
IAsyncResult ar = calc.BeginCalculate(places, callBack, calc);
// Do some work on this thread while the calculator is busy.
Console.WriteLine("Working...");
Thread.SpinWait(500000);
Console.ReadLine();
}
public static void PrintResult(IAsyncResult result)
{
Calculator c = (Calculator)result.AsyncState;
string piString = c.EndCalculate(result);
Console.WriteLine("Calling PrintResult on thread {0}; result = {1}",
Thread.CurrentThread.ManagedThreadId, piString);
}
}
Class Calculator
Public Function BeginCalculate(ByVal decimalPlaces As Integer, ByVal ac As AsyncCallback, ByVal state As Object) As IAsyncResult
Console.WriteLine("Calling BeginCalculate on thread {0}", Thread.CurrentThread.ManagedThreadId)
Dim myTask = Task(Of String).Factory.StartNew(Function(obj) Compute(decimalPlaces), state)
myTask.ContinueWith(Sub(antecedent) ac(myTask))
End Function
Private Function Compute(ByVal decimalPlaces As Integer)
Console.WriteLine("Calling compute on thread {0}", Thread.CurrentThread.ManagedThreadId)
' Simulating some heavy work.
Thread.SpinWait(500000000)
' Actual implementation left as exercise for the reader.
' Several examples are available on the Web.
Return "3.14159265358979323846264338327950288"
End Function
Public Function EndCalculate(ByVal ar As IAsyncResult) As String
Console.WriteLine("Calling EndCalculate on thread {0}", Thread.CurrentThread.ManagedThreadId)
Return CType(ar, Task(Of String)).Result
End Function
End Class
Class CalculatorClient
Shared decimalPlaces As Integer
Shared Sub Main()
Dim calc As New Calculator
Dim places As Integer = 35
Dim callback As New AsyncCallback(AddressOf PrintResult)
Dim ar As IAsyncResult = calc.BeginCalculate(places, callback, calc)
' Do some work on this thread while the calculator is busy.
Console.WriteLine("Working...")
Thread.SpinWait(500000)
Console.ReadLine()
End Sub
Public Shared Sub PrintResult(ByVal result As IAsyncResult)
Dim c As Calculator = CType(result.AsyncState, Calculator)
Dim piString As String = c.EndCalculate(result)
Console.WriteLine("Calling PrintResult on thread {0}; result = {1}",
Thread.CurrentThread.ManagedThreadId, piString)
End Sub
End Class
使用 StreamExtensions 示例代码
.NET Standard parallel extensions extras 存储库中的 StreamExtensions.cs 文件包含将 Task 对象用于异步文件和网络 I/O 的若干参考实现。
另请参阅
任务并行库 (TPL)
在 GitHub 上与我们协作
可以在 GitHub 上找到此内容的源,还可以在其中创建和查看问题和拉取请求。 有关详细信息,请参阅参与者指南。
.NET
提出文档问题
提供产品反馈
反馈
Coming soon: Throughout 2024 we will be phasing out GitHub Issues as the feedback mechanism for content and replacing it with a new feedback system. For more information see: https://aka.ms/ContentUserFeedback.
提交和查看相关反馈
此产品
此页面
查看所有页面反馈
其他资源
加州消费者隐私法案 (CCPA) 禁用图标
你的隐私选择
主题
亮
暗
高对比度
早期版本
博客
参与
隐私
使用条款
商标
© Microsoft 2024
其他资源
本文内容
加州消费者隐私法案 (CCPA) 禁用图标
你的隐私选择
主题
亮
暗
高对比度
早期版本
博客
参与
隐私
使用条款
商标
© Microsoft 2024
数据流(任务并行库) - .NET | Microsoft Learn
数据流(任务并行库) - .NET | Microsoft Learn
跳转至主内容
此浏览器不再受支持。
请升级到 Microsoft Edge 以使用最新的功能、安全更新和技术支持。
下载 Microsoft Edge
有关 Internet Explorer 和 Microsoft Edge 的详细信息
目录
退出焦点模式
语言
使用英语阅读
保存
目录
使用英语阅读
保存
打印
电子邮件
目录
数据流(任务并行库)
项目
06/05/2023
19 个参与者
反馈
本文内容
任务并行库 (TPL) 提供数据流组件,可帮助提高启用并发的应用程序的可靠性。 这些数据流组件统称为 TPL 数据流库。 这种数据流模型通过向粗粒度的数据流和管道任务提供进程内消息传递来促进基于角色的编程。 数据流组件基于 TPL 的类型和计划基础结构,并集成了 C#、Visual Basic 和 F# 语言的异步编程支持。 当您有必须相互异步沟通的多个操作或者想要在数据可用时对其处理时,这些数据流组件就非常有用。 例如,请考虑一个处理网络摄像机图像数据的应用程序。 通过使用数据流模型,当图像帧可用时,应用程序就可以处理它们。 如果应用程序增强图像帧(例如执行灯光修正或消除红眼),则可以创建数据流组件的管道。 管道的每个阶段可以使用更粗粒度的并行功能(例如 TPL 提供的功能)来转换图像。
本文档对 TPL 数据流库进行了概述。 它介绍编程模型,预定义的数据流块类型,以及如何配置数据流块来满足应用程序的特定要求。
注意
TPL 数据流库(System.Threading.Tasks.Dataflow 命名空间)不随 .NET 一起分发。 若要在 Visual Studio 中安装 System.Threading.Tasks.Dataflow 命名空间,请打开项目,选择“项目”菜单中的“管理 NuGet 包”,再在线搜索 System.Threading.Tasks.Dataflow 包。 或者,若要使用 .NET Core CLI 进行安装,请运行 dotnet add package System.Threading.Tasks.Dataflow。
编程模型
TPL 数据流库向具有高吞吐量和低滞后时间的占用大量 CPU 和 I/O 操作的应用程序的并行化和消息传递提供了基础。 它还能显式控制缓存数据的方式以及在系统中移动的方式。 为了更好地了解数据流编程模型,请考虑一个以异步方式从磁盘加载图像并创建复合图像的应用程序。 传统编程模型通常需要使用回调和同步对象(例如锁)来协调任务和访问共享数据。 通过使用数据流编程模型,您可以从磁盘读取时创建处理图像的数据流对象。 在数据流模型下,您可以声明当数据可用时的处理方式,以及数据之间的所有依赖项。 由于运行时管理数据之间的依赖项,因此通常可以避免这种要求来同步访问共享数据。 此外,因为运行时计划基于数据的异步到达,所以数据流可以通过有效管理基础线程提高响应能力和吞吐量。 有关在 Windows 窗体应用程序中使用数据流编程模型实现图像处理的示例,请参阅演练:在 Windows 窗体应用程序中使用数据流。
源和目标
TPL 数据流库包括数据流块,它是缓冲并处理数据的数据结构。 TPL 定义了三种数据流块:源块、目标块和传播器块。 源块作为数据源,可以读取。 目标块作为数据接收方,可以写入。 传播器块作为源块和目标块,可以读取和写入。 TPL 定义 System.Threading.Tasks.Dataflow.ISourceBlock
TPL 数据流库提供了多个预定义的数据流块类型,可以实现 ISourceBlock
连接块
可以连接数据流块来形成管道(这是数据流块的线性序列),或网络(这是数据流块的图形)。 管道是网络的一种形式。 在管道或网络中,当数据可用时源向目标异步传播数据。 ISourceBlock
有关连接数据流块以形成基本管道的示例,请参阅演练:创建数据流管道。 有关连接数据流块以形成更复杂网络的示例,请参阅演练:在 Windows 窗体应用程序中使用数据流。 有关在源向目标传递消息后从源取消目标链接的示例,请参阅如何:取消链接数据流块。
筛选
当您调用 ISourceBlock
重要
由于每个预定义源数据流块类型确保了消息是按照它们接收的顺序来传播的,因此每一条消息都必须在源块可以处理下一条消息之前从源块读取。 因此,当您使用筛选向一个源连接多个目标时,请确保至少一个目标块能够接收每一条消息。 否则,您的应用程序可能发生死锁。
消息传递
数据流编程模型与消息传递这一概念相关,其中程序的独立组件通过发送消息相互通信。 在应用程序组件之间传播消息的一种方法是,调用 Post(同步)和 SendAsync(异步)方法,向目标数据流块发送消息,再调用 Receive、ReceiveAsync 和 TryReceive 方法以接收源数据流块发送的消息。 通过将输入数据发送到头节点(目标块),以及从管道的终端节点或网络的终端节点(一个或多个源块)接收输出数据,可以将这些方法与数据流管道或网络结合使用。 您还可以使用 Choose 方法从提供的第一个拥有可用数据的源读取数据,并对该数据执行操作。
源数据流块通过调用方法 ITargetBlock
当目标块推迟消息以备日后使用时,OfferMessage 方法会返回 Postponed。 推迟消息的目标块可以稍后调用 ISourceBlock
数据流块完成
数据流块也支持完成概念。 完成状态的数据流块不执行任何进一步的工作。 每个数据流块都有相关的 System.Threading.Tasks.Task 对象(称为“完成任务”),表示数据流块的完成状态。 因为您可以使用完成任务等待 Task 对象完成,所以您可以等待数据流网络的一个或更多终端节点来完成任务。 IDataflowBlock 接口定义 Complete 方法(该方法向数据流块通知它完成的请求)和 Completion 属性(该属性返回数据流块的完成任务)。 ISourceBlock
有两种方法来确定数据流块完成时是否没有出错、遇到一个或多个错误或已取消。 第一种方法是在 try-catch 块(在 Visual Basic 中为 Try-Catch)中对完成任务调用 Task.Wait 方法。 下面的示例创建一个 ActionBlock
// Create an ActionBlock
// and throws ArgumentOutOfRangeException if the input
// is less than zero.
var throwIfNegative = new ActionBlock
{
Console.WriteLine("n = {0}", n);
if (n < 0)
{
throw new ArgumentOutOfRangeException();
}
});
// Post values to the block.
throwIfNegative.Post(0);
throwIfNegative.Post(-1);
throwIfNegative.Post(1);
throwIfNegative.Post(-2);
throwIfNegative.Complete();
// Wait for completion in a try/catch block.
try
{
throwIfNegative.Completion.Wait();
}
catch (AggregateException ae)
{
// If an unhandled exception occurs during dataflow processing, all
// exceptions are propagated through an AggregateException object.
ae.Handle(e =>
{
Console.WriteLine("Encountered {0}: {1}",
e.GetType().Name, e.Message);
return true;
});
}
/* Output:
n = 0
n = -1
Encountered ArgumentOutOfRangeException: Specified argument was out of the range
of valid values.
*/
' Create an ActionBlock
' and throws ArgumentOutOfRangeException if the input
' is less than zero.
Dim throwIfNegative = New ActionBlock(Of Integer)(Sub(n)
Console.WriteLine("n = {0}", n)
If n < 0 Then
Throw New ArgumentOutOfRangeException()
End If
End Sub)
' Post values to the block.
throwIfNegative.Post(0)
throwIfNegative.Post(-1)
throwIfNegative.Post(1)
throwIfNegative.Post(-2)
throwIfNegative.Complete()
' Wait for completion in a try/catch block.
Try
throwIfNegative.Completion.Wait()
Catch ae As AggregateException
' If an unhandled exception occurs during dataflow processing, all
' exceptions are propagated through an AggregateException object.
ae.Handle(Function(e)
Console.WriteLine("Encountered {0}: {1}", e.GetType().Name, e.Message)
Return True
End Function)
End Try
' Output:
' n = 0
' n = -1
' Encountered ArgumentOutOfRangeException: Specified argument was out of the range
' of valid values.
'
此示例演示在执行数据流块的委托中异常变成不可处理的情况。 建议您在这样的块主体中处理异常。 然而,你如果没能这么做,块就表现得好像是它被取消了,而且不会处理传入消息。
当显式取消数据流块时,AggregateException 对象在 OperationCanceledException 属性中包含 InnerExceptions。 有关数据流取消的详细信息,请参阅启用取消部分。
第二种确定数据流块的完成状态的方法是使用延续执行完成任务,或者使用 C# 和 Visual Basic 的异步语言功能以异步方式等待完成任务。 您提供给 Task.ContinueWith 方法的委托采用表示前面任务的 Task 对象。 就 Completion 属性来说,延续的委托自行采用完成任务。 下面的示例与前一个示例相似,不同之处在于它也使用 ContinueWith 方法创建输出整个数据流操作状态的延续任务。
// Create an ActionBlock
// and throws ArgumentOutOfRangeException if the input
// is less than zero.
var throwIfNegative = new ActionBlock
{
Console.WriteLine("n = {0}", n);
if (n < 0)
{
throw new ArgumentOutOfRangeException();
}
});
// Create a continuation task that prints the overall
// task status to the console when the block finishes.
throwIfNegative.Completion.ContinueWith(task =>
{
Console.WriteLine("The status of the completion task is '{0}'.",
task.Status);
});
// Post values to the block.
throwIfNegative.Post(0);
throwIfNegative.Post(-1);
throwIfNegative.Post(1);
throwIfNegative.Post(-2);
throwIfNegative.Complete();
// Wait for completion in a try/catch block.
try
{
throwIfNegative.Completion.Wait();
}
catch (AggregateException ae)
{
// If an unhandled exception occurs during dataflow processing, all
// exceptions are propagated through an AggregateException object.
ae.Handle(e =>
{
Console.WriteLine("Encountered {0}: {1}",
e.GetType().Name, e.Message);
return true;
});
}
/* Output:
n = 0
n = -1
The status of the completion task is 'Faulted'.
Encountered ArgumentOutOfRangeException: Specified argument was out of the range
of valid values.
*/
' Create an ActionBlock
' and throws ArgumentOutOfRangeException if the input
' is less than zero.
Dim throwIfNegative = New ActionBlock(Of Integer)(Sub(n)
Console.WriteLine("n = {0}", n)
If n < 0 Then
Throw New ArgumentOutOfRangeException()
End If
End Sub)
' Create a continuation task that prints the overall
' task status to the console when the block finishes.
throwIfNegative.Completion.ContinueWith(Sub(task) Console.WriteLine("The status of the completion task is '{0}'.", task.Status))
' Post values to the block.
throwIfNegative.Post(0)
throwIfNegative.Post(-1)
throwIfNegative.Post(1)
throwIfNegative.Post(-2)
throwIfNegative.Complete()
' Wait for completion in a try/catch block.
Try
throwIfNegative.Completion.Wait()
Catch ae As AggregateException
' If an unhandled exception occurs during dataflow processing, all
' exceptions are propagated through an AggregateException object.
ae.Handle(Function(e)
Console.WriteLine("Encountered {0}: {1}", e.GetType().Name, e.Message)
Return True
End Function)
End Try
' Output:
' n = 0
' n = -1
' The status of the completion task is 'Faulted'.
' Encountered ArgumentOutOfRangeException: Specified argument was out of the range
' of valid values.
'
您也可以使用类似延续任务主体中的属性(例如 IsCanceled)来确定有关数据流块的完成状态的其他信息。 若要深入了解延续任务及其与取消和错误处理如何相关,请参阅使用延续任务链接任务、任务取消和异常处理。
预定义的数据流块类型
TPL 数据流库提供了多个预定义的数据流块类型。 这些类型分为三个类别:缓冲块、执行块和分组块。 以下部分描述了组成这些类别的块类型。
缓冲块
缓冲块存放的数据供数据使用者使用。 TPL 数据流库提供三种缓冲块类型:System.Threading.Tasks.Dataflow.BufferBlock
BufferBlock
BufferBlock
下面的基本示例将多个 Int32 值发送到 BufferBlock
// Create a BufferBlock
var bufferBlock = new BufferBlock
// Post several messages to the block.
for (int i = 0; i < 3; i++)
{
bufferBlock.Post(i);
}
// Receive the messages back from the block.
for (int i = 0; i < 3; i++)
{
Console.WriteLine(bufferBlock.Receive());
}
/* Output:
0
1
2
*/
' Create a BufferBlock
Dim bufferBlock = New BufferBlock(Of Integer)()
' Post several messages to the block.
For i As Integer = 0 To 2
bufferBlock.Post(i)
Next i
' Receive the messages back from the block.
For i As Integer = 0 To 2
Console.WriteLine(bufferBlock.Receive())
Next i
' Output:
' 0
' 1
' 2
'
有关展示了如何对 BufferBlock
BroadcastBlock
若您必须将多条消息传递给另一个组件,而该组件只需要最新的值,则 BroadcastBlock
下面的基本示例将 Double 值发送给 BroadcastBlock
// Create a BroadcastBlock
var broadcastBlock = new BroadcastBlock
// Post a message to the block.
broadcastBlock.Post(Math.PI);
// Receive the messages back from the block several times.
for (int i = 0; i < 3; i++)
{
Console.WriteLine(broadcastBlock.Receive());
}
/* Output:
3.14159265358979
3.14159265358979
3.14159265358979
*/
' Create a BroadcastBlock
Dim broadcastBlock = New BroadcastBlock(Of Double)(Nothing)
' Post a message to the block.
broadcastBlock.Post(Math.PI)
' Receive the messages back from the block several times.
For i As Integer = 0 To 2
Console.WriteLine(broadcastBlock.Receive())
Next i
' Output:
' 3.14159265358979
' 3.14159265358979
' 3.14159265358979
'
有关展示了如何使用 BroadcastBlock
WriteOnceBlock
WriteOnceBlock
下面的基本示例将多个 String 值发送给 WriteOnceBlock
// Create a WriteOnceBlock
var writeOnceBlock = new WriteOnceBlock
// Post several messages to the block in parallel. The first
// message to be received is written to the block.
// Subsequent messages are discarded.
Parallel.Invoke(
() => writeOnceBlock.Post("Message 1"),
() => writeOnceBlock.Post("Message 2"),
() => writeOnceBlock.Post("Message 3"));
// Receive the message from the block.
Console.WriteLine(writeOnceBlock.Receive());
/* Sample output:
Message 2
*/
' Create a WriteOnceBlock
Dim writeOnceBlock = New WriteOnceBlock(Of String)(Nothing)
' Post several messages to the block in parallel. The first
' message to be received is written to the block.
' Subsequent messages are discarded.
Parallel.Invoke(Function() writeOnceBlock.Post("Message 1"), Function() writeOnceBlock.Post("Message 2"), Function() writeOnceBlock.Post("Message 3"))
' Receive the message from the block.
Console.WriteLine(writeOnceBlock.Receive())
' Sample output:
' Message 2
'
有关展示了如何使用 WriteOnceBlock
执行块
执行块为每条接收数据调用用户提供的委托。 TPL 数据流库提供三种执行块类型:ActionBlock
ActionBlock
ActionBlock
下面的基本示例将多个 Int32 值发送给 ActionBlock
// Create an ActionBlock
// to the console.
var actionBlock = new ActionBlock
// Post several messages to the block.
for (int i = 0; i < 3; i++)
{
actionBlock.Post(i * 10);
}
// Set the block to the completed state and wait for all
// tasks to finish.
actionBlock.Complete();
actionBlock.Completion.Wait();
/* Output:
0
10
20
*/
' Create an ActionBlock
' to the console.
Dim actionBlock = New ActionBlock(Of Integer)(Function(n) WriteLine(n))
' Post several messages to the block.
For i As Integer = 0 To 2
actionBlock.Post(i * 10)
Next i
' Set the block to the completed state and wait for all
' tasks to finish.
actionBlock.Complete()
actionBlock.Completion.Wait()
' Output:
' 0
' 10
' 20
'
有关展示了如何结合使用委托和 ActionBlock
TransformBlock
TransformBlock
下面的基本示例所创建的 TransformBlock
// Create a TransformBlock
// computes the square root of its input.
var transformBlock = new TransformBlock
// Post several messages to the block.
transformBlock.Post(10);
transformBlock.Post(20);
transformBlock.Post(30);
// Read the output messages from the block.
for (int i = 0; i < 3; i++)
{
Console.WriteLine(transformBlock.Receive());
}
/* Output:
3.16227766016838
4.47213595499958
5.47722557505166
*/
' Create a TransformBlock
' computes the square root of its input.
Dim transformBlock = New TransformBlock(Of Integer, Double)(Function(n) Math.Sqrt(n))
' Post several messages to the block.
transformBlock.Post(10)
transformBlock.Post(20)
transformBlock.Post(30)
' Read the output messages from the block.
For i As Integer = 0 To 2
Console.WriteLine(transformBlock.Receive())
Next i
' Output:
' 3.16227766016838
' 4.47213595499958
' 5.47722557505166
'
有关展示了如何在数据流块网络中使用 TransformBlock
TransformManyBlock
TransformManyBlock
下面的基本示例所创建的 TransformManyBlock
// Create a TransformManyBlock
// a string into its individual characters.
var transformManyBlock = new TransformManyBlock
s => s.ToCharArray());
// Post two messages to the first block.
transformManyBlock.Post("Hello");
transformManyBlock.Post("World");
// Receive all output values from the block.
for (int i = 0; i < ("Hello" + "World").Length; i++)
{
Console.WriteLine(transformManyBlock.Receive());
}
/* Output:
H
e
l
l
o
W
o
r
l
d
*/
' Create a TransformManyBlock
' a string into its individual characters.
Dim transformManyBlock = New TransformManyBlock(Of String, Char)(Function(s) s.ToCharArray())
' Post two messages to the first block.
transformManyBlock.Post("Hello")
transformManyBlock.Post("World")
' Receive all output values from the block.
For i As Integer = 0 To ("Hello" & "World").Length - 1
Console.WriteLine(transformManyBlock.Receive())
Next i
' Output:
' H
' e
' l
' l
' o
' W
' o
' r
' l
' d
'
有关展示了如何使用 TransformManyBlock
并行度
每个 ActionBlock
委托类型摘要
下表汇总了可提供给 ActionBlock
类型
同步委托类型
异步委托类型
ActionBlock
System.Action
System.Func
TransformBlock
System.Func
System.Func
TransformManyBlock
System.Func
System.Func
当处理执行块类型时,还可以使用 lambda 表达式。 有关演示如何使用 lambda 表达式处理执行块的示例,请参阅如何:在数据流块收到数据时执行操作。
分组块
分组块在各种约束下合并一个或多个源的数据。 TPL 数据流库提供三种联接块类型:BatchBlock
BatchBlock
BatchBlock
BatchBlock
下面的基本示例将多个 Int32 值发送给一批中能容纳十个元素的 BatchBlock
// Create a BatchBlock
// elements per batch.
var batchBlock = new BatchBlock
// Post several values to the block.
for (int i = 0; i < 13; i++)
{
batchBlock.Post(i);
}
// Set the block to the completed state. This causes
// the block to propagate out any remaining
// values as a final batch.
batchBlock.Complete();
// Print the sum of both batches.
Console.WriteLine("The sum of the elements in batch 1 is {0}.",
batchBlock.Receive().Sum());
Console.WriteLine("The sum of the elements in batch 2 is {0}.",
batchBlock.Receive().Sum());
/* Output:
The sum of the elements in batch 1 is 45.
The sum of the elements in batch 2 is 33.
*/
' Create a BatchBlock
' elements per batch.
Dim batchBlock = New BatchBlock(Of Integer)(10)
' Post several values to the block.
For i As Integer = 0 To 12
batchBlock.Post(i)
Next i
' Set the block to the completed state. This causes
' the block to propagate out any remaining
' values as a final batch.
batchBlock.Complete()
' Print the sum of both batches.
Console.WriteLine("The sum of the elements in batch 1 is {0}.", batchBlock.Receive().Sum())
Console.WriteLine("The sum of the elements in batch 2 is {0}.", batchBlock.Receive().Sum())
' Output:
' The sum of the elements in batch 1 is 45.
' The sum of the elements in batch 2 is 33.
'
有关展示了如何使用 BatchBlock
JoinBlock
JoinBlock
像在贪婪或非贪婪模式下运行的 BatchBlock
下面的基本示例演示 JoinBlock
// Create a JoinBlock
// two numbers and an operator.
var joinBlock = new JoinBlock
// Post two values to each target of the join.
joinBlock.Target1.Post(3);
joinBlock.Target1.Post(6);
joinBlock.Target2.Post(5);
joinBlock.Target2.Post(4);
joinBlock.Target3.Post('+');
joinBlock.Target3.Post('-');
// Receive each group of values and apply the operator part
// to the number parts.
for (int i = 0; i < 2; i++)
{
var data = joinBlock.Receive();
switch (data.Item3)
{
case '+':
Console.WriteLine("{0} + {1} = {2}",
data.Item1, data.Item2, data.Item1 + data.Item2);
break;
case '-':
Console.WriteLine("{0} - {1} = {2}",
data.Item1, data.Item2, data.Item1 - data.Item2);
break;
default:
Console.WriteLine("Unknown operator '{0}'.", data.Item3);
break;
}
}
/* Output:
3 + 5 = 8
6 - 4 = 2
*/
' Create a JoinBlock
' two numbers and an operator.
Dim joinBlock = New JoinBlock(Of Integer, Integer, Char)()
' Post two values to each target of the join.
joinBlock.Target1.Post(3)
joinBlock.Target1.Post(6)
joinBlock.Target2.Post(5)
joinBlock.Target2.Post(4)
joinBlock.Target3.Post("+"c)
joinBlock.Target3.Post("-"c)
' Receive each group of values and apply the operator part
' to the number parts.
For i As Integer = 0 To 1
Dim data = joinBlock.Receive()
Select Case data.Item3
Case "+"c
Console.WriteLine("{0} + {1} = {2}", data.Item1, data.Item2, data.Item1 + data.Item2)
Case "-"c
Console.WriteLine("{0} - {1} = {2}", data.Item1, data.Item2, data.Item1 - data.Item2)
Case Else
Console.WriteLine("Unknown operator '{0}'.", data.Item3)
End Select
Next i
' Output:
' 3 + 5 = 8
' 6 - 4 = 2
'
有关展示了如何在非贪婪模式下使用 JoinBlock
BatchedJoinBlock
BatchedJoinBlock
下面的基本示例创建了一个包含结果、BatchedJoinBlock
// For demonstration, create a Func
// returns its argument, or throws ArgumentOutOfRangeException
// if the argument is less than zero.
Func
{
if (n < 0)
throw new ArgumentOutOfRangeException();
return n;
};
// Create a BatchedJoinBlock
// seven elements per batch.
var batchedJoinBlock = new BatchedJoinBlock
// Post several items to the block.
foreach (int i in new int[] { 5, 6, -7, -22, 13, 55, 0 })
{
try
{
// Post the result of the worker to the
// first target of the block.
batchedJoinBlock.Target1.Post(DoWork(i));
}
catch (ArgumentOutOfRangeException e)
{
// If an error occurred, post the Exception to the
// second target of the block.
batchedJoinBlock.Target2.Post(e);
}
}
// Read the results from the block.
var results = batchedJoinBlock.Receive();
// Print the results to the console.
// Print the results.
foreach (int n in results.Item1)
{
Console.WriteLine(n);
}
// Print failures.
foreach (Exception e in results.Item2)
{
Console.WriteLine(e.Message);
}
/* Output:
5
6
13
55
0
Specified argument was out of the range of valid values.
Specified argument was out of the range of valid values.
*/
' For demonstration, create a Func
' returns its argument, or throws ArgumentOutOfRangeException
' if the argument is less than zero.
Dim DoWork As Func(Of Integer, Integer) = Function(n)
If n < 0 Then
Throw New ArgumentOutOfRangeException()
End If
Return n
End Function
' Create a BatchedJoinBlock
' seven elements per batch.
Dim batchedJoinBlock = New BatchedJoinBlock(Of Integer, Exception)(7)
' Post several items to the block.
For Each i As Integer In New Integer() {5, 6, -7, -22, 13, 55, 0}
Try
' Post the result of the worker to the
' first target of the block.
batchedJoinBlock.Target1.Post(DoWork(i))
Catch e As ArgumentOutOfRangeException
' If an error occurred, post the Exception to the
' second target of the block.
batchedJoinBlock.Target2.Post(e)
End Try
Next i
' Read the results from the block.
Dim results = batchedJoinBlock.Receive()
' Print the results to the console.
' Print the results.
For Each n As Integer In results.Item1
Console.WriteLine(n)
Next n
' Print failures.
For Each e As Exception In results.Item2
Console.WriteLine(e.Message)
Next e
' Output:
' 5
' 6
' 13
' 55
' 0
' Specified argument was out of the range of valid values.
' Specified argument was out of the range of valid values.
'
有关展示了如何在程序从数据库读取数据时使用 BatchedJoinBlock
配置数据流块行为
可以通过给数据流块类型的构造函数提供 System.Threading.Tasks.Dataflow.DataflowBlockOptions 对象来启用其他选项。 这些选项控制这类管理基础任务和并行度的调度程序的行为。 DataflowBlockOptions 还包含派生类型,用以指定特定于某些数据流块类型的行为。 下表汇总了与每个数据流块类型相关的选项类型。
数据流块类型
DataflowBlockOptions 类型
BufferBlock
DataflowBlockOptions
BroadcastBlock
DataflowBlockOptions
WriteOnceBlock
DataflowBlockOptions
ActionBlock
ExecutionDataflowBlockOptions
TransformBlock
ExecutionDataflowBlockOptions
TransformManyBlock
ExecutionDataflowBlockOptions
BatchBlock
GroupingDataflowBlockOptions
JoinBlock
GroupingDataflowBlockOptions
BatchedJoinBlock
GroupingDataflowBlockOptions
以下各节提供了有关重要数据流块选项(通过 System.Threading.Tasks.Dataflow.DataflowBlockOptions、System.Threading.Tasks.Dataflow.ExecutionDataflowBlockOptions 和 System.Threading.Tasks.Dataflow.GroupingDataflowBlockOptions 类提供)的其他信息。
指定任务计划程序
每个预定义的数据流块在数据可用时使用 TPL 任务计划机制执行一些活动,例如,将数据传播到目标、接收来自源的数据并运行用户定义的委托。 TaskScheduler 是抽象类,表示将任务排队成线程的任务计划程序。 默认任务计划程序 Default 使用 ThreadPool 类进行排队并执行工作。 构造数据流块对象时,您可以通过设置 TaskScheduler 属性重写默认任务计划程序。
当同一个任务计划程序管理多个数据流块时,它可在它们之间强制实施策略。 例如,如果多个数据流块分别配置为面向同一 ConcurrentExclusiveSchedulerPair 对象的独占计划程序,则会序列化这些块间运行的所有工作。 同样,如果这些块配置为面向同一 ConcurrentExclusiveSchedulerPair 对象的并发计划程序,而该计划程序配置为具有最大并发级,则这些块中所有的工作都会受到并发操作数的限制。 有关展示了如何使用 ConcurrentExclusiveSchedulerPair 类让读取操作并行执行(但写入操作独立于其他所有操作)的示例,请参阅如何:在数据流块中指定任务计划程序。 有关 TPL 中的任务计划程序的详细信息,请参阅 TaskScheduler 类主题。
指定并行度
默认情况下,TPL 数据流库提供三种执行块类型(ActionBlock
MaxDegreeOfParallelism 的默认值为 1,这保证了数据流块一次处理一条消息。 将该属性设置为大于 1 的值将使数据流块可以同时处理多条消息。 将该属性设置为 DataflowBlockOptions.Unbounded 将使基础任务计划程序管理最大并发程度。
重要
当指定大于 1 的最大并行度时,会同时处理多条消息,因此,消息可能不会按照接收的顺序进行处理。 然而,从块输出消息的顺序与接收消息的顺序相同。
由于 MaxDegreeOfParallelism 属性表示最大并行度,因此数据流块执行时的并行度可能小于指定的值。 为了达到功能要求或因为缺少可用的系统资源,数据流块可能使用较小的并行度。 数据流块选择的并行度不会超过您指定的值。
MaxDegreeOfParallelism 属性的值对于每个数据流块对象而言,都是特有的。 例如,如果四个数据流对象块中的每一个都指定 1 作为最大并行度,则所有四个数据流对象块可以并行运行。
有关设置最大并行度以启用并行冗长操作的示例,请参阅如何:指定数据流块中的并行度。
指定每个任务的消息数
预定义的数据流块类型使用任务来处理多个输入元素。 这有助于最大限度地减少需要处理数据的任务对象数,从而使应用程序可以更有效地运行。 但是,当一个数据流块集合中的任务处理数据时,其他数据流块的任务可能需要按照队列消息等待处理时间。 若要使数据流任务更加公平,请设置 MaxMessagesPerTask 属性。 当 MaxMessagesPerTask 设置为 DataflowBlockOptions.Unbounded 默认值时,数据流块使用的任务会处理尽可能多的消息。 当 MaxMessagesPerTask 设置为 Unbounded 以外的值时,数据流块为每个 Task 对象至多处理这个数量的消息。 虽然设置 MaxMessagesPerTask 属性可以提高任务间的公平性,但它可能会导致该系统创建多个非必要的任务,这会降低性能。
启用取消
TPL 提供了一种机制,能使任务以一种合作的方式协调取消。 若要启用数据流块参与此取消机制,请设置 CancellationToken 属性。 当此 CancellationToken 对象设置为已取消状态时,所有监视该标记的数据流块都会完成当前项目的执行,但不会开始处理后续项。 这些数据流块也会清除所有缓冲的消息,释放所有源和目标块的连接,并转换为已取消状态。 通过转换为已取消状态,Completion 属性具有设置为 Status 的 Canceled 属性,除非在处理过程中出现异常。 在这种情况下,Status 会设置为 Faulted。
有关演示如何在 Windows 窗体应用程序中使用取消的示例,请参阅如何:取消数据流块。 若要深入了解 TPL 中的取消,请参阅任务取消。
指定贪婪与非贪婪行为
几个分组数据流块类型可以在贪婪或非贪婪模式下运行。 默认情况下,预定义的数据流块类型在贪婪模式下运行。
对于联接块类型(如 JoinBlock
若要为数据流块指定非贪婪模式,请将 Greedy 设置为 False。 有关演示如何使用非贪婪模式使多个联接块更高效地共享数据源的示例,请参阅如何:使用 JoinBlock 从多个源读取数据。
自定义数据流块
尽管 TPL 数据流库提供了许多预定义块类型,但是您还是可以创建执行自定义行为的其他块类型。 直接实现 ISourceBlock
相关主题
Title
描述
如何:将消息写入数据流块和从数据流块读取消息
演示如何向 BufferBlock
如何:实现制造者-使用者数据流模式
描述如何使用数据流模型实现制造者-使用方模式,在这个模型中制造者向数据流块发送消息,而使用方从该块中读取消息。
如何:在数据流块收到数据时执行操作
描述如何向执行数据流块类型(ActionBlock
演练:创建数据流管道
描述如何创建从 Web 下载文本并对该文本执行操作的数据流管道。
如何:取消链接数据流块
展示了如何在源向目标提供消息后,使用 LinkTo 方法取消链接源数据流块和目标数据流块。
演练:在 Windows 窗体应用程序中使用数据流
演示如何创建在 Windows 窗体应用程序中执行图像处理的数据流块网络。
如何:取消数据流块
演示如何在 Windows 窗体应用程序中使用取消。
如何:使用 JoinBlock 从多个源读取数据
解释如何在多个源的数据可用时使用 JoinBlock
如何:指定数据流块中的并行度
描述如何设置 MaxDegreeOfParallelism 属性使执行数据流块一次处理多条消息。
如何:在数据流块中指定任务计划程序
演示在应用程序中使用数据流时如何关联特定任务计划程序。
演练:使用 BatchBlock 和 BatchedJoinBlock 提高效率
描述如何使用 BatchBlock
演练:创建自定义数据流块类型
演示创建实现自定义行为的数据流块类型的两种方法。
任务并行库 (TPL)
介绍 TPL,一个可在 .NET Framework 应用程序里简化并行和并发编程的库。
在 GitHub 上与我们协作
可以在 GitHub 上找到此内容的源,还可以在其中创建和查看问题和拉取请求。 有关详细信息,请参阅参与者指南。
.NET
提出文档问题
提供产品反馈
反馈
Coming soon: Throughout 2024 we will be phasing out GitHub Issues as the feedback mechanism for content and replacing it with a new feedback system. For more information see: https://aka.ms/ContentUserFeedback.
提交和查看相关反馈
此产品
此页面
查看所有页面反馈
其他资源
加州消费者隐私法案 (CCPA) 禁用图标
你的隐私选择
主题
亮
暗
高对比度
早期版本
博客
参与
隐私
使用条款
商标
© Microsoft 2024
其他资源
本文内容
加州消费者隐私法案 (CCPA) 禁用图标
你的隐私选择
主题
亮
暗
高对比度
早期版本
博客
参与
隐私
使用条款
商标
© Microsoft 2024
四川省政务服务网
四川省政务服务网
5秒后自动跳转
操作指南
我要办事
我要咨询
办件进度
我要建议
好差评
返回顶部
四川省人民政府网
国家政务服务平台 |
首页
个人办事
法人办事
直通部门
直通市州
政民互动
政务公开
主题服务
找地区
找部门
省发展改革委
经济和信息化厅
教育厅
科技厅
省民族宗教委
公安厅
民政厅
司法厅
财政厅
人力资源社会保障厅
自然资源厅
生态环境厅
住房城乡建设厅
交通运输厅
水利厅
农业农村厅
商务厅
文化和旅游厅
省卫生健康委
退役军人厅
应急厅
审计厅
省市场监管局
省体育局省
统计局
省机关事务管理局
省扶贫开发局
省人防办
省地方金融监管局
省经济合作局
省林草局
省广电局
省医保局
省粮食和储备局
省中医药局
省药监局
四川省安全厅
人行成都分行
省消防救援总队
省知识产权中心
省档案局
省外事办
省税务局
省市场监督管理局(原省质监局)
省国防科工办
省政务服务和资源交易服务中心
省残联
省消委会
省邮政管理局
省测绘地理信息局
省地震局
省烟草专卖局
省侨务办
省气象局省
煤监局
省网信办
省通信管理局
省新闻出版局(省电影局)
欢迎来到“天府通办”
天府通办
统一好差评
统一事项服务
统一身份认证
统一支付服务
统一证照服务
一件事服务
事项清单
便民服务
我要办理社保卡业务
我要办理公积金登记
我要开网约车
我要开出租车
我要考驾照
我要提取公积金
我要办理非机动车登记
我要办律师执业证
权利清单
责任清单
公共服务清单
权利清单调整记录
通用目录清单
跑动次数清单
公积金查询
社保卡状态查询
机动车违章查询
异地就医查询
密切接触者查询
机动车车牌号预选
律师事务所查询
县域疫情风险等级查询
办事套餐
查看更多 >
工程建设项目审批
房屋建筑
市政工程
竣工验收
营业执照办理
名称申报
企业开办
企业变更
企业注销服务专区
企业注销
业务指南
进度查询
农民工之家
子女就学
权益维护
就业服务
投资项目审批
投资登记
项目审批
信息报备
有序复工复产服务专区
政策文件
复工复产
返港就业
出入境证件便利化
社会保障
市场监管
教育医疗
企业维权
受理说明
维权办理
我提意见
新冠肺炎疫情防控服务
定点医院
捐赠指南
防护指南
场景服务
个人
法人
从“新”出发,四川政务为您提供服务
初来蜀地
欢迎来到四川
入学、旅游、就业、落户、投资…办事无忧!
我要上学入学申请 | 入学资助
我要找工作执业资格 |
工作许可
我要买房存量房网签备案
查看更多 →
在蜀生活
专“蜀”为您
教育考试、公正预约、户口登记…办事快捷
我要结婚结婚证 |
婚育证明
我要创业失业登记 |
失业保险
失业服务失业登记 |
失业保险
查看更多 →
离开蜀地
期待下次再见
异地医保服务、信息变更…出蜀地,办事高效
我要迁居户籍证明 |
迁移审批
我要退休提取公积金
查看更多 →
初来蜀地
欢迎来到四川
申请、提交、审批、发放一窗办理
企业开办企业注册 | 变更注销
资质申请变更经营区域
投资立项方案技术性审核
查看更多 →
在蜀发展
专“蜀”为您
税费申报、公司设立/变更、政策鼓励
扩大生产生产许可证核发
引进人才引进境外人才
办理社保社保补贴申请
查看更多 →
离开蜀地
期待下次再见
税务注销、银行账户注销、社保注销一键办理
申请破产失业保险服务
查看更多 →
个人服务
减少群众跑动次数,一次提交一次办成
查看更多
生育收养
病残儿医学鉴定
|
生育保险待遇申领
民族宗教
筹备设立寺院、宫观、清真寺、教堂审批
教育科研
教师资格认定
|
普通高考成绩查询
|
中小学教师资格考试报名
就业创业
民办非企业单位成立登记
|
职业素质测评
职业资格
律师执业机构变更
|
公证员执业证换发
|
教师资格认定
婚姻登记
补领结婚证
|
外国人、港澳台居民、华侨及出国人员与四川省户籍居民婚姻登记
住房保障
职工住房公积金转移
|
偿还自住住房贷款本息提取
|
再交易住房贷款
证件办理
公路超限运输许可
|
律师执业机构变更
交通出行
船舶焊工资格认定
|
船舶载运危险货物申报人员证书核发
法人服务
面向企业法人提供一站式办理服务
查看更多
设立变更
安全生产许可企业地址变更指南
|
公司变更登记
|
分公司变更登记
资质认证
船舶设计资质认定
|
安全评价机构资质证书首次申请
|
安全生产检测检验机构申请地址变更
医疗卫生
医疗机构执业审批(新办)
|
母婴保健技术服务执业首次申报(产前筛查)
人力资源
劳动人事争议仲裁申请
|
集体合同审查
|
稳岗补贴申领
社会保障
工伤认定申请
投资审批
外商投资企业分支机构变更登记
|
外商投资项目核准
|
外商投资企业注销登记
农林牧渔
权限内肥料登记
|
种畜禽生产经营许可证信息变更
|
兽药经营许可证信息变更
科技创新
科技政策咨询服务
|
科技成果登记服务
|
国家科技计划项目推荐服务
安全生产
煤矿矿井安全生产许可证延期
|
民用爆炸物品销售仓库地址变更
政务服务好差评
本月统计数据
查看全部 >
60万条
总办件数
30万条
网办量
225条
差评量
98.7%
整改率
8.5分
平均分
查看全部
直通部门服务
包含58个部门的11421项政务服务事项
省发展改革委
经济和信息化厅
教育厅
科技厅
省民族宗教委
公安厅
民政厅
司法厅
财政厅
人力资源社会保障厅
自然资源厅
生态环境厅
住房城乡建设厅
交通运输厅
水利厅
农业农村厅
商务厅
文化和旅游厅
省卫生健康委
退役军人厅
应急厅
审计厅
省市场监管局
省体育局省
统计局
省机关事务管理局
省扶贫开发局
省人防办
省地方金融监管局
省经济合作局
省林草局
省广电局
省医保局
省粮食和储备局
省中医药局
省药监局
四川省安全厅
人行成都分行
省消防救援总队
省知识产权中心
省档案局
省外事办
省税务局
省市场监督管理局(原省质监局)
省国防科工办
省政务服务和资源交易服务中心
省残联
省消委会
省邮政管理局
省测绘地理信息局
省地震局
省烟草专卖局
省侨务办
省气象局省
煤监局
省网信办
省通信管理局
省新闻出版局(省电影局)
直通市州服务
包含21个市州、209个区县
市级
成都市
自贡市
攀枝花市
泸州市
德阳市
绵阳市
广元市
遂宁市
内江市
乐山市
南充市
宜宾市
广安市
达州市
巴中市
雅安市
眉山市
资阳市
阿坝州
甘孜州
凉山州
县(市、区)
市(州)本级
高新区
天府新区
锦江区
青羊区
金牛区
武侯区
成华区
龙泉驿区
青白江区
新都区
温江区
双流区
简阳市
都江堰市
彭州市
邛崃市
崇州市
郫都区
金堂县
新津县
大邑县
蒲江县
市(州)本级
自贡高新区
自流井区
贡井区
大安区
沿滩区
荣县
富顺县
市(州)本级
东区
西区
仁和区
米易县
盐边县
市(州)本级
泸州高新区
中国(四川)自由贸易试验区川南临港片区
江阳区
龙马潭区
纳溪区
泸县
合江县
叙永县
古蔺县
市(州)本级
德阳经开区
德阳凯州新城
旌阳区
德阳市罗江区
广汉市
什邡市
绵竹市
中江县
市(州)本级
绵阳高新区
绵阳经开区
科学城
科创区
涪城区
游仙区
安州区
江油市
三台县
梓潼县
盐亭县
平武县
北川羌族自治县
仙海区
市(州)本级
广元经开区
苍溪县
旺苍县
青川县
剑阁县
昭化区
利州区
朝天区
市(州)本级
遂宁高新区
遂宁经开区
遂宁河东新区
船山区
安居区
射洪市
蓬溪县
大英县
市(州)本级
市中区
东兴区
隆昌市
资中县
威远县
市(州)本级
乐山市高新区
乐山市中区
夹江县
井研县
沙湾区
五通桥区
沐川县
马边彝族自治县
峨眉山市
峨边县
犍为县
金口河区
市(州)本级
顺庆区
高坪区
嘉陵区
阆中市
南部县
西充县
仪陇县
营山县
蓬安县
市(州)本级
临港经开区
翠屏区
叙州区
南溪区
江安县
长宁县
高县
筠连县
珙县
兴文县
屏山县
两海示范区
市(州)本级
广安经开区
枣山物流商贸园区
协兴园区
广安区
前锋区
华蓥市
岳池县
武胜县
邻水县
市(州)本级
达州经济开发区
万源市
宣汉县
渠县
大竹县
达川区
通川区
开江县
市(州)本级
巴州区
南江县
恩阳区
平昌县
通江县
市(州)本级
雨城区
名山区
天全县
芦山县
宝兴县
荥经县
汉源县
石棉县
市(州)本级
天府新区(眉山片区)
东坡区
彭山区
仁寿县
洪雅县
丹棱县
青神县
市(州)本级
资阳高新区
临空经济区
雁江区
安岳县
乐至县
市(州)本级
马尔康市
金川县
小金县
阿坝县
若尔盖县
红原县
壤塘县
汶川县
理县
茂县
黑水县
松潘县
九寨沟县
市(州)本级
康定市
泸定县
丹巴县
九龙县
雅江县
道孚县
炉霍县
甘孜县
色达县
德格县
白玉县
石渠县
新龙县
理塘县
巴塘县
乡城县
稻城县
得荣县
市(州)本级
西昌市
德昌县
会理县
会东县
宁南县
普格县
布拖县
昭觉县
金阳县
雷波县
美姑县
甘洛县
越西县
喜德县
冕宁县
盐源县
木里县
微信公众号
天府通办APP
主办:四川省人民政府办公厅承办:四川省大数据中心支持:中国电信四川公司联系电话:12345网站标识码:5100000101
蜀ICP备13001288号-3川公安网安备
5101040200532号邮政编码610000工作时间:8:30-12:00,14:00-17:30地址:人民中路三段35号
网站声明 |关于我们
Produced By 大汉网络 大汉版通发布系统