一个程序至少有一个进程。一个进程至少有一个线程。进程拥有自己独立的存储空间,而线程能够看作是轻量级的进程,共享进程内的全部资源。能够把进程看作一个工厂。线程看作工厂内的各个车间,每一个车间共享整个工厂内的全部资源。就像每一个进程有一个进程ID一样,每一个线程也有一个线程ID,进程ID在整个系统中是唯一的。但线程ID不同,线程ID仅仅在它所属的进程环境中有效。
线程ID的数据类型为pthread_t,一般是无符号长整型。
typedef unsigned long int pthread_t; // pthreadtypes.h
GCC编译线程相关文件时。要使用-pthread參数。先介绍两个实用的函数:
#include <pthread.h>
pthread_t pthread_self(void);
int pthread_equal(pthread_t t1, pthread_t t2);
pthread_self函数获取自身的线程ID。pthread_equal函数比較两个线程ID是否相等。这样做的目的是提高代码的可移植性。由于不同系统的pthread_t类型可能不同。
1、怎样创建线程
创建线程调用pthread_create函数:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
若成功则返回0,否则返回错误编号。当pthread_create成功返回时,由thread參数指向的内存单元被设置为新创建线程的线程ID。
attr參数用于定制各种不同的线程属性,可设置为NULL,创建默认属性的线程。新创建的线程从start_routine函数的地址開始执行,该函数仅仅有一个无类型指针參数arg,假设须要向start_routine函数传递的參数不止一个,那么须要把这些參数放到一个结构中,然后把这个结构的地址作为arg參数传入。
线程新创建时并不能保证哪个线程会先执行,是新创建的线程还是调用线程。新创建的线程能够訪问进程的地址空间,并且继承调用线程的浮点环境和信号屏蔽字,可是该线程的未决信号集被清除。pthread函数在调用失败时一般会返回错误码,它们并不像其他的POSIX函数一样设置errno。
每一个线程都提供errno副本,这仅仅是为了与使用errno的现有函数兼容。在线程中,从函数中返回错误码更为清晰整洁,不须要依赖那些随着函数执行不断变化的全局状态,由于能够把错误的范围限制在引起出错的函数中。
以下看一个样例pthreadEx.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
pthread_t g_tid;
void printIds(const char *s)
{
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int)pid,
(unsigned int)tid, (unsigned int)tid);
}
void* threadFunc(void *arg)
{
printIds("new thread: ");
return ((void*)0);
}
int main(void)
{
int err;
err = pthread_create(&g_tid, NULL, threadFunc, NULL);
if (0 != err) {
printf("can't create thread: %s\n", strerror(err));
abort();
}
printIds("main thread: ");
sleep(1);
exit(0);
}
GCC编译:
gcc -o thread pthreadEx.c -pthread
执行结果:
./thread
main thread: pid 5703 tid 1397368640 (0x534a2740)
new thread: pid 5703 tid 1389127424 (0x52cc6700)
上面样例中,使用sleep处理主线程和新线程之间的竞争。防止新线程执行之前主线程已经退出,这样的行为特征依赖于操作系统中的线程实现和调度算法。新线程ID的获取是通过pthread_self函数而不是g_tid全局变量,原因是主线程把新线程ID存放在g_tid中。可是新线程可能在主线程调用pthread_create返回之前就開始执行了,这时g_tid的内容是不对的。从上面的样例能够看出,两个线程的pid是同样的,不同的是tid,这是合理的,但两个线程的输出顺序是不定的,另外执行结果在不同的操作系统下可能是不同的。这依赖于详细的实现。
2、怎样终止线程
假设进程中的任一线程调用了exit、_Exit或者_exit。这时整个进程就会终止。而不单单是调用线程。与此相似。假设信号的默认动作是终止进程。把这个信号发送到某个线程也会终止整个进程。那么。单个线程怎样退出呢?
(1)线程仅仅是从启动例程中返回,返回值是线程的退出码。
(2)线程能够被同一进程中的其他线程取消。
(3)线程调用pthread_exit函数。
retval是一个无类型的指针,与传给启动例程的单个參数相似,进程中的其他线程能够通过调用pthread_join函数訪问到这个指针。调用线程将一直堵塞,直到指定的线程调用pthread_exit、从启动例程中返回或者被取消。假设线程仅仅是从它的启动例程返回,retval将包括返回码。假设线程被取消。由retval指定的内存单元就置为PTHREAD_CANCELED。能够通过调用pthread_join自己主动把线程置于分离状态,这样资源就能够恢复。假设线程已经处于分离状态,pthread_join调用就会失败,返回EINVAL。
假设对线程的返回值并不感兴趣,能够把retval置为NULL。在这样的情况下。调用pthread_join函数将等待指定的线程终止。但并不获取线程的终止状态。
#include <pthread.h>
void pthread_exit(void *retval);
int pthread_join(pthread_t thread, void **retval);
且看以下的样例pthreadRet.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
void* threadFunc1(void *arg)
{
printf("thread 1 returning\n");
return (void*)1;
}
void* threadFunc2(void *arg)
{
printf("thread 2 exiting\n");
pthread_exit((void*)2);
}
int main(void)
{
int err;
pthread_t tid1, tid2;
void *ret;
err = pthread_create(&tid1, NULL, threadFunc1, NULL);
if (0 != err) {
printf("can't create thread 1: %s\n", strerror(err));
abort();
}
err = pthread_create(&tid2, NULL, threadFunc2, NULL);
if (0 != err) {
printf("can't create thread 2: %s\n", strerror(err));
abort();
}
err = pthread_join(tid1, &ret);
if (0 != err) {
printf("can't join with thread 1: %s\n", strerror(err));
abort();
}
printf("thread 1 exit code %x\n", (int)ret);
err = pthread_join(tid2, &ret);
if (0 != err) {
printf("can't join with thread 2: %s\n", strerror(err));
abort();
}
printf("thread 2 exit code %d\n", (int)ret);
exit(0);
}
执行结果:
thread 2 exiting
thread 1 returning
thread 1 exit code 1
thread 2 exit code 2
从上面的样例中能够看出。当一个线程调用pthread_exit退出或者简单地从启动例程中返回时,进程中的其他线程能够通过调用pthread_join函数获得该线程的退出状态。
pthread_create和pthread_exit函数的无类型指针參数能传递的数值能够不止一个,该指针能够传递包括更复杂信息的结构的地址,可是注意这个结构所使用的内存在调用者完毕调用以后必须仍然是有效的,否则就会出现无效或非法内存訪问。以下的样例pthreadProb.c使用了分配在栈上的自己主动变量。说明了这个问题:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
struct foo
{
int a, b, c, d;
};
void printFoo(const char *s, const struct foo *fp)
{
printf(s);
printf(" structure at 0x%x\n", (unsigned)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* threadFunc1(void *arg)
{
struct foo foo = {1, 2, 3, 4};
printFoo("thread 1:\n", &foo);
pthread_exit((void*)&foo);
}
void* threadFunc2(void *arg)
{
printf("thread 2: ID is %u\n", (unsigned int)pthread_self());
pthread_exit((void*)0);
}
int main(void)
{
int err;
pthread_t tid1, tid2;
struct foo *fp;
err = pthread_create(&tid1, NULL, threadFunc1, NULL);
if (0 != err) {
printf("can't create thread 1: %s\n", strerror(err));
abort();
}
err = pthread_join(tid1, (void*)&fp);
if (0 != err) {
printf("can't join with thread 1: %s\n", strerror(err));
abort();
}
sleep(1);
printf("parent starting second thread\n");
err = pthread_create(&tid2, NULL, threadFunc2, NULL);
if (0 != err) {
printf("can't create thread 2: %s\n", strerror(err));
abort();
}
sleep(1);
printFoo("parent: \n", fp);
exit(0);
}
输出结果:
thread 1:
structure at 0xb18eae90
foo.a = 1
foo.b = 2
foo.c = 3
foo.d = 4
parent starting second thread
thread 2: ID is 119400192
parent:
structure at 0xb18eae90
foo.a = 0
foo.b = 0
foo.c = 1
foo.d = 0
所以,为了避免这个问题。能够使用全局结构,或者调用malloc函数分配结构。
线程能够通过调用pthread_cancel函数来请求取消同一进程中的其他线程。
#include <pthread.h>
int pthread_cancel(pthread_t thread);
在默认的情况下。pthread_cancel函数会使得thread參数标识的线程的行为表现为如同调用了參数为PTHREAD_CANCELED的pthread_exit函数,可是线程能够选择忽略取消方式或是控制取消方式。pthread_cancel并不等待线程终止,它仅仅提出请求。
线程能够安排它退出时须要调用的函数。这与进程能够用atexit函数安排进程退出是须要调用的函数是相似的,这样的函数称为线程清理处理程序。线程能够建立多个清理处理程序,处理程序记录在栈中。也就是说它们的执行顺序与它们注冊的顺序相反。
#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);
当线程执行以下动作时调用清理函数,调用參数为arg。清理函数routine的调用顺序是由pthread_cleanup_push函数来安排的。
(1)调用phtread_exit时。
(2)响应取消请求时。
(3)用非零execute參数调用pthread_cleanup_pop时。假设execute參数为0,清理函数将不被调用。
不管哪种情况。pthread_cleanup_pop都将删除上次pthread_cleanup_push调用建立的清理处理程序。
以下的样例pthreadClean.c。仅仅调用了第二个线程的清理处理程序,原因是第一个线程是通过从它的启动例程中返回而终止,而非上面提到的三个调用清理处理程序的条件之中的一个。
// pthreadClean.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
void cleanup(void *arg)
{
printf("cleanup: %s\n", (char*)arg);
}
void* threadFunc1(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* threadFunc2(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(void)
{
int err;
pthread_t tid1, tid2;
void *ret;
err = pthread_create(&tid1, NULL, threadFunc1, (void*)1);
if (0 != err) {
printf("can't create thread 1: %s\n", strerror(err));
abort();
}
err = pthread_create(&tid2, NULL, threadFunc2, (void*)1);
if (0 != err) {
printf("can't create thread 2: %s\n", strerror(err));
abort();
}
err = pthread_join(tid1, &ret);
if (0 != err) {
printf("can't join with thread 1: %s\n", strerror(err));
abort();
}
printf("thread 1 exit code %d\n", (int)ret);
err = pthread_join(tid2, &ret);
if (0 != err) {
printf("can't join with thread 2: %s\n", strerror(err));
abort();
}
printf("thread 2 exit code %d\n", (int)ret);
exit(0);
}
执行结果:
thread 1 start
thread 1 push complete
thread 2 start
thread 2 push complete
thread 1 exit code 1
cleanup: thread 2 second handler
cleanup: thread 2 first handler
thread 2 exit code 2
在默认情况下,线程的终止状态会保存到对该线程调用pthread_join,假设线程已经处于分离状态,线程的底层存储资源能够在线程终止时马上被收回。当线程被分离时,并不能用pthread_join函数等待它的终止状态。对分离状态的线程进行pthread_join的调用会产生失败,返回EINVAL。
pthread_detach调用能够用于使线程进入分离状态。
#include <pthread.h>
int pthread_detach(pthread_t thread);
3、线程同步
线程同步是一个非常重要的概念。当多个线程同一时候改动或訪问一块内存时。假设没有保护措施非常easy发生冲突,这时就须要使用线程同步技术,以下介绍三种线程同步技术:相互排斥量、读写锁、条件变量。
(1)相互排斥量mutex
#include <pthread.h>
int pthread_mutex_init (pthread_mutex_t *mutex,
const pthread_mutexattr_t *mutexattr);
int pthread_mutex_destroy (pthread_mutex_t *mutex);
int pthread_mutex_trylock (pthread_mutex_t *mutex)
int pthread_mutex_lock (pthread_mutex_t *mutex);
int pthread_mutex_unlock (pthread_mutex_t *mutex);
能够通过使用pthread的相互排斥接口保护数据,确保同一时间仅仅有一个线程訪问数据。相互排斥量mutex从本质上说是一把锁。在訪问共享资源前对相互排斥量进行加锁。在訪问完毕后释放相互排斥量上的锁。对相互排斥量进行加锁以后,不论什么其他试图再次对相互排斥量加锁的线程将会被堵塞直到当前线程释放该相互排斥锁。
假设释放相互排斥锁时有多个线程堵塞,全部在该相互排斥锁上的堵塞线程都会变成可执行状态,第一个变为执行状态的线程能够对相互排斥量加锁。其他线程将会看到相互排斥锁依旧被锁住,仅仅能回去再次等待它又一次变为可用。在这样的方式下。每次仅仅有一个线程能够向前执行。
在设计时须要规定全部的线程必须遵守同样的数据訪问规则。仅仅有这样,相互排斥机制才干正常工作。
操作系统并不会做数据訪问的串行化。
假设同意当中的某个线程在没有得到锁的情况下也能够訪问共享资源。那么即使其他的线程在使用共享资源前都获取了锁,也还是会出现数据不一致的问题。
相互排斥变量用pthread_mutex_t数据类型来表示,在使用相互排斥变量曾经,必须首先对它进行初始化。能够把它置为常量PTHREAD_MUTEX_INITIALIZER。这个是仅仅对静态分配的相互排斥量。也能够通过调用pthread_mutex_init函数进行初始化。要用默认的属性初始化相互排斥量,仅仅需把mutexattr设置为NULL。假设动态地分配相互排斥量,比如通过调用malloc函数。那么在释放内存前须要调用pthread_mutex_destroy函数。
对相互排斥量进行加锁,须要调用pthread_mutex_lock。假设相互排斥量已经上锁,调用线程将堵塞直到相互排斥量被锁住。
对相互排斥量解锁。须要调用pthread_mutex_unlock。假设线程不希望被堵塞,它能够使用pthread_mutex_trylock尝试对相互排斥量进行加锁。假设调用pthread_mutex_trylock时相互排斥量处于未锁住状态,那么pthread_mutex_trylock将锁住相互排斥量,不会出现堵塞并返回0,否则pthread_mutex_trylock就会失败,不能锁住相互排斥量,而返回EBUSY。
以下是一个使用相互排斥量保护数据结构的样例:
#include <stdlib.h>
#include <pthread.h>
struct foo {
int f_count;
pthread_mutex_t f_lock;
};
struct foo* foo_alloc(void)
{
struct foo *fp;
if ((fp = malloc(sizeof(struct foo))) != NULL) {
fp->f_count = 1;
if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
free(fp);
return NULL;
}
}
return fp;
}
void foo_hold(struct foo *fp)
{
pthread_mutex_lock(&fp->f_lock);
fp->f_count++;
pthread_mutex_unlock(&fp->f_lock);
}
void foo_release(struct foo *fp)
{
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);
}
}
在对引用计数加1、减1以及检查引用计数是否为0这些操作之前须要锁住相互排斥量。在foo_alloc函数将引用计数初始化为1时不是必需加锁,由于在这个操作之前分配线程是唯一引用该对象的线程。可是在这之后假设要将该对象放到一个列表中,那么它就有可能被别的线程发现。因此须要首先对它加锁。在使用该对象前,线程须要对这个对象的引用计数加1。当对象使用完毕时,须要对引用计数减1。当最后一个引用被释放时。对象所占的内存空间就被释放。
死锁——
使用相互排斥量时。一个非常重要发问题就是避免死锁。
比如,假设线程试图对同一个相互排斥量加锁两次,那么它自身就会陷入死锁状态。假设两个线程都在相互请求还有一个线程拥有的资源。那么这两个线程都无法向前执行,也会产生死锁。
一个经常使用的避免死锁的方法是控制相互排斥量加锁的顺序,全部线程总是对几个相互排斥量的加锁顺序保持一致;有时候加锁顺序难以控制,我们会先释放自己已经占有的锁,然后去尝试获取别的锁。
以下的样例使用了两个相互排斥量,加锁顺序同样,hashlock相互排斥量保护foo数据结构中的fh散列表和f_next散列链字段,foo结构中的f_lock相互排斥量保护对foo结构中的其他字段的訪问。
#include <stdlib.h>
#include <pthread.h>
#define NHASH 29
#define HASH(fp) (((unsigned long)fp)%NHASH)
struct foo* fh[NHASH];
pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER;
struct foo {
int f_count;
pthread_mutex_t f_lock;
struct foo *f_next;
int f_id;
};
struct foo* foo_alloc(void)
{
struct foo *fp;
int idx;
if ((fp = malloc(sizeof(struct foo))) != NULL) {
fp->f_count = 1;
if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
free(fp);
return NULL;
}
idx = HASH(fp);
pthread_mutex_lock(&hashlock);
fp->f_next = fh[idx];
fh[idx] = fp->f_next;
pthread_mutex_lock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
pthread_mutex_unlock(&fp->f_lock);
}
return fp;
}
void foo_hold(struct foo *fp)
{
pthread_mutex_lock(&fp->f_lock);
fp->f_count++;
pthread_mutex_unlock(&fp->f_lock);
}
struct foo* foo_find(int id)
{
struct foo *fp;
int idx;
idx = HASH(fp);
pthread_mutex_lock(&hashlock);
for (fp = fh[idx]; 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)
{
struct foo *tfp;
int idx;
pthread_mutex_lock(&fp->f_lock);
if (fp->f_count == 1) {
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_lock(&hashlock);
pthread_mutex_lock(&fp->f_lock);
if (fp->f_count != 1) {
fp->f_count--;
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
return;
}
idx = HASH(fp);
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);
}
}
上面样例中加、减锁太复杂,能够使用散列列表锁来保护结构引用计数,结构相互排斥量能够用于保护foo结构中的其他不论什么东西,例如以下:
#include <stdlib.h>
#include <pthread.h>
#define NHASH 29
#define HASH(fp) (((unsigned long)fp)%NHASH)
struct foo* fh[NHASH];
pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER;
struct foo {
int f_count;
pthread_mutex_t f_lock;
struct foo *f_next;
int f_id;
};
struct foo* foo_alloc(void)
{
struct foo *fp;
int idx;
if ((fp = malloc(sizeof(struct foo))) != NULL) {
fp->f_count = 1;
if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
free(fp);
return NULL;
}
idx = HASH(fp);
pthread_mutex_lock(&hashlock);
fp->f_next = fh[idx];
fh[idx] = fp->f_next;
pthread_mutex_lock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
}
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)
{
struct foo *fp;
int idx;
idx = HASH(fp);
pthread_mutex_lock(&hashlock);
for (fp = fh[idx]; fp != NULL; fp = fp->f_next) {
if (fp->f_id == id) {
fp->f_count++;
break;
}
}
pthread_mutex_unlock(&hashlock);
return fp;
}
void foo_release(struct foo *fp)
{
struct foo *tfp;
int idx;
pthread_mutex_lock(&hashlock);
if (--(fp->f_count) == 0) {
idx = HASH(fp);
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);
}
}
假设锁的粒度太粗,就会出现非常多线程堵塞等待同样的锁。源自并发性发改善微乎其微。假设锁的粒度太细,那么过多的锁开销会使系统性能收到影响。并且代码变得相当复杂。
作为一个程序猿。须要在满足锁需求的情况下,在代码复杂性和优化性能之间找好平衡点。
以下以一个简单的样例说明多线程共享资源的问题,主线程内启动5个线程。这5个线程分别对初始值为0的全局变量连续5次累加1、10、100、1000、10000。代码例如以下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
// pthread_mutex_t value_lock = PTHREAD_MUTEX_INITIALIZER;
int value = 0;
void* thread_func1(void *arg)
{
// pthread_mutex_lock(&value_lock);
int count = 1;
while (count++ <= 5) {
value += 1;
printf("thread 1: value = %d\n", value);
}
// pthread_mutex_unlock(&value_lock);
pthread_exit((void*)1);
}
void* thread_func2(void *arg)
{
// pthread_mutex_lock(&value_lock);
int count = 1;
while (count++ <= 5) {
value += 10;
printf("thread 2: value = %d\n", value);
}
// pthread_mutex_unlock(&value_lock);
pthread_exit((void*)2);
}
void* thread_func3(void *arg)
{
// pthread_mutex_lock(&value_lock);
int count = 1;
while (count++ <= 5) {
value += 100;
printf("thread 3: value = %d\n", value);
}
// pthread_mutex_unlock(&value_lock);
pthread_exit((void*)3);
}
void* thread_func4(void *arg)
{
// pthread_mutex_lock(&value_lock);
int count = 1;
while (count++ <= 5) {
value += 1000;
printf("thread 4: value = %d\n", value);
}
// pthread_mutex_unlock(&value_lock);
pthread_exit((void*)4);
}
void* thread_func5(void *arg)
{
// pthread_mutex_lock(&value_lock);
int count = 1;
while (count++ <= 5) {
value += 10000;
printf("thread 5: value = %d\n", value);
}
// pthread_mutex_unlock(&value_lock);
pthread_exit((void*)5);
}
int main(void)
{
int err;
pthread_t tid1, tid2, tid3, tid4, tid5;
err = pthread_create(&tid1, NULL, thread_func1, NULL);
if (0 != err) {
printf("can't create thread 1: %s\n", strerror(err));
abort();
}
err = pthread_create(&tid2, NULL, thread_func2, NULL);
if (0 != err) {
printf("can't create thread 2: %s\n", strerror(err));
abort();
}
err = pthread_create(&tid3, NULL, thread_func3, NULL);
if (0 != err) {
printf("can't create thread 3: %s\n", strerror(err));
abort();
}
err = pthread_create(&tid4, NULL, thread_func4, NULL);
if (0 != err) {
printf("can't create thread 4: %s\n", strerror(err));
abort();
}
err = pthread_create(&tid5, NULL, thread_func5, NULL);
if (0 != err) {
printf("can't create thread 5: %s\n", strerror(err));
abort();
}
sleep(1);
printf("main thread end\n");
exit(0);
}
执行结果例如以下(全然乱套了):
thread 2: value = 11
thread 2: value = 121
thread 2: value = 1131
thread 2: value = 1141
thread 2: value = 1151
thread 3: value = 111
thread 3: value = 1251
thread 3: value = 1351
thread 3: value = 1451
thread 3: value = 1551
thread 1: value = 1
thread 1: value = 1552
thread 1: value = 1553
thread 1: value = 1554
thread 1: value = 1555
thread 4: value = 1121
thread 4: value = 2555
thread 4: value = 3555
thread 4: value = 4555
thread 4: value = 5555
thread 5: value = 15555
thread 5: value = 25555
thread 5: value = 35555
thread 5: value = 45555
thread 5: value = 55555
main thread end
作为对照,我们使用相互排斥量。5个线程都要使用相互排斥量,要不结果也是不可预料的。把刚才代码的凝视打开就可以。结果例如以下(预期结果bingo):
thread 1: value = 1
thread 1: value = 2
thread 1: value = 3
thread 1: value = 4
thread 1: value = 5
thread 2: value = 15
thread 2: value = 25
thread 2: value = 35
thread 2: value = 45
thread 2: value = 55
thread 3: value = 155
thread 3: value = 255
thread 3: value = 355
thread 3: value = 455
thread 3: value = 555
thread 4: value = 1555
thread 4: value = 2555
thread 4: value = 3555
thread 4: value = 4555
thread 4: value = 5555
thread 5: value = 15555
thread 5: value = 25555
thread 5: value = 35555
thread 5: value = 45555
thread 5: value = 55555
main thread end
以下演示一个死锁的样例,两个线程分别訪问两个全局变量。这两个全局变量分别被两个相互排斥量保护,但由于相互排斥量的使用顺序不同。导致死锁:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
pthread_mutex_t value_lock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t value2_lock = PTHREAD_MUTEX_INITIALIZER;
int value = 0;
int value2 = 0;
void* thread_func1(void *arg)
{
pthread_mutex_lock(&value_lock);
int count = 1;
while (count++ <= 5) {
value += 1;
printf("thread 1: value = %d\n", value);
}
pthread_mutex_lock(&value2_lock);
count = 1;
while (count++ <= 5) {
value2 += 1;
printf("thread 1: value2= %d\n", value2);
}
pthread_mutex_unlock(&value_lock);
pthread_mutex_unlock(&value2_lock);
pthread_exit((void*)1);
}
void* thread_func2(void *arg)
{
pthread_mutex_lock(&value2_lock);
int count = 1;
while (count++ <= 5) {
value += 10;
printf("thread 2: value = %d\n", value);
}
pthread_mutex_lock(&value_lock);
count = 1;
while (count++ <= 5) {
value2 += 10;
printf("thread 2: value2= %d\n", value2);
}
pthread_mutex_unlock(&value_lock);
pthread_mutex_unlock(&value2_lock);
pthread_exit((void*)1);
}
int main(void)
{
int err;
pthread_t tid1, tid2;
err = pthread_create(&tid1, NULL, thread_func1, NULL);
if (0 != err) {
printf("can't create thread 1: %s\n", strerror(err));
abort();
}
err = pthread_create(&tid2, NULL, thread_func2, NULL);
if (0 != err) {
printf("can't create thread 2: %s\n", strerror(err));
abort();
}
sleep(1);
printf("main thread end\n");
exit(0);
}
执行结果例如以下(value2没有结果,假设相互排斥量使用顺序同样就正常了):
thread 1: value = 1
thread 1: value = 12
thread 1: value = 13
thread 1: value = 14
thread 1: value = 15
thread 2: value = 11
thread 2: value = 25
thread 2: value = 35
thread 2: value = 45
thread 2: value = 55
main thread end
(2)读写锁
读写锁rwlock,也叫共享-独占锁,指的是一个线程独占写锁或者多个线程共享读锁。
读锁与写锁不能共存,当某个线程拥有写锁而还没有解锁前,其他线程试图对这个锁加锁都会被堵塞;当某个线程拥有读锁而还没有解锁前,其他线程对这个锁加读锁能够成功,但加写锁时会堵塞,并且会堵塞后面的读锁,这样能够避免读锁长期占用,而等待的写锁请求一直得不到满足。
读写锁经常使用于读次数或读频率远大于写的情况。能够提高并发性。
以下是读写锁相关的几个函数:
int pthread_rwlock_init (pthread_rwlock_t * restrict rwlock,
const pthread_rwlockattr_t * restrict attr);
int pthread_rwlock_destroy (pthread_rwlock_t *rwlock);
int pthread_rwlock_rdlock (pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock (pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock (pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock (pthread_rwlock_t *rwlock);
int pthread_rwlock_unwrlock (pthread_rwlock_t *rwlock);
上面几个函数的使用方法同相互排斥量的使用方法一样。以下以一个简单的样例说明,主线程中启动10个线程。4个写锁。6个读锁:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
pthread_rwlock_t value_lock = PTHREAD_RWLOCK_INITIALIZER;
int value = 0;
void* thread_read(void *arg)
{
pthread_rwlock_rdlock(&value_lock);
printf("thread %d: value = %d\n", (int)arg, value);
pthread_rwlock_unlock(&value_lock);
pthread_exit(arg);
}
void* thread_write(void *arg)
{
pthread_rwlock_wrlock(&value_lock);
printf("thread %d: wrlock\n", (int)arg);
int count = 1;
while (count++ <= 10) {
value += 1;
}
usleep(1000);
pthread_rwlock_unlock(&value_lock);
pthread_exit(arg);
}
int main(void)
{
pthread_t tid;
pthread_create(&tid, NULL, thread_read, (void*)1);
pthread_create(&tid, NULL, thread_read, (void*)2);
pthread_create(&tid, NULL, thread_write, (void*)3);
pthread_create(&tid, NULL, thread_write, (void*)4);
pthread_create(&tid, NULL, thread_read, (void*)5);
pthread_create(&tid, NULL, thread_read, (void*)6);
pthread_create(&tid, NULL, thread_write, (void*)7);
pthread_create(&tid, NULL, thread_write, (void*)8);
pthread_create(&tid, NULL, thread_read, (void*)9);
pthread_create(&tid, NULL, thread_read, (void*)10);
sleep(1);
printf("main thread end\n");
exit(0);
}
结果例如以下:
thread 2: value = 0
thread 1: value = 0
thread 3: wrlock
thread 4: wrlock
thread 5: value = 20
thread 6: value = 20
thread 7: wrlock
thread 8: wrlock
thread 9: value = 40
thread 10: value = 40
main thread end
假设把上面样例中的写锁去掉后,结果就出乎意料了(明明更新了value,输出的value却还是0,乱套了,所以还是加上写锁为好):
thread 5: value = 0
thread 4: wrlock
thread 9: value = 10
thread 2: value = 0
thread 10: value = 10
thread 3: wrlock
thread 6: value = 0
thread 7: wrlock
thread 1: value = 0
thread 8: wrlock
main thread end
(3)条件变量
条件变量是线程可用的还有一种同步机制。
条件变量给多个线程提供了一个会合的场所。条件变量与相互排斥量一起使用时,同意线程以无竞争的方式等待特定的条件发生。条件本身是由相互排斥量保护的。线程在改变条件状态前必须首先锁住相互排斥量,其他线程在获取相互排斥量之前不会察觉到这样的改变。由于必须锁定相互排斥量以后才干计算条件。
先来看一下条件变量cond相关的函数:
int pthread_cond_init (pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict cond_attr);
int pthread_cond_destroy (pthread_cond_t *cond);
int pthread_cond_signal (pthread_cond_t *cond);
int pthread_cond_broadcast (pthread_cond_t *cond);
int pthread_cond_wait (pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait (pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
使用pthread_cond_wait等待条件变为真,假设在给定的时间内条件不能满足,那么会生成一个代表出错码的返回变量。**传递给pthread_cond_wait的相互排斥量对条件进行保护,调用者把锁住的相互排斥量传给函数。
函数把调用线程放到等待条件的线程列表上。然后对相互排斥量解锁,这两个操作是原子操作。这样就关闭了条件检查和线程进入休眠状态等待条件改变这个两个操作之间的时间通道。这样线程就不会错过条件的不论什么变化。
pthread_cond_wait返回时,相互排斥量再次被锁住。**pthread_cond_timedwait指定了等待的时间abstime,时间值是个绝对值。假设希望等待n分钟。就须要把当前时间加上n分钟再转换到timespec结构,而不是把n分钟直接转换为timespec结构,时间可通过函数gettimeofday获取。
假设时间值到了可是条件还是没有出现,函数将又一次获取相互排斥量然后返回错误ETIMEDOUT。
假设等待成功返回,线程须要又一次计算条件。由于其他的线程可能已经在执行并改变了条件。
有两个函数能够用于通知线程条件已经满足。pthread_cond_signal函数将唤醒等待该条件的某个线程,而pthread_cond_broadcast函数将唤醒等待该条件的全部线程,调用这两个函数也称为向线程或条件发送信号,必须注意一定要在改变条件状态后再给线程发送信号。
以下一个样例说明相互排斥量与条件变量一起使用的使用方法:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
int value = 0;
pthread_cond_t value_cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t value_mutex = PTHREAD_MUTEX_INITIALIZER;
void* thread_read(void *arg)
{
pthread_mutex_lock(&value_mutex);
pthread_cond_wait(&value_cond, &value_mutex);
printf("thread %d: value = %d\n", (int)arg, value);
pthread_mutex_unlock(&value_mutex);
pthread_exit(arg);
}
void* thread_write(void *arg)
{
pthread_mutex_lock(&value_mutex);
printf("thread %d: mutex\n", (int)arg);
value += 100;
pthread_cond_signal(&value_cond);
pthread_mutex_unlock(&value_mutex);
pthread_exit(arg);
}
int main(void)
{
pthread_t tid;
pthread_create(&tid, NULL, thread_read, (void*)1);
usleep(500 * 1000);
pthread_create(&tid, NULL, thread_write, (void*)2);
sleep(1);
printf("main thread end\n");
exit(0);
}
输出结果:
thread 2: mutex
thread 1: value = 100
main thread end
上面样例中,read线程和write线程之间增加了usleep,保证read线程先执行,但read线程中加锁之后,在还没有释放锁之前。write线程中的内容竟然也開始执行了,这是由于read线程中使用了条件变量,它会解锁,write线程趁机执行。并发送signal,read线程收到signal即wait返回时会又一次加锁,然后完毕剩余动作。
关于线程同步的几个使用方法:相互排斥量就相当于一把锁。有排它性,用于多个线程訪问共享数据时确保数据訪问的正确性,但前提是这些线程都必须使用同一个相互排斥量,要不也没有效果,假设某个线程占有这个锁时,其他线程就不能占用,从而保证了共享数据的安全;读写锁可提高并发性。可同一时候拥有一个写锁或者多个读锁,当然系统也规定了读锁的上限;条件变量拉近了线程间的关系,但同一时候也要使用相互排斥量来保护条件变量。