多线程同步(信号量,互斥锁,条件变量)

多线程编程

线程:程序中一个完成独立任务的的完整序列,即一个可调度的实体。
内核线程:运行在内核空间,由内核调度。
用户线程:运行在用户空间,有线程库来调度。
线程的实现分三种模式:
1.完全用户空间实现:
优点:创建和调度无需内核参与,速度快。不占用额外内核资源。
缺点:对于多处理器系统,一个进程的多个线程无法运行在不同的CUP。
2.完全有内核调度:
有趣点:与1.相反。
3.双层调度:
兼具1.2.优点;充分利用多处理器优势。

查看当前系统使用的线程库:
$ getconf GNU_LIBPTHREAD_VERSION
  NPTL   2.14.90

线程同步的机制下面讲3种:信号量、互斥量和条件变量。
POSIX信号量

线程同步的机制下面讲3种:信号量、互斥量和条件变量。

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);

这些函数的第一个参数sem指向被操作的信号量。

sem_int用于初始化一个未命名的信号量。pshared参数指定信号量的类型。如果pshared参考指定信号量的类型。如果其值为0,就表示这个信号量是当前进程的局部信号量,否则该信号量就可以在多个进程之间共享。value参数指定信号量的初始值。此外,初始化一个已经被初始化的信号量将导致不可预期的结果。

sem_destroy函数用于销毁信号量,以释放期占用的内核资源。如果销毁一个正在被其他线程等待的信号量,则将导致不可预期的后果。

sem_wait函数以原子操作的方式将信号量减1。如果信号量的值为0,则sem_wait将被阻塞,直到这个信号量具有非0值。

sem_trywait与sem_wait函数相似,不过它始终立即返回,而不论被操作的信号是否具有非0值,相当于sem_wait的非阻塞版本。当信号量的值非0时,sem_trywait对信号量执行减1操作。当信号量的值非0时,sem_trywait对信号量执行减1操作。当信号量的值为0时,它将返回-1并设置errno为EAGAIN。

sem_post函数以原子操作的方式将信号量的值加1.当信号量的值大于0时,其他正在调用sem_wait等待信号量的线程将被唤醒。

上面这些函数成功时返回0,失败是返回-1并设置errno。

互斥锁

互斥锁,(互斥量)可用于保护关键代码段,以确保其独占式的访问。
互斥锁基础API

POSIX互斥锁的相关函数主要有如下5个:

#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);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

这些函数的第一个参数mutex指向操作的目标互斥锁,互斥锁的类型是pthread_mutex_t结构体。

pthread_mutex_init函数用于初始化互斥锁。mutexattr参数指定互斥锁的属性。如果将它设置为NULL,则表示使用默认属性。除了这个函数外,我们还可以用如下方式初始化一个互斥锁:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

宏PTHREAD_MUTEX_INITIALIZER实际上只是把互斥锁的各个字段都初始化为0。

pthread_mutex_destroy函数用于销毁互斥锁,以释放期占用的内核资源。销毁一个已经加锁的互斥锁将导致不可预期的后果。

pthread_mutex_lock函数以原子操作的方式给一个互斥锁加锁。如果目标互斥锁已经被锁上,则pthread_mutex_lock调用将阻塞,直到该互斥锁的占有者将其解锁。

pthread_mutex_trylock与pthread_mutex_lock函数类似,不过它始终立即返回,而不论被操作的互斥锁是否已经加锁,相当于pthread_mutex_lock的非阻塞版本。当目标互斥锁未被加锁时,pthread_mutex_trylock对互斥锁执行加锁操作。当互斥锁已经被加锁时,pthread_mutex_trylock将返回错误码EBUSY。需要注意的是,这里讨论的pthread_mutex_lock和pthread_mutex_trylock的行为是针对普通锁而言的。

pthread_mutex_unlock函数以院子操作的方式给一个互斥锁解锁。如果此时有其他线程正在等待这个互斥锁,则这些线程中的某一个将获得它。

上面这些函数成功时返回0,失败时返回错误码。

互斥锁属性

pthread_mutexattr_t结构体定义了一套完整的互斥锁属性。线程库提供了一系列函数来操作pthread_mutexattr_t类型变量,以方便我们获取和设置互斥锁属性。这里我们列出其中一些主要的函数:

#include <pthread.h>
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *
restrict attr, int *restrict pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);
int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

这里只讨论互斥锁的两种常用属性:pshared和type。互斥锁属性pshared指定是否允许跨进程共享互斥锁,其可选值有两个:

PTHREAD_PROCESS_SHARED。互斥锁可以被跨进程共享。
PTHREAD_PROCESS_PRIVATE。互斥锁只能被和锁的初始化线程隶属于同一个进程的线程共享。

互斥锁属性type指定互斥锁的类型。Linux支持如下4种类型的互斥锁:

PTHREAD_MUTEX_NORMAL,普通锁。这是互斥锁默认的类型。当一个线程对一个普通锁加锁以后,其余请求该所的线程将形成一个等待队列,并在该所解锁后按优先级获得它。这种锁类型保证了资源分配的公平性。但这种锁也很容易引发问题:一个线程如果对一个已经加锁的普通锁再次加锁,将引发死锁;对一个已经被其他线程加锁的普通锁解锁,或者对一个已经解锁的普通锁解锁将导致不可预期的后果。

PTHREAD_MUTEX_ERRORCHECK,检错锁。一个线程如果对一个已经加锁的检错锁再次加锁,则加锁操作返回EDEADLK。对一个已经被其让他线程加锁的检错锁解锁,或者对一个已经解锁的检错锁再次解锁,则检错锁返回EPERM。

PTHREAM_MUTEX_RECURSIVE,嵌套锁。这种锁允许一个线程在释放锁之前对他加锁而不发生死锁。不过其他线程如果要获得这个锁,则当前锁的拥有者必须执行相应次数的解锁操作。对一个已经被其他线程枷锁的嵌套锁解锁,或者对一个已经解锁的嵌套锁再次解锁,则解锁操作返回EPERM。

PTHREAD_MUTEX_DEFAULT,默认锁。一个线程如果对一个已经加锁的默认锁再次加锁,或者对一个已经被其他线程加锁的默认锁解锁,或者对一个已经解锁的默认锁再次解锁,将导致不可预期的后果。
条件变量

如果说互斥锁是用于同步线程对共享数据的访问的话,那么条件变量则是用于线程之间同步共享数据的值。条件变量提供了一种线程间的通信机制:当某个共享数据达到某个值得时候,唤醒等待这个共享数据的线程。

条件本身是由互斥量保护的。线程在改变条件状态前必须首先锁住互斥量,其他现成在获得互斥量之前不会察觉到这种变化,因为必须锁住互斥量以后才能计算条件。

条件变量的相关函数主要有如下5个:

#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *restrict cond, phread_mutex_t *restrict mutex)

这些函数的第一个参数cond指向要操作的目标条件变量,条件变量的类型是pthread_cond_t结构体。

pthread_cond_init函数用于初始化条件变量。cond_attr参数指定条件变量的属性。如果将它设置为NULL,则表示使用默认属性。条件变量的属性不多,而且和互斥锁的属性类型相似,所以我们不在累赘。除了pthread_cond_init函数外,我们还可以使用如下方式初始化一个条件变量:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

宏PTHREAD_COND_INITIALIZER实际上只是把条件变量的 各个字段都初始化为0。

pthread_cond_destroy函数用于销毁条件变量,以释放其占用的内核资源。销毁一个正在等待的条件变量将失败并返回EBUSY。

pthread_cond_broadcast函数以广播的方式唤醒所有等待目标条件变量的线程。pthread_cond_signal函数用于唤醒一个等待目标条件变量的线程。至于哪个线程将被唤醒,则取决于线程的优先级和调度策略。有时候我们可能想唤醒一个指定的线程,但pthread没有对该需求提供解决方法。不过我们可以间接地实现该需求:定义一个能够唯一表示目标现成的全局变量,在唤醒等待条件变量的线程前先设置该全局变量为目标线程,然后采用广播方式唤醒所有等待条件变量的线程,这些线程被唤醒后都检查该变量以判断被唤醒的是否是自己,如果是就开始执行后续代码,如果不是则返回继续等待。

pthread_cond_wait函数用于等待目标条件变量。mutex参数是用于保护条件变量的互斥锁,以确pthread_cond_wait操作的原子性。在调用pthread_cond_wait前,必须确保互斥锁mutex已经加锁,否则将导致不可预期的结果。
pthread_cond_wait函数执行时,首先把调用线程放入条件变量的等待队列中,然后将互斥锁mutex解锁。可见,从pthread_cond_wait开始执行到其调用线程被放入条件变量的等待队列之间的这段时间内,pthread_cond_signal和pthread_cond_broadcast等函数不会修改条件变量。换言之,pthread_cond_wait函数不会错过目标条件变量的任何变化。当pthread_cond_wait函数成功返回时,互斥锁mutex将再次被锁上。

上面这些函数成功时返回0,失败时返回错误码。


可重入函数

如果一个函数能被多个线程同时调用且不发生竞态条件,则我们称它是线程安全的,或者说它是可重入的。

线程和进程

如果一个线程程序的某个线程调用了fork函数,那么新创建的子进程是否创建和父进程相同数量的线程呢?答案是否,子进程只拥有一个执行线程,它是调用fork的那个线程的完整复制。并且子进程将自动继承父进程中互斥锁(条件变量与之类似)的状态。也就是说,父进程中已经被加锁的互斥锁在子进程中也是被锁住的。

线程与信号

每个线程都可以独立地设置信号掩码,在多线程环境下使用如下函数设置线程信号掩码:

#include <signal.h>
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);

该函数与sigprocmask参数完全相同,不在累赘。pthread_sigmask成功时返回0,失败时返回错误码。

由于进程中的所有线程共享该进程的信号,所以线程库将根据线程掩码决定把信号发送给哪个具体的线程。因此,如果我们在每个子线程中都单独设置信号掩码,就很容易导致逻辑错误。此外,所有线程共享信号处理函数,也就是说当我们在一个线程中设置了某个信号的信号处理函数后,它将覆盖其他线程为同一个信号设置的信号处理函数。这两点都说明,我们应该定义一个专门的线程来处理所有的信号。这可以通过以下两个步骤实现:

1) 在主线程创建出其他子线程之前就调用pthread_sigmask来设置好信号掩码,所有新创建的子线程都将自动继承这个信号掩码。这样做之后,实际上所有线程都不会响应被屏蔽的信号了。
2) 在某个线程中调用如下函数来等待信号并处理之

#include <signal.h>
int sigwait(const sigset_t *set, int *sig);

set参数指定需要等待的信号的集合。我们可以简单地将其指定为第1步中创建的信号掩码,表示在该线程中等待所有被屏蔽的信号。参数sig指向的整数用于存储该函数返回的信号值。sigwait函数会自动取消信号集的阻塞状态,直到有新的信号被递送。在返回前,sigwwait将恢复线程信号的屏蔽字。sigwait成功时返回0,失败则返回错误码。一旦sigwait正确返回,我们就可以对接收到的信号做处理了。
最后,pthread还提供了下面的方法,使得我们可以明确地将一个信号发送给指定的线程:

#include <signal.h>
int pthread_kill(pthread_t thread, int sig);

其中,pthread参数指定目标线程,sig参数指定待发送的信号。如果sig为0,则pthread_kill不发送信号,但它仍然会执行错误检查。我们可以利用这种方式检测目标线程是否存在。pthread_kill成功时返回0,失败时返回错误代码。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值