APUE 第11-12章 线程和线程控制

第11章 线程

每个线程都包含表示执行环境必需的信息,包括线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno变量以及线程私有数据。一个进程的所有信息对该进程的所有线程是共享的,包括代码段、全局内存、堆、栈和文件描述符。

线程ID不像进程ID,只在其进程上下文才有意义。进程ID用pid_t数据类型表示,线程ID用pthread_t数据类型表示,不一定是整型,可能是一个数据结构,所以可移植的实现应该提供一组函数来对它进行操作。

pthread_equal()比较两个线程ID是否相同,pthread_self()可以获取自身的线程ID。

int pthread_create(pthread_t *tidp,const pthread_attr_t *attr, (void*)(*start_rtn)(void*),void *arg);

若线程创建成功,则返回0。若线程创建失败,则返回出错编号,并且*thread中的内容是未定义的。
返回成功时,由tidp指向的内存单元被设置为新创建线程的线程ID。attr参数用于指定各种不同的线程属性。新创建的线程从start_rtn函数的地址开始运行,该函数只有一个万能指针参数arg,如果需要向start_rtn函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg的参数传入。

线程的3种停止方式:
- 简单地返回
- 被同一进程的其他线程取消
- 调用pthread_exit

pthread_join()进程中其他线程可以调用它一直阻塞,直到指定线程结束。它可以取得指定线程调用pthread_exit()时写入的一个结构(以一个空指针指向),但要注意不能指向会被释放的变量,如栈区的。

进程有一个主线程。

pthread_cancel()用于取消同一进程中另一线程。

如同atexit()一样,线程也可登记多个线程清理处理程序,pthread_cleanup_push(),pthread_cleanup_pop(),记录于栈中,所以执行顺序与注册顺序相反。atexit也如是。请注意,直接返回而终止的线程将不进行清理,必须调用pthread_exit返回才可以。

默认情况下,线程的终止状态会保存直到对该线程调用pthread_join,如果该线程已经被分离,它的底层存储资源可以在线程终止时立即被回收,此时也就不能在调用pthread_join等待它。可以使用pthread_detach分离线程。

ptread_mutex_t

posix下抽象了一个锁类型的结构:ptread_mutex_t。通过对该结构的操作,来判断资源是否可以访问。顾名思义,加锁(lock)后,别人就无法打开,只有当锁没有关闭(unlock)的时候才能访问资源。

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
int pthread_mutex_lock(pthread_mutex_t *mutex)//锁不住会阻塞
int pthread_mutex_unlock(pthread_mutex_t *mutex)
int pthread_mutex_trylock(pthread_mutex_t *mutex)//锁不住直接返回EBUSY
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict tsptr)//锁不住超过指定时间则返回ETIMEDOUT

如果锁的粒度粗,则可能会出现很多线程阻塞等待相同的锁,可能会影响并行性;如果锁的粒度细,则控制复杂,性能也会有影响。

读写锁

读写锁又称为共享互斥锁(shared-exclusive lock),相比互斥锁更加灵活,适合读次数较多的互斥量。

int pthread_rwlock_init(pthread_rwlock_t *rwptr, const pthread_rwlockattr_t *attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwptr);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_unlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_timedrdlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict tsptr)
int pthread_rwlock_timedwrlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict tsptr)

pthread_cond_t

条件变量,用于多线程同步

int pthread_cond_init(pthread_cond_t *cv,const pthread_condattr_t *cattr);
int pthread_cond_wait(pthread_cond_t *cv,pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cv);
int pthread_cond_timedwait(pthread_cond_t *cv,pthread_mutex_t *mp, const structtimespec * abstime);
int pthread_cond_broadcast(pthread_cond_t *cv);
int pthread_cond_destroy(pthread_cond_t *cv);

wait()和signal()需配合mutex一起使用!条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待”条件变量的条件成立”而挂起;另一个线程使”条件成立”(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

S(){
    pthread_mutex_lock(&lock);
    flag = 1;
    pthread_mutex_unlock(&lock);
    pthread_cond_signal(&waitloc);
}
W(){
    pthread_mutex_lock(&lock);
    while(flag == 0)
        pthread_cond_wait(&waitloc, &lock);    
    pthread_mutex_unlock(&lock);
}

如果S中不使用互斥锁修改flag,考虑这样一种情况,当S修改flag和W测试flag一句重合,flag已经修改且已经signal,但测试的结果仍是flag==0,此时还没运行到wait函数,则过会wait将错失了signal,它将一直阻塞,程序错误了。但是用了lock之后,可以保证设置flag=1时,W要么还没测试flag,要么已经在wait处等待了(因为wait会释放lock),所以程序正确。

自旋锁

互斥锁通过休眠使进程阻塞,而自旋锁则进行忙等,当锁的持有时间较短,自旋锁效率较高。有的互斥锁的实现是先自旋一会,不行再休眠。

int pthread_spin_destroy(pthread_spinlock_t *);
int pthread_spin_init(pthread_spinlock_t *, int pshared);
int pthread_spin_lock (pthread_spinlock_t *lock);
int pthread_spin_trylock (pthread_spinlock_t *lock);
int pthread_spin_unlock (pthread_spinlock_t *lock);

pshared的取值及其含义:
- PTHREAD_PROCESS_SHARED:该自旋锁可以在多个进程中的线程之间共享。
- PTHREAD_PROCESS_PRIVATE:仅初始化本自旋锁的线程所在的进程内的线程才能够使用该自旋锁。

屏障(barrier)

int pthread_barrier_init(pthread_barrier_t *restrict barrier, const pthread_barrierattr_t *restrict attr, unsigned count);
int pthread_barrier_wait(pthread_barrier_t *barrier);
int pthread_barrier_destroy(pthread_barrier_t *barrier);

count指定屏障需要等待的线程数。如果算上主线程,经常要+1.注意wait()可以放在任何地方,只要指定的是同一个barrier结构,就一起计数。

pthread_once

pthread_once_t once_control=PTHREAD_ONCE_INIT;

int pthread_once(pthread_once_t *once_control,void(*init_routine)(void));

once_control:         控制变量
init_routine:         初始化函数

只执行一次

第12章 线程控制

pthread接口允许通过设置每个对象关联的不同属性来细调线程和同步对象的行为。就是线程属性和同步对象属性

有以下模式:
1. 每个对象与自己类型的属性对象进行关联(线程与线程属性关联,互斥量与互斥量属性关联等)。一个对象属性可以代表多个属性。
2. 有一个初始化函数,把属性设置为默认值
3. 有一个销毁属性对象的函数。如果初始化函数有分配相关资源,它则负责释放
4. 每个属性都有一个从属性对象中获取属性值的函数
5. 每个属性都有一个设置属性值的函数

线程属性

pthread_create()参数列表有一个指向pthread_attr_t结构的指针,它就是线程的属性对象。可以用

int pthread_attr_t_init(pthread_attr_t *attr);
int pthread_attr_t_destroy(pthread_attr_t *attr);

初始化和销毁该对象。它支持4个属性:detachstate,guradsize,stackaddr,stacksize。分别代表线程的分离状态属性、线程栈末尾的警戒缓冲区大小、线程栈的最低地址、线程栈的最小长度。

例如,如果在创建线程时不需要了解线程的终止状态,就可以修改pthread_attr_t结构中detachstate线程属性,让线程一开始就处于分离状态
detachstate可以设置成两个合法值之一:PTHREAD_CREATE_DETACHED和PTHREAD_CREATE_JOINABLE(默认)。可以使用下面两个函数。

int pthread_attr_getdetachstate(const pthread_attr_t *restrict attr,int *detachstate);
int pthread_attr_setdetachstate(pthread_attr_t *attr,int *detachstate);

进程的栈大小是固定的,因为进程只有一个栈,多个线程共享该栈则可能会导致超界。如果线程栈用完了,可以动态地为它分配空间,pthread_attr_t_setstack用来改变新建线程的栈位置,由stackaddr指定的地址用作线程栈的内存范围的最低可寻址地址。

同样的,可以使用下面函数对线程栈属性进行管理:

int pthread_attr_t_getstack(const pthread_attr_t *restrict attr,void **restrict stackaddr,size_t *restrict stacksize);
int pthread_attr_t_setstack(pthread_attr_t *attr,void *stackaddr,size_t stacksize);

可以使用下面函数来设置和读取线程属性stacksize:

int pthread_attr_getstacksize(const pthread_attr_t *restrict attr,size_t *restrict stacksize);
int pthread_attr_setstacksize(pthread_attr_t *attr,size_t stacksize);

线程属性guardsize控制着线程栈末尾之后用以避免栈溢出的扩展内存的大小:

int pthread_attr_getguardsize(const pthread_attr_t *restrict attr,size_t *restrict guardsize);
int pthread_attr_setguardsize(pthread_attr_t *attr,size_t guardsize);

如果修改了stackaddr,系统就认为我们将自己管理栈,进而使栈的警戒缓冲区机制无效,等价于将guardsize设为0.

同步属性

互斥量属性

int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

值得注意的3个属性是:进程共享属性、健壮属性和类型属性。

下面两个函数用于设置跟获取进程共享属性:

int pthread_mutexattr_getpshared(const pthread_mutexattr_t *restrict attr,int *restrict pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,int pshared);

pshared可以设置为两个值:PTHREAD_PROCESS_PRIVATE(默认),PTHREAD_PROCESS_SHARED(从多个进程彼此之间共享的内存数据块中分配的互斥量就可以用于这些线程的同步)

下面两个函数用于设置跟获取健壮属性:

int pthread_mutexattr_getrobust(const pthread_mutexattr_t *restrict attr,int *restrict robust);
int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr,int robust);

类型属性:标准,提供错误检查,允许同一线程解锁前多次加锁的递归互斥量,默认。

使用下面两个函数可以设置跟获取互斥量类型属性:

int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr,int *restrict type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr,int type);

pthread_cond_wait函数如果使用递归互斥量,则会出现问题。

读写锁属性

与互斥量相似

int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);

读写锁支持的唯一属性是进程共享属性。它与互斥量的进程共享属性是相同的。

int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *restrict attr,int *restrict pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr,int pshared);

条件变量属性

有一对函数用于初始化和反初始化条件变量属性

int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destory(pthread_condattr_t *attr);

目前定义了条件变量的两个属性:进程共享属性和时钟属性

与其他的同步属性一样,条件变量支持进程共享属性,下面两个函数用于设置跟获取进程共享属性

int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr,int *restrict pshared);
int pthread_condattr_setpshared(pthrea_condattr_t *attr,int *pshared);

时钟属性控制计算pthread_cond_timedwait函数的超时参数采用的是哪个时钟,下面两个函数用于设置跟获取时钟属性

int pthread_condattr_getclock(const pthread_condattr_t *restrict attr,clockid_t *restrict clock_id);
int pthread_condattr_setclock(pthread_condattr_t *attr,clockid_t *clock_id);

屏障属性

int pthread_barrierattr_init(pthread_barrierattr_t *attr);
int pthread_barrierattr_destroy(pthread_barrierattr_t *attr);

目前定义的屏障属性只有进程共享属性,作用与其他同步对象一样

int pthread_barrierattr_getpshared(const pthread_barrierattr_t *restrict attr,int *restrict pshared);
int pthread_barrierattr_setpshared(pthread_barrierattr_t *attr,int pshared);

重入

如果一个函数在相同的时间点可以被多个线程安全调用(重入),则称其为线程安全的。但这不代表对信号处理程序而言该函数也是可重入的。如果函数对异步信号处理函数的重入是安全的,则说它是异步信号安全的

POSIX.1还提供了以线程安全的方式管理FILE对象的方法。可以使用flockfile和ftrylockfile获取给定FILE对象关联的锁。
这个锁是递归的:当你占有这把锁的时候,还是可以再次获取该锁,而且不会导致死锁。

int ftrylockfile(FILE *fp);
void flockfile(FILE *fp);
void funlockfile(FILE *fp); 

如果使用锁机制来进行一次一个字符的标准IO,性能会十分低下,所以提供了不加锁版本的标准IO,这应当在flockfile和funlockfile的调用中使用,可以将加解锁操作的开销均摊。

线程特定数据

线程特定数据,也称为线程私有数据,是存储和查询某个特定线程相关数据的一种机制。

在分配线程特定数据之前,需要创建与该数据关联的键。这个键将用于获取对线程特定数据的访问。使用pthread_key_create创建一个键:

int pthread_key_create(pthread_key_t *keyp,void (*destructor)(void *));

创建的键存储在keyp指向的内存单元中,这个键可以被进程中的所有线程使用,但每个线程把这个键与不同的线程特定数据地址进行关联。
创建新建时,每个线程的数据地址设为空值。
pthread_key_create可以为该键关联一个可选择的析构函数。当这个线程退出时,如果数据地址已经被置为非空值,那么析构函数就会被调用。
线程通常使用malloc为线程特定数据分配内存。析构函数通常释放已分配的内存。
对于所有的线程,我们都可以通过调用pthread_key_delete来取消键与线程特定数据值之间的关联关系。

int pthread_key_delete(pthread_key_t key);

键一旦创建以后,就可以通过pthread_setspecific函数把键和线程特定数据关联起来,可以通过pthread_getspecific函数获得线程特定数据的地址。

void *pthread_getspecific(pthread_key_t key);
int pthread_setspecific(pthread_key_t key,const void *value);

取消选项

有两个线程属性并没有包含在pthread_attr_t结构中,它们是可取消状态和可取消类型,这两个属性影响着线程在相应pthread_cancel函数调用时锁呈现的行为。

可取消状态属性可以是PTHREAD_CANCEL_ENABLE,也可以是PTHREAD_CANCLE_DISABLE。线程可以通过调用pthread_setcancelstate修改它的可取消状态。

int pthread_setcancelstat(int state,int *oldstate);

pthread_cancle调用并不等待线程终止。在默认情况下,线程在取消请求发出以后还是继续运行,知道线程达到某个取消点。

取消点是线程检查它是否被取消的一个位置,如果取消了,则按照请求行事。POSIX.1保证在线程调用如下列出的任何函数时,取消点就会出现。以下有一堆函数。。。

线程启动时默认的可取消状态时PTHREAD_CANCEL_ENABLE。当状态设为PTHREAD_CANCLE_DISABLE时,对pthread_cancel的调用并不会杀死线程。

取消请求对这个线程来说还处于挂起状态,当取消状态再次变为PTHREAD_CANCLE_ENABLE时,线程将在下一个取消点上对所有的取消请求进行处理。

可以调用pthread_testcancel函数在程序中添加自己的取消点。

void pthread_testcancel(void);

上面描述的默认取消类型是推迟取消。可以通过调用pthread_setcanceltype来修改取消类型。

int pthread_setcanceltype(int type,int *oldtype);

type参数可以是PTHREAD_CANCEL_DEFERRED(默认),也可以是PTHREAD_CANCEL_ASYNCHRONOUS(异步取消)。如果使用异步取消。线程可以在任意时间撤销,而不是遇到取消点才能被取消。

线程和信号

信号的处理是进程中所有线程共享的。如果一个线程选择忽略某个给定信号,那么另一个线程就可以通过以下两种方式撤销上述线程的信号选择:恢复信号的默认处理行为,或者为信号设置一个新的信号处理程序。

进程中的信号是递送给单个线程的。如果一个信号与硬件故障相关,那么该信号一般会被发送到引起该事件的线程中去,而其他的信号则被发送到任意一个线程。

第十章讨论了进程如何使用sigprocmask函数来阻止信号发送。而线程则必须使用pthread_sigmask。

int pthread_sigmask(int how,const sigset_t *restrict set,sigset_t *restrict oset);

pthread_sigmask函数与sigprocmask函数基本相同,不过pthread_sigmask工作在线程中,失败时返回错误码,不再像sigprocmask函数那样设置errno并返回-1。

每个线程有自己的信号屏蔽字。

线程可以通过sigwait等待一个或多个信号的出现。

int sigwait(const sigset_t *restrict set,int *restrict signop); //发送信号的数量

如果信号集中的某些信号在sigwait调用时已经处于挂起,那么函数将无阻塞返回,并移除那些信号,如果有多个同样信号在排队,只移除一个。

要把信号发送给线程,可以调用pthread_kill。

int pthread_kill(pthread_t thread,int signo);

注意,闹钟定时器是进程资源,所以多线程无法互不干扰地使用它。

多线程如何接收信号


  1. 如果是异常产生的信号(比如程序错误,像SIGPIPE、SIGEGV这些),则只有产生异常的线程收到并处理。
  2. 如果是用pthread_kill产生的内部信号,则只有pthread_kill参数中指定的目标线程收到并处理。
  3. 如果是外部使用kill命令产生的信号,通常是SIGINT、SIGHUP等job control信号,则会遍历所有线程,直到找到一个不阻塞该信号的线程,然后调用它来处理。(一般从主线程找起),注意只有一个线程能收到。
    其次,每个线程都有自己独立的signal mask,但所有线程共享进程的signal action。这意味着,你可以在线程中调用pthread_sigmask(不是sigmask)来决定本线程阻塞哪些信号。但你 不能调用sigaction来指定单个线程的信号处理方式。如果在某个线程中调用了sigaction处理某个信号,那么这个进程中的未阻塞这个信号的线程在收到这个信号都会按同一种方式处理这个信号。

另外,注意子线程的mask是会从主线程继承而来的。
signal action不能共享。

线程和fork

当调用fork时,就为整个子进程创建了整个进程地址空间的副本,子进程会从父进程那儿继承了每个互斥量、读写锁和条件变量的状态。

子进程继承锁,但并不继承线程,所以必须清理它继承的锁。如果它直接exec,就无所谓了。要清除锁状态,可以通过调用pthread_atfork函数建立fork处理程序。

int pthread_atfork(void (*prepare)(void),void (*parent)(void),void (*child)(void));
  • prepare处理程序在父进程fork**创建子进程前**调用,作用是获取父进程定义的所有锁。
  • parent处理程序是创建子进程之后、返回之前在父进程上下文中调用的,作用是对获取的所有锁进行解锁。
  • child处理程序在fork返回之前在子进程上下文中调用,跟parent处理程序一样,作用是对获取的所有锁进行解锁。

线程与I/O

线程共享文件描述符,第三章介绍了pread函数和pwrite函数,它们使偏移量和数据的读写成为一个原子操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值