C#中子线程如何发送一个消息字串给主线程,当主线程c语言接收字符串到这个消息更新UI

Android 中的消息机制
写一个路线的博客,如事件分发机制,自定义View,View的绘制机制和加载过程,Activity的加载过程等等 。 进入正题。执行耗时的操作,比如网络请求,IO操作等,需要在子线程中运行,
写一个路线的博客,如事件分发机制,自定义View,View的绘制机制和加载过程,Activity的加载过程等等 。
进入正题。执行耗时的操作,比如网络请求,IO操作等,需要在子线程中运行,不然会阻塞主线程。
而执行完网络请求等耗时操作后通常需要更新UI,如果在子线程中更新UI,那么程序会崩溃。因为Android的UI是线程不安全的。
解决的方案是只需把更新UI的操作切换到主线程即可,这时就轮到Handler出场了,相信大家都对Handler的用法很熟悉了。当我们在子线程向服务端拉取数据后,主线程是不知道的,这时handler在子线程发送一个消息到主线程告诉主线程:我已经请求数据完毕,现在你要更新UI了。然后handlerMessage方法接收到消息即可处理数据更新UI。
这一切都是那么的自然。
Android中的消息机制,由四个角色承担。分别是Handler,Looper(消息循环),MessageQueue(消息队列),Thread。
看到下图这四个角色的关联,先有一个大概的认识
那接下来就从源码(Api-23)的角度一起来学习消息机制吧
先从handler对象的创建开始,接着再分析handler怎么发送消息。
我们通常在主线程中创建handler,看看他的构造方法。
public Handler() {
this(null, false);
可以看到这个无参数的构造方法,内部使用this关键字调用含有两个参数的构造方法。那就找到两个参数的构造方法,源码如下:
public Handler(Callback callback, boolean async) {
if (FIND_POTENTIAL_LEAKS) {
final Class&? extends Handler& klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, &The following Handler class should be static or leaks might occur: & +
klass.getCanonicalName());
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
&Can't create handler inside thread that has not called Looper.prepare()&);
mQueue = mLooper.mQ
mCallback =
mAsynchronous =
可以看到Looper调用myLooper方法获取到Looper对象, 如果mLooper == null的话,会抛出
Can't create handler inside thread that has not called Looper.prepare()
的异常。大概的意思就是无法在没有调用Looper.prepare()的线程中创建handler。
我在刚开始学习Handler的时候经常会遇到这个错误。不急,等下在分析到底为什么,现在我们只需要知道如果Looper.myLooper()没有获取到Looper对象的话就会报这个错。
到了这里,Handler和Looper就建立起了关联。接着往下看完最后几行代码
mQueue = mLooper.mQ
从Looper对象中取出MessageQueue对象并赋值。MessageQueue就是消息队列,那么他里面存储着很多消息吗?
到了这一步,Handler通过Looper与MessageQueue也建立起了关联。
我们跟踪Looper的myLooper方法进去,解决为什么会抛出Can&t create handler inside thread that has not called Looper.prepare()异常。
myLooper方法源码如下:
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
只有一行代码,从线程中取出Looper对象,那么我们有理由相信,这个ThreadLocal是通过set方法把Looper对象设置进去的。
想一想ThreadLocal在哪里把Looper对象设置进去了呢。回到刚才想要解决的问题:Can&t create handler inside thread that has not called Looper.prepare() 。那会不会是Looper的prepare方法呢?
public static void prepare() {
prepare(true);
prepare方法调用了它的一个参数的重载,那么我们就看看那个重载的方法
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException(&Only one Looper may be created per thread&);
sThreadLocal.set(new Looper(quitAllowed));
找到了线索,ThreadLocal确实是在Looper的prepare方法里把Looper对象设置进去的,而且从第一行的判断可以知道,一个线程只有一个Looper对象。
到了这里,Looper与ThreadLocal建立起了关联。可以看下Looper的构造方法
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
创建了一个MessageQueue对象。
好,结合我们的分析可以知道,如果Looper没有调用prepare方法,ThreadLocal的get方法就会返回空,那么Looper.myLooper()也会返回空,所以就抛出了&Can't create handler inside thread that has not called Looper.prepare()&的异常。
那么问题又来了,我们写程序时好像没有手动调用Looper.prepare()吧,也不会抛出异常。前面提到,我们通常都是在主线程,也就是UI线程中创建handler的。而在主线程中,系统已经为我们创建了一个Looper对象,所以不会抛出异常了。。。而那些会抛出异常报错的情况,是在子线程中创建的handler,但是又没有调用Looper.prepare()去创建Looper对象。
继续前进。那就来看看,主线程在什么时候创建了Looper对象吧。
在ActivityThread的main方法,这个方法是应用程序的入口。
main方法的源码如下:
public static void main(String[] args) {
//代码省略
Looper.prepareMainLooper();
ActivityThread thread = new ActivityThread();
thread.attach(false);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, &ActivityThread&));
// End of event ActivityThreadMain.
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop();
throw new RuntimeException(&Main thread loop unexpectedly exited&);
找到了Looper.prepareMainLooper(),这和Looper.prepare()太像了吧,跟进去看看
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException(&The main Looper has already been prepared.&);
sMainLooper = myLooper();
又兜了回来,还是调用了prepare方法的。所以主线程是已经创建了一个Looper对象的。
Handler的创建过程分析完毕,现在总算搞明白了。
那先总结一下,Handler的创建是依赖于Looper的。而主线程是默认创建了一个Looper对象的。每一个Looper会关联一个线程(ThreadLocal中封装了Looper)。每一个Looper中又会封装一个消息队列。
这样一来,handler,Looper,MessageQueue,Thread四个角色就关联了起来,你中有我,我中有你。
handler在主线程中创建,是因为要和主线程的消息队列关联起来,那样handler的handleMessage方法才会在主线程中执行,那么这样在更新UI就是线程安全的了。
接着继续吧,还很多问题没有解决
相信你更想了解Handler是怎么发送消息的。通常我们是创建一个Message对象,并将一些从服务端拉取的数据,标记,参数等赋值到Message的一些字段what,arg1,obj等,handler调用sendMessage方法发送,就能将这个数据发送到主线程,然后在handlerMessage方法处理更新UI即可。
那我们就从handler的sendMessage方法开始寻找信息
public final boolean sendMessage(Message msg)
return sendMessageDelayed(msg, 0);
sendMessage会调用sendMessageDelayed方法并将message对象传进去,第二个参数是延时时间,使用sendMessage方法时默认为0的。
那么来到sendMessageDelayed方法
public final boolean sendMessageDelayed(Message msg, long delayMillis)
if (delayMillis & 0) {
delayMillis = 0;
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
兜兜转转,最终会调用sendMessageAtTime方法,并将message对象传进。
继续跟进sendMessageAtTime方法,
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQ
if (queue == null) {
RuntimeException e = new RuntimeException(
this + & sendMessageAtTime() called with no mQueue&);
Log.w(&Looper&, e.getMessage(), e);
return enqueueMessage(queue, msg, uptimeMillis);
上面分析了,在创建Looper对象的时候,会创建一个MessageQueue,所以只要Looper是正常创建的话,消息队列是不为空的。
那么到最后一行的enqueueMessage方法,源码如下:
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target =
if (mAsynchronous) {
msg.setAsynchronous(true);
return queue.enqueueMessage(msg, uptimeMillis);
可以看到最后一行调用了MessageQueue的enqueueMessage方法。
注意: 上面贴出的enqueueMessage是Handler的方法,不是MessageQueue的,只是做了一层包装而已,真正的入队消息队列的操作当然是在MessageQueue中。而且从第一行的msg.target = this中可以知道,msg的target字段,其实就是handler。
MessageQueue的enqueueMessage方法源码如下:
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
throw new IllegalArgumentException(&Message must have a target.&);
if (msg.isInUse()) {
throw new IllegalStateException(msg + & This message is already in use.&);
synchronized (this) {
if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + & sending message to a Handler on a dead thread&);
Log.w(TAG, e.getMessage(), e);
msg.recycle();
msg.markInUse();
msg.when =
Message p = mM
boolean needW
if (p == null || when == 0 || when & p.when) {
// New head, wake up the event queue if blocked.
msg.next =
mMessages =
needWake = mB
// Inserted within the middle of the queue.
Usually we don't have to wake
// up the event queue unless there is a barrier at the head of the queue
// and the message is the earliest asynchronous message in the queue.
needWake = mBlocked && p.target == null && msg.isAsynchronous();
for (;;) {
if (p == null || when & p.when) {
if (needWake && p.isAsynchronous()) {
needWake =
msg.next = // invariant: p == prev.next
prev.next =
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
Messagequeue中有一个对象mMessage用于指向当前传进的msg,即最新的消息。而刚才的sendMessageAtTime(Message msg, long uptimeMillis)方法,第二个参数指定了时间,然后在这里按照这个uptimeMillis来进行消息的排序,而我分析的结果msg.next是指向下一个消息,这样每一个消息都是按照时间的排序关联了起来,排在前面的消息指向了排在后面的消息。
以上是进入消息队列的分析,handler调用sendMessage方法的最终将message对象传进messagequeue。
完毕,那么消息是怎么从消息队列出来的呢?
这时我们要回看ActiviryThread的main方法,去寻找点线索。源码在上面已贴出。
发现了倒数第二行的Looper.loop(),简单理解就是消息循环执行循环操作。
这里一定能满足我们的好奇心。那么跟进。loop方法的源码如下:
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException(&No L Looper.prepare() wasn't called on this thread.&);
final MessageQueue queue = me.mQ
// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
// This must be in a local variable, in case a UI event sets the logger
Printer logging = me.mL
if (logging != null) {
logging.println(&&&&&& Dispatching to & + msg.target + & & +
msg.callback + &: & + msg.what);
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println(&&&&&& Finished to & + msg.target + & & + msg.callback);
// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, &Thread identity changed from 0x&
+ Long.toHexString(ident) + & to 0x&
+ Long.toHexString(newIdent) + & while dispatching to &
+ msg.target.getClass().getName() + & &
+ msg.callback + & what=& + msg.what);
msg.recycleUnchecked();
抓重点看就好。首先是调用myLooper方法获取到Looper对象,这里是没问题的,那就继续
MessageQueue queue = me.mQueue
然后从Looper对象中取出关联的消息队列,
接着进入了一个死循环,调用messagequeue的next方法取出message对象。这个next方法我没看懂,所以不贴源码出来分析了,反正next方法的作用就是取出message对象的。有兴趣的同学自己去研究研究吧。
到这里可以总结一下:通过Looper.prepare()来创建Looper(消息循环)对象,然后通过Looper.loop()来执行消息循环,Looper.prepare()和Looper.loop()通常是成对出现的。
好,回来继续
经过一系列的判断后会来到这里,很重点
msg.target.dispatchMessage(msg);
上面已经分析,msg.target就是handler,那么这行代码的意义就是调用handler的dispatchMessgage的方法去分发消息,
那么看到dispatchMessage的方法源码,相信谜底就要揭开了
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
handleMessage(msg);
从上述的代码跟踪中,都没有发现给message的callback字段赋值,那么我们就先不搭理,默认callback为空,那么就一定会来到handleMessage方法。
message对象传递到了handleMessage方法。
* Subclasses must implement this to receive messages.
public void handleMessage(Message msg) {
handleMessage是一个空方法,需要我们去重写。
至此,海阔天空。完美的从子线程切换到主线程,我不得不说Android的源码设计是多么精彩。
以上就是handler使用sendMessage方法发送消息的源码分析。
为什么我会这么说呢?因为handler还有一种方法可以发送消息,是post方法,理解这个方法。可以解决刚才没搭理的那个message的callback字段的问题。看到post方法源码
public final boolean post(Runnable r)
sendMessageDelayed(getPostMessage(r), 0);
接收一个实现了Runable接口的对象,然后将其传进getPostMessage()方法。跟进getPostMessage()方法看看
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback =
其实就是将Runable包装成message的callback嘛。
所以,如果我们使用post方法发送消息,callback字段是不为空的,那么就会执行handleCallback()方法,而不是执行handleMessage方法了。
handleCallback方法源码如下:
private static void handleCallback(Message message) {
message.callback.run();
直接是调用run方法,表明我们直接在run方法里进行UI操作就行了。
我们发现不管是使用post方法还是sendMessage方法来发送消息,最终都会调用sendMessageDelayed方法。handler将消息追加到消息队列中的过程都是一样的,然后Looper不断的从MessageQueue中取出消息,并由handler去分发消息,处理消息,这样就构成了完善的Android消息机制体系。
呼,终于结束了,搞定了Android的消息机制,才能更深刻的理解Android中的多线程。以后的时间里一起打怪升级。
(责任编辑:最模板)
------分隔线----------------------------
Android的标题栏是很重要的一个模块,App是否易用很大一部分要看...
在Android中,我们经常会用到TextView这个控件,在使用的过程中,...
建造者模式(Builder Pattern)也叫生成器模式,其定义如下: se...
最近android更新了support library, 版本到了23.2, 从官方blog中我们还是...
在数据访问中,内存的访问速度肯定是最快的,所以对于有些文...
CopyRight (C)
最模板 , 深圳奇好科技有限公司 All Rights Reserved.ASP.NET(3)
本文实例总结了C#子线程更新UI控件的方法,对于桌面应用程序设计的UI界面控制来说非常有实用价值。分享给大家供大家参考之用。具体分析如下:
一般在winform C/S程序中经常会在子线程中更新控件的情况,桌面程序UI线程是主线程,当试图从子线程直接修改控件属性时会出现“从不是创建控件的线程访问它”的异常提示。
跨线程更新UI控件的常用方法有两种:
1.使用控件自身的invoke/BeginInvoke方法
2.使用SynchronizationContext的Post/Send方法更新
具体实现如下:
1.使用控件自身的invoke/BeginInvoke方法
Control类实现了ISynchronizeInvoke 接口,我们看该接口的定义:
Control类的invoke方法有两个实现
Object Invoke(Delegate); //在拥有此控件的基础窗口句柄的线程上执行指定的委托
Object Invoke(Delegate,Object[] );
可以看出继承Control类的UI控件都可以使用Invoke方法异步更新。以下代码段实现在子线程中更新Label控件的Text属性
void button6_Click(object
sender, EventArgs e)&
&&&Thread demoThread =new
Thread(new
ThreadStart(threadMethod));&
&&&demoThread.IsBackground =
&&&demoThread.Start();
threadMethod()&
&&&Action&String& AsyncUIDelegate=delegate(string
n){label1.Text=n;};/&span style=&font-family: Arial, Helvetica, sans-&&/定义一个委托&/span&&
&&&label1.Invoke(AsyncUIDelegate,new
object[]{&修改后的label1文本&});&
2.使用SynchronizationContext的Post/Send方法更新
SynchronizationContext类在System.Threading命令空间下,可提供不带同步的自由线程上下文,其中Post方法签名如下:
public virtual void Post(SendOrPostCallback d,Object state)&&& //将异步消息调度到一个同步上下文
可以看出我们要异步更新UI控件,第一是要获取UI线程的上下文了,第二就是调用post方法了,代码实现:
SynchronizationContext _syncContext =
void button6_Click(object
sender, EventArgs e)&
&&Thread demoThread =new
Thread(new
ThreadStart(threadMethod));&
&&demoThread.IsBackground =
&&demoThread.Start();
&&InitializeComponent();&
&&_syncContext = SynchronizationContext.C&
void threadMethod()&
&&&_syncContext.Post(SetLabelText,
&修改后的文本&);
void SetLabelText(object
&&this.lable1.Text = text.ToString();&
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:79065次
积分:1562
积分:1562
排名:千里之外
原创:80篇
转载:27篇
(1)(1)(1)(3)(3)(1)(6)(1)(1)(1)(3)(2)(1)(2)(1)(1)(3)(11)(2)(5)(16)(40)(1)今天看啥 热点:
.NET面试题解析(07)-多线程编程与线程同步,.net多线程编程& 系列文章目录地址:
.NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引
关于线程的知识点其实是很多的,比如多线程编程、线程上下文、异步编程、线程同步构造、GUI的跨线程访问等等,本文只是从常见面试题的角度(也是开发过程中常用)去深入浅出线程相关的知识。如果想要系统的学习多线程,没有捷径的,也不要偷懒,还是去看专业书籍的比较好。
& 常见面试题目:
1. 描述线程与进程的区别?
2. 为什么GUI不支持跨线程访问控件?一般如何解决这个问题?
3. 简述后台线程和前台线程的区别?
4. 说说常用的锁,lock是一种什么样的锁?
5. lock为什么要锁定一个参数,可不可锁定一个值类型?这个参数有什么要求?
6. 多线程和异步有什么关系和区别?
7. 线程池的优点有哪些?又有哪些不足?
8. Mutex和lock有何不同?一般用哪一个作为锁使用更好?
9. 下面的代码,调用方法DeadLockTest(20),是否会引起死锁?并说明理由。
public void DeadLockTest(int i)
lock (this)
//或者lock一个静态object变量
if (i & 10)
Console.WriteLine(i--);
DeadLockTest(i);
10. 用双检锁实现一个单例模式Singleton。
11.下面代码输出结果是什么?为什么?如何改进她?
int a = 0;
System.Threading.Tasks.Parallel.For(0, 100000, (i) =&
Console.Write(a);
& 线程基础
线程是操作系统调度的基本单元。线程是由操作系统来调度和执行的,她的基本状态如下图。
补充一句,CLR线程是直接对应于一个Windows线程的。
还记得以前学校里学习计算机课程里讲到,计算机的核心计算资源就是CPU核心和CPU寄存器,这也就是线程运行的主要战场。操作系统中那么多线程(一般都有上千个线程,大部分都处于休眠状态),对于单核CPU,一次只能有一个线程被调度执行,那么多线程怎么分配的呢?Windows系统采用时间轮询机制,CPU计算资源以时间片(大约30ms)的形式分配给执行线程。
计算鸡资源(CPU核心和CPU寄存器)一次只能调度一个线程,具体的调度流程:
把CPU寄存器内的数据保存到当前线程内部(线程上下文等地方),给下一个线程腾地方;
线程调度:在线程集合里取出一个需要执行的线程;
加载新线程的上下文数据到CPU寄存器;
新线程执行,享受她自己的CPU时间片(大约30ms),完了之后继续回到第一步,继续轮回;
上面线程调度的过程,就是一次线程切换,一次切换就涉及到线程上下文等数据的搬入搬出,性能开销是很大的。因此线程不可滥用,线程的创建和消费也是很昂贵的,这也是为什么建议尽量使用线程池的一个主要原因。
对于Thread的使用太简单了,这里就不重复了,总结一下线程的主要几点性能影响:
线程的创建、销毁都是很昂贵的;
线程上下文切换有极大的性能开销,当然假如需要调度的新线程与当前是同一线程的话,就不需要线程上下文切换了,效率要快很多;
这一点需要注意,GC执行回收时,首先要(安全的)挂起所有线程,遍历所有线程栈(根),GC回收后更新所有线程的根地址,再恢复线程调用,线程越多,GC要干的活就越多;
当然现在硬件的发展,CPU的核心越来越多,多线程技术可以极大提高应用程序的效率。但这也必须在合理利用多线程技术的前提下,了线程的基本原理,然后根据实际需求,还要注意相关资源环境,如磁盘IO、网络等情况综合考虑。
单线程的使用这里就略过了,那太easy了。上面总结了线程的诸多不足,因此微软提供了可供多线程编程的各种技术,如线程池、任务、并行等等。
ThreadPool.QueueUserWorkItem(t =& Console.WriteLine("Hello thread pool"));
每个CLR都有一个线程池,线程池在CLR内可以多个AppDomain共享,线程池是CLR内部管理的一个线程集合,初始是没有线程的,在需要的时候才会创建。线程池的主要结构图如下图所示,基本流程如下:
线程池内部维护一个请求列队,用于缓存用户请求需要执行的代码任务,就是ThreadPool.QueueUserWorkItem提交的请求;
有新任务后,线程池使用空闲线程或新线程来执行队列请求;
任务执行完后线程不会销毁,留着重复使用;
线程池自己负责维护线程的创建和销毁,当线程池中有大量闲置的线程时,线程池会自动结束一部分多余的线程来释放资源;
线程池是有一个容量的,因为他是一个池子嘛,可以设置线程池的最大活跃线程数,调用方法ThreadPool.SetMaxThreads可以设置相关参数。但很多编程实践里都不建议程序猿们自己去设置这些参数,其实微软为了提高线程池性能,做了大量的优化,线程池可以很智能的确定是否要创建或是消费线程,大多数情况都可以满足需求了。
线程池使得线程可以充分有效地被利用,减少了任务启动的延迟,也不用大量的去创建线程,避免了大量线程的创建和销毁对性能的极大影响。
上面了解了线程的基本原理和诸多优点后,如果你是一个爱思考的猿类,应该会很容易发现很多疑问,比如把任务添加到线程池队列后,怎么取消或挂起呢?如何知道她执行完了呢?下面来总结一下线程池的不足:
线程池内的线程不支持线程的挂起、取消等操作,如想要取消线程里的任务,.NET支持一种协作式方式取消,使用起来也不少很方便,而且有些场景并不满足需求;
线程内的任务没有返回值,也不知道何时执行完成;
不支持设置线程的优先级,还包括其他类似需要对线程有更多的控制的需求都不支持;
因此微软为我们提供了另外一个东西叫做Task来补充线程池的某些不足。
//创建一个任务
Task&int& t1 = new Task&int&(n =&
System.Threading.Thread.Sleep(1000);
return (int)n;
//定制一个延续任务计划
t1.ContinueWith(task =&
Console.WriteLine("end" + t1.Result);
}, TaskContinuationOptions.AttachedToParent);
t1.Start();
//使用Task.Factory创建并启动一个任务
var t2 = System.Threading.Tasks.Task.Factory.StartNew(() =&
Console.WriteLine("t1:" + t1.Status);
Task.WaitAll();
Console.WriteLine(t1.Result);
并行Parallel内部其实使用的是Task对象(TPL会在内部创建System.Threading.Tasks.Task的实例),所有并行任务完成后才会返回。少量短时间任务建议就不要使用并行Parallel了,并行Parallel本身也是有性能开销的,而且还要进行并行任务调度、创建调用方法的委托等等。
这是很多开发C/S客户端应用程序会遇到的问题,GUI程序的界面控件不允许跨线程访问,如果在其他线程中访问了界面控件,运行时就会抛出一个异常,就像下面的图示,是不是很熟悉!这其中的罪魁祸首就是,就是&GUI的线程处理模型&。
因为Windows是基于消息机制的,我们在UI上所有的键盘、鼠标操作都是以消息的形式发送给各个应用程序的。GUI线程内部就有一个消息队列,GUI线程不断的循环处理这些消息,并根据消息更新UI的呈现。如果这个时候,你让GUI线程去处理一个耗时的操作(比如花10秒去下载一个文件),那GUI线程就没办法处理消息队列了,UI界面就处于假死的状态。
//1.Winform:Invoke方法和BeginInvoke
this.label.Invoke(method, null);
//2.WPF:Dispatcher.Invoke
this.label.Dispatcher.Invoke(method, null);
② 使用.NET中提供的BackgroundWorker执行耗时计算操作,在其任务完成事件RunWorkerCompleted 中更新UI控件
using (BackgroundWorker bw = new BackgroundWorker())
bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler((ojb,arg) =&
this.label.Text = "anidng";
bw.RunWorkerAsync();
③ 看上去很高大上的方法:使用GUI线程处理模型的同步上下文来送封UI控件修改操作,这样可以不需要调用UI控件元素
.NET中提供一个用于同步上下文的类SynchronizationContext,利用它可以把应用程序模型链接到他的线程处理模型,其实它的本质还是调用的第一步①中的方法。
实现代码分为三步,第一步定义一个静态类,用于GUI线程的UI元素访问封装:
public static class GUIThreadHelper
public static System.Threading.SynchronizationContext GUISyncContext
get { return _GUISyncC }
set { _GUISyncContext = }
private static System.Threading.SynchronizationContext _GUISyncContext =
System.Threading.SynchronizationContext.C
/// &summary&
/// 主要用于GUI线程的同步回调
/// &/summary&
/// &param name="callback"&&/param&
public static void SyncContextCallback(Action callback)
if (callback == null) return;
if (GUISyncContext == null)
callback();
GUISyncContext.Post(result =& callback(), null);
/// &summary&
/// 支持APM异步编程模型的GUI线程的同步回调
/// &/summary&
public static AsyncCallback SyncContextCallback(AsyncCallback callback)
if (callback == null) return
if (GUISyncContext == null) return
return asynresult =& GUISyncContext.Post(result =& callback(result as IAsyncResult), asynresult);
第二步,在主窗口注册当前SynchronizationContext:
public partial class MainWindow : Window
public MainWindow()
InitializeComponent();
CLRTest.ConsoleTest.GUIThreadHelper.GUISyncContext = System.Threading.SynchronizationContext.C
第三步,就是使用了,可以在任何地方使用
GUIThreadHelper.SyncContextCallback(() =&
this.txtMessage.Text = res.ToString();
this.btnTest.Content = "DoTest";
this.btnTest.IsEnabled = true;
& 线程同步构造
多线程编程中很常用、也很重要的一点就是线程同步问题,掌握线程同步对临界资源正确使用、线程性能有至关重要的作用!基本思路是很简单的,就是加锁嘛,在临界资源的门口加一把锁,来控制多个线程对临界资源的访问。但在实际开发中,根据资源类型不同、线程访问方式的不同,有多种锁的方式或控制机制(基元用户模式构造和基元内核模式构造)。.NET提供了两种线程同步的构造模式,需要理解其基本原理和使用方式。
基元线程同步构造分为:基元用户模式构造和基元内核模式构造,两种同步构造方式各有优缺点,而混合构造(如lock)就是综合两种构造模式的优点。
线程1请求了临界资源,并在资源门口使用了用户模式构造的锁;
线程2请求临界资源时,发现有锁,因此就在门口等待,并不停的去询问资源是否可用;
线程1如果使用资源时间较长,则线程2会一直运行,并且占用CPU时间。占用CPU干什么呢?她会不停的轮询锁的状态,直到资源可用,这就是所谓的活锁;
缺点有没有发现?线程2会一直使用CPU时间(假如当前系统只有这两个线程在运行),也就意味着不仅浪费了CPU时间,而且还会有频繁的线程上下文切换,对性能影响是很严重的。
当然她的优点是效率高,适合哪种对资源占用时间很短的线程同步。.NET中为我们提供了两种原子性操作,利用原子操作可以实现一些简单的用户模式锁(如自旋锁)。
System.Threading.Interlocked:易失构造,它在包含一个简单数据类型的变量上执行原子性的读或写操作。
Thread.VolatileRead 和 Thread.VolatileWrite:互锁构造,它在包含一个简单数据类型的变量上执行原子性的读和写操作。
以上两种原子性操作的具体内涵这里就细说了(有兴趣可以去研究文末给出的参考书籍或资料),针对题目11,来看一下题目代码:
int a = 0;
System.Threading.Tasks.Parallel.For(0, 100000, (i) =&
Console.Write(a);
上面代码是通过并行(多线程)来更新共享变量a的值,结果肯定是小于等于100000的,具体多少是不稳定的。解决方法,可以使用我们常用的Lock,还有更有效的就是使用System.Threading.Interlocked提供的原子性操作,保证对a的值操作每一次都是原子性的:
System.Threading.Interlocked.Add(ref a, 1);//正确
下面的图是一个简单的性能验证测试,分别使用Interlocked、不用锁、使用lock锁三种方式来测试。不用锁的结果是95,这答案肯定不是你想要的,另外两种结果都是对的,性能差别却很大。
System.Threading.Tasks.Parallel.For(0, 100, (i) =&
lock (_obj)
a++; //不正确
Thread.Sleep(20);
线程1请求了临界资源,并在资源门口使用了内核模式构造的锁;
线程2请求临界资源时,发现有锁,就会被系统要求睡眠(阻塞),线程2就不会被执行了,也就不会浪费CPU和线程上下文切换了;
等待线程1使用完资源后,解锁后会发送一个通知,然后操作系统会把线程2唤醒。假如有多个线程在临界资源门口等待,则会挑选一个唤醒;
看上去是不是非常棒!彻底解决了用户模式构造的缺点,但内核模式也有缺点的:将线程从用户模式切换到内核模式(或相反)导致巨大性能损失。调用线程将从托管代码转换为内核代码,再转回来,会浪费大量CPU时间,同时还伴随着线程上下文切换,因此尽量不要让线程从用户模式转到内核模式。
她的优点就是阻塞线程,不浪费CPU时间,适合那种需要长时间占用资源的线程同步。
内核模式构造的主要有两种方式,以及基于这两种方式的常见的锁:
基于事件:如AutoResetEvent、ManualResetEvent
基于信号量:如Semaphore
既然内核模式和用户模式都有优缺点,混合构造就是把两者结合,充分利用两者的优点,把性能损失降到最低。大概的思路很好理解,就是如果是在没有资源竞争,或线程使用资源的时间很短,就是用用户模式构造同步,否则就升级到内核模式构造同步,其中最典型的代表就是Lock了。
常用的混合锁还不少呢!如SemaphoreSlim、ManualResetEventSlim、Monitor、ReadWriteLockSlim,这些锁各有特点和锁使用的场景。这里主要就使用最多的lock来详细了解下。
lock的本质就是使用的Monitor,lock只是一种简化的语法形式,实质的语法形式如下:
bool lockTaken = false;
Monitor.Enter(obj, ref lockTaken);
if (lockTaken) Monitor.Exit(obj);
那lock或Monitor需要锁定的那个对象是什么呢?注意这个对象才是锁的关键,在此之前,需要先回顾一下引用对象的同步索引块(AsynBlockIndex),这是前面文章中提到过的引用对象的标准配置之一(还有一个是类型对象指针TypeHandle),它的作用就在这里了。
同步索引块是.NET中解决对象同步问题的基本机制,该机制为每个堆内的对象(即引用类型对象实例)分配一个同步索引,她其实是一个地址指针,初始值为-1不指向任何地址。
创建一个锁对象Object obj,obj的同步索引块(地址)为-1,不指向任何地址;
Monitor.Enter(obj),创建或使用一个空闲的同步索引块(如下图中的同步块1),(图片来源),这个才是真正的同步索引块,其内部结构就是一个混合锁的结构,包含线程ID、递归计数、等待线程统计、内核对象等,类似一个混合锁AnotherHybridLock。obj对象(同步索引块AsynBlockIndex)指向该同步块1;
Exit时,重置为-1,那个同步索引块1可以被重复利用;
首先还是尽量避免线程同步,不管使用什么方式都有不小的性能损失。一般情况下,大多使用Lock,这个锁是比较综合的,适应大部分场景。在性能要求高的地方,或者根据不同的使用场景,可以选择更符合要求的锁。
在使用Lock时,关键点就是锁对象了,需要注意以下几个方面:
这个对象肯定要是引用类型,值类型可不可呢?值类型可以装箱啊!你觉得可不可以?但也不要用值类型,因为值类型多次装箱后的对象是不同的,会导致无法锁定;
不要锁定this,尽量使用一个没有意义的Object对象来锁;
不要锁定一个类型对象,因类型对象是全局的;
不要锁定一个字符串,因为字符串可能被驻留,不同字符对象可能指向同一个字符串;
不要使用[pilerServices.MethodImpl(MethodImplOptions.Synchronized)],这个可以使用在方法上面,保证方法同一时刻只能被一个线程调用。她实质上是使用lock的,如果是实例方法,会锁定this,如果是静态方法,则会锁定类型对象;
& 题目答案解析:
1. 描述线程与进程的区别?
一个应用程序实例是一个进程,一个进程内包含一个或多个线程,线程是进程的一部分;
进程之间是相互独立的,他们有各自的私有内存空间和资源,进程内的线程可以共享其所属进程的所有资源;
2. 为什么GUI不支持跨线程访问控件?一般如何解决这个问题?
因为GUI应用程序引入了一个特殊的线程处理模型,为了保证UI控件的线程安全,这个线程处理模型不允许其他子线程跨线程访问UI元素。解决方法还是比较多的,如:
利用UI控件提供的方法,Winform是控件的Invoke方法,WPF中是控件的Dispatcher.Invoke方法;
使用BackgroundWorker;
使用GUI线程处理模型的同步上下文SynchronizationContext来提交UI更新操作
上面几个方式在文中已详细给出。
3. 简述后台线程和前台线程的区别?
应用程序必须运行完所有的前台线程才可以退出,或者主动结束前台线程,不管后台线程是否还在运行,应用程序都会结束;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。
通过将 Thread.IsBackground 设置为 true,就可以将线程指定为后台线程,主线程就是一个前台线程。
4. 说说常用的锁,lock是一种什么样的锁?
常用的如如SemaphoreSlim、ManualResetEventSlim、Monitor、ReadWriteLockSlim,lock是一个混合锁,其实质是Monitor['m?n?t?]。
5. lock为什么要锁定一个参数,可不可锁定一个值类型?这个参数有什么要求?
lock的锁对象要求为一个引用类型。她可以锁定值类型,但值类型会被装箱,每次装箱后的对象都不一样,会导致锁定无效。
对于lock锁,锁定的这个对象参数才是关键,这个参数的同步索引块指针会指向一个真正的锁(同步块),这个锁(同步块)会被复用。
6. 多线程和异步有什么关系和区别?
多线程是实现异步的主要方式之一,异步并不等同于多线程。实现异步的方式还有很多,比如利用硬件的特性、使用进程或纤程等。在.NET中就有很多的异步编程支持,比如很多地方都有Begin***、End***的方法,就是一种异步编程支持,她内部有些是利用多线程,有些是利用硬件的特性来实现的异步编程。
7. 线程池的优点有哪些?又有哪些不足?
优点:减小线程创建和销毁的开销,可以复用线程;也从而减少了线程上下文切换的性能损失;在GC回收时,较少的线程更有利于GC的回收效率。
缺点:线程池无法对一个线程有更多的精确的控制,如了解其运行状态等;不能设置线程的优先级;加入到线程池的任务(方法)不能有返回值;对于需要长期运行的任务就不适合线程池。
8. Mutex和lock有何不同?一般用哪一个作为锁使用更好?
Mutex是一个基于内核模式的互斥锁,支持锁的递归调用,而Lock是一个混合锁,一般建议使用Lock更好,因为lock的性能更好。
9. 下面的代码,调用方法DeadLockTest(20),是否会引起死锁?并说明理由。
public void DeadLockTest(int i)
lock (this)
//或者lock一个静态object变量
if (i & 10)
Console.WriteLine(i--);
DeadLockTest(i);
不会的,因为lock是一个混合锁,支持锁的递归调用,如果你使用一个ManualResetEvent或AutoResetEvent可能就会发生死锁。
10. 用双检锁实现一个单例模式Singleton。
public static class Singleton&T& where T : class,new()
private static T _I
private static object _lockObj = new object();
/// &summary&
/// 获取单例对象的实例
/// &/summary&
public static T GetInstance()
if (_Instance != null) return _I
lock (_lockObj)
if (_Instance == null)
var temp = Activator.CreateInstance&T&();
System.Threading.Interlocked.Exchange(ref _Instance, temp);
11.下面代码输出结果是什么?为什么?如何改进她?
int a = 0;
System.Threading.Tasks.Parallel.For(0, 100000, (i) =&
Console.Write(a);
输出结果不稳定,小于等于100000。因为多线程访问,没有使用锁机制,会导致有更新丢失。具体原因和改进在文中已经详细的给出了。
版权所有,文章来源:/anding
个人能力有限,本文内容仅供学习、探讨,欢迎指正、交流。
.NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引
& 参考资料:
书籍:CLR via C#
书籍:你必须知道的.NET
.NET基础拾遗(5)多线程开发基础
归纳一下:C#线程同步的几种方法
C#并行编程-相关概念
多线程之旅七&&GUI线程模型,消息的投递(post)与处理(IOS开发前传)
C# 温故而知新: 线程篇(一)
相关搜索:
相关阅读:
相关频道:
&&&&&&&&&&&&&&&&
C#教程最近更新

我要回帖

更多关于 c语言接收字符串 的文章

 

随机推荐