学习第七章的时候,提到了进程和线程的区别。线程是计算机中独立运行的最小单位,运行时占用很少的系统资源。在用户看来,多个线程是同时执行,但从操作系统调度来看,各个线程是交替执行。系统不停的在各个线程之间切换,每个线程只有在系统分配给它的时间片内才能取得CPU的控制权,执行线程中的代码。
线程创建
如果在主线程里面创建线程,程序就会在创建线程的地方产生分支,变成两个程序执行。这似乎和躲进程一样,其实不然。子进程是通过拷贝父进程的地址空间来实现的;而线程与进程内的线程共享程序代码,一段代码可以同时被多个线程执行。线程创建pthread_create();
线程终止
Linux下有两种方式可以使线程终止。第一种是通过return
从线程函数返回,第二种是通过调用函数pthread_exit()
使线程退出,pthread_exit
在头文件 pthrea.h中声明。
有两种特殊情况需要注意,一种情况是在主线程中,如果从main函数返回或是调用了exit
退出主进程,则整个进程将终止。此时进程中所有的线程也将终止,因此主线程中不能过早地从main
函数返回,另一种情况是如果主线程调用pthread_exit
函数,则仅仅是主线程消亡,进程不会结束,进程内的其他线程也不会终止,知道所有的线程结束,进程才会结束。
线程终止最重要的问题是资源释放的问题,特别是一些临界资源。临界资源在一段时间内只能被一个线程所持有。当线程要使用临界资源时需提出请求,如果该资源未被使用则申请成功,否则等待。临界资源是用完必后要释放以便其他线程可以使用。临界资源为一个线程所独占,当一个线程终止时,如果不释放其占有的临界资源,则该资源会被认为还被已经退出的线程所使用,因为永远不会得到释放。如果一个线程在等待使用这个临界资源,它就可能无限的等待下去,这就形成了死锁,而这往往是灾难性的。
为此,Linux系统提供了一对函数:pthread_cleanu_push()
、pthread_cleanup_pop()
用于自动释放资源,从pthread_cleanup_push()
调用点到pthread_cleanup_pop()
之间的程序段中的终止动作都将执行pthread_cleanup_push()
所指定的清理函数。(注意这两个函数必须成对存在)
线程终止时另外一个需要注意的问题是线程间的同步问题。一般情况下,进程中各个线程的运行是相互独立的,线程的终止并不会相互通知,也不会影响其他线程,终止的线程所占用的系统资源不会随着线程的终止而归还系统,而是仍为线程所在的进程持有。正如进程之间可以使用wait()
系统调用来等待其他进程结束一样,线程也有类似的函数:pthread_join()
函数。
函数pthread_join
用来等待一个线程的结束。pthread_join()
的调用者将被挂起并等待线程终止。需要注意的是一个线程仅允许一个线程使用pthread_join()
等待它的终止,并且被等待的线程应该处于可join状态。
一个可”join”的线程所占用的内存仅当有线程对其执行了pthread_join()
后才会释放,因此为了避免内存的泄漏,所有线程终止时,要么已被设为DETACHED,要么使用pthread_join()
来回收资源。
私有数据
在多线程环境下,进程内的所有线程共享进程的数据空间,因此全局变量为所有线程共有,在程序设计中有时需要保存线程自己的全局变量,这还总特殊的变量尽在某个线程内部有效。比如常见的变量errno
,它返回标准的出错码。errno
不应该是一个局部变量,否则在一个线程里输出的很可能是另一个线程的出错信息,这个问题可以通过创建线程的私有数据(TSD)来解决。在线程内部,线程私有数据可以被各个函数访问,但它对其他线程是屏蔽的。
私有数据采用了一种被称为一键多值的技术,即一个键对应多个数。访问数据时都是通过键值来访问,好象是对一个变量进行访问,其实在访问不同的数据。使用线程私有数据时,首先要为每个线程数据创建一个相关联的键。在各个线程内部,都使用这个公共的键来指代线程数据,但是,在不同的线程中,这个键代表的数据是不同的。操作线程私有数据主要有四个:pthread_key_create
(创建一个键),pthread_key_setspecific
(为一个键设置线程私有数据),pthread_key_getspecific
(从一个键读取线程私有数据),pthread_key_delete
(删除一个键)。
key
一旦被创建,所有线程都可以访问它,但各线程可根据自己的需要往key
填入不同的值,这就相当于提供了一个同名而不同值的全局变量,一键多值。一键多值靠的是一个关键数据结构数组,即TSD池
。
线程同步
互斥锁通过锁机制来实现线程间的同步,在同一个时刻它通常只允许一个线程执行一个关键部分的代码。
使用互斥锁
前必须先进行初始化操作。初始化有两种方式,一种是静态赋值法,将宏结构常量赋给互斥锁,另外一种方式是通过pthread_mutex_init
函数初始化互斥锁。初始化后就可以给给互斥锁加锁了。加锁有两个函数:pthread_mutex_lock()
和pthread_mutex_unlock()
。
用pthread_mutex_lock()
加锁的时候,如果mutex已经被锁住,当前尝试加锁的进程就会阻塞,知道互斥锁被其他线程释放,当pthread_mutex_lock
函数返回时,说明互斥锁已经被当前进程成功加锁。pthread_mutex_trylock
函数则不同,如果mutex已经被加锁,它将立即返回,返回的错误码为EBUSY,而不是阻塞等待。
用pthread_mutex_unlock
函数解锁时,要满足两个条件:意识互斥锁必须处于加锁状态,而是调用本函数的线程必须是给互斥锁加锁的线程。解锁后如果有其他线程在等待互斥锁,等待队列中的第一个将获得互斥锁。
当一个互斥锁使用完毕后,必须进行清除,清除互斥锁使用函数pthread_mutex_destroy
。
清除一个互斥锁意味着释放它所占用的资源。清除锁时要求当前处于开放状态,若锁处于锁定状态,函数放回EBUSY,该函数成功之行时返回0。由于在Linux中,互斥锁并不占用内存,因此pthread_mutex_destroy()
除了解除互斥锁的状态外没有其他操作。
条件变量
条件变量是利用线程见共享的全局变量进行同步的一种机制。条件变量宏观上类似if语句,符合条件就能执行某段程序,否则只能等待条件成立。
使用条件变量主要包括两个动作:一个等待使用资源的线程等待”条件变量被设置为真”;另一个线程在使用完资源后”设置条件为真”,这样就可以保证线程间的同步了。这样就存在一个关键问题,这就是要保证条件变量能被正确的修改,条件变量要受到特殊的保护,实际使用中互斥锁扮演者这样一个保护者的角色。Linux也提供了一系列对条件变量操作的函数。
与互斥锁一样,条件变量的初始话也有两种方式,一种是静态赋值法,将宏结构常量PTHREAD_COND_INITIALIZER赋给互斥锁。另一种方式是使用函数pthread_cond_init
。
pthread_cond_wait
函数释放有mutex指向的互斥锁,同时使当前线程关于cond所指向的条件变量阻塞,直到条件被信号唤醒。通常条件表达式在互斥锁的保护下求值,如果条件表达式为假,那么线程基于条件变量阻塞。当一个线程改变条件变量的同时,条件变量获得一个信号,使得条件变量的线程退出阻塞状态。
pthread_cond_timedwait
函数和pthread_cond_wait
函数用法类似,差别在于pthread_cond_timedwait
函数将阻塞直到条件变量获得信号或者经过abstime指定的时间,也就是说,rugosa再给定时刻前条件没有满足,则返回ETIMEOUT,结束等待。
线程被条件变量阻塞后,可以通过函数pthread_cond_signal
和pthread_cond_broadcast
激活。
pthread_cond_signal
激活一个等待条件成立的线程,存在多个等待线程时,按入队顺序激活其中一个,而pthread_cond_broadcast
则激活所有等待线程。
当一个条件变量不再使用时,需要将其清除。清除一个条件变量通过调用pthread_cond_destroy()
实现。
pthread_cond_destroy
函数清除由cond指向的条件变量。注意:只有在没有线程等待该条件变量的时候才能清除这个条件变量,否则返回EBUSY
异步信号
在Linux系统中,线程是在内核外实现的,它不像进程那样在内核中实现,Linux线程本质上是轻量级的进程。信号可以被进程用来进行相互通信,一个进程通过信号通知另一个进程发生了某件事件,比如该进程所需要的输入数据已经就绪。线程桶金程一样也可以接收和处理信号,信号也是一种线程同步的手段。
信号于任何线程都是异步的,也就是说信号到达线程的时间是不定的。如果有多个线程可以接收异步信号,则只有一个被选中,如果并发的多个同样的信号被送到一个进程,每一个将被不同的线程处理,如果所有的线程都屏蔽该信号,则这些信号将被挂起,知道有信号解除屏蔽来处理它们。
其中函数pthread_kill
用来向特定的线程发送信号signal,函数pthread_sigmask
用来设置线程的信号屏蔽码,但对不允许屏蔽的Cancel信号和不允许相应的Restart信号进行了保护,函数sigwait
用来阻塞线程。
书上的一些小项目例子我也已经上传到了github上,进程和线程之间还有很多关系和区别,还需要拓展,更深入了解其中的原理和实现。