只要是进行Linux应用开发,多线程编程肯定是绕不开的,而线程和进程是两种有千丝万缕关系的模型,本文先不去讲太书面化的定义,先从一个入门+使用的角度来分析多线程。
先说几个总结性的关键句:
1、进程是资源分配的最小单位,线程是CPU调度的最小单位
这句话的意思,关键是最后一句,线程是CPU调度的最小单位,也就是说,本质上操作系统调度的是线程 而不是进程,因为任何一个进程都包含一个 主线程,所以操作系统是对线程进行调度的,这一点,也 解释了,为什么在 线程中做各种循环执行程序(do..while、for、while)时,程序不会死机了,因为线程本身就是会被CPU循环调度。
2、与 进程相比,创建的子线程从主线程那继承了什么?
我们知道父子进程间的继承是很强大的,基本上可以认为是数据段、代码段、堆栈段全盘复制,这就意味着,子进程不仅继承代码、堆栈、甚至会继承“中间变量”,不过这里的“继承”具体到代码是拷贝副本,举例来说,在父进程中定义了一个局部变量var,那么子进程中也可以对var进行读写,尤其是写var时,此var已经不是彼var了,也就是说“同名不同意”,当然了,因为 在linux中 广泛采用了一个技术叫copy-on-write(写时拷贝),这就意味着先不直接全盘拷贝副本,而是在进行对应“写”操作时,才会进行copy。
对于线程而言,并没有继承数据段和代码段,包括栈,而线程最大的优势是,多线程是共享地址空间的,从一个线程切换到同一个进程下的另一个线程运行,页表、数据区等很多 都已经在内存或缓存里,而从一个进程切换到 另一个进程,由于进程的空间都是独立的,所以切换就涉及到开销。 这一点其实对于现在的硬件 来讲,尤其是程序员来说,整体速度 上影响不大,只是不方便同步。所以线程并不会继承那些看得见的东西(比如变量、代码等)。
3、线程之间的同步
所谓“同步”,其实可以理解为线程或进程间的“遵纪守法不插队”,最简单的举例就是大家同时访问 共同变量或 内存时, 有先有后,不要互相抢,因为 互相抢的结果,只能是紊乱,反而不好。
由于子线程和父线程共享地址空间,可以理解成同一进程下的线程之间是单片机的裸机程序下的不同功能,那么互相同步的时候就比较简单了,可以使用全局变量、信号量,当然还有一些更高级的同步方式,本文只介绍常用的。
全局变量是最方便也 是最容易的一种同步方式,尽管 全局变量有很多的缺点,不过对于简单的功能,该用的时候还是要用。
信号量: 用的比较多的就是互斥型信号量,任意的线程想要访问一个资源,需要先等,一旦得到这个资源后,就先上锁,这个可以打一个不太 恰当的例子,厕所里只有一个蹲坑,蹲坑就是“资源”, 想蹲坑的人就是线程,如果只有一个人,也就无所谓了,而如果是多个人都想蹲坑,那就需要注意了,一旦轮到谁蹲坑,那么就需要把门锁上,而那些也像蹲坑的人,就需要在门外面挨个等着(这里没有先后的,就看谁快),等到蹲坑完成,把门打开,也就是开锁,以此类推。
Linux下互斥信号量的两个配套函数如下:
pthread_mutex_t xLock = PTHREAD_MUTEX_INITIALIZER;
( void )pthread_mutex_lock( &xLock );
( void )pthread_mutex_unlock( &xLock );
其中“xLock”就是那个互斥信号量,也就是上面例子中的那把“锁”。
其实这是有好处的,因为这样 比较安全。这里插一句,好的程序代码,肯定是需要有天才的创意,但是对于大多数不是天才的我们,如何写出好的代码呢?个人愚见,我们需要敬畏代码,需要严谨,更需要“遵纪守法”,而多线程之间的同步,恰恰就是遵纪守法的体现,不管你的程序功能多复杂,但是终归是要拆分成一个个小的单元对象,而各对象之间,需要有条理顺序的去执行,越是排队,效率和稳定性会越好。
接下来,我们看 一个简单的多线程程序:
#include
#include
#include
#include
typedef int u16;
typedef char u8;
void SetThreadState(u8 stat );
void GetThreadState(u8 *stat );
static pthread_mutex_t xLock = PTHREAD_MUTEX_INITIALIZER;
static u8 thread_stat = 0;
u16 cnt = 0;
void print_hello_linux(void)
{
u8 stat;
while( 1 )
{
GetThreadState( &stat );
if( stat == 20)
pthread_exit(0);
else
{
printf("thread cnt:%d\n",cnt);
sleep(1);
}
}
}
int
main( void )
{
pthread_t pid;
pthread_create(&pid, NULL, (void *)print_hello_linux, NULL);
while( 1 )
{
cnt++;
sleep(1);
SetThreadState( ( u8 )cnt );
printf("master cnt:%d\n",cnt);
}
pthread_join(pid, NULL);
exit(0);
}
void SetThreadState(u8 stat )
{
( void )pthread_mutex_lock( &xLock );
thread_stat = stat;
( void )pthread_mutex_unlock( &xLock );
}
void GetThreadState(u8 *stat )
{
( void )pthread_mutex_lock( &xLock );
*stat = thread_stat;
( void )pthread_mutex_unlock( &xLock );
}
这个程序看起来简单,但是包含的信息却又很多:
1)线程创建函数:pthread_create(&pid, NULL, (
void
*)print_hello_linux, NULL);
2)线程退出函数:
pthread_exit(0);,当然了,线程也可以自然退出,也就是程序执行完毕,跳出大括号{}。
3)线程擦屁股函数:
pthread_join(pid, NULL);,这个函数一般是在主线程中调用, 它的作用就是给子线程“擦屁股”,具体为,当子线程退出或执行完毕,我们需要使用该函数回收线程占用的资源。不过要特别注意,这个函数会阻塞等待, 简单的讲,就是这个函数比较一根筋,如果把这个函数放到主线程的while循环体内,那主线程就不能干别的事儿了,因为执行到它时,它会原地踏步等待。
4) 线程间同步方式-互斥信号量:
void SetThreadState(u8 stat )
{
( void )pthread_mutex_lock( &xLock );
thread_stat = stat;
( void )pthread_mutex_unlock( &xLock );
}
void GetThreadState(u8 *stat )
{
( void )pthread_mutex_lock( &xLock );
*stat = thread_stat;
( void )pthread_mutex_unlock( &xLock );
}
通过xxx_lock和xxx_unlock函数, 进行加锁和去锁的去访问那个资源 thread_stat,可能有些人会觉得这个是不是多此一举啊,完全可以把thread_stat当成全局变量使用啊?,其实不然,这个就恰恰说明了前面讲的编程理念,这样做 虽然繁琐,但是更严谨,也更安全。全局变量的缺点介绍,可以参考文章:《全局变量的优缺点及为什么要少用全局变量》
还有一点特别 重要:我们使用1个线程,一般想实现2种目的,要么是希望它 像个任务一样,有条件的循环执行,比如循环处理一些“计算活”,比如返回数据解析、计算等,这个时候就要用while等循环框架,只要线程没有执行完自己或者别别人强退出,操作系统都会不停的调度它的。第2种目的,就像是一锤子买卖,比如一次接收 了大量的数据,但是主程序并不想费那功夫,毕竟要把主要的资源用在整体流程把控上,这个时候就需要这个线程去做这些“体力活”,反正它不会影响大局,重点不会阻塞CPU。
主线程中,不用上来就 pthread_join给子线程擦屁股,因为这个函数会直接阻塞CPU,但是这个功能还是要有的,位置需要放在主线程退出前,但是又必须是在主线程的while循环体外。