对于linux下的多线程而言,这里我们需要区分几个概念:
1、信号量
2、互斥变量(递归和非递归)
3、条件变量
4、共享锁(读写锁)(适用于读的次数远大于写的情况)
信号量(sem)相当于是操作系统中PV操作的实现,支持wait和post操作,当信号量的值为0的时候,wait操作将会阻塞当前线程,而当post操作之后,信号量的值将递增1,阻塞线程将会恢复运行状态。信号量不一定是资源的锁定,也可以是某些计算处理的发生。
互斥变量(mutex)强调的是资源的共享使用,当试图使用资源的时候必须先尝试获取互斥变量进行加锁操作(lock),如果当前互斥变量被其他线程占用,则阻塞当前线程并等待解锁,互斥变量不适合于等待某种条件满足而阻塞等待的情况,因为等待的时间并无法确定。它包括 4 种操作,分别是创建,销毁,加锁和解锁。
条件变量(condition)强调的是对于资源或者某种条件满足的等待,它需要和互斥变量结合使用,它弥补了互斥变量不确定等待所导致的缺陷问题。它包括5 种操作:创建,销毁,触发,广播和等待。这里需要注意的是,条件变量本身由互斥量保护,所以在改变条件状态前必须锁住互斥量。也就是说,pthread_cond_wait和pthread_cond_timewait必须在mutex的锁定区域内使用。此外,pthread_cond_signal唤醒等待该条件的某个线程,pthread_cond_broadcast唤醒等待该条件的所有线程,可以认为,pthread_cond_signal是对pthread_cond_broadcast的优化,signal效率较broadcast高些(When in doubt, broadcast!)。这里建议在 Linux 平台上要出发条件变量之前要检查是否有等待的线程,只有当有线程在等待时才对条件变量进行触发。
共享锁(rw_lock)支持读共享,写互斥,它适用于读的情况多于写的情况。
对于上述的各种同步机制,都有相应的初始化方式,其中包括静态初始化(宏初始化)和动态初始化(*_init)。
需要清楚的是,linux系统中线程程序库是POSIX pthread,该线程库是基于C进行的,而我们知道C语言是面向过程的编程语言,而OO思想(面向对象思想)则更适合大部分的程序设计,那么如何采用C++来实现多线程编程则成为了一个必须解决的问题。对于多线程的创建,需要提供正确的线程函数的地址,然而,对于C++的类而言,类成员是在类被实例化成为对象后才存在的,即在编译时是不存在的,编译器无法取得函数的确切入口地址,自然无法通过编译。而为了解决函数地址的确定问题,可以采用静态类成员的方式,但是静态成员只能调用类的其他静态成员函数,这样有可能导致所有的类成员最后都成为静态成员。值得注意的是,线程函数允许传入一个参数,这个参数为解决上述的问题提供了可能性。具体的做法是,线程函数仍未静态成员,但是传入的参数则是某个类实例的指针,然后在线程函数里面调用具体的成员函数,以此来解决静态成员函数的调用问题。
对于多线程的释放问题,这里涉及到线程的属性问题,线程在创建的时候可以指定线程的状态,包括joinable和detach两种状态,对于前者,线程的资源由主线程来负责释放,并且同一个线程只能被一个线程执行join操作,即等待结束,然后由等待线程负责对线程的资源进行释放。而对于后者,当线程结束的时候,由操作系统负责线程资源的回收。两种方式确保在使用多线程的时候避免潜在的内存泄漏问题。需要注意的是,如果设置一个线程的分离属性,而这个线程运行又非常快,那么它很可能在pthread_create 函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这时调用pthread_create 的线程就得到了错误的线程号。而线程的结束有三种方式:一种是线程函数运行结束并返回,一种是线程函数调用pthread_exit退出,最后一种是由其他线程调用线程结束函数pthread_cancel来结束指定线程,而被指定线程则在函数结束点尝试结束线程活动,这里的函数结束点通常是sleep函数运行处(延迟取消)。
根据对linux内核的学习,进程的创建需要在内核表中创建相应的结构体,任何拷贝父进程的所有资源,这里的拷贝也只是写时复制的方式,然后对CPU的一些参数(寄存器的状态)做必要的调整。而对于线程而言,它则是轻量级的进程,线程按照其调度者可以分为用户级线程和核心级线程两种。不同的线程库可能采用不同的方式使线程和进程进行对应,包括NPTL(Native POSIX Threading Library)和NGPT(NextGeneration POSIX Threads)。运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间。
对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信(进程间的通信)的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享则存在互斥访问问题以及线程之间的同步问题。
对于mutex应该多大的问题,这里的大小是相对的,如mutex锁定到解锁之间的代码只有一行,比起有10行的就小了。原则是:尽可能大,但不要太大(As big as neccessary, but no bigger)。考虑下面的因素:
1> mutex并不是免费的,是有开销的,不要太小了,太小了程序只忙于锁定和解锁了。
2> mutex锁定的区域是线性执行的,若太大了,没有发挥出并发的优越性。
3> 自己掂量1和2,根据实际情况定,或者尝试着去做。