UNIX环境高级编程 学习笔记 第十一章 线程

多线程时,每个线程处理各自的任务,进程就可在某一时刻做不止一件事,只有在两任务处理过程互不依赖情况下,才能交叉执行。

多进程必须使用其他机制才能共享内存和文件描述符,而多线程自动地共享进程的所有信息,包括可执行程序代码、程序全局内存和堆内存、栈、文件描述符。

交互程序也可通过多线程改善响应时间,可把程序中处理用户输入输出部分与其他部分分开。

多线程程序在串行化任务时不得不阻塞,由于某些线程阻塞时还有另外的线程可以运行,因此多线程程序在单处理器上也可以改善响应时间和吞吐量。

单核CPU上运行的多线程程序, 同一时间只能一个线程在跑。

本章线程接口来自POSIX.1-2001,线程接口也称pthread和POSIX线程。它的功能测试宏是_POSIX_THREADS,可编译时确定是否支持posix线程,也可用_SC_THREADS调用sysconf运行时确定是否支持posix线程。它原本是可选功能,在SUSv4中成为了基本功能,遵循SUSv4的系统定义_POSIX_THREADS值为200809L。

线程id仅在它所属进程中才有意义。

线程id用pthread_t类型表示,由于实现不同,必须用函数比较两个线程id:
在这里插入图片描述
有些系统的pthread_t是结构类型,这样不能用可移植方式打印它的值。

线程可用pthread_self函数获得自身线程id:
在这里插入图片描述
主线程可能把工作任务放到一个队列中,用线程id控制每个工作线程处理哪些作业,此时可用此函数识别线程:
UNIX环境高级编程 学习笔记 第十一章 线程_第1张图片
在POSIX线程(pthread)情况下,程序开始运行时,也是以单进程中的单个控制线程启动的,新增线程:
UNIX环境高级编程 学习笔记 第十一章 线程_第2张图片
成功返回时,由参数tidp指向的内存单元被设置为新创建线程的线程ID;attr参数指定不同的线程属性,NULL为默认属性;新线程从参数start_rtn指向的函数的地址开始运行,该函数只有一个void *参数;如果需要向start_rtn传递的参数有一个以上,则需要将这些参数放到参数arg指向的结构中。

新线程和调用线程哪个先运行是不确定的。新线程会继承调用线程的浮点环境和信号屏蔽字,但挂起信号集不会继承。

pthread调用失败会返回错误码,并不设置errno,这样比较整洁,不会被其他函数影响,可把错误范围局限在引起出错的函数中。每个线程都提供errno的副本。

打印线程id:

#include 
#include 
#include 

pthread_t ntid;

void printids(const char *s) {
    pid_t pid;
    pthread_t tid;

    pid = getpid();
    tid = pthread_self();
    printf("%s pid %lu tid %lu (0x%lx)\n", s, (unsigned long)pid, (unsigned long)tid, (unsigned long)tid);    // %x表示十六进程输出值
}

void *thr_fn(void *arg) {
    printids("new thread: ");
    return (void *)0;
}

int main() {
    int err;

    err = pthread_create(&ntid, NULL, thr_fn, NULL);
    if (err != 0) {
        printf("%d, can't create thread", err);
		exit(1);
    }
    printids("main thread: ");
    sleep(1);
    exit(0);
}

执行它:
在这里插入图片描述
此程序中,主线程需要休眠,否则它就可能退出,新线程还没机会运行可能就终止了。新线程是通过pthread_self函数获取自己线程id的,而不是从共享内存中读出的或从线程启动例程中以参数形式接收到的。新建的线程不能安全地使用调用线程的参数ntid,如果新线程在主线程调用的pthread_create返回前就运行了,那么新线程看到的是未经初始化的ntid内容,它并不是正确的线程id。

Linux是使用ul来表示线程id的。

Linux 2.4中,LinuxThreads是用单独的进程实现每个线程的,它很难与POSIX线程匹配。Linux 2.6采用了被称为Native POSIX的线程库的新线程实现。

任意线程调用三个exit函数其中一个,进程就会终止。如果信号的默认动作是终止进程,那么发送到线程的信号会终止整个进程。

只退出线程:
1.线程从启动例程返回,返回值是线程的退出码。
2.线程可被同一进程中其他线程取消。
3.线程调用pthread_exit:
在这里插入图片描述
参数rval_ptr是无类型指针,用于存放线程返回值。进程中其他线程也能通过调用pthread_join访问此指针:
在这里插入图片描述
此函数使调用线程一直阻塞,直到指定线程调用pthread_exit、从启动例程返回、被取消。如果线程简单地从启动例程返回,参数rval_ptr指向的内存中存放返回码;如果线程被取消,则参数rval_ptr指定的内存单元被设为PTHREAD_CANCELED。

可调用pthread_join把线程置于分离状态,这样资源就可以回收。如果线程已经是分离状态,pthread_join调用会立即失败,返回EINVAL。

如对线程返回值不感兴趣,可把参数rval_ptr置为NULL,此时调用pthread_join可等待指定线程终止,但不获取线程状态。

获取线程退出状态:

#include 
#include 
#include 

void *thr_fn1(void *arg) {
    printf("thread 1 returning\n");
    return ((void *)1);
}

void *thr_fn2(void *arg) {
    printf("thread 2 exiting\n");
    pthread_exit ((void *)2);
}

int main() {
    int err; 
    pthread_t tid1, tid2;
    void *tret;

    err = pthread_create(&tid1, NULL, thr_fn1, NULL);
    if (err != 0) {
        printf("%d, can't create thread 1\n", err);
		exit(1);
    }
    err = pthread_create(&tid2, NULL, thr_fn2, NULL);
    if (err != 0) {
        printf("%d, can't create thread2\n", err);
		exit(1);
    }

    err = pthread_join(tid1, &tret);
    if (err != 0) {
        printf("%d, can't join with thread1\n", err);
		exit(1);
    }
    printf("thread 1 exit code %ld\n", (long)tret);

    err = pthread_join(tid2, &tret);
    if (err != 0) {
        printf("&d, can't join with thread 2\n");
		exit(1);
    }
    printf("thread 2 exit code %ld\n", (long)tret);
}

执行它:
在这里插入图片描述
可看到,线程调用pthread_exit或简单地从启动例程返回时,其他线程可用pthread_join函数获取它的退出状态。

pthread_create和pthread_exit函数的无类型指针可指向包含复杂信息的结构的地址,但要保证结构所在内存在栈被销毁(线程返回后)仍可用。

pthread_exit函数返回时使用栈上的自动变量时会出问题:

#include 
#include 
#include 

struct foo {
    int a, b, c, d;
};

void printfoo(const char *s, const struct foo *fp) {
    printf("%s", s);
    printf("  structure at 0x%lx\n", (unsigned long)fp);
    printf("  foo.a = %d\n", fp->a);
    printf("  foo.b = %d\n", fp->b);
    printf("  foo.c = %d\n", fp->c);
    printf("  foo.d = %d\n", fp->d);
}

void *thr_fn1(void *arg) {
    struct foo foo = {1, 2, 3, 4};

    printfoo("thread 1:\n", &foo);
    pthread_exit ((void *)&foo);
}

void *thr_fn2(void *arg2) {
    printf("thread 2: ID is %lu\n", (unsigned long)pthread_self());
    pthread_exit ((void *)0);
}

int main() {
    int err;
    pthread_t tid1, tid2;
    struct foo *fp;

    err = pthread_create(&tid1, NULL, thr_fn1, NULL);
    if (err != 0) {
        printf("%d, can't create thread1", NULL);
		exit(1);
    }

    err = pthread_join(tid1, (void *)&fp);
    if (err != 0) {
        printf("%d, can't join with thread1", err);
		exit(1);
    }

    sleep(1);
    printf("parent starting second thread\n");

    err = pthread_create(&tid2, NULL, thr_fn2, NULL);
    if (err != 0) {
        printf("%d, can't starting second thread\n", err);
		exit(1);
    }

    sleep(1);

    printfoo("parent:\n", fp);
    exit(0);
}

执行结果:
UNIX环境高级编程 学习笔记 第十一章 线程_第3张图片
以上代码中第二个线程作用是覆盖第一个线程的栈。

在Mac OS X 运行结果:
UNIX环境高级编程 学习笔记 第十一章 线程_第4张图片
此时,父线程试图访问第一个线程传给它的结构时,内存不再有效,此时得到SIGSEGV信号。

而在FreeBSD上,线程2不会覆盖线程1的栈,但不能总是期望这样。

线程可调用pthread_cancel请求取消同一进程中的其他线程:
在这里插入图片描述
默认,此函数会使线程id为参数tid的线程的行为表现为如同调用了参数为PTHREAD_CANCELED的pthread_exit函数。线程可选择忽略取消或控制如何被取消。此函数并不等待线程终止,它仅仅是提出请求。

线程可安排退出时要调用的函数,这样的函数称为线程清理处理程序。一个线程可以有多个清理处理程序,它们被记录在栈中,先入后出:
在这里插入图片描述
线程执行以下操作时,调用参数rtn指向的函数,调用时只有一个参数arg:
1.调用pthread_exit时。
2.响应取消请求时。
3.用非0的execute参数调用pthread_cleanup_pop时。

如果execute参数为0,则清理函数不被调用。无论哪种情况,pthread_clean_pop都将删除上次pthread_cleanup_push函数建立的清理处理程序。

这两个函数可以实现为宏,因此必须在线程中的相同的作用域中以匹配对的形式使用。pthread_cleanup_push的宏定义中可以包含{,此时在pthread_cleanup_pop定义中会有对应的}。

使用线程清理处理的程序:

#include 
#include 
#include 

void cleanup(void *arg) {
    printf("cleanup: %s\n", (char *)arg);
}

void *thr_fn1(void *arg) {
    printf("thread 1 start\n");
    pthread_cleanup_push(cleanup, "thread 1 first handler");
    pthread_cleanup_push(cleanup, "thread 1 second handler");
    printf("thread 1 push complete\n");
    if (arg) {
        return (void *)1;
    }

    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
    return (void *)1;
}

void *thr_fn2(void *arg) {
    printf("thread 2 start\n");
    pthread_cleanup_push(cleanup, "thread 2 first handler");
    pthread_cleanup_push(cleanup, "thread 2 second handler");
    printf("thread 2 push complete\n");
    if (arg) {
        pthread_exit((void *)2);
    }

    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
    pthread_exit((void *)2);
}

int main() {
    int err;
    pthread_t tid1, tid2;
    void *tret;

    err = pthread_create(&tid1, NULL, thr_fn1, (void *)1);
    if (err != 0) {
        printf("%d, can't create thread 1\n", err);
    }

    err = pthread_create(&tid2, NULL, thr_fn2, (void *)1);
    if (err != 0) { 
        printf("%d, can't create thread 2\n", err);
    }

    err = pthread_join(tid1, &tret);
    if (err != 0) {
        printf("%d, can't join with thread 1\n", err);
    }
    printf("thread 1 exit code %ld\n", (long)tret);

    err = pthread_join(tid2, &tret);
    if (err != 0) {
        printf("%d, can't join with thread 2\n", err);
    }
    printf("thread 2 exit code %ld\n", (long)tret);

    exit(0);
}

执行它:
UNIX环境高级编程 学习笔记 第十一章 线程_第5张图片
只有第二个线程的清理处理程序被调用了,因此,线程通过它的启动例程返回而终止时,不调用清理处理程序。

在Free BSD或Mac OS X运行相同程序时,会出现段异常并产生core文件。这是因为在这两个平台上,pthread_cleanup_push是用宏实现的,而宏把某些上下文存放在栈上。当线程1调用push和pop之间返回时,栈已被改写,在调用清理处理程序时用了这个被改写的上下文。在SUS中,函数如果在调用push和pop之间返回,会产生未定义行为。唯一可移植方法是使用pthread_exit函数退出线程。

UNIX环境高级编程 学习笔记 第十一章 线程_第6张图片
上图中pthread_cancel_push应改为pthread_cleanup_push。

默认,线程的终止状态会保存直到对该线程调用pthread_join。如果线程已被分离,则现成的底层存储资源可以在线程终止时立即被收回。对分离状态的线程调用pthread_join是未定义行为。分离线程:
在这里插入图片描述
当多个线程可以读写同一变量时,要对线程进行同步,确保它们不会访问到无效的值。

当线程在修改一个变量时,其他线程在读取这个变量时可能看到一个不一致的值。当变量的修改时间长于一个存储器访问周期时,当存储器读与存储器写两个周期交叉时,这种不一致就会出现。这种行为也与处理器体系结构有关。

UNIX环境高级编程 学习笔记 第十一章 线程_第7张图片
为解决这个问题,线程需要使用锁,使同一时间只允许一个线程访问该变量。

两个或多个线程同时修改同一变量也需同步,比如递增,一般递增操作步骤为:
1.从内存单元读入寄存器。
2.在寄存器中对变量做递增操作。
3.把新值写回内存单元。

UNIX环境高级编程 学习笔记 第十一章 线程_第8张图片
如果修改是原子操作,就不会存在竞争。上例中,只要递增操作需要一个存储器周期(连续启动两次读或写操作所需间隔),就没有竞争存在。

如果数据总是以顺序一致出现的,就不需要额外的同步。当多个线程观察不到数据的不一致时,操作就是顺序一致的。在顺序一致环境中,可以把数据修改操作解释为运行线程的顺序操作步骤,可将操作描述为A递增1后B再递增1,或B递增1后A再递增1,最后结果都是2,这两个线程的任何操作顺序都不可能让变量出现除上述值以外的其他值。

可以用pthread的互斥接口来保护数据,确保同一时间只有一个线程访问数据。互斥量本质上是一把锁,在访问共享资源前对互斥量进行设置(加锁),在访问完成后释放互斥量(解锁)。加锁后,其他试图再次对互斥量加锁的线程都会被阻塞,直到当前线程释放该互斥锁。如果释放互斥量时有一个以上的线程阻塞,则所有该锁上的阻塞线程都会变成可运行状态,第一个运行的线程可对互斥量加锁,其他线程就会看到互斥量依然是锁着的,只能回去再次等待它重新变为可用。

只有将所有线程都设计成遵守相同数据访问规则的,互斥机制才正常工作。

互斥量类型为pthread_mutex_t,在使用互斥量前,要先对其进行初始化,可将它设为常量PTHREAD_MUTEX_INITIALIZER(只适用于静态分配的互斥量),也可调用pthread_mutex_init函数进行初始化。如果动态分配互斥量(如,通过malloc分配),释放内存时需要调用pthread_mutex_destroy:
UNIX环境高级编程 学习笔记 第十一章 线程_第9张图片
要用默认的属性初始化互斥量,只需把attr参数设为NULL。

可调用pthread_mutex_lock对互斥量加锁,如果互斥量已经上锁,调用线程将阻塞直到互斥量被解锁,可调用pthread_mutex_unlock解锁互斥量:
UNIX环境高级编程 学习笔记 第十一章 线程_第10张图片
如果不希望线程被阻塞,可调用pthread_mutex_trylock尝试对互斥量加锁,此时,如果互斥量未被锁住,则会锁住互斥量,不会出现阻塞直接返回0,否则,函数会失败,不能锁住互斥量,返回EBUSY。

保护数据结构时,可嵌入引用计数,在所有使用该对象的线程完成数据访问之后,释放对象内存空间:

#include 
#include 

struct foo {
    int f_count;
    pthread_mutex_t f_lock;
    int f_id;
};

struct foo *foo_alloc(int id) {    // allocate the object
    struct foo *fp;

    if ((fp = malloc(sizeof(struct foo))) != NULL) {
        fp->f_count = 1;
		fp->f_id = id;
		if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
		    free(fp);
		    return NULL;
		}
    }
    return fp;
}

void foo_hold(struct foo *fp) {    // add a reference to the object
    pthread_mutex_lock(&fp->f_lock);
    fp->f_count++;
    pthread_mutex_unlock(&fp->f_lock);
}

void foo_rele(struct foo *fp) {    // release a reference to the object
    pthread_mutex_lock(&fp->f_lock);
    if (--fp->f_count == 0) {
        pthread_mutex_unlock(&fp->f_lock);
		pthread_mutex_destroy(&fp->f_lock);
		free(fp);
    } else {
        pthread_mutex_unlock(&fp->f_lock);
    }
}

如果线程试图对同一个互斥量加锁两次,它自身就会陷入死锁状态。

程序中使用一个以上互斥量时,如果允许一个线程一直占有第一个互斥量,且在试图锁住第二个互斥量时处于阻塞状态,此时拥有第二个互斥量的线程也在试图锁住第一个互斥量,由于两个线程都在互相请求对方拥有的资源,产生死锁。

可通过控制互斥量加锁顺序避免死锁发生,假设需要两个互斥量A、B同时加锁,如果所有线程都在对B(A)加锁前先锁住A(B),那么使用这两个互斥量不会产生死锁。只有在两个线程以相反顺序锁住互斥量时才可能死锁。

实践中,如果需要多个互斥量,可以调用pthread_mutex_trylock尝试占用锁,如果返回成功,可继续前进,否则,可以先释放已占有的锁,做好清理工作,过一段时间再尝试。

使用两个互斥量时,总是让它们以相同的顺序加锁。以下代码中互斥量hashlock维护着一个用于跟踪foo数据结构的散列列表,这样既可以保护foo结构的散列表fh,又可以保护散列链字段f_next,结构foo中的f_lock互斥量保护对foo结构中其他字段的访问:

#include 
#include 

#define NHASH 29
#define HASH(id) (((unsigned long)id)%NHASH)

struct foo *fh[NHASH];

pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER;

struct foo {
    int f_count;
    pthread_mutex_t f_lock;
    int f_id;
    struct foo *f_next;
};

struct foo *foo_alloc(int id) {
    struct foo *fp;
    int idx;

    if ((fp = malloc(sizeof(struct foo))) != NULL) {
        fp->f_count = 1;
		fp->f_id = id;
		if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
		    free(fp);
		    return NULL;
		}
		idx = HASH(id);
		pthread_mutex_lock(&hashlock);
		fp->f_next = fh[idx];
		fh[idx] = fp;
		pthread_mutex_lock(&fp->f_lock);
		pthread_mutex_unlock(&hashlock);
		// continue initialization
		pthread_mutex_unlock(&fp->f_lock);
    }
    return fp;
}

void foo_hold(struct foo *fp) {    // add a reference to the object
    pthread_mutex_lock(&fp->f_lock);
    fp->f_count++;
    pthread_mutex_unlock(&fp->f_lock);
}

struct foo *foo_find(int id) {    // find an existing object
    struct foo *fp;
    pthread_mutex_lock(&hashlock);
    for (fp = fh[HASH(id)]; fp != NULL; fp = fp->f_next) {
       if (fp->f_id == id) {
           foo_hold(fp);
	   	   break;
       }
    }
    pthread_mutex_unlock(&hashlock);
    return fp;
}

void foo_release(struct foo *fp) {    // release a reference to the object
    struct foo *tfp;
    int idx;

    pthread_mutex_lock(&fp->f_lock);    // 上锁查看f_count值
    if (fp->f_count == 1) {    // last reference 
        pthread_mutex_unlock(&fp->f_lock);
		pthread_mutex_lock(&hashlock);
		pthread_mutex_lock(&fp->f_lock);    // 按先hashlock后f_lock的顺序上锁,因此需要先解锁再加锁
		// need to recheck the condition
		if (fp->f_count != 1) {
		    fp->f_count--;
		    pthread_mutex_unlock(&fp->f_lock);
		    pthread_mutex_unlock(&hashlock);
		    return;
		}
		// remove from list 
		idx = HASH(fp->f_id);
		tfp = fh[idx];    // 在此哈希散列链中寻找本结构,找到后删除它
		if (tfp == fp) {
		    fh[idx] = fp->f_next;
		} else {
		    while (tfp->f_next != fp) {
		        tfp = tfp->f_next;
		    }
		    tfp->f_next = fp->f_next;
		}
		pthread_mutex_unlock(&hashlock);
		pthread_mutex_unlock(&fp->f_lock);
		pthread_mutex_destroy(&fp->f_lock);
		free(fp);
    } else {
        fp->f_count--;
		pthread_mutex_unlock(&fp->f_lock);
    }
}

以上锁比较复杂,也可用散列列表锁hashlock同时保护结构引用计数,结构互斥量可用于保护foo结构中其他任何东西,简化版:

#include 
#include 

#define NHASH 29
#define HASH(id) (((unsigned long)id)%NHASH)

struct foo *fh[NHASH];
pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER;

struct foo {
    int f_count;
    pthread_mutex_t f_lock;
    int f_id;
    struct foo *f_next;
};

struct foo *foo_alloc(int id) {    // no difference
    struct foo *fp;
    int idx;

    if ((fp = malloc(sizeof(struct foo))) != NULL) {
        fp->f_count = 1;
	    fp->f_id = id;
		if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
		    free(fp);
		    return NULL;
		}
		idx = HASH(id);
		pthread_mutex_lock(&hashlock);
		fp->f_next = fh[idx];
		fh[idx] = fp;
		pthread_mutex_lock(&fp->f_lock);
	    pthread_mutex_unlock(&hashlock);
		// continue initialization
		pthread_mutex_unlock(&fp->f_lock);
    }
    return fp;
}

void foo_hold(struct foo *fp) {
    pthread_mutex_lock(&hashlock);
    fp->f_count++;
    pthread_mutex_unlock(&hashlock);
}

struct foo *foo_find(int id) {    // no difference
    struct foo *fp;

    pthread_mutex_lock(&hashlock);
    for (fp = fh[HASH(id)]; fp != NULL; fp = fp->f_next) {
        if (fp->f_id == id) {
		    fp->f_count++;
		    break;
		}
    }
    pthread_mutex_unlock(&hashlock);
    return fp;
}

void foo_rele(struct foo *fp) {
    struct foo *tfp;
    int idx;

    pthread_mutex_lock(&hashlock);
    if (--fp->f_count == 0) {
        idx = HASH(fp->f_id);
		tfp = fh[idx];
		if (tfp == fp) {
		    fh[idx] = fp->f_next;
		} else {
		    while (tfp->f_next != fp) {
		        tfp = tfp->f_next;
		    }
		    tfp->f_next = fp->f_next;
		}
		pthread_mutex_unlock(&hashlock);
		pthread_mutex_destroy(&fp->f_lock);
		free(fp);
    } else {
        pthread_mutex_unlock(&hashlock);
    }
}

如上,散列列表锁和引用计数锁的排序问题就不存在了。但锁的粒度较粗,会出现多个线程阻塞相同的锁,并不改善并发性。而如果锁的粒度太细,那么过多的锁开销使性能下降,且代码变得复杂。要在满足锁需求的情况下,找到平衡。

线程获取一个已加锁的互斥量时可用以下函数避免永久阻塞:
UNIX环境高级编程 学习笔记 第十一章 线程_第11张图片
使用的是绝对时间而非要等待的时间,参数tsptr的类型为timespec *,它用s和ns描述时间。阻塞到tsptr表示的时间时,返回错误码ETIMEOUT。

避免永久阻塞:

#include 
#include 
#include 
#include 
#include 
#include 

int main() {
    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);    // 根据timespec的时间值填充结构tm的值,tm中包含该时间的秒数、日期、是一年中的第几天等信息
    strftime(buf, sizeof(buf), "%r", tmp);    // 将tm结构的时间值按第三个参数转换为字符串存放到第一个参数表示的地址中,%r表示12小时制的时间(带am和pm的那种)
    printf("current time is %s\n", buf);

    tout.tv_sec += 10;    // 10 seconds from now
    err = pthread_mutex_timedlock(&lock, &tout);
    
    clock_gettime(CLOCK_REALTIME, &tout);
    tmp = localtime(&tout.tv_sec);
    strftime(buf, sizeof(buf), "%r", tmp);
    printf("the time is now %s\n", buf);
    if (err == 0) {
        printf("mutex locked again!\n");
    } else {
        printf("can't lock mutex again:%s\n", strerror(err));
    }
    exit(0);
}

执行它:
在这里插入图片描述
阻塞的时间可能会有所不同,可能开始时间在某秒的中间位置,系统时钟的精度不足以精确到我们指定的超过时间值,或程序继续运行前,会有调度时间。

读写锁与互斥量类似,但它允许更高的并行性。读写锁有三种状态:读模式加锁状态、写模式加锁状态、不加锁状态。一次只有一个线程能占有写模式的读写锁,但多个线程可以同时占有读模式的读写锁。

读写锁是写加锁状态时,会阻塞所有试图加锁的线程。读写锁在读加锁状态时,所有试图以读模式加锁的线程都可以获得访问权,但会阻塞试图以写模式加锁的线程,直到所有读锁都被释放。当读写锁处于读模式锁住状态,而此时有一个线程试图以写模式获取锁时,读写锁通常会阻塞随后的读模式锁请求,这样可以避免读模式锁长期占用。

读写锁适合读多写少的情况。

读写锁也叫共享互斥锁,读模式锁时,是以共享模式锁住的,写模式锁时,是以互斥模式锁住的。

读写锁在使用前必须初始化,释放它们的底层内存前必须销毁(以回收资源):
UNIX环境高级编程 学习笔记 第十一章 线程_第12张图片
如希望读写锁具有默认属性,可传给初始化函数的attr参数一个null指针。

SUS在XSI扩展中定义了PTHREAD_RWLOCK_INITIALIZER常量,可用它初始化默认属性的静态分配的读写锁。

在释放读写锁占用内存前要先用pthread_rwlock_destroy函数释放其获得的资源,否则直接释放读写锁占用的内存空间就会导致分配给这个锁的资源丢失。

读模式锁定读写锁时,调用pthread_rwlock_rdlock;写模式锁定读写锁时,调用pthread_rwlock_wrlock;不管以何种模式锁住读写锁,都能用pthread_rwlock_unlock函数解锁:
UNIX环境高级编程 学习笔记 第十一章 线程_第13张图片
各种实现可能限制获取的读写锁数量。实现可能会限制共享模式下读写锁的可获取次数。

尝试获取锁:
UNIX环境高级编程 学习笔记 第十一章 线程_第14张图片
不能获取锁时,返回EBUSY。

使用读写锁:

#include 
#include 

struct job {
    struct job *j_next;
    struct job *j_prev;
    pthread_t j_id;    // tell which thread handles this job
};

struct queue {
    struct job *q_head;
    struct job *q_tail;
    pthread_rwlock_t q_lock;
};

int queue_init(struct queue *qp) {    // initialize a queue
    int err;

    qp->q_head = NULL;
    qp->q_tail = NULL;
    err = pthread_rwlock_init(&qp->q_lock, NULL);
    if (err != 0) {
        return err;
    }
    return 0;
}

void job_insert(struct queue *qp, struct job *jp) {    // insert a job at the head of the queue
    pthread_rwlock_wrlock(&qp->q_lock);
    jp->j_next = qp->q_head;
    jp->j_prev = NULL;
    if (qp->q_head != NULL) {
        qp->q_head->j_prev = jp;    // set head's j_prev
    } else {
        qp->q_tail = jp;
    }
    qp->q_head = jp;
    pthread_rwlock_unlock(&qp->q_lock);
}

void job_append(struct queue *qp, struct job *jp) {    // append a job on the tail of the queue
    pthread_rwlock_wrlock(&qp->q_lock);
    jp->j_next = NULL;
    jp->j_prev = qp->q_tail;
    if (qp->q_tail != NULL) {
        qp->q_tail->j_next = jp;    // set tail's j_next
    } else {
        qp->q_head = jp;
    }
    qp->q_tail = jp;
    pthread_rwlock_unlock(&qp->q_lock);
}

void job_remove(struct queue *qp, struct job *jp) {    // remove the given job from a queue
    pthread_rwlock_wrlock(&qp->q_lock);
    if (jp == qp->q_head) {
        qp->q_head = jp->j_next;
		if (qp->q_tail == jp) {
		    qp->q_tail = NULL;
		} else {
		    jp->j_next->j_prev = jp->j_prev;    // 此时jp->j_prev为NULL
		}
	} else if (jp == qp->q_tail) {
	    qp->q_tail = jp->j_prev;
		jp->j_prev->j_next = jp->j_next;    // 此时jp->j_next为NULL
	} else {
        jp->j_prev->j_next = jp->j_next;
		jp->j_next->j_prev = jp->j_prev;
    }
    pthread_rwlock_unlock(&qp->q_lock);
}

struct job *job_find(struct queue *qp, pthread_t id) {
    struct job *jp;

    if (pthread_rwlock_rdlock(&qp->q_lock) != 0) {
        return NULL;
    }

    for (jp = qp->q_head; jp != NULL; jp = jp->j_next) {
	    if (pthread_equal(jp->j_id, id)) {
		    break;
		}
    }

    pthread_rwlock_unlock(&qp->q_lock);
    return jp;
}

上例中,向队列中增删作业用写模式锁,而搜索队列时用读模式锁。在读频率远高于写频率时,读写锁才能改善性能。作业结构逻辑上只能被一个线程使用,不需要额外的加锁。

带超时的读写锁:
UNIX环境高级编程 学习笔记 第十一章 线程_第15张图片
参数tsptr指向应该停止阻塞的绝对时间,到期时,返回ETIMEDOUT错误。

条件变量与互斥量一起使用,允许线程以无竞争的方式等待特定的条件发生。

条件本身由互斥量保护,在锁定互斥量后才能计算条件。

使用条件变量前,先对其初始化,用pthread_cond_t数据类型表示条件变量,有两种方式初始化,可把常量PTHREAD_COND_INITIALIZER赋给静态分配的条件变量,或用以下函数初始化动态分配的条件变量:

UNIX环境高级编程 学习笔记 第十一章 线程_第16张图片
在释放条件变量底层的内存空间之前,先要调用pthread_cond_destroy对条件变量进行反初始化。

pthread_cond_init函数的attr参数可设为NULL以创建具有默认属性的条件变量。

可使用pthread_cond_wait函数等待条件变量为真,如在给定时间不能满足,函数pthread_cond_timedwait返回错误码:
UNIX环境高级编程 学习笔记 第十一章 线程_第17张图片
传给以上函数的互斥量对条件进行保护,调用者把锁住的互斥量传给函数,函数会原子地把调用线程放在等待条件的线程列表上和解锁互斥量。像这样先锁住互斥量,再进行条件检查就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间窗口。当pthread_cond_wait函数返回时,互斥量再次被锁住。

pthread_cond_timedwait函数指定了超时的绝对时间。

可调用clock_gettime获取timespec结构的当前时间,但不是所有平台都支持此函数。因此也可调用gettimeofday获取timeval结构表示的当前时间,再转换为timespec结构:

#include 
#include 
using namespace std;

void maketimeout(timespec *tsp, long minutes) {
    timeval now;

    gettimeofday(&now, NULL);
    tsp->tv_sec = now.tv_sec;
    tsp->tv_nsec = now.tv_usec * 1000;

    tsp->tv_sec += minutes * 60;
}

如果超时时条件还没出现,pthread_cond_timewait函数会重新获取互斥量,并返回错误ETIMEOUT,从pthread_cond_wait和pthread_cond_timewait函数成功返回时,线程需要重新计算条件,因为另一个线程可能改变了条件。

以下函数通知线程条件已满足:
在这里插入图片描述
pthread_cond_signal函数至少唤醒一个等待该条件的线程(POSIX为简化该函数实现,允许它唤醒一个以上线程);pthread_cond_broadcast函数唤醒等待该条件的所有线程。

调用以上两函数时,我们说这是在给线程或条件发信号。

使用条件变量和互斥量对线程进行同步:

#include 

struct msg {
    struct msg *m_next;
};

struct msg *workq;    // work queue

pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;

void process_msg() {    // 消费者
    struct msg *mp;

    for( ; ; ) {
        pthread_mutex_lock(&qlock);
		while (workq == NULL) {    // while the work queue is empty
	    	pthread_cond_wait(&qready, &qlock);
		}
		mp = workq;
		workq = mp->m_next;    // get one msg from work queue to mp
		pthread_mutex_unlock(&qlock);
		// now process the message mp
    }
}

void enqueue_msg(struct msg *mp) {    // 生产者
    pthread_mutex_lock(&qlock);
    mp->m_next = workq;
    workq = mp;    // put mp into the work queue
    pthread_mutex_unlock(&qlock);
    pthread_cond_signal(&qready);
}

条件变量是工作队列的状态。上例中生产者在解锁互斥量之后才发送signal,如果现在有一个线程正在等待,还有一个线程正好判断到workq是否为空,这两个线程就构成了竞争关系,如果运行到判断while循环的条件的线程先运行了,并取出了工作队列中的msg,然后一直在等待的线程才接收到signal继续运行时,之后接收到signal的线程需要再次进行条件判断(即工作队列workq是否为空),这种情况下会发现还是为空,因此会继续等待,这就是while循环的作用。

条件变量使用模型为:
1.先对互斥量加锁,再判断条件(例如上例中判断工作队列workq是否为空),线程进入休眠状态与解锁互斥量是一个原子操作。条件的判断和状态改变需要同一互斥量,这样对互斥量加锁后,条件就不能改变了,就可以避免判断条件后,线程休眠前,条件被改变且pthread_cond_signal被调用,导致线程永远休眠等待。
2.等pthread_cond_wait函数解锁互斥量后,其他线程在使条件变为真前,需要先对同一互斥量加锁,然后使条件变为真,然后有两种运行模式,一种是先调用pthread_cond_signal再解锁互斥量;一种是先解锁互斥量再调用pthread_cond_signal。第一种情况下,调用pthread_cond_signal后休眠线程的pthread_cond_wait函数就会从内核态回到用户态,然后在返回前会试图对互斥量重新加锁,此时可能调用pthread_cond_signal的进程还没有对互斥量解锁,就会导致等待线程又从用户态回到内核态等待加锁,性能比较差,但在Linux中没有这个问题,调用pthread_cond_signal只会令休眠线程从等待队列移到加锁队列,不会切换到用户态。而第二种情况中,由于是先解锁互斥量,调用pthread_cond_signal后,可能有第三个线程先加锁并进入到了条件判断且进行了条件为真时的处理,之后第三个线程解锁互斥量,在第三个线程解锁互斥量后,休眠线程才从pthread_cond_wait函数返回(因为返回前需要对互斥量加锁),此时条件为真时的情况已经被处理了(例如上例中工作队列又变为空),休眠进程被虚假唤醒,因此需要一个while循环来判断条件是否为真,可参考上例。
3.等待的线程在收到信号后,对互斥量加锁(从pthread_cond_wait函数返回时自动完成),然后需要使用while循环再判断一次条件,如果条件为真(即没有被虚假唤醒)再处理临界资源,然后再解锁互斥量。这是由于可能会有多个休眠线程,当调用pthread_cond_broadcast时,所有线程都会被唤醒,但只有一个线程能在返回前对互斥量重新加锁返回,其余线程都会虚假唤醒,因此需要while循环重新判断条件是否为真,这是while循环的另一个使用原因。

自旋锁类似于互斥量,但它不是睡眠阻塞,而是忙等(自旋)阻塞,可用于锁被持有的时间短,线程不想花费被调度的成本的情况。

自旋锁常被用作其他类型锁的底层原语实现。

自旋锁在非抢占式内核中很有用,除了它能提供互斥机制,它还能阻塞中断,这样中断处理程序就不会因获取已被加锁的自旋锁而让系统陷入死锁状态(把中断想象成另一种抢占)。在这种内核中,程序不能睡眠,因此只能用自旋锁作为同步原语。

但在用户层,自旋锁用处不大,除非运行在不允许抢占的实时调度类中,运行在分时调度的用户层线程可被取消调度(如时间片到期或更高优先级的线程从就绪变成可运行时),此时线程拥有自旋锁也会进入休眠,阻塞在锁上的其他线程自旋的时间可能比预期的要长。

有些互斥量的实现是在试图获取到互斥量时先自旋一小段时间,然后再休眠。

可用以下函数对自旋锁进行初始化和反初始化:
UNIX环境高级编程 学习笔记 第十一章 线程_第18张图片
自旋锁只有一个持有属性,该属性只在支持线程进程共享同步选项(该选项在SUS中是强制的)的平台上才用得上。pshared参数表示进程共享属性,表明自旋锁是如何获取的,如果将它设为PTHREAD_PROCESS_SHARED,则自旋锁也能被可以访问锁底层内存的其他进程中的线程所获取;否则就得将它设为PTHREAD_PROCESS_PRIVATE,此时自旋锁只能被初始化该锁进程的内部线程所访问。

可调用pthread_spin_trylock尝试对自旋锁加锁,如不能获取锁,则不会一直自旋,而是立即返回EBUSY错误:
UNIX环境高级编程 学习笔记 第十一章 线程_第19张图片
如果对已加锁的自旋锁调用pthread_spin_lock加锁,结果是未定义的。pthread_spin_lock函数可能会返回EDEADLK或其他错误,可能会永久自旋,具体行为依赖于实现。对没有加锁的自旋锁解锁,结果也是未定义的。

函数pthread_spin_lock和pthread_spin_trylock返回0时说明自旋锁已被加锁。在锁上自旋锁时,不要调用可能会进入休眠状态的函数,否则其他线程获取自旋锁时需要等待的时间就延长了(还需等待休眠的函数结束休眠,并解除自旋锁)。

屏障用于协调多个线程同步,它允许每个线程等待,直到所有合作线程都到达某一点,然后从该点继续执行。pthread_join函数就是一种屏障,它允许一个线程等待,直到另一个线程退出。

屏障的初始化和反初始化:
在这里插入图片描述
在这里插入图片描述
初始化屏障时,count参数含义为,在允许所有线程继续运行之前,必须到达屏障的线程数目;attr参数指定屏障的属性。

可用以下函数表明线程已完成所有工作,准备等其他线程赶上:
在这里插入图片描述
调用该函数的线程在屏障计数未满足条件时,会进入休眠状态。如果该线程调用了pthread_barrier_wait后,满足了屏障计数,此时所有线程都被唤醒。

一组调用了该函数的线程中,只有随机一个线程看到的返回值为PTHREAD_BARRIER_SERIAL_THREAD,剩下的线程看到的返回值都为0,这使得一个线程可以作为主线程,工作在其他线程已完成的工作结果上。

一旦屏障达到了计数值,且线程处于非阻塞状态,屏障就可以被重用。只有在调用了pthread_barrier_destroy后,再调用pthread_barrier_init初始化,屏障计数值才会被改变。

使用屏障同步线程:

#include 
#include 
#include 
#include 
#include 

#define NTHR 8    // number of threads
#define NUMNUM 8000000L    // number of numbers to sort
#define TNUM (NUMNUM/NTHR)    // number to sort per thread

long nums[NUMNUM];    // 存放生成的数字和每个线程排序后的结果(局部排序的结果)
long snums[NUMNUM];    // 存放最终的排序结果

pthread_barrier_t b;

#ifdef SOLARIS
#define heapsort qsort    // 在solaris上使用qsort代替heapsort
#else
extern int heapsort(void *, size_t, size_t, int (*)(const void *, const void *));
#endif

// compare two long integers (helper function for heapsort)
int complong(const void *arg1, const void *arg2) {
    long l1 = *(long *)arg1;
    long l2 = *(long *)arg2;

    if (l1 == l2) {
        return 0;
    } else if (l1 < l2) {
        return -1;
    } else {
        return 1;
    }
}

// worker thread to sort a portion of the set of numbers
void *thr_fn(void *arg) {    // 参数arg是排序的初始位置
    long idx = (long)arg;

    heapsort(&nums[idx], TNUM, sizeof(long), complong);    // 每次从idx开始排序TNUM个数字
    pthread_barrier_wait(&b);

    return (void *)0;
}

// merge the results of the individual sorted ranges
void merge() {
    long idx[NTHR];
    long i, minidx, sidx, num;    // sidx代表结果数组的下标

    for (i = 0; i < NTHR; ++i) {
        idx[i] = i * TNUM;    // 计算每组排序数组的起点
    }
    for (sidx = 0; sidx < NUMNUM; ++sidx) {    // 每次for循环把最小值取出放入snums,每次循环能向结果数组snums中放入1个数字
        num = LONG_MAX;    // LONG_MAX为最大long值,这样所有值都小于num了
		for (i = 0; sidx < NUMNUM; ++i) {    // i的作用为idx的下标,idx[i]表示目前第i个线程的局部有序数组中的偏移量,即线程i排序的局部有序数组中,还未放入结果数组的数的起始下标
		    if ((idx[i] < (i + 1) * TNUM) && (nums[idx[i]] < num)) {    // 如果线程i的局部有序数组还未全部放入结果数组,且线程i的局部有序数组里还未放入结果数组的最小数字比num小 
		        num = nums[idx[i]];    // 更新最小数字
				minidx = i;    // 记录下最小数字是哪个线程的
		    }
		}
		snums[sidx] = nums[idx[minidx]];    // 将最小数字放入结果数组
		++idx[minidx];    // 增加最小数字所在局部有序数组的偏移量
    }
}

int main() {
    unsigned long i;
    struct timeval start, end;
    long long startusec, endusec;
    double elapsed;
    int err;
    pthread_t tid;

    // create the initial set of numbers to sort
    srandom(1);    // 用种子1初始化随机数产生器,每次运行产生的随机数相同
    for (i = 0; i < NUMNUM; ++i) {
        nums[i] = random();
    }

    // create 8 threads to sort the numbers
    gettimeofday(&start, NULL);
    pthread_barrier_init(&b, NULL, NTHR + 1);    // 设置屏障计数为NTHR+1,多的一个用在此线程中
    for (i = 0; i < NTHR; ++i) {
        err = pthread_create(&tid, NULL, thr_fn, (void *)(i * TNUM));    
		if (err != 0) {
		    printf("can't create thread");
		    exit(1);
		}
    }
    pthread_barrier_wait(&b);
    merge();
    gettimeofday(&end, NULL);

    // print the sorted list
    startusec = start.tv_sec * 1000000 + start.tv_usec;    // 换算成微秒
    endusec = end.tv_sec * 1000000 + end.tv_usec;
    elapsed = (double)(endusec - startusec) / 1000000.0;    // elapsed单位为秒
    printf("sort took %.4f seconds\n", elapsed);    // 保留4位小数
    for (i = 0; i < NUMNUM; ++i) {
        printf("%ld\n", snums[i]);
    }
    exit(0);
}

以上实例中,使用8个线程分解了800万个数的排序工作。我们不需要使用pthread_barrier_wait函数的返回值PTHREAD_BARRIER_SERIAL_THREAD决定哪个线程执行结果的合并工作,因为我们使用了主线程完成这个操作,这也是把屏障计数设为工作线程数+1的原因。

UNIX环境高级编程 学习笔记 第十一章 线程_第20张图片
两种情况都可能是正确的,但都有不足之处。

如果是第一种情况,在发完信号后,互斥锁还存在,此时,pthread_cond_wait函数返回,但返回时会对互斥量上锁,但已经上锁了,这会造成所有线程运行,但之后马上被阻塞。

如果是第二种情况,假设是生产者消费者情形,在生产者对互斥量解锁后,给等待条件的线程发信号前,如果有一个线程又获取了互斥量,并且消耗了商品,之后再向等待的消费者发信号时,还必须再判断一次是否有商品,如果有才继续。

你可能感兴趣的:(UNIX环境高级编程(第三版),unix,服务器)