标签:多线程
一个程序至少有一个进程,一个进程至少有一个线程,进程拥有自己独立的存储空间,而线程可以看作是轻量级的进程,共享进程内的所有资源。可以把进程看作一个工厂,线程看作工厂内的各个车间,每个车间共享整个工厂内的所有资源。就像每个进程有一个进程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类型可能不同。
创建线程调用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,这是合理的,但两个线程的输出顺序是不定的,另外运行结果在不同的操作系统下可能是不同的,这依赖于具体的实现。
如果进程中的任一线程调用了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);
线程同步是一个很重要的概念,当多个线程同时修改或访问一块内存时,如果没有保护措施很容易发生冲突,这时就需要使用线程同步技术,下面介绍三种线程同步技术:互斥量、读写锁、条件变量。
(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返回时会重新加锁,然后完成剩余动作。
关于线程同步的几个用法:互斥量就相当于一把锁,有排它性,用于多个线程访问共享数据时确保数据访问的正确性,但前提是这些线程都必须使用同一个互斥量,要不也没有效果,如果某个线程占有这个锁时,其它线程就不能占用,从而保证了共享数据的安全;读写锁可提高并发性,可同时拥有一个写锁或者多个读锁,当然系统也规定了读锁的上限;条件变量拉近了线程间的关系,但同时也要使用互斥量来保护条件变量。
版权声明:本文为博主原创文章,未经博主允许不得转载。
标签:多线程
原文地址:http://blog.csdn.net/ieearth/article/details/46725235