一、多线程使用情景:
1.用户需要同时得到多个反馈,例如下载过程中进度条改变,读取文件的时候显示结果。
2.提高程序执行性能,提高CPU使用效率,。
多线程的主要是需要处理大量的IO操作或者处理的情况需要花大量的时间等等,比如读写文件,网络数据接收,视频图像的采集,处理显示保存等 操作缓慢的情形和需大幅度的提高性能的程序 中使用。
但也不是都使用多线程,因为多线程过多的线程一般会导致数据共享问题,太多多线程切换也是会影响性能的,所以一般不须采用多线程的不用多线程效果更好。
二、多线程原理和类型:
1.多线程原理
线程是分配CPU资源的最小单位,单CPU多线程是时间轮片的切换,多CPU可以真正的做到多CPU同时工作。
2.多线程具体的实现
每个线程具有自己的栈空间和CPU寄存器副本, 多个线程可以执行同一份函数(私有空间) 。 其他资源(如文件、全局变量、静态数据、堆内存、同步锁变量、组件对象)由进程中的所有线程共享(共有资源) 。 使用这些公共资源的线程必须同步。 Win32 提供了几种同步资源的方式,包括 信号、临界区、事件和互斥体 。
3.线程的堆栈
说一下线程自己的堆栈问题。
是的,生成子线程后,它会获取一部分该进程的堆栈空间,作为其名义上的独立的私有空间。(为何是名义上的呢?)由于,这些线程属于同一个进程,其他 线程只要获取了你私有堆栈上某些数据的指针,其他线程便可以自由访问你的名义上的私有空间上的数据变量。(注:而多进程是不可以的,因为不同的进程,相同 的虚拟地址,基本不可能映射到相同的物理地址)
4.创建线程的类型
这个多线程的例子应该很明了了,主线程做自己的事情,生成2个子线程,task1为分离,任其自生自灭,而task2还是继续送外卖,需要等待返回。(因该还记得前面说过僵尸进程吧,线程也是需要等待的。如果不想等待,就设置线程为分离线程)。
1)自生自灭类型:
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); //设置attr结构为分离
pthread_create(&pid1, &attr, task1, NULL); //创建线程,返回线程号给pid1,线程属性设置为attr的属性,线程函数入口为task1,参数为NULL
2)继续等待类型:
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
pthread_create(&pid2, &attr, task2, NULL);
//前台工作
ret=pthread_join(pid2, &p); //等待pid2返回,返回值赋给p
printf("after pthread2:ret=%d,p=%d/n", ret,(int)p);
三、线程安全函数:
1)可重入函数:概念基本没有比较正式的完整解释,但是它比线程安全要求更严格。根据经验,所谓“重入”,常见的情况是,程序执行到某个函数foo()时,收到信号,于是暂停目前正在执行的函数,转到信号处理函数,而这个信号处理函数的执行过程中,又恰恰也会进入到刚刚执行的函数foo(),这样便发生了所谓的重入。此时如果foo()能够正确的运行,而且处理完成后,之前暂停的foo()也能够正确运行,则说明它是可重入的, 反复调用都得到正确的结果。
可重入函数的判断条件:
要确保函数可重入,需满足以下几个条件:
1、不在函数内部使用静态或全局数据
2、不返回静态或全局数据,所有数据都由函数的调用者提供。
3、使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
4、不调用不可重入函数。
可重入与线程安全并不等同,一般说来,可重入的函数一定是线程安全的,但反过来不一定成立。
2)线程安全函数:
要 确保函数线程安全,主要需要考虑的是线程之间的共享变量。属于同一进程的不同线程会共享进程内存空间中的全局区和堆,而私有的线程空间则主要包括栈和寄存 器。因此,对于同一进程的不同线程来说,每个线程的局部变量都是私有的,而全局变量、局部静态变量、分配于堆的变量都是共享的。在对这些共享变量进行访问 时,如果要保证线程安全,则必须通过加锁的方式。
判断线程安全函数的条件:
如果是可重入的函数那么是线程安全的,如果是线程不安全的函数,那么需要通过对共享数据(全局变量/静态变量,文件对象,堆内存)进行加锁同步(事件,信号,临界区,互斥体)可以转换为线程安全的函数。
线程函数和线程调用的函数要求细则:
1.正确的要求,有子线程回调调用那么要求可重入的,否则至少是线程安全的:
多线程中调用,如果是一个线程来回(回调)反复调用的那么要求是可重入的函数;如果是没有回调的普通线程调用的函数那么需要是线程安全的函数。
2.不区分直接调用导致的后果:
如果对可重入函数,线程安全函数不做处理,那么会导致不可预料的后果:
那在多线程调用的情况下,可能导致的后果是显而易见的——共享变量的值由于不同线程的访问,可能发生不可预料的变化,进而导致程序的错误,甚至崩溃。
“线程安全”是一个什么概念?
以前常听高手告诫MFC对象不要跨线程使用,因为MFC不是线程安全的。比如CWnd对象不要跨线程使用,可以用窗口句柄(HWND)代替。 CSocket/CAsyncSocket对象不要跨线程使用,用SOCKET句柄代替.那么到底什么是线程安全呢?什么时候需要考虑?如果程序涉及到多 线程的话,就应该考虑线程安全问题。比如说设计的接口,将来需要在多线程环境中使用,或者需要跨线程使用某个对象时,这个就必须考虑了。关于线程安全也没 什么权威定义。在这里我只说说我的理解:所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不 用考虑同步的问题。
一般而言“线程安全”由多线程对共享资源的访问引起。如果调用某个接口时需要我们自己采取同步措施来保护该接口访问的共享资源,则这样的接口不是线程安全 的.MFC和STL都不是线程安全的. 怎样才能设计出线程安全的类或者接口呢?
1)如果接口中访问的数据都属于私有数据,那么这样的接口是线程安全的.
2)或者几个接口对共享数据都是只读操作,那么这 样的接口也是线程安全的
3)如果多个接口之间有共享数据,而且有读有写的话,如果设计者自己采取了同步措施,调用者不需要考虑数据同步问题,则这样的接口是 线程安全的,否则不是线程安全的。
四、线程不安全函数例子-操作其它线程的界面控件出错
一个线程调用外部主线程的函数代码,实现回调的功能不知道行不行?
是可以的,会维护一份自己的栈和寄存器信息,但是系统一般对于线程安全的函数是可以调用的,线程不安全的函数是会运行时报错的(例如一个工作线程调用函数处理一个界面线程的控件,C#中可以用委托实现,C/C++中可以发送消息给界面线程由界面线程处理)。
线程间不安全函数调用失败原因:
因为控件是窗口,窗口是和显示器驱动相关的,因此窗口对象的写不是线程安全的,所以window的线程安全策略不允许子线程中对窗口这种线程不安全的对象进行写操作。最好的办法是发送消息,通知主线程更新控件内容。
MFC子线程中可以用发送消息给主线程实现对控件的操作,主线程通过消息映射函数,收到消息更新界面信息。
.NET中可以通过委托实现子线程对主线程控件的操作。
cocos2dx中是通过CPP引擎层主线程的更新,设置进度条百分比,然后回调到lua里面的函数实现界面控件的更新,而下载和解压线程是子线程,下载解压子线程通过发送消息到更新信息队列,主线程通过互斥锁实现访问更新队列。也就是通过子线程发送消息,主线程每帧去无阻塞的取消息,如果取到消息那么回调实现界面的更新。
cocos2dx中是纹理读取,xml/json文件读取,网络下载,网络消息接收中使用了多线程。
五 、多线程的程序设计应该注意的内容
1、 尽量少的使用全局变量、static变量做共享数据,尽量使用参数传递对象。被参数传递的对象,应该只包括必需的成员变量。所谓必需的成员变量,就是 必定会被多线程操作的 。很多人图省事,会把this指针(可能是任意一个对象指针)当作线程参数传递,致使线程内部有过多的操作权限,对this中的参数 任意妄为。整个程序由一个人完成,可能会非常注意,不会出错,但只要一转手,程序就会面目全非。当两个线程同时操作一个成员变量的时候,程序就开始崩溃 了,更糟的是,这种错误很难被重现。(我就在郁闷这个问题,我们是几个人,把程序编成debug版,经过数天使用,才找到错误。而找到错误只是开始,因为 你要证明这个bug被修改成功了,也非常困难。)其实,线程间数据交互大多是单向的,在线程回调函数入口处,尽可能的将传入的数据备份到局部变量中(当 然,用于线程间通讯的变量不能这么处理),以后只对局部变量做处理,可以很好的解决这种问题。
2、 在MFC中请慎用线程。因为MFC的框架假定你的消息处理都是在主线程中完成的。首先窗口句柄是属于线程的,如果拥有窗口句柄的线程退出了,如果另一 个线程处理这个窗口句柄,系统就会出现问题。 而MFC为了避免这种情况的发生,使你在子线程中调用消息(窗口)处理函数时,就会不停的出Assert错 误,烦都烦死你。典型的例子就时CSocket,因为CSocket是使用了一个隐藏窗口实现了假阻塞,所以不可避免的使用了消息处理函数,如果你在子线 程中使用CSocket,你就可能看到assert的弹出了。
3、 不要在不同的线程中同时注册COM组件 。两个线程,一个注册1.ocx, 2.ocx, 3.ocx, 4.ocx; 而另一个则注册5.ocx, 6.ocx, 7.ocx, 8.ocx,结果死锁发生了,分别死在FreeLibrary和DllRegisterServer,因为这8个ocx是用MFC中做的,也可能是MFC 的Bug,但DllRegisterServer却死在GetModuleFileName里,而GetModuleFileName则是个API唉!如 果有过客看到,恰巧又知道其原因,请不吝赐教。
4、 不要把线程搞的那么复杂。很多初学者,恨不能用上线程相关的所有的函数,这里互斥,那里等待,一会儿起线程,一会儿关线程的,比起goto语句有过之 而无不及。好的多线程程序,应该是尽量少的使用线程。这句话怎么理解呐,就是说尽量统一一块数据共享区存放数据队列,工作子线程从队列中取数据,处理,再 放回数据,这样才会模块化,对象化 ;而不是每个数据都起一个工作子线程处理,处理完了就关闭,写的时候虽然直接,等维护起来就累了。