保护对共享变量的访问:互斥锁
- 用于保护线程/进程的共享数据: 确保同一时间只有一个线程/进程能够访问数据
- 互斥量上锁之后, 任何其他试图再次对互斥量加锁的线程都会被阻塞,直到当前线程解锁(释放互斥量)
- 如果释放互斥量之后又一个以上的线程阻塞,那么所有该锁上的阻塞线程都会变成可运行状态,第一个变为可运行状态的线程就可以对互斥量加锁,其他线程就会看到互斥量仍是锁着的,只能回去再次等待它重新变为可用
- 互斥锁是协作锁
- 如果其中某个线程在没有得到锁的情况下也可以访问共享资源,那么即使其他线程在使用线程之前都申请锁,也会出现线程安全问题
初始化
互斥量是属于 pthread_mutex_t 类型的变量。在使用之前必须对其初始化。
静态初始化
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; //注意 PTHREAD_MUTEX_INITIALIZER不要省略(虽然PTHREAD_MUTEX_INITIALIZER被定义为0,而静态分配的变量会被自动初始化为0)
动态初始化
NAME
pthread_mutex_destroy, pthread_mutex_init - 销毁并初始化互斥锁
SYNOPSIS
#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);
DESCRIPTION
关于 attr, 表示互斥锁的属性。该对象在函数调用之前已经过了初始化处理,用于定义互斥量的属性.若将 attr 参
数置为 NULL,则该互斥量的各种属性会取默认值。
互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。当前(glibc2.2.3,linuxthreads0.9)有四个值可供选择:
* PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
* PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
* PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
* PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
SUSv3 规定,初始化一个业已初始化的互斥量将导致未定义的行为,应当避免这一行为
销毁未锁定的已初始化互斥锁应该是安全的。 尝试销毁锁定的互斥锁会导致未定义的行为。
在默认互斥体属性合适的情况下,宏 PTHREAD_MUTEX_INITIALIZER 可用于初始化静态分配的互斥体。 除了不执行
错误检查外,效果应等同于通过调用 pthread_mutex_init() 并将参数 attr 指定为 NULL 进行动态初始化。
返回值:
成功返回0; 失败返回错误编码
静态初始值 PTHREAD_MUTEX_INITIALIZER,只能用于对如下互斥量进行初始化:经由静态分配且携带默认属性。其他情况下,必须调用 pthread_mutex_init()对互斥量进行动态初始化
- 动态分配于堆中的互斥量。例如,动态创建针对某一结构的链表,表中每个结构都包含一个 pthread_mutex_t 类型的字段来存放互斥量,借以保护对该结构的访问
- 互斥量是在栈中分配的自动变量
- 初始化经由静态分配,且不使用默认属性的互斥量。
当不再需要经由自动或动态分配的互斥量时,应使用 pthread_mutex_destroy()将其销毁。(对于使用 PTHREAD_MUTEX_INITIALIZER 静态初始化的互斥量,无需调用 pthread_mutex_destroy()。
只有当互斥量处于未锁定状态,且后续也无任何线程企图锁定它时,将其销毁才是安全的。若互斥量驻留于动态分配的一片内存区域中,应在释放(free)此内存区域前将其销毁。对于自动分配的互斥量,也应在宿主函数返回前将其销毁。
经由 pthread_mutex_destroy()销毁的互斥量,可调用 pthread_mutex_init()对其重新初始化
加锁和解锁
初始化之后,互斥量处于未锁定状态。函数 pthread_mutex_lock()可以锁定某一互斥量,而
函数 pthread_mutex_unlock()则可以将一个互斥量解锁
#include <pthread.h>
// 均返回:如果成功则返回0,如果出错则正的Exxx值
int pthread_mutex_lock(pthread_mutex_t *mutex); // 上锁
int pthread_mutex_trylock(pthread_mutex_t *mutex); // 尝试上锁
int pthread_mutex_unlock(pthreadd_mutex_t *mutex); // 解锁
pthread_mutex_lock
和pthread_mutex_trylock
的区别:
- 当某个进程/线程尝试给某一把锁上锁时,会先检查是不是已经上锁了:
- 如果没有上锁,
pthread_mutex_lock
和pthread_mutex_trylock
都可以加锁成功,然后返回0 - 如果已经上锁:
pthread_mutex_lock
会阻塞住线程(挂起,不占用任何CPU),直到成功获取到了锁pthread_mutex_trylock
不会阻塞线程,而是立即返回EBUSY
- 如果没有上锁,
如果发起 pthread_mutex_lock()调用的线程自身之前已然将目标互斥量锁定,对于互斥量
的默认类型而言,可能会产生两种后果—视具体实现而定:线程陷入死锁(deadlock),因
试图锁定已为自己所持有的互斥量而遭到阻塞;或者调用失败,返回 EDEADLK 错误。在 Linux上,默认情况下线程会发生死锁
函数 pthread_mutex_unlock()将解锁之前已遭调用线程锁定的互斥量。以下行为均属错误:
- 对处于未锁定状态的互斥量进行解锁
- 解锁由其他线程锁定的互斥量
如果有不止一个线程在等待获取由函数 pthread_mutex_unlock()解锁的互斥量,则无法判断究竟哪个线程将如愿以偿
执行锁超时
NAME
pthread_mutex_timedlock - lock a mutex (ADVANCED REALTIME)
SYNOPSIS
#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict abs_timeout);
DESCRIPTION
当前调用线程最多阻塞__abstime时间,如果超时还未回去到锁,就返回ETIMEDOUT。如果成功在给定
时间了得到了锁,返回0
除了调用者可以指定一个附加参数 abstime(设置线程等待获取互斥量时休眠的时间限制)外,函数 pthread_mutex_timedlock()与 pthread_mutex_lock()没有差别。如果参数 abstime 指定的时间间隔期满,而调用线程又没有获得对互斥量的所有权,那么函数pthread_mutex_timedlock()返回 ETIMEDOUT 错误
互斥量的属性
typedef union
{
char __size[__SIZEOF_PTHREAD_MUTEXATTR_T];
int __align;
} pthread_mutexattr_t;
/*
* 返回值: 成功0,失败返回错误编号
*/
int pthread_mutexattr_init (pthread_mutexattr_t *__attr)
/*
* 返回值: 成功0,失败返回错误编号
*/
int pthread_mutexattr_destroy (pthread_mutexattr_t *__attr)
注意:
- 同一线程不应对同一互斥量加锁两次。
- 线程不应对不为自己所拥有的互斥量解锁(亦即,尚未锁定互斥量)。
- 线程不应对一尚未锁定的互斥量做解锁动作。
准确地说,上述情况的结果将取决于互斥量类型(type)。SUSv3 定义了以下互斥量类型。
-
PTHREAD_MUTEX_NORMAL
- 该类型互斥量不具有死锁检测功能。如果线程试图对已由自己锁定的互斥量进行加锁,则发生死锁。
- 互斥量处于未锁定状态,或者已由其他线程锁定,对其解锁会导致不确定的结果。
- (在 Linux 上,对这类互斥量的上述两种操作都会成功。)
-
PTHREAD_MUTEX_ERRORCHECK
- 对此类互斥量的所有操作都会执行错误检查。所有上述 3 种情况都会导致相关 Pthreads 函数返回错误。
- 这类互斥量运行起来比一般类型要慢,不过可将其作为调试工具,以发现程序在哪里违反了互斥量使用的基本原则
-
PTHREAD_MUTEX_RECURSIVE
- 递归互斥量维护由一个锁计数器。当线程第 1 次取得互斥量时,会将锁计数器置 1。后续由同一线程执行的每次加锁操作会递增锁计数器的数值,而解锁操作则递减计数器计数。
- 只有当锁计数器值降为0时,才会release该互斥锁(亦即可为其他线程所用)
- 解锁时如目标互斥量处于未锁定状态,或是已由其他线程锁定,操作都会失败。
Linux 的线程实现针对以上各种类型的互斥量提供了非标准的静态初始值(例如,PTHREAD_ RECURSIVE_MUTEX_INITIALIZER_NP),以便对那些通过静态分配的互斥量进行初始化,而无需使用 pthread_mutex_init()函数。不过,为保证程序的可移植性,应该避免使用这些初始值。
除了上述类型,SUSv3 还定义了 PTHREAD_MUTEX_DEFAULT 类型。使用 PTHREAD_ MUTEX_INITIALIZER 初始化的互斥量,或是经调用参数 attr 为 NULL 的 pthread_mutex_init()函数所创建的互斥量,都属于此类型。至于该类型互斥量在本节开始处 3 个场景中的行为,规范有意未作定义,意在为互斥量的高效实现保留最大的灵活性。Linux 上,PTHREAD_MUTEX_DEFAULT类型互斥量的行为与 PTHREAD_MUTEX_NORMAL 类型相仿。
下面例子创建了一个错误检查属性的互斥量:
pthread_mutex_t mtx;
pthread_mutexattr_t mtxAttr;
int s, type;
s = pthread_mutexattr_init(&mtxAttr);
if(s != 0){
exit(1);
}
s = pthread_mutexattr_settype(&mutex, PTHREAD_MUTEX_ERRORCHECK );
if(s != 0){
exit(1);
}
s = pthread_mutex_lock(&lock);
if(s != 0){
exit(1);
}
s = pthread_mutex_unlock(&lock); // 解锁
if(s != 0){
exit(1);
}
实践
初始化
- 静态初始化:
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
常用于全局锁。
#include <iostream>
#include <pthread.h>
#include <thread>
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int a = 0;
void thread_tast(){
for(int i = 0; i < 100000; i++){
pthread_mutex_lock(&lock); // 上锁(如果锁已经锁上,会阻塞直到拿到了锁)
a++;
pthread_mutex_unlock(&lock); // 解锁
}
printf("a = %d\n", a); // 锁能够保证最终结果一定相同(但不保证过程),如果不加锁的话每次运行结果都不一样
}
int main() {
std::thread t1(thread_tast);
std::thread t2(thread_tast);
t1.detach();
t2.detach();
getchar();
return 0;
}
- 动态初始化:
#include <iostream>
#include <pthread.h>
#include <thread>
pthread_mutex_t lock;
int a = 0;
void thread_tast(){
for(int i = 0; i < 100000; i++){
pthread_mutex_lock(&lock);
a++;
pthread_mutex_unlock(&lock);
}
printf("a = %d\n", a);
}
int main() {
pthread_mutex_init(&lock, nullptr); // nullptr会创建一个默认的普通锁
std::thread t1(thread_tast);
std::thread t2(thread_tast);
t1.detach();
t2.detach();
getchar();
pthread_mutex_destroy(&lock);
return 0;
}
锁在单线程多进程中
#include <malloc.h>
#include <pthread.h>
#include <zconf.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <wait.h>
static void *thread_main(void * arg){
pthread_mutex_t *__lock;
int ret;
char ebuf[256];
pid_t pid;
__lock = (pthread_mutex_t*) calloc(1, sizeof(pthread_mutex_t));
pthread_mutex_init(__lock, NULL);
printf("current pid: %d, tid: %lu\r\n", getpid(), pthread_self());
if( (ret = pthread_mutex_lock(__lock)) == 0){
printf("0: main thread(%lu) lock ok\r\n", pthread_self());
}else{
snprintf(ebuf, sizeof(ebuf), "%s", strerror(errno));
printf("0: main thread(%lu) lock error(%d:%s)\r\n",pthread_self(), ret,ebuf);
}
switch ((pid = fork())) {
case -1:
printf("fork failed\r\n");
free(__lock);
exit (0);
case 0:
/* 先解锁 */
if ((ret = pthread_mutex_unlock(__lock)) == 0)
printf("1: child thread(%lu) unlock ok\r\n",pthread_self());
else{
snprintf(ebuf, sizeof(ebuf), "%s", strerror(errno));
printf("1: child thread(%lu) unlock error(%d:%s)\r\n",pthread_self(), ret,ebuf);
}
sleep(1);
/* 再加锁 */
if ((ret = pthread_mutex_lock(__lock)) == 0)
printf("1: child thread(%lu) lock ok\r\n", pthread_self());
else{
snprintf(ebuf, sizeof(ebuf), "%s", strerror(errno));
printf("1: child thread(%lu) unlock error(%d:%s)\r\n",pthread_self(), ret,ebuf);
}
/* destroy锁--该函数会报错: 不能在没有lock锁的情况下destroy锁 */
if ((ret = pthread_mutex_destroy(__lock)) == 0)
printf("1: child thread(%lu) destroy ok\r\n", pthread_self());
else{
snprintf(ebuf, sizeof(ebuf), "%s", strerror(errno));
printf("1: child thread(%lu) destroy error(%d:%s)不能在没有lock锁的情况下destroy锁 \r\n",pthread_self(), ret,ebuf);
}
// 不能在destroy锁之后在lock 锁
/* if ((ret = pthread_mutex_lock(__lock)) == 0)
printf("1: child thread(%lu) lock ok\r\n", pthread_self());
else{
snprintf(ebuf, sizeof(ebuf), "%s", strerror(errno));
printf("1: child thread(%lu) unlock error(%d:%s)\r\n",pthread_self(), ret,ebuf);
}*/
free(__lock);
printf("1: child thread(%lu) free lock, then exit\r\n",pthread_self());
exit (0);
default:
printf("0: parent, child pid: %d, tid: %lu\r\n",
getpid(), pthread_self());
{
int status;
pid_t pp;
pp = wait(&status);
printf("wait's pid: %d, pid: %d, status: %d\r\n",pp, pid, status);
}
// 默认情况下,锁是共享的,当锁在某个线程中被销毁之后,就不能再次使用或者重复销毁了
if ((ret = pthread_mutex_destroy(__lock)) == 0)
printf("0: parent thread(%lu) destroy lock ok\r\n",
pthread_self());
else{
snprintf(ebuf, sizeof(ebuf), "%s", strerror(errno));
printf("0: parent thread(%lu) destroy err(%d:%s) 默认情况下,锁是共享的,当锁在某个线程中被销毁之后,就不能再次使用或者重复销毁了\r\n",
pthread_self(), ret, ebuf);
}
free(__lock);
break;
}
return NULL ;
}
static void test_thread(){
pthread_attr_t attr;
pthread_t t1;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, 10240000);
pthread_create(&t1, &attr, thread_main, NULL);
pthread_join(t1, NULL);
printf(">>> test_pthread_once: over now, enter any key to exit...\n");
getchar();
}
int main(void)
{
test_thread();
return 0;
}
超时锁
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <string.h>
int main(void)
{
int err;
struct timespec tout;
struct tm *tmp;
char buf[64];
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
//pthread_mutex_lock(&lock);
//printf("mutex is locked\n");
clock_gettime(CLOCK_REALTIME, &tout);
tmp = localtime(&tout.tv_sec);
strftime(buf, sizeof(buf), "%r", tmp);
printf("current time is %s\n", buf);
tout.tv_sec += 10;
err = pthread_mutex_timedlock(&lock, &tout);
if (err == 0){
printf("mutex locked again!\n");
}else{
printf("can't lock mutex again: %s\n", strerror(err));
}
clock_gettime(CLOCK_REALTIME, &tout);
tmp = localtime(&tout.tv_sec);
strftime(buf, sizeof(buf), "%r", tmp);
printf("the time is now %s\n", buf);
exit(0);
}
生产者消费者
- 生产者生产完所有的产品之后之后启动一个消费者
#include "unpipc.h"
#define MAXNITEMS 1000000 //最大条目数
#define MAXNTHREADS 100 //最大线程数
int nitems;
struct { // 这些变量只能在拥有互斥锁的时候访问
pthread_mutex_t mutex;
int buff[MAXNITEMS];
int nput; //buff数组下一次存放的元素下标
int nval; //下一次存放的值
} shared = {PTHREAD_MUTEX_INITIALIZER}; // PTHREAD_MUTEX_INITIALIZER 用在静态类型的互斥量中,而且应该在互斥量定义的时候就用 PTHREAD_MUTEX_INITIALIZER 进行初始化
void *produce(void *), *consume(void *);
int main(int argc,char ** argv)
{
int i, nthreads, count[MAXNTHREADS];
pthread_t tid_produce[MAXNTHREADS], tid_consume; //用来保存线程ID数组
if (argc != 3){
err_quit("usage: prodcons1 <#items> <#threads>");
}
nitems = min(atoi(argv[1]), MAXNITEMS);
nthreads = min(atoi(argv[2]), MAXNTHREADS);
printf("we create 最大放置空间 = %d, 工人个数 = %d, nput = %d, 线程车间大小 = %lu, count.size = %lu\n",
nitems, nthreads, shared.nput, sizeof(tid_produce)/sizeof(tid_produce[0]), sizeof(count)/sizeof(count[0]));
// Set_concurrency(nthreads);
for(i = 0; i < nthreads; i++){ // 创建生产者线程
count[i] = 0;
Pthread_create(&tid_produce[i], NULL, produce, &count[i]); // (线程ID, 默认属性, 线程启动后要执行的函数, 传给线程启动函数的参数)
}
int total = 0;
for (i = 0; i < nthreads; i++) {
Pthread_join(tid_produce[i], NULL); // 等待线程1完成, 等待线程2完成, 等待线程3完成
total = total + count[i];
printf("count[%d] = %d\n", i, count[i]);
}
printf("total = %d\n",total);
Pthread_create(&tid_consume, NULL, consume, NULL);
Pthread_join(tid_consume, NULL);
}
void *produce(void *arg){
while (1){
pthread_mutex_lock(&shared.mutex);
if (shared.nput >= nitems){ //
// printf("移动的下标超过最大空间数,没有空间可以存放了. 当前数组的下标 = %d, 最大空间 = %d, workID = %d\n", shared.nput, nitems, *((int *) arg));
pthread_mutex_unlock(&shared.mutex);
return (NULL); /* 退出当前进程 */
}
shared.buff[shared.nput] = shared.nval; // 将当前值存放到数组对应位置
shared.nput++; // 数组下标加1
shared.nval++; // 数组存放的值加1
pthread_mutex_unlock(&shared.mutex);
*((int *) arg) += 1;
}
}
void *consume(void *arg)
{
int i;
for (i = 0; i < nitems; i++) {
if (shared.buff[i] != i)
printf("buff[%d] = %d\n", i, shared.buff[i]);
}
return(NULL);
}
- 所有生产者线程启动之后立刻启动消费者线程
#include "unpipc.h"
#define MAXNITEMS 1000000 //最大条目数
#define MAXNTHREADS 100 //最大线程数
int nitems;
struct { // 这些变量只能在拥有互斥锁的时候访问
pthread_mutex_t mutex;
int buff[MAXNITEMS];
int nput; //buff数组下一次存放的元素下标
int nval; //下一次存放的值
} shared = {PTHREAD_MUTEX_INITIALIZER}; // PTHREAD_MUTEX_INITIALIZER 用在静态类型的互斥量中,而且应该在互斥量定义的时候就用 PTHREAD_MUTEX_INITIALIZER 进行初始化
void *produce(void *), *consume(void *);
int main(int argc,char ** argv)
{
int i, nthreads, count[MAXNTHREADS];
pthread_t tid_produce[MAXNTHREADS], tid_consume; //用来保存线程ID数组
if (argc != 3){
err_quit("usage: prodcons1 <#items> <#threads>");
}
nitems = min(atoi(argv[1]), MAXNITEMS);
nthreads = min(atoi(argv[2]), MAXNTHREADS);
printf("we create 最大放置空间 = %d, 工人个数 = %d, nput = %d, 线程车间大小 = %lu, count.size = %lu\n",
nitems, nthreads, shared.nput, sizeof(tid_produce)/sizeof(tid_produce[0]), sizeof(count)/sizeof(count[0]));
for(i = 0; i < nthreads; i++){ // 创建生产者线程
count[i] = 0;
Pthread_create(&tid_produce[i], NULL, produce, &count[i]); // (线程ID, 默认属性, 线程启动后要执行的函数, 传给线程启动函数的参数)
}
Pthread_create(&tid_consume, NULL, consume, NULL);
int total = 0;
for (i = 0; i < nthreads; i++) {
Pthread_join(tid_produce[i], NULL); // 等待线程1完成, 等待线程2完成, 等待线程3完成
total = total + count[i];
printf("count[%d] = %d\n", i, count[i]);
}
printf("total = %d\n",total);
Pthread_join(tid_consume, NULL);
}
void *produce(void *arg){
while (1){
pthread_mutex_lock(&shared.mutex);
if (shared.nput >= nitems){ //
// printf("移动的下标超过最大空间数,没有空间可以存放了. 当前数组的下标 = %d, 最大空间 = %d, workID = %d\n", shared.nput, nitems, *((int *) arg));
pthread_mutex_unlock(&shared.mutex);
return (NULL); /* 退出当前进程 */
}
shared.buff[shared.nput] = shared.nval; // 将当前值存放到数组对应位置
shared.nput++; // 数组下标加1
shared.nval++; // 数组存放的值加1
pthread_mutex_unlock(&shared.mutex);
*((int *) arg) += 1;
}
}
void consume_wait(int i)
{
for ( ; ; ) {
Pthread_mutex_lock(&shared.mutex);
if (i < shared.nput) {
Pthread_mutex_unlock(&shared.mutex);
return; /* an item is ready */
}
Pthread_mutex_unlock(&shared.mutex);
}
}
void *consume(void *arg)
{
int i;
for (i = 0; i < nitems; i++) {
consume_wait(i); // 必须等待生产完了才能消费(通过判断当前的i是不是小于shared.nput--->shared_put是共享变量,我们在查看nput之前必须先获取到锁,因为某个生产者线程可能正处于更新该变量的过程中)
if (shared.buff[i] != i)
printf("buff[%d] = %d\n", i, shared.buff[i]);
}
return(NULL);
}
当消费某个变量时,这个变量还没有准备好,这里一次次轮循,每次给互斥量解锁又上锁来确保生产者准备好了,但是这非常浪费CPU。
解决方法:我们可以让当前消费者线程睡一会儿,让出相关资源(包括CPU和锁)
sched_yield
#include "unpipc.h"
#define MAXNITEMS 1000000
#define MAXNTHREADS 100
int nitems; /* read-only by producer and consumer */
struct {
pthread_mutex_t mutex;
int buff[MAXNITEMS];
int nput;
int nval;
} shared = { PTHREAD_MUTEX_INITIALIZER };
void *produce(void *), *consume(void *);
/* include main */
int main(int argc, char **argv)
{
int i, nthreads, count[MAXNTHREADS];
pthread_t tid_produce[MAXNTHREADS], tid_consume;
if (argc != 3)
err_quit("usage: prodcons4 <#items> <#threads>");
nitems = min(atoi(argv[1]), MAXNITEMS);
nthreads = min(atoi(argv[2]), MAXNTHREADS);
Set_concurrency(nthreads + 1);
/* 4create all producers and one consumer */
for (i = 0; i < nthreads; i++) {
count[i] = 0;
Pthread_create(&tid_produce[i], NULL, produce, &count[i]);
}
Pthread_create(&tid_consume, NULL, consume, NULL);
/* wait for all producers and the consumer */
for (i = 0; i < nthreads; i++) {
Pthread_join(tid_produce[i], NULL);
printf("count[%d] = %d\n", i, count[i]);
}
Pthread_join(tid_consume, NULL);
exit(0);
}
/* end main */
void * produce(void *arg)
{
for ( ; ; ) {
Pthread_mutex_lock(&shared.mutex);
if (shared.nput >= nitems) {
Pthread_mutex_unlock(&shared.mutex);
return(NULL); /* array is full, we're done */
}
shared.buff[shared.nput] = shared.nval;
shared.nput++;
shared.nval++;
Pthread_mutex_unlock(&shared.mutex);
*((int *) arg) += 1;
}
}
/* include consume */
void consume_wait(int i)
{
for ( ; ; ) {
Pthread_mutex_lock(&shared.mutex);
if (i < shared.nput) {
Pthread_mutex_unlock(&shared.mutex);
return; /* an item is ready */
}
Pthread_mutex_unlock(&shared.mutex);
sched_yield();
}
}
void *consume(void *arg)
{
int i;
for (i = 0; i < nitems; i++) {
consume_wait(i);
if (shared.buff[i] != i)
printf("buff[%d] = %d\n", i, shared.buff[i]);
}
return(NULL);
}
/* end consume */
sched_yield
会让出当前线程的CPU占有权,然后把线程放到静态队列的尾端,然后一个新的CPU会占用CPU。那这个和sleep又什么区别呢?
sched_yield
这个函数可以使用另一个级别等于或者高于当前线程的线程先运行,如果没有符合条件的线程,那么这个函数会立即返回然后继续执行当前线程的程序sleep
则是等待一定时间之后等待CPU的调度。
那么什么时候使用sched_yield
呢?
- 有策略的调用
sched_yield
能在资源竞争情况很严重的时候,通过给其他的线程或者进程运行机会的方式来提升程序的性能,让其他的各个线程或者进程都有机会运行。
条件变量
- 互斥量用于上锁,条件变量用于等待
- 每个条件变量总是有一个互斥锁与之相关联。
API
- 初始化一个条件变量
条件变量是类型为pthread_cond_t
的变量
#include <pthread.h>
/*
* 作用:初始化一个条件变量。当参数cattr为空指针时,函数创建的是一个缺省的条件变量。否则条件变量的属性将由cattr中的属性值来决定
* 返回值:函数成功返回0;任何其他返回值都表示错误
* 注意:不能由多个线程同时初始化一个条件变量。当需要重新初始化或释放一个条件变量时,应用程序必须保证这个条件变量未被使用。
*
*/
int pthread_cond_init(pthread_cond_t *cv, const pthread_condattr_t *cattr);
可以用宏PTHREAD_COND_INITIALIZER来初始化静态定义的条件变量,使其具有缺省属性。这和用pthread_cond_init函数动态分配的效果是一样的。初始化时不进行错误检查。如:
pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
- 阻塞等待条件变量
#include <pthread.h>
/*
* 作用:等其线程阻塞等待条件变量上的pthread_mutex_t。被阻塞的线程可以被pthread_cond_signal函数,pthread_cond_broadcast函数唤醒,也可能在被信号中断后被唤醒。
* 返回值:函数成功返回0;任何其他返回值都表示错误
*/
int pthread_cond_wait(pthread_cond_t *cv, pthread_mutex_t *mutex);
- 注意:
pthread_cond_wait
必须放在pthread_mutex_lock
和pthread_mutex_unlock
之间,因为他要根据共享变量的状态来决定是否要等待,而为了不永远等待下去所以必须要在lock/unlock队中 pthread_cond_wait()
函数一进入wait状态就会自动release mutex
。当其他线程通过pthread_cond_signal()
或pthread_cond_broadcast
,把该线程唤醒,使pthread_cond_wait()
通过(返回)时,该线程又自动获得该mutex
。Pthread_cond_wait
在返回之前一定一定会获取到mutex
并给它上锁
- 唤醒某一个条件变量的线程
/*
* 作用:发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态。
* 唤醒阻塞在条件变量上的所有线程的顺序由调度策略决定,如果线程的调度策略是SCHED_OTHER类型的,系统将根据线程的优先级唤醒线程。如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。
* 如果没有线程被阻塞在条件变量上,那么调用pthread_cond_signal()将没有作用
* 返回值:函数成功返回0;任何其他返回值都表示错误
* 注意:
*/
int pthread_cond_signal(pthread_cond_t *cv);
- 使用
pthread_cond_signal
不会有“惊群现象”产生,他最多只给一个线程发信号。( 但是pthread_cond_signal
在多处理器上可能同时唤醒多个线程,当你只能让一个线程处理某个任务时,其它被唤醒的线程就需要继续wait
) pthread_cond_signal
即可以放在pthread_mutex_lock
和pthread_mutex_unlock
之间,也可以放在pthread_mutex_lock
和pthread_mutex_unlock
之后,但是各有各缺点。
pthread_mutex_lock
xxxxxxx
pthread_cond_signal
pthread_mutex_unlock
-
缺点:在某下线程的实现中,会造成等待线程从内核中唤醒(由于
cond_signal
)然后又回到内核空间(因为cond_wait返回后会有原子加锁的行为),所以一来一回会有性能的问题。 -
我们假设系统中有线程1和线程2,他们都想获取mutex后处理共享数据,再释放mutex。请看这种序列:
- 1)线程1获取mutex,在进行数据处理的时候,线程2也想获取mutex,但是此时被线程1所占用,线程2进入休眠,等待mutex被释放。
- 2)线程1做完数据处理后,调用pthread_cond_signal()唤醒等待队列中某个线程,在本例中也就是线程2。线程1在调用pthread_mutex_unlock()前,因为系统调度的原因,线程2获取使用CPU的权利,那么它就想要开始处理数据,但是在开始处理之前,mutex必须被获取,很遗憾,线程1正在使用mutex,所以线程2被迫再次进入休眠。
- 3)然后就是线程1执行pthread_mutex_unlock()后,线程2方能被再次唤醒。
从这里看,使用的效率是比较低的,如果再多线程环境中,这种情况频繁发生的话,是一件比较痛苦的事情。
但是在LinuxThreads或者NPTL里面,就不会有这个问题,因为在Linux 线程中,有两个队列,分别是cond_wait队列和mutex_lock队列, cond_signal只是让线程从cond_wait队列移到mutex_lock队列,而不用返回到用户空间,不会有性能的损耗。
pthread_mutex_lock
xxxxxxx
pthread_mutex_unlock
pthread_cond_signal
- 优点:不会出现之前说的那个潜在的性能损耗,因为在signal之前就已经释放锁了
- 缺点:如果unlock和signal之前,有个低优先级的线程正在mutex上等待的话,那么这个低优先级的线程就会抢占高优先级的线程(cond_wait的线程),而这在上面的放中间的模式下是不会出现的。
- 阻塞直到指定时间
#include <pthread.h>
#include <time.h>
/*
* 作用:函数到了一定的时间,即使条件未发生也会解除阻塞。这个时间由参数abstime指定。函数返回时,相应的互斥锁往往是锁定的,即使是函数出错返回。
* 返回值:函数成功返回0;任何其他返回值都表示错误(超时还没有收到信号返回的错误码是ETIMEDOUT)
*/
int pthread_cond_timedwait(pthread_cond_t *cv, pthread_mutex_t *mp, const structtimespec * abstime);
有关于超时时间structtimespec
,指的时一天中的某个时刻,比如
pthread_timestruc_t to;
to.tv_sec = time(NULL) + TIMEOUT; //秒
to.tv_nsec = 0; // 那么
- 释放条件变量
#include <pthread.h>
/*
作用: 释放条件变量
返回值:函数成功返回0;任何其他返回值都表示错误
注意:条件变量占用的空间并未被释放。
*/
int pthread_cond_destroy(pthread_cond_t *cv);
实践
#include "unpipc.h"
#define MAXNITEMS 1000000
#define MAXNTHREADS 100
/* globals shared by threads */
int nitems; /* read-only by producer and consumer */
int buff[MAXNITEMS];
struct {
pthread_mutex_t mutex;
pthread_cond_t cond;
int nput;
int nval;
int nready;
} nready = { PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER };
void *produce(void *), *consume(void *);
/* include main */
int main(int argc, char **argv)
{
int i, nthreads, count[MAXNTHREADS];
pthread_t tid_produce[MAXNTHREADS], tid_consume;
if (argc != 3)
err_quit("usage: prodcons5 <#items> <#threads>");
nitems = min(atoi(argv[1]), MAXNITEMS);
nthreads = min(atoi(argv[2]), MAXNTHREADS);
Set_concurrency(nthreads + 1);
for (i = 0; i < nthreads; i++) {
count[i] = 0;
Pthread_create(&tid_produce[i], NULL, produce, &count[i]);
}
Pthread_create(&tid_consume, NULL, consume, NULL);
int total = 0;
for (i = 0; i < nthreads; i++) {
Pthread_join(tid_produce[i], NULL);
printf("count[%d] = %d\n", i, count[i]);
total += count[i];
}
printf("total = %d\n", total);
Pthread_join(tid_consume, NULL);
exit(0);
}
/* end main */
void *produce(void *arg)
{
for ( ; ; ) {
Pthread_mutex_lock(&nready.mutex); // 去获取锁,并上锁
if (nready.nput >= nitems) { // 是不是还有活干
Pthread_mutex_unlock(&nready.mutex);
return(NULL); /* array is full, we're done */
}
buff[nready.nput] = nready.nval; // 干活
nready.nput++; // 干活
nready.nval++; // 干活
nready.nready++; // 干活
// Pthread_cond_signal(&nready.cond); // 通知活干好了
Pthread_mutex_unlock(&nready.mutex); // 解锁
Pthread_cond_signal(&nready.cond); // 通知某一个”人“好了,与上面的注释任意一个就好
*((int *) arg) += 1; // 积分加一
}
}
/* include consume */
void *consume(void *arg)
{
int i;
for (i = 0; i < nitems; i++) {
Pthread_mutex_lock(&nready.mutex);
while (nready.nready == 0){ // 如果还没有准备好
Pthread_cond_wait(&nready.cond, &nready.mutex); //发现还没有准备好:先把锁还回去,然后退回队伍中
// 先解除之前的pthread_mutex_lock锁定的mutex;第二 挂起,阻塞并在等待对列里休眠
} // 发现有个短信通知了”我“(并且送了一把锁过来),”我“上锁,然去查看是不是准备好
nready.nready--; // 发现柜子里准备好了(因为一定是按照顺序准备的),拿走东西
Pthread_mutex_unlock(&nready.mutex); // 消费完了,把锁还回去
if (buff[i] != i) // 比较刚刚柜子里的东西与我是不是相配
printf("buff[%d] = %d\n", i, buff[i]);
}
return(NULL);
}
/* end consume */
有可能生产了多个,然后才消费一个,但是一定会等待到有生产了才去消费。 问题是:如果有消费者正在消费,那么生产者就不能生产了(因为它们共用一把锁)。
分析:生产者需要改变放置物体的空间, 并且这个空间是共享的,但是这个空间有一个干了另一个就不要干了,因此这个生产者之间一定要共用一把锁以放置不安全。
问题是生产者和消费者需不需要共享这边锁呢?
生产者用过的空间就不需要访问了,从某种程度上,生产者和消费者是时间隔离空间共享的,有隔离就不构成危险共享,因此可以独用一把锁(如果共有一把锁那么生产者生产的时候消费者不能消费,消费者消费的时候生产者就不能生产了),就解决了这个问题(生产者生产的时候消费者尽管消费就好,只要这个产品已经生产好了,如果还没有生产好,就等待)
#include "unpipc.h"
#define MAXNITEMS 1000000
#define MAXNTHREADS 100
int nitems; /* read-only by producer and consumer */
int buff[MAXNITEMS];
struct {
pthread_mutex_t mutex;
int nput; /* next index to store */
int nval; /* next value to store */
} put = { PTHREAD_MUTEX_INITIALIZER };
struct {
pthread_mutex_t mutex;
pthread_cond_t cond;
int nready; /* number ready for consumer */
} nready = { PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER };
void *produce(void *), *consume(void *);
int main(int argc, char **argv)
{
int i, nthreads, count[MAXNTHREADS];
pthread_t tid_produce[MAXNTHREADS], tid_consume;
if (argc != 3)
err_quit("usage: prodcons6 <#items> <#threads>");
nitems = min(atoi(argv[1]), MAXNITEMS);
nthreads = min(atoi(argv[2]), MAXNTHREADS);
Set_concurrency(nthreads + 1);
for (i = 0; i < nthreads; i++) {
count[i] = 0;
Pthread_create(&tid_produce[i], NULL, produce, &count[i]);
}
Pthread_create(&tid_consume, NULL, consume, NULL);
for (i = 0; i < nthreads; i++) {
Pthread_join(tid_produce[i], NULL);
printf("count[%d] = %d\n", i, count[i]);
}
Pthread_join(tid_consume, NULL);
exit(0);
}
void *produce(void *arg)
{
for ( ; ; ) {
Pthread_mutex_lock(&put.mutex); // 生产上锁
if (put.nput >= nitems) {
Pthread_mutex_unlock(&put.mutex);
return(NULL); /* array is full, we're done */
}
buff[put.nput] = put.nval;
put.nput++;
put.nval++;
Pthread_mutex_unlock(&put.mutex); // 生产解锁
Pthread_mutex_lock(&nready.mutex); // 消费上锁
nready.nready++; // 消费++
Pthread_mutex_unlock(&nready.mutex); // 解锁
Pthread_cond_signal(&nready.cond); // 通知人来消费
*((int *) arg) += 1;
}
}
void *consume(void *arg)
{
int i;
for (i = 0; i < nitems; i++) {
Pthread_mutex_lock(&nready.mutex); // 上锁
while (nready.nready == 0) // 没有可以消费的
Pthread_cond_wait(&nready.cond, &nready.mutex); // 解锁,然后等待唤醒----》唤醒了一定能获取到锁
nready.nready--; // 消费
Pthread_mutex_unlock(&nready.mutex); //消费之后解锁
if (buff[i] != i) // 处理消费品
printf("buff[%d] = %d\n", i, buff[i]);
}
return(NULL);
}
上面的代码还是有问题:没有必要生产一个就马上通知消费者来消费吧。可能cond_wait队列中没有等待消费的人呢?
/* include globals */
#include "unpipc.h"
#define MAXNITEMS 1000000
#define MAXNTHREADS 100
/* globals shared by threads */
int nitems; /* read-only by producer and consumer */
int buff[MAXNITEMS];
struct {
pthread_mutex_t mutex;
int nput; /* next index to store */
int nval; /* next value to store */
} put = { PTHREAD_MUTEX_INITIALIZER };
struct {
pthread_mutex_t mutex;
pthread_cond_t cond;
int nready; /* number ready for consumer */
} nready = { PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER };
/* end globals */
int nsignals;
void *produce(void *), *consume(void *);
/* include main */
int
main(int argc, char **argv)
{
int i, nthreads, count[MAXNTHREADS];
pthread_t tid_produce[MAXNTHREADS], tid_consume;
if (argc != 3)
err_quit("usage: prodcons7 <#items> <#threads>");
nitems = min(atoi(argv[1]), MAXNITEMS);
nthreads = min(atoi(argv[2]), MAXNTHREADS);
Set_concurrency(nthreads + 1);
/* 4create all producers and one consumer */
for (i = 0; i < nthreads; i++) {
count[i] = 0;
Pthread_create(&tid_produce[i], NULL, produce, &count[i]);
}
Pthread_create(&tid_consume, NULL, consume, NULL);
/* wait for all producers and the consumer */
for (i = 0; i < nthreads; i++) {
Pthread_join(tid_produce[i], NULL);
printf("count[%d] = %d\n", i, count[i]);
}
Pthread_join(tid_consume, NULL);
printf("nsignals = %d\n", nsignals);
exit(0);
}
/* end main */
void *produce(void *arg)
{
for ( ; ; ) {
Pthread_mutex_lock(&put.mutex);
if (put.nput >= nitems) {
Pthread_mutex_unlock(&put.mutex);
return(NULL); /* array is full, we're done */
}
buff[put.nput] = put.nval;
put.nput++;
put.nval++;
Pthread_mutex_unlock(&put.mutex);
Pthread_mutex_lock(&nready.mutex);
if (nready.nready == 0) { //发现没有消费品了,现在我又生产了一个
Pthread_cond_signal(&nready.cond); // 通知人来消费
nsignals++;
}
nready.nready++;
Pthread_mutex_unlock(&nready.mutex);
*((int *) arg) += 1;
}
}
void *consume(void *arg)
{
int i;
for (i = 0; i < nitems; i++) {
Pthread_mutex_lock(&nready.mutex);
while (nready.nready == 0)
Pthread_cond_wait(&nready.cond, &nready.mutex);
nready.nready--;
Pthread_mutex_unlock(&nready.mutex);
if (buff[i] != i)
printf("buff[%d] = %d\n", i, buff[i]);
}
return(NULL);
}
问题:生产者线程Pthread_cond_signal
发送信号之后,系统立即调度等待在其上的消费者线程,该消费者开始运行,但是立即停止,因为它不能获取到相应的互斥锁
/* include globals */
#include "unpipc.h"
#define MAXNITEMS 1000000
#define MAXNTHREADS 100
/* globals shared by threads */
int nitems; /* read-only by producer and consumer */
int buff[MAXNITEMS];
struct {
pthread_mutex_t mutex;
int nput; /* next index to store */
int nval; /* next value to store */
} put = { PTHREAD_MUTEX_INITIALIZER };
struct {
pthread_mutex_t mutex;
pthread_cond_t cond;
int nready; /* number ready for consumer */
} nready = { PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER };
/* end globals */
int nsignals;
void *produce(void *), *consume(void *);
/* include main */
int
main(int argc, char **argv)
{
int i, nthreads, count[MAXNTHREADS];
pthread_t tid_produce[MAXNTHREADS], tid_consume;
if (argc != 3)
err_quit("usage: prodcons7 <#items> <#threads>");
nitems = min(atoi(argv[1]), MAXNITEMS);
nthreads = min(atoi(argv[2]), MAXNTHREADS);
Set_concurrency(nthreads + 1);
/* 4create all producers and one consumer */
for (i = 0; i < nthreads; i++) {
count[i] = 0;
Pthread_create(&tid_produce[i], NULL, produce, &count[i]);
}
Pthread_create(&tid_consume, NULL, consume, NULL);
/* wait for all producers and the consumer */
for (i = 0; i < nthreads; i++) {
Pthread_join(tid_produce[i], NULL);
printf("count[%d] = %d\n", i, count[i]);
}
Pthread_join(tid_consume, NULL);
printf("nsignals = %d\n", nsignals);
exit(0);
}
/* end main */
void *produce(void *arg)
{
for ( ; ; ) {
Pthread_mutex_lock(&put.mutex);
if (put.nput >= nitems) {
Pthread_mutex_unlock(&put.mutex);
return(NULL); /* array is full, we're done */
}
buff[put.nput] = put.nval;
put.nput++;
put.nval++;
Pthread_mutex_unlock(&put.mutex);
int dosignal;
Pthread_mutex_lock(&nready.mutex);
dosignal = (nready.nready == 0);
nready.nready++;
Pthread_mutex_unlock(&nready.mutex);
if(dosignal){
Pthread_cond_signal(&nready.cond);
nsignals++;
}
*((int *) arg) += 1;
}
}
void *consume(void *arg)
{
int i;
for (i = 0; i < nitems; i++) {
Pthread_mutex_lock(&nready.mutex);
while (nready.nready == 0)
Pthread_cond_wait(&nready.cond, &nready.mutex);
nready.nready--;
Pthread_mutex_unlock(&nready.mutex);
if (buff[i] != i)
printf("buff[%d] = %d\n", i, buff[i]);
}
return(NULL);
}
互斥锁与条件变量的属性
#include "unpipc.h"
int main(int argc, char **argv)
{
pthread_mutexattr_t mattr; // 互斥锁的默认属性
pthread_condattr_t cattr; // 条件变量的默认属性
for ( ; ; ) {
Pthread_mutexattr_init(&mattr); // 初始化
Pthread_mutexattr_destroy(&mattr); //销毁
Pthread_condattr_init(&cattr);
Pthread_condattr_destroy(&cattr);
}
exit(0);
}
如果需要在不同进程中共享互斥锁或者条件变量,而不是在单个进程中不同进程间共享怎么办?
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *mattr, int *pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *mattr, int pshared);
int pthread_condattr_getpshared(const pthread_condattr_t *mattr, int *pshared);
int pthread_condattr_setpshared(pthread_condattr_t *mattr, int pshared);