我们知道,进程在各自独立的地址空间中运行,进程之间共享数据需要用mmap或者进程间通信机制,有些情况需要在一个进程中同时执行多个控制流程,
这时候线程就派上了用场,比如实现一个图形界面的下载软件,一方面需要和用户交互,等待和处理用户的鼠标键盘事件,另一方面又需要同时下载多个
文件,等待和处理从多个网络主机发来的数据,这些任务都需要一个“等待-处理”的循环,可以用多线程实现,一个线程专门负责与用户交互,另外几个线
程每个线程负责和一个网络主机通信。信号处理函数是同一个进程地址空间中的多个控制流程,多线程也是如此,但是比信号处理函数更加灵活,信号处
理函数的控制流程只是在信号递达时产生,在处理完信号之后就结束,而多线程的控制流程可以长期并存,操作系统会在各线程之间调度和切换,就像在
多个进程之间调度和切换一样。由于同一进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在
各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
但有些资源是每个线程各有一份的:
- 线程id
- 上下文,包括各种寄存器的值、程序计数器和栈指针
- 栈空间
- errno变量
- 信号屏蔽字
- 调度优先级
1.创建线程
include <pthread.h>
int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void*), void *restrict arg);
返回值:成功返回0,失败返回错误号。以前学过的系统函数都是成功返回0,失败返回-1,而错误号保存在全局变量errno中,而pthread库的函数都是
通过返回值返回错误号,虽然每个线程也都有一个errno,但这是为了兼容其它函数接口而提供的,pthread库本身并不使用它,通过返回值返回错误码更
加清晰。在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给
pthread_create的函数指针start_routine决定。
2.终止线程
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
- 线程可以调用pthread_exit终止自己。
pthread_cancel
Cancellation是一种一个线程可以结束另一个线程执行的机制。更确切的说,一个线程可以发生Cancellation请求给另一个线程。
根据线程的设置,收到请求的线程可以忽视这个请求,立即执行这个请求或者延迟到一个cancellation点执行。
当一个线程执行Cancellation请求,相当于在那个点执行pthread_exit操作退出:所有cleanup句柄被反向调用,所有析构函数被调用结束线程并返
回pthread_canceled.
#include <pthread.h>
pthread_exit结束调用线程的执行.所有通过pthread_cleanup_push设置的清除句柄将会被反序执行(后进先出)。
所以key值非空的线程特定数据Finalization functions被调用(参见pthread_key_create)。
最后调用线程被终止。
retval是这个线程结束的返回值,可以通过在别的线程中调用pthread_join来获取这个值。
没有返回值。
#include <pthread.h>
int pthread_join(pthread_t th, void **thread_return);
挂载一个在执行的线程直到该线程通过调用pthread_exit或者cancelled结束。
如果thread_return不为空,则线程th的返回值会保存到thread_return所指的区域。
th的返回值是它给pthread_exit的参数,或者是pthread_canceled
如果是被cancelled的。
被依附的线程th必须是joinable状态。一定不能是detached通过使用pthread_detach或者pthread_create中使用pthread_create_detached属性。
当一个joinable线程结束时,他的资源(线程描述符和堆栈)不会被释放直到另一个线程对它执行pthread_join操作。
如果成功,返回值存储在thread_return中,并返回0,否则返回错误码:
ESRCH:找不到指定线程
EINVAL:线程th是detached或者已经存在另一个线程在等待线程th结束
EDEADLK:th的参数引用它自己(即线程不能join自身)
一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终
止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL。对一个尚
未detach的线程调用pthread_join或pthread_detach都可以把该线程置为detach状态,也就是说,不能对同一线程调用两次pthread_join,或者如果
已经对一个线程调用了pthread_detach就不能再调用pthread_join了。
#include <pthread.h>
int pthread_detach(pthread_t tid);
返回值:成功返回0,失败返回错误号
以下为一个例子:
程序执行结果:
3.线程间同步
对于多线程的程序,访问冲突的问题是很普遍的,解决的办法是引入互斥锁(Mutex,Mutual Exclusive Lock),获得锁的线程可以完成“读-修改-写”的操作,
然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据,这样“读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执
行到中间被打断,也不会在其它处理器上并行做这个操作。
Mutex用pthread_mutex_t类型的变量表示,可以这样初始化和销毁:
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
返回值:成功返回0,失败返回错误号。
pthread_mutex_init函数对Mutex做初始化,参数attr设定Mutex的属性,如果attr为NULL则表示缺省属性,Mutex属性以后再讲。用pthread_mutex_init
函数初始化的Mutex可以用pthread_mutex_destroy销毁。如果Mutex变量是静态分配的(全局变量或static变量),也可以用宏定PTHREAD_MUTEX_INITIALIZER来初始化,相当于用pthread_mutex_init初始化并且attr参数为NULL。Mutex的加锁和解锁操作可以用下
列函数:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号。
一个线程可以调用pthread_mutex_lock获得Mutex,如果这时另一个线程已经调用pthread_mutex_lock获得了该 Mutex,则当前线程需要挂起等待,直到
另一个线程调用pthread_mutex_unlock释放Mutex,当前线程被唤醒,才能获得该 Mutex并继续执行。如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock,如果Mutex已经被另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。
以下为程序例子:
信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。而互斥锁是用在多线程多任务互斥的,
一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这 个资源。只要信号量的value大于0,其他线
程就可以sem_wait成功,成功后信号量的value减一。若value值不大于0,则sem_wait使得线程阻塞,直到sem_post释放后value值加一,但是sem_wait
返回之前还是会将此value值减一.