一个进程可以包含多个线程,线程共享进程的资源,包括储存地址空间和文件描述符。进程的所有信息对该进程的线程是共享的,包括可执行程序的代码,程序的全局变量和堆内存,栈以及文件描述符。
线程包含:线程ID(只在进程上下文有效,pthread_t类型),一组寄存器值,栈,调度优先级和策略信号屏蔽字,errno变量,线程私有数据。
POSIX线程函数:pthread
int pthread_create(pthread_t *restrict tidp, restrict关键字表示所
有修改该指针所指向内存中内容操作必须通过改指针进行修改
const pthread_attr_t *restrict attr
void *(*start_rtn)(void*),void*restrict arg)
如果线程是通过从它的启动例程返回而终止的话,它的清理函数就不会被调用。
当2个或则多个线程试图在同一时间修改同一变量时,需要同步,增量操作通常分解为3步:
1,从内存单元中读入寄存器
2,在寄存器中对变量进行增量操作
3,把新的值写入内存单元
在现代操作系统中,存储访问需要多个总线周期,所以我们并不能保证数据顺序是一致的。
互斥量
使用pthread的互斥接口来保护数据,确保同一时间只有一个线程访问数据。
互斥量本质上来说就是一把锁。访问数据前进行加锁,访问完后进行解锁。对互斥量进行加锁之后,任何其他试图再次对互斥量加锁的线程都会被阻塞直到当前线程释放互斥锁。
只有将所有线程设计成准守相同数据访问规则的,互斥机制才能正常工作。
互斥量使用pthread_mutex_t数据类型表示。PTHREAD_MUTEX_INITIALIZER
pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthreaqd_mutexattr_t * restrict attr)
pthread_mutex_destory(pthread_mutex_t *mutex)
pthread_mutex_lock(pthread_mutex_t *mutex)
pthread_mutex_unlock(pthread_mutex_t *mutex)
死锁
1,如果一个线程试图对一个互斥量加锁2次,那么它就会陷入死锁。
2,程序中使用一个以上的互斥量时,如果允许一个线程一直占有第一个互斥量,并且试图锁住第二个互斥量时处于阻塞状态,但是拥有第二个互斥量的线程也在试图锁住第一个互斥量,就产生了死锁。
可以使用 pthread_mutex_trylock()接口避免死锁。如果已经占有某些锁,而且pthread_mutex_trylock()返回成功,那么可以继续前景,但是如果返回不成功,则可以先释放已经占有的锁,做好清理工作,然后过一段时间再试试。
pthread_mutex_timelock(pthread_mutex_t *restrict mutex, const struct imespec *restrict tsptr)
当线程试图获取一个已加锁的互斥量时,上述函数允许绑定线程阻塞时间。
读写锁(互斥共享锁)
读写锁允许更大的并行性。读写锁有3中状态。1,读模式下加锁状态;2,写模式下加锁状态;3,不加锁状态
一次只有一个线程可以占用写模式下的锁,但是多个线程可以占用读模式下的读写锁。
1 当读写锁处于写加锁的状态时,在这个锁被解锁前,所有试图对这个锁加锁的线程都会被阻塞
2 当读写锁处于读加锁的状态时,所有试图以读模式对它进行加锁的都可以得到访问权限,但是任何希望以写模式加锁的都会被阻塞,直到所有线程释放它们的读锁为止
3 当处于读加锁的时候,如果有一个线程请求写加锁,那么读写锁会阻塞后面的读模式加锁的请求
适合于读的次数远远大于写次数的模式
条件变量
pthread_cond_t 表示条件变量
使用PTHREAD_COND_INITIALIZER静态初始化
使用pthread_cond_init()函数进行初始化(动态初始化)
pthread_cond_wait等待条件信息变为真,使用该函数必须在pthread_mutes_lock() 和pthread_mutex_unlock()之间,且使用该函数之后互斥锁解锁,该线程进入休眠状态。等待pthread_cond_signal()发送信号唤醒该线程,并重新获得互斥锁
自旋锁
自旋锁和互斥锁类似,但是它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等阻塞状态。当线程自旋等待锁变为可用时,cpu不能做其他事情。所以自旋锁用于锁被持有时间比较短,线程并不希望在重新调度上花费太多时间。所以在非抢占内核中是非常有用的。
线程属性 pthread_attr_t
1,线程创建默认是分离状态或者不分离状态,分离状态就是如果对线程的终止状态不感兴趣的话,分离状态的线程结束后操作系统回收它所占用的资源。
2,线程栈的大小,线程栈使用的是进程栈,有一个默认大小,可以改变改大小,或者自己malloc或者mmap来替代栈空间。
3,控制栈末尾用于避免栈溢出的扩展内存大小。
互斥量属性 pthread_mutexattr_t
1,进程共享属性 有PTHREAD_PROCESS_PRIVATE ,PTHREAD_PROCESS_SHARE 用于不同进程之间共享同一数据的,默认是private
2,健壮性,用于不同进程之间,当一个持有互斥量进程终止时,互斥量处于锁定状态,恢复起来很难,其他阻塞在这个锁的进程将会一直阻塞下去
PTHREAD_MUTEX_STALLED,意味着持有互斥量的进程终止时不采取任何特别措施,这种情况下使用互斥量行为是未定义的,等待互斥量解锁是被拖住的
PTHREAD_MUTEX_ROBUST,这个值将导致线程调用pthread_mutex_lock获取锁,而该所被另一个进程持有,但他终止时并没有对该所进行解锁,此时线程会阻塞,从pthread_mutex_lock返回值为EOWNERDEAD而不是0,应用程序可以通过这个特殊值获知互斥量需要回复。
如果应用状态无法回复,在线程对互斥量解锁后,该互斥量将处于永久不可用状态,为了避免这种情况,可以调用pthread_mutex_consistent函数,指明该互斥量相关状态和互斥量解锁之前是一致的。也就是在解锁之前调用pthread_mutex_consistent函数
当一个锁的owner死掉后,其它线程再去lock这个锁的时候,不会被阻塞,而是通过返回值EOWNERDEAD来报告错误。
那么你可以根据这个错误来进行处理:首先是应该调用pthread_mutex_consistent函数来恢复该锁的一致性,
然后调用解锁pthread_mutex_unlock,
接下来在调用加锁,这样该锁的行为就恢复正常了
3,类型属性
错误检测以及死锁检测等
递归锁:pthread_mutexattr_settype(pthread_mutexattr_t *attr,int type) ,PTHREAD_MUTEX_RECURSIVE
允许同一线程在对一个互斥量解锁之前,再次加锁,递归互斥量维护计数,加锁次数和解锁次数需要一致,不然不能解锁。
线程和信号
每个信号都有自己的信号屏蔽字,但是信号的处理是进程中所有线程共享的,这意味着单个线程可以阻止某个信号,但当某个线程修改了与某个给定信号相关的处理行为以后,所有线程都必须共享这个处理行为改变。
intpthread_sigmask()改变单个线程的信号屏蔽字。线程通过sigwait等待一个或多个信号的出现,如果信号集中的某个信号在调用sigwait的时候刚刚好处于挂起的状态,sigwait将无阻塞返回,并从进程中移除挂起等待状态的信号。
为避免错误,线程调用sigwait之前,必阻塞等待信号,sigwait原子地取消信号集阻塞状态,直到新消息被传递,sigwait返回之前,将恢复线程信号屏蔽字。为了防止中断线程,可以将中断信号等加入所有线程屏蔽字,然后使用专门线程处理这些中断,不需要担心信号处理程序中那些函数是安全的。
进程中处理信号可以使用sigal()函数,异步等待信号的发生,但是线程不一样,线程可以在主线程中屏蔽掉对应信号,单独开辟一个子进程来处理信号,使用sigwait函数,同步处理信号。
要把信号发给进程用kill,把型号发给线程用pthread_kill,如果信号的默认处理动作是总之该信号,那么把信号传递给某个线程仍然会杀死整个进程,闹钟信号可以在线程中互不干扰地使用。
线程和fork
当线程调用fork的时候,为子进程穿件了整个进程地址空间的副本,写时复制,子进程与父进程是完全不同的进程,只要两者都没有对内存内容作出改动,父进程与子进程之间还可以共享内存也的副本。
所以当线程fork子进程的时候,互斥锁也被子进程继承了,因为写时复制,所以子进程内部只有一个线程(它是父进程中调用fork线程的副本完成的),如果这个线程占有锁,但是子进程并不包含占有锁的副本,所以如果子进程调用exec启动其他程序,放弃当前地址空间,那么竟不会有问题,但是如果子进程需要继续处理程序,那么就需要pthread_atfork来处理互斥锁。
int pthread_atfork(void(*prepare)(void), void(*parent)(void), void(*child)(void));c:返回0,r:返回错误编码
prepare:函数是在fork被调用之前做准备工作的函数,任务是获得父进程中所有的锁(将所有的锁lock掉),parent是在父进程返回之前,处理父进程中互斥锁的问题,child是在子进程返回之前处理子进程中互斥锁的问题。
线程和I/O
pread和pwrite在多线程环境下是非常有用的。pread在使用偏移量和读取数据成为一个原子操作,不会被其他线程中断。