C中多线程开发
1 引言
线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期。solaris是这方面的佼佼者。传统的 Unix也支持线程的概念,可是在一个进程(process)中仅仅同意有一个线程。这样多线程就意味着多进程。如今。多线程技术已经被很多操作系统所支持,包含Windows也包含Linux。
为什么有了进程的概念后,还要再引入线程呢?使用多线程究竟有哪些优点?什么的系统应该选用多线程?我们首先必须回答这些问题。
使用多线程的理由之中的一个是和进程相比,它是一种很"节俭"的多任务操作方式。我们知道,在Linux系统下。启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段。这是一种"昂贵"的多任务工作方式。
而执行于一个进程中的多个线程。它们彼此之间使用同样的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,并且。线程间彼此切换所需的时间也远远小于进程间切换所须要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在详细的系统上,这个数据可能会有较大的差别。
使用多线程的理由之二是线程间方便的通信机制。
对不同进程来说,它们具有独立的数据空间。要进行数据的传递仅仅能通过通信的方式进行。这样的方式不仅费时,并且非常不方便。线程则不然,因为同一进程下的线程之间共享数据空间。所以一个线程的数据能够直接为其他线程所用,这不仅快捷,并且方便。当然。数据的共享也带来其他一些问题,有的变量不能同一时候被两个线程所改动,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最须要注意的地方。
除了以上所说的长处外,不和进程比較,多线程程序作为一种多任务、并发的工作方式,当然有下面的长处:
1) 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时非常长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,能够避免这样的尴尬的情况。
2) 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程执行于不同的CPU上。
3) 改善程序结构。
一个既长又复杂的进程能够考虑分为多个线程,成为几个独立或半独立的执行部分。这种程序会利于理解和改动。
以下我们先来尝试编写一个简单的多线程程序。
2 简单的多线程编程
Linux系统下的多线程遵循POSIX线程接口,称为pthread。
编写Linux下的多线程程序,须要使用头文件pthread.h,连接时须要使用库libpthread.a。顺便说一下,Linux下pthread的实现是通过系统调用clone()来实现的。
clone()是Linux所特有的系统调用,它的使用方式类似fork。关于clone()的具体情况,有兴趣的读者能够去查看有关文档说明。以下我们展示一个最简单的多线程程序 example1.c。
- void *threadA(void *);
- void *threadB(void *);
- typedef struct shu {
- int num;
- char *s;
- } xiao;
- int main(int argc, char *argv[])
- {
- xiao *t;
- t = (xiao *) malloc(sizeof(xiao));
- t->num = 1;
- t->s = "123";
- int i = 100;
- int *x = &i;
- void *rec;
- pthread_t a, b;
- pthread_create(&a, NULL, (void *)threadA, (void *)t);
- pthread_create(&b, NULL, (void *)threadB, (void *)x);
- pthread_join(a, &rec);
- pthread_join(b, NULL);
- printf("%s\n", (char *)rec);
- return 0;
- }
- void *threadA(void *a)
- {
- int i;
- for (i = 0; i < 10; i++) {
- printf("123%s\n", (char *)((xiao *) a)->s);
- }
- return "ok";
- }
- void *threadB(void *a)
- {
- int i;
- for (i = 0; i < 10; i++) {
- printf("456%d\n", *((int *)a));
- }
- }
上面的演示样例中。我们使用到了两个函数, pthread_create和pthread_join,并声明了一个pthread_t型的变量。
pthread_t在头文件/usr/include/bits/pthreadtypes.h中定义:
typedef unsigned long int pthread_t;
它是一个线程的标识符。函数pthread_create用来创建一个线程,它的原型为:
extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,
void *(*__start_routine) (void *), void *__arg));
第一个參数为指向线程标识符的指针。第二个參数用来设置线程属性。第三个參数是线程执行函数的起始地址,最后一个參数是执行函数的參数。这里。我们的函数thread不须要參数,所以最后一个參数设为空指针。第二个參数我们也设为空指针。这样将生成默认属性的线程。
对线程属性的设定和改动我们将在下一节阐述。当创建线程成功时,函数返回0,若不为0则说明创建线程失败。常见的错误返回代码为EAGAIN和EINVAL。
前者表示系统限制创建新的线程。比如线程数目过多了。后者表示第二个參数代表的线程属性值非法。
创建线程成功后。新创建的线程则执行參数三和參数四确定的函数,原来的线程则继续执行下一行代码。
函数pthread_join用来等待一个线程的结束。函数原型为:
extern int pthread_join __P ((pthread_t __th, void **__thread_return));
第一个參数为被等待的线程标识符,第二个參数为一个用户定义的指针。它能够用来存储被等待线程的返回值。这个函数是一个线程堵塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。一个线程的结束有两种途径。一种是象我们上面的样例一样,函数结束了,调用它的线程也就结束了;还有一种方式是通过函数pthread_exit来实现。
它的函数原型为:
extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));
唯一的參数是函数的返回代码,仅仅要pthread_join中的第二个參数thread_return不是NULL,这个值将被传递给 thread_return。
最后要说明的是,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join的线程则返回错误代码ESRCH。
在这一节里,我们编写了一个最简单的线程,并掌握了最经常使用的三个函数pthread_create。pthread_join和pthread_exit。以下。我们来了解线程的一些经常使用属性以及怎样设置这些属性。
3 改动线程的属性
在上一节的样例里。我们用pthread_create函数创建了一个线程。在这个线程中,我们使用了默认參数。即将该函数的第二个參数设为NULL。
的确,对大多数程序来说,使用默认属性就够了,但我们还是有必要来了解一下线程的有关属性。
属性结构为pthread_attr_t。它相同在头文件/usr/include/pthread.h中定义,喜欢追根问底的人能够自己去查看。属性值不能直接设置,须使用相关函数进行操作。初始化的函数为pthread_attr_init。这个函数必须在pthread_create函数之前调用。属性对象主要包含是否绑定、是否分离、堆栈地址、堆栈大小、优先级。默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程相同级别的优先级。
关于线程的绑定,牵涉到另外一个概念:轻进程(LWP:Light Weight Process)。轻进程能够理解为内核线程。它位于用户层和系统层之间。
系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程能够控制一个或多个线程。默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的。这样的状况即称为非绑定的。绑定状况下。则顾名思义。即某个线程固定的"绑"在一个轻进程之上。被绑定的线程具有较高的响应速度,这是由于CPU时间片的调度是面向轻进程的,绑定的线程能够保证在须要的时候它总有一个轻进程可用。
通过设置被绑定的轻进程的优先级和调度级能够使得绑定的线程满足诸如实时反应之类的要求。
设置线程绑定状态的函数为 pthread_attr_setscope,它有两个參数,第一个是指向属性结构的指针,第二个是绑定类型,它有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)。以下的代码即创建了一个绑定的线程。
#include <pthread.h>
pthread_attr_t attr;
pthread_t tid;
/*初始化属性值,均设为默认值*/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
pthread_create(&tid, &attr, (void *) my_function, NULL);
线程的分离状态决定一个线程以什么样的方式来终止自己。在上面的样例中,我们採用了线程的默认属性,即为非分离状态,这样的情况下,原有的线程等待创建的线程结束。仅仅有当pthread_join()函数返回时,创建的线程才算终止,才干释放自己占用的系统资源。
而分离线程不是这样子的,它没有被其它的线程所等待,自己执行结束了,线程也就终止了,立即释放系统资源。
程序猿应该依据自己的须要,选择适当的分离状态。设置线程分离状态的函数为 pthread_attr_setdetachstate(pthread_attr_t *attr,
int detachstate)。第二个參数可选为PTHREAD_CREATE_DETACHED(分离线程)和 PTHREAD _CREATE_JOINABLE(非分离线程)。这里要注意的一点是。假设设置一个线程为分离线程,而这个线程执行又非常快。它非常可能在 pthread_create函数返回之前就终止了。它终止以后就可能将线程号和系统资源移交给其它的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这样的情况能够採取一定的同步措施,最简单的方法之中的一个是能够在被创建的线程里调用
pthread_cond_timewait函数。让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间。是在多线程编程里经常使用的方法。可是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠。并不能解决线程同步的问题。
另外一个可能经常使用的属性是线程的优先级。它存放在结构sched_param中。
用函数pthread_attr_getschedparam和函数 pthread_attr_setschedparam进行存放,一般说来,我们总是先取优先级。对取得的值改动后再存放回去。
以下即是一段简单的样例。
#include <pthread.h>
#include <sched.h>
pthread_attr_t attr;
pthread_t tid;
sched_param param;
int newprio=20;
pthread_attr_init(&attr);
pthread_attr_getschedparam(&attr, ¶m);
param.sched_priority=newprio;
pthread_attr_setschedparam(&attr, ¶m);
pthread_create(&tid, &attr, (void *)myfunction, myarg);
4 线程的数据处理
和进程相比。线程的最大长处之中的一个是数据的共享性,各个进程共享父进程处沿袭的数据段,能够方便的获得、改动数据。
但这也给多线程编程带来了很多问题。我们必须当心有多个不同的进程訪问同样的变量。
很多函数是不可重入的,即同一时候不能执行一个函数的多个拷贝(除非使用不同的数据段)。在函数中声明的静态变量经常带来问题。函数的返回值也会有问题。由于假设返回的是函数内部静态声明的空间的地址,则在一个线程调用该函数得到地址后使用该地址指向的数据时。别的线程可能调用此函数并改动了这一段数据。
在进程中共享的变量必须用keywordvolatile来定义,这是为了防止编译器在优化时(如gcc中使用-OX參数)改变它们的使用方式。为了保护变量,我们必须使用信号量、相互排斥等方法来保证我们对变量的正确使用。以下。我们就逐步介绍处理线程数据时的有关知识。
4.1 线程数据
在单线程的程序里。有两种主要的数据:全局变量和局部变量。但在多线程程序里。还有第三种数据类型:线程数据(TSD: Thread-Specific Data)。
它和全局变量非常象,在线程内部。各个函数能够象使用全局变量一样调用它,但它对线程外部的其他线程是不可见的。
这样的数据的必要性是显而易见的。比如我们常见的变量errno,它返回标准的出错信息。
它显然不能是一个局部变量,差点儿每一个函数都应该能够调用它;但它又不能是一个全局变量,否则在 A线程里输出的非常可能是B线程的出错信息。要实现诸如此类的变量,我们就必须使用线程数据。我们为每一个线程数据创建一个键。它和这个键相关联,在各个线程里,都使用这个键来指代线程数据,但在不同的线程里,这个键代表的数据是不同的。在同一个线程里,它代表相同的数据内容。
和线程数据相关的函数主要有4个:创建一个键;为一个键指定线程数据;从一个键读取线程数据;删除键。
创建键的函数原型为:
extern int pthread_key_create __P ((pthread_key_t *__key,void (*__destr_function) (void *)));
第一个參数为指向一个键值的指针。第二个參数指明了一个destructor函数,假设这个參数不为空。那么当每一个线程结束时,系统将调用这个函数来释放绑定在这个键上的内存块。这个函数常和函数pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))一起使用,为了让这个键仅仅被创建一次。函数pthread_once声明一个初始化函数,第一次调用pthread_once时它运行这个函数。以后的调用将被它忽略。
在以下的样例中,我们创建一个键,并将它和某个数据相关联。我们要定义一个函数 createWindow。这个函数定义一个图形窗体(数据类型为Fl_Window *。这是图形界面开发工具FLTK中的数据类型)。
因为各个线程都会调用这个函数。所以我们使用线程数据。
/* 声明一个键*/
pthread_key_t myWinKey;
/* 函数 createWindow */
void createWindow ( void ) {
Fl_Window * win;
static pthread_once_t once=PTHREAD_ONCE_INIT;
/* 调用函数createMyKey,创建键*/
pthread_once ( & once, createMyKey) ;
/*win指向一个新建立的窗体*/
win=new Fl_Window( 0, 0, 100, 100, "MyWindow");
/* 对此窗体作一些可能的设置工作。如大小、位置、名称等*/
setWindow(win);
/* 将窗体指针值绑定在键myWinKey上*/
pthread_setpecific ( myWinKey, win);
}
/* 函数 createMyKey,创建一个键,并指定了destructor */
void createMyKey ( void ) {
pthread_keycreate(&myWinKey, freeWinKey);
}
/* 函数 freeWinKey。释放空间*/
void freeWinKey ( Fl_Window * win){
delete win;
}
这样。在不同的线程中调用函数createMyWin,都能够得到在线程内部均可见的窗体变量,这个变量通过函数 pthread_getspecific得到。在上面的样例中,我们已经使用了函数pthread_setspecific来将线程数据和一个键绑定在一起。这两个函数的原型例如以下:
extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer));
extern void *pthread_getspecific __P ((pthread_key_t __key));
这两个函数的參数意义和用法是显而易见的。要注意的是,pthread_setspecific为一个键指定新的线程数据时,必须自己释放原有的线程数据以回收空间。这个过程函数pthread_key_delete用来删除一个键。这个键占用的内存将被释放。但相同要注意的是,它仅仅释放键占用的内存,并不释放该键关联的线程数据所占用的内存资源,并且它也不会触发函数pthread_key_create中定义的destructor函数。
线程数据的释放必须在释放键之前完毕。
4.2 相互排斥锁
相互排斥锁用来保证一段时间内仅仅有一个线程在运行一段代码。必要性显而易见:如果各个线程向同一个文件顺序写入数据,最后得到的结果一定是灾难性的。
我们先看以下一段代码。这是一个读/敲代码。它们公用一个缓冲区,而且我们假定一个缓冲区仅仅能保存一条信息。
即缓冲区仅仅有两个状态:有信息或没有信息。
- /*
- * =====================================================================================
- *
- * Filename: pthread2.c
- *
- * Description: A Program of mutex
- *
- * Version: 1.0
- * Created: 03/11/2009 08:32:51 PM
- * Revision: none
- * Compiler: gcc
- *
- * Author: Futuredaemon (BUPT), gnuhpc@gmail.com
- * Company: BUPT_UNITED
- *
- * =====================================================================================
- */
- #include <pthread.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <time.h>
- void reader_function ( void );
- void writer_function ( void );
- int buffer_has_item=0;
- pthread_mutex_t mutex;
- int main ( void )
- {
- pthread_t reader;
- pthread_mutex_init (&mutex,NULL);
- pthread_create(&reader, NULL, (void *)&reader_function, NULL);
- writer_function( );
- return 0;
- }
- void writer_function (void)
- {
- while (1)
- {
- pthread_mutex_lock (&mutex);
- if (buffer_has_item==0)
- {
- buffer_has_item=1;
- printf("Write once!/n");
- }
- pthread_mutex_unlock(&mutex);
- }
- }
- void reader_function(void)
- {
- while (1)
- {
- pthread_mutex_lock(&mutex);
- if (buffer_has_item==1)
- {
- buffer_has_item=0;
- printf("Read once!/n");
- }
- pthread_mutex_unlock(&mutex);
- }
- }
这里声明了相互排斥锁变量mutex,结构pthread_mutex_t为不公开的数据类型,当中包括一个系统分配的属性对象。函数 pthread_mutex_init用来生成一个相互排斥锁。
NULL參数表明使用默认属性。假设须要声明特定属性的相互排斥锁。须调用函数 pthread_mutexattr_init。函数pthread_mutexattr_setpshared和函数 pthread_mutexattr_settype用来设置相互排斥锁属性。前一个函数设置属性pshared,它有两个取值,PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。
前者用来不同进程中的线程同步,后者用于同步本进程的不同线程。在上面的样例中。我们使用的是默认属性PTHREAD_PROCESS_
PRIVATE。
后者用来设置相互排斥锁类型。可选的类型有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、 PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它们分别定义了不同的上所、解锁机制,普通情况下。选用最后一个默认属性。
pthread_mutex_lock声明開始用相互排斥锁上锁,此后的代码直至调用pthread_mutex_unlock为止,均被上锁,即同一时间仅仅能被一个线程调用运行。当一个线程运行到pthread_mutex_lock处时。假设该锁此时被还有一个线程使用,那此线程被堵塞,即程序将等待到还有一个线程释放此相互排斥锁。在上面的样例中,我们使用了pthread_delay_np函数。让线程睡眠一段时间。就是为了防止一个线程始终占领此函数。
上面的样例非常easy。就不再介绍了,须要提出的是在使用相互排斥锁的过程中非常有可能会出现死锁:两个线程试图同一时候占用两个资源,并按不同的次序锁定对应的相互排斥锁。比如两个线程都须要锁定相互排斥锁1和相互排斥锁2,a线程先锁定相互排斥锁1。b线程先锁定相互排斥锁2,这时就出现了死锁。此时我们能够使用函数 pthread_mutex_trylock,它是函数pthread_mutex_lock的非堵塞版本号,当它发现死锁不可避免时,它会返回对应的信息。程序猿能够针对死锁做出对应的处理。另外不同的相互排斥锁类型对死锁的处理不一样,但最基本的还是要程序猿自己在程序设计注意这一点。
总结一下:
1) 仅仅能用于"锁"住临界代码区域
2) 一个线程加的锁必须由该线程解锁.
锁差点儿是我们学习同步时最開始接触到的一个策略,也是最简单, 最直白的策略.
4.3 条件变量
前一节中我们讲述了怎样使用相互排斥锁来实现线程间数据的共享和通信,相互排斥锁一个明显的缺点是它仅仅有两种状态:锁定和非锁定。
而条件变量通过同意线程堵塞和等待还有一个线程发送信号的方法弥补了相互排斥锁的不足。它常和相互排斥锁一起使用。
使用时。条件变量被用来堵塞一个线程。当条件不满足时。线程往往解开对应的相互排斥锁并等待条件发生变化。一旦其他的某个线程改变了条件变量,它将通知对应的条件变量唤醒一个或多个正被此条件变量堵塞的线程。这些线程将又一次锁定相互排斥锁并又一次測试条件是否满足。
一般说来,条件变量被用来进行线程间的同步。
条件变量,与锁不同,
条件变量用于等待某个条件被触发
1) 大体使用的伪码:
// 线程一代码
pthread_mutex_lock(&mutex);
// 设置条件为true
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
// 线程二代码
pthread_mutex_lock(&mutex);
while (条件为false)
pthread_cond_wait(&cond, &mutex);
改动该条件
pthread_mutex_unlock(&mutex);
须要注意几点:
1)
第二段代码之所以在pthread_cond_wait外面包括一个while循环不停測试条件是否成立的原因是,
在 pthread_cond_wait被唤醒的时候可能该条件已经不成立,这个情况举例:在pthread_cond_wait解锁、測试到信号后可是在加锁前这个条件不成立了。那么通过这个While还要再检測这个条件是不是成立,那么即使收到了这样一个不稳定的错误信号,while也是跳不出去的。
UNPV2对这个的描写叙述是:"Notice that when
pthread_cond_wait returns, we always test the condition again, because
spurious wakeups can occur: a wakeup when the desired condition is
still not true.".
2)
pthread_cond_wait调用必须和某一个mutex一起调用, 这个mutex是在外部进行加锁的mutex,
这个锁的作用是相互排斥,由于两个线程要对线程间共享的某个数据作操作,相互排斥就是不可缺少的了。所以说pthread_cond_wait既进行了线程间的相互排斥还进行了线程间的同步。
在调用pthread_cond_wait时, 内部的实现将首先将这个mutex解锁, 然后等待条件变量被唤醒, 假设没有被唤醒,
该线程将一直休眠, 也就是说, 该线程将一直堵塞在这个pthread_cond_wait调用中, 而当此线程被唤醒时,
将自己主动将这个mutex加锁.
man文档中对这部分的说明是:
pthread_cond_wait
atomically unlocks the mutex (as per pthread_unlock_mutex) and waits
for the condition variable cond to be signaled. The thread execution
is suspended and does not consume any CPU time until the condition
variable is
signaled. The mutex must be locked by the calling thread
on entrance to pthread_cond_wait. Before returning to the calling
thread, pthread_cond_wait re-acquires mutex (as per pthread_lock_mutex).
也就是说pthread_cond_wait实际上能够看作是下面几个动作的合体:
a.解锁线程锁
b.等待条件为true
c.加锁线程锁.
- /*
- * =====================================================================================
- *
- * Filename: pthread3.c
- *
- * Description: A program of showing semaphore
- *
- * Version: 1.0
- * Created: 03/11/2009 10:03:23 PM
- * Revision: none
- * Compiler: gcc
- *
- * Author: Futuredaemon (BUPT), gnuhpc@gmail.com
- * Company: BUPT_UNITED
- *
- * =====================================================================================
- */
- #include <stdio.h>
- #include <pthread.h>
- #include <unistd.h>
- pthread_mutex_t count_lock;
- pthread_cond_t count_nonzero;
- unsigned count = 0;
- void * decrement_count(void *arg)
- {
- pthread_mutex_lock (&count_lock);
- printf("decrement_count get count_lock/n");
- while (count==0)
- {
- printf("decrement_count count == 0 /n");
- printf("decrement_count before cond_wait /n");
- pthread_cond_wait( &count_nonzero, &count_lock);
- printf("decrement_count after cond_wait /n");
- }
- count = count -1;
- pthread_mutex_unlock (&count_lock);
- }
- void * increment_count(void *arg)
- {
- pthread_mutex_lock(&count_lock);
- printf("increment_count get count_lock/n");
- if (count==0)
- {
- printf("increment_count before cond_signal/n");
- pthread_cond_signal(&count_nonzero);
- printf("increment_count after cond_signal/n");
- }
- count=count+1;
- pthread_mutex_unlock(&count_lock);
- }
- int main(void)
- {
- pthread_t tid1,tid2;
- pthread_mutex_init(&count_lock,NULL);
- pthread_cond_init(&count_nonzero,NULL);
- pthread_create(&tid1,NULL,decrement_count,NULL);
- sleep(2);
- pthread_create(&tid2,NULL,increment_count,NULL);
- sleep(10);
- pthread_exit(0);
- }
我们如今要讨论的是什么时候单一Mutex不够,还须要这么麻烦用条件变量?
如果有共享的资源sum,与之相关联的mutex是lock_s.如果每一个线程对sum的操作非常easy的,与sum的状态无关,比方仅仅是sum++.那么仅仅用mutex足够了.程序猿仅仅要确保每一个线程操作前,取得lock,然后sum++,再unlock就可以.
每一个线程的代码将像这样
add()
{
pthread_mutex_lock(lock_s);
sum++;
pthread_mutex_unlock(lock_s);
}
如果操作比較复杂,如果线程t0,t1,t2的操作是sum++,而线程t3则是在sum到达100的时候,打印出一条信息,并对sum清零.这样的情况下,
假设仅仅用mutex,
则t3须要一个循环,每一个循环里先取得lock_s,然后检查sum的状态,假设sum>=100,则打印并清零,然后unlock.假设sum&
lt;100,则unlock,并sleep()本线程合适的一段时间.
这个时候,t0,t1,t2的代码不变,t3的代码例如以下
print()
{
while (1)
{
pthread_mutex_lock(lock_s);
if(sum<100)
{
printf(“sum reach 100!”);
pthread_mutex_unlock(lock_s);
}
else
{
pthread_mutex_unlock(lock_s);
my_thread_sleep(100);
return OK;
}
}
}
这样的办法有两个问题
1) sum在大多数情况下不会到达100,那么对t3的代码来说,大多数情况下,走的是else分支,仅仅是lock和unlock,然后sleep().这浪费了CPU处理时间.
2) 为了节省CPU处理时间,t3会在探測到sum没到达100的时候sleep()一段时间.这样却又带来另外一个问题,亦即t3响应速度下降.可能在sum到达200的时候,t4才会醒过来.
3) 这样,程序猿在设置sleep()时间的时候陷入两难境界,设置得太短了节省不了资源,太长了又减少响应速度.真是难办啊!
这个时候,condition variable内裤外穿,从天而降,解救了焦头烂额的你.
你首先定义一个condition variable.
pthread_cond_t cond_sum_ready=PTHREAD_COND_INITIALIZER;
t0,t1,t2的代码仅仅要后面加两行,像这样
add()
{
pthread_mutex_lock(lock_s);
sum++;
pthread_mutex_unlock(lock_s);
if(sum>=100)
pthread_cond_signal(&cond_sum_ready);
}
而t3的代码则是
print
{
pthread_mutex_lock(lock_s);
while(sum<100)
pthread_cond_wait(&cond_sum_ready, &lock_s);
printf(“sum is over 100!”);
sum=0;
pthread_mutex_unlock(lock_s);
return OK;
}
注意两点:
1)
在thread_cond_wait()之前,必须先lock相关联的mutex,
由于假如目标条件未满足,pthread_cond_wait()实际上会unlock该mutex,
然后block,在目标条件满足后再又一次lock该mutex, 然后返回.
2)
为什么是while(sum<100),而不是if(sum<100)
?
这是由于在pthread_cond_signal()和pthread_cond_wait()返回之间,有时间差,如果在这个时间差内,还有另外一
个线程t4又把sum降低到100下面了,那么t3在pthread_cond_wait()返回之后,显然应该再检查一遍sum的大小.这就是用
while的用意.
这么一说就知道什么时候要用条件变量了~就在涉及推断共同变量状态时,换句话说。也就是本节所说的要进程同步的时候用~
4.3信号量
信号量既能够作为二值计数器(即0,1),也能够作为资源计数器.
信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的訪问。
当公共资源添加时。调用函数sem_post()添加信号量。
仅仅有当信号量值大于0时,才干使用公共资源。使用后,函数sem_wait()降低信号量。
函数sem_trywait()和函数pthread_ mutex_trylock()起相同的作用,它是函数sem_wait()的非堵塞版本号。以下我们逐个介绍和信号量有关的一些函数。它们都在头文件 /usr/include/semaphore.h中定义。
信号量的数据类型为结构sem_t。它本质上是一个长整型的数。函数sem_init()用来初始化一个信号量。它的原型为:
extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
sem为指向信号量结构的一个指针。pshared不为0时此信号量在进程间共享,否则仅仅能为当前进程的全部线程共享;value给出了信号量的初始值。
而函数int sem_getvalue(sem_t *sem, int *sval);则用于获取信号量当前的计数. 函数sem_destroy(sem_t *sem)用来释放信号量sem。
能够用信号量模拟锁和条件变量:
1) 锁,在同一个线程内同一时候对某个信号量先调用sem_wait再调用sem_post, 两个函数调用当中的区域就是所要保护的临界区代码了,这个时候事实上信号量是作为二值计数器来使用的.只是在此之前要初始化该信号量计数为1,见以下样例中的代码.
2) 条件变量,在某个线程中调用sem_wait, 而在还有一个线程中调用sem_post.
只是, 信号量除了能够作为二值计数器用于模拟线程锁和条件变量之外, 还有比它们更加强大的功能, 信号量能够用做资源计数器, 也就是说初始化信号量的值为某个资源当前可用的数量, 使用了一个之后递减, 归还了一个之后递增。
信号量与线程锁,条件变量相比还有下面几点不同:
1)锁必须是同一个线程获取以及释放, 否则会死锁.而条件变量和信号量则不必.
2)信号的递增与降低会被系统自己主动记住, 系统内部有一个计数器实现信号量,不必操心会丢失, 而唤醒一个条件变量时,假设没有对应的线程在等待该条件变量, 这次唤醒将被丢失.
- /*
- * =====================================================================================
- *
- * Filename: pthread4.c
- *
- * Description: A program of Semaphore
- *
- * Version: 1.0
- * Created: 03/13/2009 11:54:35 PM
- * Revision: none
- * Compiler: gcc
- *
- * Author: Futuredaemon (BUPT), gnuhpc@gmail.com
- * Company: BUPT_UNITED
- *
- * =====================================================================================
- */
- #include <stdio.h>
- #include <string.h>
- #include <pthread.h>
- #include <errno.h>
- #include <semaphore.h>
- #define BUFSIZE 4
- #define NUMBER 8
- int sum_of_number=0;
- /* 可读 和 可写资源数*/
- sem_t write_res_number;
- sem_t read_res_number;
- /* 循环队列 */
- struct recycle_buffer{
- int buffer[BUFSIZE];
- int head,tail;
- }re_buf;
- /* 用于实现临界区的相互排斥锁。我们对其初始化*/
- pthread_mutex_t buffer_mutex=PTHREAD_MUTEX_INITIALIZER;
- static void *producer(void * arg)
- {
- int i;
- for(i=0;i<=NUMBER;i++)
- {
- /* 降低可写的资源数 */
- sem_wait(&write_res_number);
- /* 进入相互排斥区 */
- pthread_mutex_lock(&buffer_mutex);
- /*将数据拷贝到缓冲区的尾部*/
- re_buf.buffer[re_buf.tail]=i;
- re_buf.tail=(re_buf.tail+1)%BUFSIZE;
- printf("procuder %d write %d./n",(int)pthread_self(),i);
- /*离开相互排斥区*/
- pthread_mutex_unlock(&buffer_mutex);
- /*添加可读资源数*/
- sem_post(&read_res_number);
- }
- /* 线程终止。假设有线程等待它们结束,则把NULL作为等待其结果的返回值*/
- return NULL;
- }
- static void * consumer(void * arg)
- {
- int i,num;
- for(i=0;i<=NUMBER;i++)
- {
- /* 降低可读资源数 */
- sem_wait(&read_res_number);
- /* 进入相互排斥区*/
- pthread_mutex_lock(&buffer_mutex);
- /* 从缓冲区的头部获取数据*/
- num = re_buf.buffer[re_buf.head];
- re_buf.head = (re_buf.head+1)%BUFSIZE;
- printf("consumer %d read %d./n",pthread_self(),num);
- /* 离开相互排斥区*/
- pthread_mutex_unlock(&buffer_mutex);
- sum_of_number+=num;
- /* 添加客写资源数*/
- sem_post(&write_res_number);
- }
- /* 线程终止,假设有线程等待它们结束,则把NULL作为等待其结果的返回值*/
- return NULL;
- }
- int main(int argc,char ** argv)
- {
- /* 用于保存线程的线程号 */
- pthread_t p_tid;
- pthread_t c_tid;
- int i;
- re_buf.head=0;
- re_buf.tail=0;
- for(i=0;i<BUFSIZE;i++)
- re_buf.buffer[i] =0;
- /* 初始化可写资源数为循环队列的单元数 */
- sem_init(&write_res_number,0,BUFSIZE); // 这里限定了可写的bufsize,当写线程写满buf时。会堵塞,等待读线程读取
- /* 初始化可读资源数为0 */
- sem_init(&read_res_number,0,0);
- /* 创建两个线程,线程函数各自是 producer 和 consumer */
- /* 这两个线程将使用系统的缺省的线程设置,如线程的堆栈大小、线程调度策略和对应的优先级等等*/
- pthread_create(&p_tid,NULL,producer,NULL);
- pthread_create(&c_tid,NULL,consumer,NULL);
- /*等待两个线程完毕退出*/
- pthread_join(p_tid,NULL);
- pthread_join(c_tid,NULL);
- printf("The sum of number is %d/n",sum_of_number);
- }
C中多线程开发的更多相关文章
- [Android学习笔记]Android中多线程开发的一些概念
线程安全: 在多线程的情况下,不会因为线程之间的操作而导致数据错误. 线程同步: 同一个资源,可能在同一时间被多个线程操作,这样会导致数据错误.这是一个现象,也是一个问题,而研究如何解决此类问题的相关 ...
- iOS开发中多线程基础
耗时操作演练 代码演练 编写耗时方法 - (void)longOperation { for (int i = 0; i < 10000; ++i) { NSLog(@"%@ %d&q ...
- .NET基础拾遗(5)多线程开发基础
Index : (1)类型语法.内存管理和垃圾回收基础 (2)面向对象的实现和异常的处理基础 (3)字符串.集合与流 (4)委托.事件.反射与特性 (5)多线程开发基础 (6)ADO.NET与数据库开 ...
- iOS中多线程知识总结(一)
这一段开发中一直在处理iOS多线程的问题,但是感觉知识太散了,所以就把iOS中多线程的知识点总结了一下. 1.基本概念 1)什么是进程?进程的特性是什么? 进程是指在系统中正在运行的一个应用程序. ...
- Java多线程开发系列之番外篇:事件派发线程---EventDispatchThread
事件派发线程是java Swing开发中重要的知识点,在安卓app开发中,也是非常重要的一点.今天我们在多线程开发中,穿插进来这个线程.分别从线程的来由.原理和使用方法三个方面来学习事件派发线程. 一 ...
- Java多线程开发系列之四:玩转多线程(线程的控制2)
在上节的线程控制(详情点击这里)中,我们讲解了线程的等待join().守护线程.本节我们将会把剩下的线程控制内容一并讲完,主要内容有线程的睡眠.让步.优先级.挂起和恢复.停止等. 废话不多说,我们直接 ...
- Java多线程开发系列之一:走进多线程
对编程语言的基础知识:分支.选择.循环.面向对象等基本概念理解后,我们需要对java高级编程有一定的学习,这里不可避免的要接触到多线程开发. 由于多线程开发整体的系统比较大,我会写一个系列的文章总结介 ...
- Java之多线程开发时多条件Condition接口的使用
转:http://blog.csdn.net/a352193394/article/details/39454157 我们在多线程开发中,可能会出现这种情况.就是一个线程需要另外一个线程满足某某条件才 ...
- Java进阶(三)多线程开发关键技术
原创文章,同步发自作者个人博客,转载请务必以超链接形式在文章开头处注明出处http://www.jasongj.com/java/multi_thread/. sleep和wait到底什么区别 其实这 ...
随机推荐
- mysql 查看存储过程 并导出
查询数据库中的存储过程 select * from mysql.proc where db = dbName and `type` = 'PROCEDURE' show procedure statu ...
- python 双冒号
Python序列切片地址可以写为[开始:结束:步长],其中的开始和结束可以省略 1.range(n)生成[0,n)区间整数 range(10) [0,1,2,3,4,5,6,7,8,9] 2.开始st ...
- Server.MapPath() 用法
Server.MapPath() ./当前目录/网站主目录../上层目录~/网站虚拟目录 如果当前的网站目录为E:\wwwroot 应用程序虚拟目录为E:\wwwroot\company 浏览的页 ...
- django显示图片
dirctory vickey_django vickey(projectname) vickey __init__.py __pycache__ settings.py urls.py wsgi.p ...
- IOS开发之关键字synchronized
static Config * instance = nil; +(Config *) Instance { @synchronized(self) { if(nil == instance) { ...
- 数据库的ACID 简谈
一.事务 定义:所谓事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位. 准备工作:为了说明事务的ACID原理,我们使用银行账户及资金管理的案例进行分析. 二.ACI ...
- Linux下启动tomcat报java.lang.OutOfMemoryError: PermGen space
一.错误信息 java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke ...
- 83-MACD 移动平均汇总/分离指标.(2015.7.3)
MACD 移动平均汇总/分离指标 ~计算: · EMA(12,t)=EMA(12,t-1) * 11/13 + Close * 2/13 · EMA(26,t)=EMA(26,t-1) * 25/27 ...
- 程序如何实现设置系统WIFI共享
1.以管理员身份运行命令提示符:快捷键win+R(win+X 以管理员方式运行CMD)→输入cmd→回车2.启用并设定虚拟WiFi网卡:运行命令:netsh wlan set hostednetwor ...
- 修改windows 2012/win8、win7远程桌面连接默认端口的方法
远程桌面连接服务基于端口3389,入侵者一般先扫描主机开放端口,一旦发现其开放了3389端口,就会进行下一步的入侵,所以我们只需要修改该务默认端口就可以避开大多数入侵者的耳目. 修改windows 2 ...