1. Linux多线程概述
1.1. 概述
进程是系统中程序执行和资源分配的基本单位。每个进程有自己的数据段、代码段和堆栈段。这就造成进程在进行切换等操作时都需要有比较负责的上下文切换等动作。为了进一步减少处理器的空转时间支持多处理器和减少上下文切换开销,也就出现了线程。
线程通常叫做轻量级进程。线程是在共享内存空间中并发执行的多道执行路径,是一个更加接近于执行体的概念,拥有独立的执行序列,是进程的基本调度单元,每个进程至少都有一个main线程。它与同进程中的其他线程共享进程空间{堆 代码 数据 文件描述符 信号等},只拥有自己的栈空间,大大减少了上下文切换的开销。
线程和进程在使用上各有优缺点:线程执行开销小,占用的CPU少,线程之间的切换快,但不利于资源的管理和保护;而进程正相反。从可移植性来讲,多进程的可移植性要好些。
同进程一样,线程也将相关的变量值放在线程控制表内。一个进程可以有多个线程,也就是有多个线程控制表及堆栈寄存器,但却共享一个用户地址空间。要注意的是,由于线程共享了进程的资源和地址空间,因此,任何线程对系统资源的操作都会给其他线程带来影响。
1.2. 线程分类
按调度者分为用户级线程和核心级线程
·用户级线程:主要解决上下文切换问题,调度算法和调度过程全部由用户决定,在运行时不需要特定的内核支持。缺点是无法发挥多处理器的优势
·核心级线程:允许不同进程中的线程按照同一相对优先调度方法调度,发挥多处理器的并发优势
现在大多数系统都采用用户级线程和核心级线程并存的方法。一个用户级线程可以对应一个或多个核心级线程,也就是“一对一”或“一对多”模型。
1.3. 线程创建的Linux实现
Linux的线程是通过用户级的函数库实现的,一般采用pthread线程库实现线程的访问和控制。它用第3方posix标准的pthread,具有良好的可移植性。编译的时候要在后面加上 –lpthread
创建 退出 等待
多进程 fork() exit() wait()
多线程 pthread_create pthread_exit() pthread_join()
2. 线程的创建和退出
创建线程实际上就是确定调用该线程函数的入口点,线程的创建采用函数pthread_create。在线程创建以后,就开始运行相关的线程函数,在该函数运行完之后,线程就退出,这也是线程退出的一种方式。
另一种线程退出的方式是使用函数pthread_exit()函数,这是线程主动退出行为。这里要注意的是,在使用线程函数时,不能随意使用exit退出函数进行出错处理,由于exit的作用是使调用进程终止,往往一个进程包括了多个线程,所以在线程中通常使用pthread_exit函数来代替进程中的退出函数exit。
由于一个进程中的多个线程是共享数据段的,因此通常在线程退出之后,退出线程所占用的资源并不会随着线程的终止而得到释放。正如进程之间可以通过wait()函数系统调用来同步终止并释放资源一样,线程之间也有类似的机制,那就是pthread_join函数。pthread_join函数可以用于将当前线程挂起,等待线程的结束。这个函数是一个线程阻塞函数,调用它的函数将一直等待直到被等待的线程结束为止,当函数返回时,被等待线程的资源被回收。
函数原型:
#include <pthread.h>
int pthread_create(pthread_t* thread, pthread_attr_t * attr, void *(*start_routine)(void *), void * arg);
void pthread_exit(void *retval);
通常的形式为:
pthread_t pthid;
pthread_create(&pthid,NULL,pthfunc,NULL);或pthread_create(&pthid,NULL,pthfunc,(void*)3);
pthread_exit(NULL);或pthread_exit((void*)3);//3作为返回值被pthread_join函数捕获。
函数pthread_create用来创建线程。返回值:成功,则返回0;失败,则返回-1。各参数描述如下:
·参数thread是传出参数,保存新线程的标识;
·参数attr是一个结构体指针,结构中的元素分别指定新线程的运行属性,attr可以用pthread_attr_init等函数设置各成员的值,但通常传入为NULL 即可;
·参数start_routine是一个函数指针,指向新线程的入口点函数,线程入口点函数带有一个void *的参数由pthread_create的第4个参数传入;
·参数arg用于传递给第3个参数指向的入口点函数的参数,可以为NULL,表示不传递。
函数pthread_exit表示线程的退出。其参数可以被其它线程用pthread_join函数捕获。
示例:
1 #include<stdio.h> 2 #include<pthread.h> 3 4 void *threadFunc(void *parg) 5 { 6 int i = 0; 7 for(; i < 10; i++) 8 { 9 printf("Hi,I‘m child thread, arg is: %d\n",(int)parg); 10 sleep(1); 11 } 12 pthread_exit(NULL); 13 } 14 15 int main() 16 { 17 pthread_t thdid; 18 pthread_create(&thdid,NULL,threadFunc,(void*)123); 19 int i = 0; 20 for(; i < 10; i++) 21 { 22 printf("Hi,I‘m main thread,child thread id is:%d\n",thdid); 23 sleep(1); 24 } 25 return 0; 26 }
编译时需要带上线程库选项:
gcc -o a a.c -lpthread
3. 线程的等待退出
3.1. 等待线程退出
线程从入口点函数自然返回,或者主动调用pthread_exit()函数,都可以让线程正常终止
线程从入口点函数自然返回时,函数返回值可以被其它线程用pthread_join函数获取
pthread_join原型为:
#include <pthread.h>
int pthread_join(pthread_t th, void **thread_return);
1. 该函数是一个阻塞函数,一直等到参数th指定的线程返回;与多进程中的wait或waitpid类似。
thread_return是一个传出参数,接收线程函数的返回值。如果线程通过调用pthread_exit()终止,则pthread_exit()中的参数相当于自然返回值,照样可以被其它线程用pthread_join获取到。
Example:返回值的例子
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<pthread.h> 4 5 void *threadFunc(void *parg) 6 { 7 int iarg = (int)parg; 8 sleep(iarg); 9 if(iarg < 3) 10 return (void*)(iarg*3); 11 else 12 pthread_exit((void*)(iarg*2)); 13 } 14 15 int main() 16 { 17 pthread_t thdid; 18 int iret = 0; 19 20 pthread_create(&thdid,NULL,threadFunc,(void*)2); 21 pthread_join(thdid,(void**)&iret); 22 printf("The first child thread ret is :%d\n",iret); 23 24 pthread_create(&thdid,NULL,threadFunc,(void*)4); 25 pthread_join(thdid,(void**)&iret); 26 printf("The second child thread ret is:%d\n",iret); 27 return 0; 28 }
2. 该函数还有一个非常重要的作用,由于一个进程中的多个线程共享数据段,因此通常在一个线程退出后,退出线程所占用的资源并不会随线程结束而释放。如果th线程类型并不是自动清理资源类型的,则th线程退出后,线程本身的资源必须通过其它线程调用pthread_join来清除,这相当于多进程程序中的waitpid。
Example:子线程释放空间
1 #include<stdio.h> 2 #include<pthread.h> 3 #include<malloc.h> 4 5 void *threadFunc(void *args) 6 { 7 char *p = (char*)malloc(10); 8 int i = 0; 9 for(; i < 10; i++) 10 { 11 printf("Hello,My name is CP!\n"); 12 sleep(2); 13 } 14 free(p); 15 printf("p is freed\n"); 16 pthread_exit((void*)3); 17 } 18 19 int main() 20 { 21 pthread_t thdid; 22 pthread_create(&thdid,NULL,threadFunc,NULL); 23 int i = 1; 24 for(; i < 5; i++) 25 { 26 printf("Hi,nice to meet you\n"); 27 sleep(2); 28 } 29 int retvalue = 0; 30 pthread_join(thdid,(void**)&retvalue); 31 printf("return value is:%d\n",retvalue); 32 33 return 0; 34 }
3.2. 线程的取消
线程也可以被其它线程杀掉,在Linux中的说法是一个线程被另一个线程取消(cancel)。
线程取消的方法是一个线程向目标线程发cancel信号,但是如何处理cancel信号则由目标线程自己决定,目标线程或者忽略、或者立即终止、或者继续运行至cancelation-point(取消点)后终止。
取消点:
根据POSIX标准,pthread_join()、pthread_testcancel()、pthread_cond_wait()、pthread_cond_timedwait()、sem_wait()、sigwait()等函数以及read()、write()等会引起阻塞的系统调用都是Cancelation-point,而其他pthread函数都不会引起Cancelation动作。但是pthread_cancel的手册页声称,由于Linux线程库与C库结合得不好,因而目前C库函数都不是Cancelation-point;但CANCEL信号会使线程从阻塞的系统调用中退出,并置EINTR错误码,因此可以在需要作为Cancelation-point的系统调用前后调用pthread_testcancel(),从而达到POSIX标准所要求的目标,即如下代码段:
pthread_testcancel();
retcode = read(fd, buffer, length);
pthread_testcancel();
但是从RedHat9.0的实际测试来看,至少有些C库函数的阻塞函数是取消点,如read(),getchar()等,而sleep()函数不管线程是否设置了pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,NULL),都起到取消点作用。总之,线程的取消一方面是一个线程强行杀另外一个线程,从程序设计角度看并不是一种好的风格,另一方面目前Linux本身对这方面的支持并不完善,所以在实际应用中应该谨慎使用!!
int pthread_cancel(pthread_t thread); //尽量不要用,linux支持并不完善
3.3. 线程终止清理函数
不论是可预见的线程终止还是异常终止,都会存在资源释放的问题,在不考虑因运行出错而退出的前提下,如何保证线程终止时能顺利的释放掉自己所占用的资源,特别是锁资源,就是一个必须考虑解决的问题。
最经常出现的情形是资源独占锁的使用:线程为了访问临界共享资源而为其加上锁,但在访问过程中该线程被外界取消,或者发生了中断,则该临界资源将永远处于锁定状态得不到释放。外界取消操作是不可预见的,因此的确需要一个机制来简化用于资源释放的编程。
在POSIX线程API中提供了一个pthread_cleanup_push()/pthread_cleanup_pop()函数对用于自动释放资源--从pthread_cleanup_push()的调用点到pthread_cleanup_pop()之间的程序段中的终止动作都将执行pthread_cleanup_push()所指定的清理函数。API定义如下:
void pthread_cleanup_push(void (*routine) (void *), void *arg)
void pthread_cleanup_pop(int execute)
pthread_cleanup_push()/pthread_cleanup_pop()采用先入后出的栈结构管理
void routine(void *arg)函数在调用pthread_cleanup_push()时压入清理函数栈,多次对pthread_cleanup_push()的调用将在清理函数栈中形成一个函数链,在执行该函数链时按照压栈的相反顺序弹出。execute参数表示执行到pthread_cleanup_pop()时是否在弹出清理函数的同时执行该函数,为0表示不执行,非0为执行;这个参数并不影响异常终止时清理函数的执行。
pthread_cleanup_push()/pthread_cleanup_pop()是以宏方式实现的,这是pthread.h中的宏定义:
#define pthread_cleanup_push(routine,arg) \
{
struct _pthread_cleanup_buffer _buffer; \
_pthread_cleanup_push (&_buffer, (routine), (arg));
#define pthread_cleanup_pop(execute) \
_pthread_cleanup_pop (&_buffer, (execute));
}
可见,pthread_cleanup_push()带有一个"{",而pthread_cleanup_pop()带有一个"}",因此这两个函数必须成对出现,且必须位于程序的同一级别的代码段中才能通过编译。
pthread_cleanup_pop的参数execute如果为非0值,则按栈的顺序注销掉一个原来注册的清理函数,并执行该函数;当pthread_cleanup_pop()函数的参数为0时,仅仅在线程调用pthread_exit函数或者其它线程对本线程调用pthread_cancel函数时,才在弹出“清理函数”的同时执行该“清理函数”。
示例:
#include <stdio.h>
#include <pthread.h>
void CleanFunc(void *pArg)
{
printf("CleanFunc(%d)\n",(int)pArg);
}
void *ThreadFunc(void *pArg)
{
pthread_cleanup_push(CleanFunc,(void *)1);
pthread_cleanup_push(CleanFunc,(void *)2);
sleep(2);
pthread_cleanup_pop(1);
pthread_cleanup_pop(1);
}
int main()
{
pthread_t thdId;
pthread_create(&thdId, NULL, ThreadFunc, (void *)2);
pthread_join(thdId,NULL);
return 0;
}
运行结果为:
CleanFunc(2)
CleanFunc(1)
如果将里面的两次pthread_cleanup_pop(1);改为pthread_cleanup_pop(0);推测一下结果是怎样?
没有任何输出(此时CleanFunc函数得不到执行)
如果修改为0之后,再在sleep(2)之后添加pthread_exit(NULL);则此时的结果又是如何:
跟pthread_cleanup_pop(1);实现的结果一样了。
Example:用pthread_cleanup_push和pthread_cleanup_pop来释放子线程分配的内存空间
#include <stdio.h>
#include <pthread.h>
#include <malloc.h>
void freemem(void * args)
{
free(args);
printf("clean up the memory!\n");
}
void* threadfunc(void *args)
{
char *p = (char*)malloc(10); //自己分配了内存
pthread_cleanup_push(freemem,p);
int i = 0;
for(; i < 10; i++)
{
printf("hello,my name is wangxiao!\n");
sleep(1);
}
pthread_exit((void*)3);
pthread_cleanup_pop(0);
}
int main()
{
pthread_t pthid;
pthread_create(&pthid, NULL, threadfunc, NULL);
int i = 1;
for(; i < 5; i++)//父线程的运行次数比子线程的要少,当父线程结束的时候,如果没有pthread_join函数等待子线程执行的话,子线程也会退出,即子线程也只执行了4次。
{
printf("hello,nice to meet you!\n");
sleep(1);
if(i % 3 == 0)
pthread_cancel(pthid); //表示当i%3==0的时候就取消子线程,该函数将导致直接退出,不会执行上面紫色的free部分的代码,即释放空间失败。要想释放指针类型的变量p,必须要用pthread_cleanup_push和pthread_cleanup_pop函数释放空间
}
int retvalue = 0;
pthread_join(pthid,(void**)&retvalue); //等待子线程释放空间,并获取子线程的返回值
printf("return value is :%d\n",retvalue);
return 0;
}
4. 线程的同步与互斥
4.1. 线程的互斥
在Posix Thread中定义了一套专门用于线程互斥的mutex函数。mutex是一种简单的加锁的方法来控制对共享资源的存取,这个互斥锁只有两种状态(上锁和解锁),可以把互斥锁看作某种意义上的全局变量。为什么需要加锁,就是因为多个线程共用进程的资源,要访问的是公共区间时(全局变量),当一个线程访问的时候,需要加上锁以防止另外的线程对它进行访问,实现资源的独占。在一个时刻只能有一个线程掌握某个互斥锁,拥有上锁状态的线程能够对共享资源进行操作。若其他线程希望上锁一个已经上锁了的互斥锁,则该线程就会挂起,直到上锁的线程释放掉互斥锁为止。
1. 创建和销毁锁
有两种方法创建互斥锁,静态方式和动态方式。
·静态方式:
POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁,方法如下:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
在Linux Threads实现中,pthread_mutex_t是一个结构,而PTHREAD_MUTEX_INITIALIZER则是一个宏常量。
·动态方式:
动态方式是采用pthread_mutex_init()函数来初始化互斥锁,API定义如下:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
其中mutexattr用于指定互斥锁属性(见下),如果为NULL则使用缺省属性。通常为NULL
pthread_mutex_destroy()用于注销一个互斥锁,API定义如下:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在Linux中,互斥锁并不占用任何资源,因此Linux Threads中的pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。
2. 互斥锁属性
互斥锁属性结构体的定义为:
typedef struct
{
int __mutexkind; //注意这里是两个下划线
} pthread_mutexattr_t;
互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性__mutexkind,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同也就是是否阻塞等待。有三个值可供选择:
·PTHREAD_MUTEX_TIMED_NP,这是缺省值(直接写NULL就是表示这个缺省值),也就是普通锁(或快速锁)。当一个线程加锁以后,其余请求锁的线程将形成一个阻塞等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性
示例:初始化一个快速锁。
pthread_mutex_t lock;
pthread_mutex_init(&lock, NULL);
·PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
示例:初始化一个嵌套锁。
pthread_mutex_t lock;
pthread_mutexattr_t mutexattr;
mutexattr.__mutexkind = PTHREAD_MUTEX_RECURSIVE_NP;
pthread_mutex_init(&lock, &mutexattr);
·PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。如果锁的类型是快速锁,一个线程加锁之后,又加锁,则此时就是死锁。
示例:初始化一个嵌套锁。
pthread_mutex_t lock;
pthread_mutexattr_t mutexattr;
mutexattr.__mutexkind = PTHREAD_MUTEX_ERRORCHECK_NP;
pthread_mutex_init(&lock, &mutexattr);
3.锁操作
锁操作主要包括
加锁 int pthread_mutex_lock(pthread_mutex_t *mutex)
解锁 int pthread_mutex_unlock(pthread_mutex_t *mutex)
测试加锁 int pthread_mutex_trylock(pthread_mutex_t *mutex)
·pthread_mutex_lock:加锁,不论哪种类型的锁,都不可能被两个不同的线程同时得到,而必须等待解锁。对于普通锁类型,解锁者可以是同进程内任何线程;而检错锁则必须由加锁者解锁才有效,否则返回EPERM;对于嵌套锁,文档和实现要求必须由加锁者解锁,但实验结果表明并没有这种限制,这个不同目前还没有得到解释。在同一进程中的线程,如果加锁后没有解锁,则任何其他线程都无法再获得锁。
·pthread_mutex_unlock:根据不同的锁类型,实现不同的行为:
对于快速锁,pthread_mutex_unlock解除锁定;
对于递规锁,pthread_mutex_unlock使锁上的引用计数减1;
对于检错锁,如果锁是当前线程锁定的,则解除锁定,否则什么也不做。
·pthread_mutex_trylock:语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待。
Example:比较pthread_mutex_trylock()与pthread_mutex_lock()
#include <stdio.h>
#include <pthread.h>
pthread_mutex_t lock;
void* pthfunc(void *args)
{
pthread_mutex_lock(&lock); //先加一次锁
pthread_mutex_lock(&lock); //再用lock加锁,会挂起阻塞
//pthread_mutex_trylock(&lock); //用trylock加锁,则不会挂起阻塞
printf("hello\n");
sleep(1);
pthread_exit(NULL);
}
main()
{
pthread_t pthid = 0;
pthread_mutex_init(&lock,NULL);
pthread_create(&pthid,NULL,pthfunc,NULL);
pthread_join(pthid,NULL);
pthread_mutex_destroy(&lock);
}
4. 加锁注意事项
如果线程在加锁后解锁前被取消,锁将永远保持锁定状态,因此如果在关键区段内有取消点存在,则必须在退出回调函数pthread_cleanup_push/pthread_cleanup_pop中解锁。同时不应该在信号处理函数中使用互斥锁,否则容易造成死锁。
5. 互斥锁实例
Example:火车站售票(此处不加锁,则会出现卖出负数票的情况)
#include <stdio.h>
#include <pthread.h>
int ticketcount = 20; //火车票,公共资源(全局)
void* salewinds1(void* args) //售票口1
{
while(ticketcount > 0) //如果有票,则卖票
{
printf("windows1 start sale ticket!the ticket is:%d\n",ticketcount);
sleep(3); //卖一张票需要3秒的操作时间
ticketcount --; //出票
printf("sale ticket finish!,the last ticket is:%d\n",ticketcount);
}
}
void* salewinds2(void* args) //售票口2
{
while(ticketcount > 0) //如果有票,则卖票
{
printf("windows2 start sale ticket!the ticket is:%d\n",ticketcount);
sleep(3); //卖一张票需要3秒的操作时间
ticketcount --; //出票
printf("sale ticket finish!,the last ticket is:%d\n",ticketcount);
}
}
int main()
{
pthread_t pthid1 = 0;
pthread_t pthid2 = 0;
pthread_create(&pthid1,NULL,salewinds1,NULL); //线程1
pthread_create(&pthid2,NULL,salewinds2,NULL); //线程2
pthread_join(pthid1,NULL);
pthread_join(pthid2,NULL);
return 0;
}
Example:加锁之后的火车售票
#include <stdio.h>
#include <pthread.h>
int ticketcount = 20;
pthread_mutex_t lock;
void* salewinds1(void* args)
{
while(1)
{
pthread_mutex_lock(&lock); //因为要访问全局的共享变量,所以就要加锁
if(ticketcount > 0) //如果有票
{
printf("windows1 start sale ticket!the ticket is:%d\n",ticketcount);
sleep(3); //卖一张票需要3秒的操作时间
ticketcount --; //出票
printf("sale ticket finish!,the last ticket is:%d\n",ticketcount);
}
else //如果没有票
{
pthread_mutex_unlock(&lock); //解锁
pthread_exit(NULL); //退出线程
}
pthread_mutex_unlock(&lock); //解锁
sleep(1); //要放到锁的外面,让另一个有时间锁
}
}
void* salewinds2(void* args)
{
while(1)
{
pthread_mutex_lock(&lock); //因为要访问全局的共享变量,所以就要加锁
if(ticketcount>0) //如果有票
{
printf("windows2 start sale ticket!the ticket is:%d\n",ticketcount);
sleep(3); //卖一张票需要3秒的操作时间
ticketcount --; //出票
printf("sale ticket finish!,the last ticket is:%d\n",ticketcount);
}
else //如果没有票
{
pthread_mutex_unlock(&lock); //解锁
pthread_exit(NULL); //退出线程
}
pthread_mutex_unlock(&lock); //解锁
sleep(1); //要放到锁的外面,让另一个有时间锁
}
}
int main()
{
pthread_t pthid1 = 0;
pthread_t pthid2 = 0;
pthread_mutex_init(&lock,NULL); //初始化锁
pthread_create(&pthid1,NULL,salewinds1,NULL); //线程1
pthread_create(&pthid2,NULL,salewinds2,NULL); //线程2
pthread_join(pthid1,NULL);
pthread_join(pthid2,NULL);
pthread_mutex_destroy(&lock); //销毁锁
return 0;
}
总结:线程互斥mutex:加锁步骤如下:
1. 定义一个全局的pthread_mutex_t lock; 或者用
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; //则main函数中不用init
2. 在main中调用 pthread_mutex_init函数进行初始化
3. 在子线程函数中调用pthread_mutex_lock加锁
4. 在子线程函数中调用pthread_mutex_unlock解锁
5. 最后在main中调用 pthread_mutex_destroy函数进行销毁
4.2. 线程的同步
6 条件变量
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待条件变量的条件成立而挂起;另一个线程使条件成立(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
1、 创建和注销
条件变量和互斥锁一样,都有静态、动态两种创建方式:
静态方式使PTHREAD_COND_INITIALIZER常量,如下:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
动态方式调用pthread_cond_init()函数,API定义如下:
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
尽管POSIX标准中为条件变量定义了属性,但在Linux Threads中没有实现,因此cond_attr值通常为NULL,且被忽略。
注销一个条件变量需要调用pthread_cond_destroy(),只有在没有线程在该条件变量上等待的时候能注销这个条件变量,否则返回EBUSY。因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。API定义如下:
int pthread_cond_destroy(pthread_cond_t *cond);
2、 等待和激发
等待条件有两种方式:无条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait():
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
线程解开mutex指向的锁并被条件变量cond阻塞。其中计时等待方式表示经历abstime段时间后,即使条件变量不满足,阻塞也被解除。无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait(),下同)的竞争条件(Race Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP),且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。(也就是说在做pthread_cond_wait之前,往往要用pthread_mutex_lock进行加锁,而调用pthread_cond_wait函数会将锁解开,然后将线程挂起阻塞。直到条件被pthread_cond_signal激发,再将锁状态恢复为锁定状态,最后再用pthread_mutex_unlock进行解锁)。
激发条件有两种形式,pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程
3、 其他
pthread_cond_wait()和pthread_cond_timedwait()都被实现为取消点,也就是说如果pthread_cond_wait()被取消,则退出阻塞,然后将锁状态恢复,则此时mutex是保持锁定状态的,而当前线程已经被取消掉,那么解锁的操作就会得不到执行,此时锁得不到释放,就会造成死锁,因而需要定义退出回调函数来为其解锁。
以下示例集中演示了互斥锁和条件变量的结合使用,以及取消对于条件等待动作的影响。在例子中,有两个线程被启动,并等待同一个条件变量,如果不使用退出回调函数(见范例中的注释部分),则tid2将在pthread_mutex_lock()处永久等待。如果使用回调函数,则tid2的条件等待及主线程的条件激发都能正常工作。
实例:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
void ThreadClean(void *arg)
{
pthread_mutex_unlock(&mutex);
}
void * child1(void *arg)
{
//pthread_cleanup_push(ThreadClean,NULL); //1
while(1){
printf("thread 1 get running \n");
printf("thread 1 pthread_mutex_lock returns %d\n", pthread_mutex_lock(&mutex));
pthread_cond_wait(&cond,&mutex); //等待父进程发送信号
printf("thread 1 condition applied\n");
pthread_mutex_unlock(&mutex);
sleep(5);
}
//pthread_cleanup_pop(0); //2
return 0;
}
void *child2(void *arg)
{
while(1){
sleep(3); //3
printf("thread 2 get running.\n");
printf("thread 2 pthread_mutex_lock returns %d\n", pthread_mutex_lock(&mutex));
pthread_cond_wait(&cond,&mutex);
printf("thread 2 condition applied\n");
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
int main(void)
{
pthread_t tid1,tid2;
printf("hello, condition variable test\n");
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
pthread_create(&tid1,NULL,child1,NULL);
pthread_create(&tid2,NULL,child2,NULL);
while(1){ //父线程
sleep(2); //4
pthread_cancel(tid1); //5
sleep(2); //6
pthread_cond_signal(&cond);
}
sleep(10);
return 0;
}
不做注释1,2则导致child1中的unlock得不到执行,锁一直没有关闭,而child2中的锁不能执行lock,则会一直在pthread_mutex_lock()处永久等待。如果不做注释5的pthread_cancel()动作,即使没有那些sleep()延时操作,child1和child2都能正常工作。注释3和注释4的延迟使得child1有时间完成取消动作,从而使child2能在child1退出之后进入请求锁操作。如果没有注释1和注释2的回调函数定义,系统将挂起在child2请求锁的地方,因为child1没有释放锁;而如果同时也不做注释3和注释4的延时,child2能在child1完成取消动作以前得到控制,从而顺利执行申请锁的操作,但却可能挂起在pthread_cond_wait()中,因为其中也有申请mutex的操作。child1函数给出的是标准的条件变量的使用方式:回调函数保护,等待条件前锁定,pthread_cond_wait()返回后解锁。
条件变量机制和互斥锁一样,不能用于信号处理中,在信号处理函数中调用pthread_cond_signal()或者pthread_cond_broadcast()很可能引起死锁。
Example:火车售票,利用条件变量,当火车票卖完的时候,再重新设置票数为10;
#include<pthread.h>
#include<stdio.h>
int ticketcount = 10;
pthread_mutex_t lock; //互斥锁
pthread_cond_t cond; //条件变量
void* salewinds1(void* args)
{
while(1)
{
pthread_mutex_lock(&lock); //因为要访问全局的共享变量ticketcount,所以就要加锁
if(ticketcount > 0) //如果有票
{
printf("windows1 start sale ticket!the ticket is:%d\n",ticketcount);
ticketcount --;//则卖出一张票
if(ticketcount == 0)
pthread_cond_signal(&cond); //通知没有票了
printf("sale ticket finish!,the last ticket is:%d\n",ticketcount);
}
else //如果没有票了,就解锁退出
{
pthread_mutex_unlock(&lock);
break;
}
pthread_mutex_unlock(&lock);
sleep(1); //要放到锁的外面
}
}
void* salewinds2(void* args)
{
while(1)
{
pthread_mutex_lock(&lock);
if(ticketcount > 0)
{
printf("windows2 start sale ticket!the ticket is:%d\n",ticketcount);
ticketcount --;
if(ticketcount == 0)
pthread_cond_signal(&cond); //发送信号
printf("sale ticket finish!,the last ticket is:%d\n",ticketcount);
}
else
{
pthread_mutex_unlock(&lock);
break;
}
pthread_mutex_unlock(&lock);
sleep(1);
}
}
void *setticket(void *args) //重新设置票数
{
pthread_mutex_lock(&lock); //因为要访问全局变量ticketcount,所以要加锁
if(ticketcount > 0)
pthread_cond_wait(&cond,&lock); //如果有票就解锁并阻塞,直到没有票就执行下面的
ticketcount = 10; //重新设置票数为10
pthread_mutex_unlock(&lock); //解锁
sleep(1);
pthread_exit(NULL);
}
main()
{
pthread_t pthid1,pthid2,pthid3;
pthread_mutex_init(&lock,NULL); //初始化锁
pthread_cond_init(&cond,NULL); //初始化条件变量
pthread_create(&pthid1,NULL, salewinds1,NULL); //创建线程
pthread_create(&pthid2,NULL, salewinds2,NULL);
pthread_create(&pthid3,NULL, setticket,NULL);
pthread_join(pthid1,NULL); //等待子线程执行完毕
pthread_join(pthid2,NULL);
pthread_join(pthid3,NULL);
pthread_mutex_destroy(&lock); //销毁锁
pthread_cond_destroy(&cond); //销毁条件变量
}
7 信号灯
信号灯与互斥锁和条件变量的主要不同在于"灯"的概念,灯亮则意味着资源可用(即加锁),灯灭则意味着不可用(即解锁)。如果说后两种同步方式侧重于"等待"操作,即资源不可用的话,信号灯机制则侧重于点灯,即告知资源可用;没有等待线程的解锁或激发条件都是没有意义的,而没有等待灯亮的线程的点灯操作则有效,且能保持灯亮状态。当然,这样的操作原语也意味着更多的开销。
信号灯的应用除了灯亮/灯灭这种二元灯以外,也可以采用大于1的灯数,以表示资源数大于1,这时可以称之为多元灯。
1. 创建和注销
POSIX信号灯标准定义了有名信号灯和无名信号灯两种,但LinuxThreads的实现仅有无名灯,同时有名灯除了总是可用于多进程之间以外,在使用上与无名灯并没有很大的区别,因此下面仅就无名灯进行讨论。
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value); //通常pshared为0.表示线程间
这是创建信号灯的API,其中value为信号灯的初值,pshared表示是否为多进程共享而不仅仅是用于一个进程之间的多线程共享。LinuxThreads没有实现多进程共享信号灯,因此所有非0值的pshared输入都将使sem_init()返回-1,且置errno为ENOSYS。初始化好的信号灯由sem变量表征,用于以下点灯、灭灯操作。
int sem_destroy(sem_t * sem);
被注销的信号灯sem要求已没有线程在等待该信号灯,否则返回-1,且置errno为EBUSY。除此之外,LinuxThreads的信号灯注销函数不做其他动作。
2、 点灯和灭灯
int sem_post(sem_t * sem);
点灯操作将信号灯值原子地加1,表示增加一个可访问的资源。只有信号灯值大于0,才能访问公共资源。主要用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不在阻塞。
int sem_wait(sem_t * sem);
int sem_trywait(sem_t * sem);
sem_wait()为灭灯操作(等待灯亮操作),主要被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。等待灯亮(信号灯值大于0),然后将信号灯原子地减1,并返回。sem_trywait()为sem_wait()的非阻塞版,如果信号灯计数大于0,则原子地减1并返回0,否则立即返回-1,errno置为EAGAIN。
3. 获取灯值
int sem_getvalue(sem_t * sem, int * sval);
读取sem中的灯计数,存于*sval中,并返回0。
4. 其他
sem_wait()被实现为取消点,而且在支持原子"比较且交换"指令的体系结构上,sem_post()是唯一能用于异步信号处理函数的POSIX异步信号安全的API。
Example:sem_post表示点灯(资源可用,V操作), sem_wait表示灭灯(资源不可用,P操作)
#include<pthread.h>
#include<stdio.h>
#include <semaphore.h> //头文件包含
int ticketcount = 10;
sem_t lock;
void *chk1(void *args)
{
while(1)
{
sem_wait(&lock); //因为要访问全局的共享变量ticketcount,所以就要加锁
if(ticketcount > 0) //如果有票
{
printf("windows1 start sale ticket!the ticket is:%d\n",ticketcount);
ticketcount --;//则卖出一张票
sleep(3);
printf("sale ticket finish!,the last ticket is:%d\n",ticketcount);
}
else //如果没有票了,就解锁退出
{
sem_post(&lock);
break;
}
sem_post(&lock);
sleep(1); //要放到锁的外面
}
pthread_exit(NULL);
}
void *chk2(void *args)
{
while(1)
{
sem_wait(&lock); //因为要访问全局的共享变量ticketcount,所以就要加锁
if(ticketcount > 0) //如果有票
{
printf("windows2 start sale ticket!the ticket is:%d\n",ticketcount);
ticketcount --;//则卖出一张票
sleep(3);
printf("sale ticket finish!,the last ticket is:%d\n",ticketcount);
}
else //如果没有票了,就解锁退出
{
sem_post(&lock);
break;
}
sem_post(&lock);
sleep(1); //要放到锁的外面
}
pthread_exit(NULL);
}
main()
{
pthread_t pthid1,pthid2;
sem_init(&lock,0,1); //信号灯值初始为1,表示资源可用
pthread_create(&pthid1,NULL,chk1,NULL);
pthread_create(&pthid2,NULL,chk2,NULL);
pthread_join(pthid1,NULL);
pthread_join(pthid2,NULL);
sem_destroy(&lock);
}
5. 生产者消费者问题
Example1、链表实现如下:
#include <pthread.h>
#include <unistd.h>
#include<stdio.h>
#include<malloc.h>
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
struct node {
int n_number;
struct node *n_next;
} *head = NULL;
static void cleanup_handler(void *arg)
{
printf("Cleanup handler of second thread\n");
free(arg);
pthread_mutex_unlock(&mtx);
}
static void *thread_func(void *arg)//消费者
{
struct node *p = NULL;
pthread_cleanup_push(cleanup_handler, p);
while (1)
{
pthread_mutex_lock(&mtx);
while (head == NULL){ pthread_cond_wait(&cond,&mtx);}
p = head;
head = head->n_next;
printf("Got %d from front of queue\n", p->n_number);
free(p);
pthread_mutex_unlock(&mtx);
}
pthread_exit(NULL);
pthread_cleanup_pop(0); //必须放在最后一行
}
int main(void)
{
pthread_t tid;
int i;
struct node *p;
pthread_create(&tid, NULL, thread_func, NULL);
for (i = 0; i < 10; i++)//生产者
{
p = (struct node*)malloc(sizeof(struct node));
p->n_number = i;
pthread_mutex_lock(&mtx);//因为head是共享的,访问共享数据必须要加锁
p->n_next = head;
head = p;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mtx);
sleep(1);
}
printf("thread 1 wanna end the line.So cancel thread 2.\n");
pthread_cancel(tid);
pthread_join(tid, NULL);
printf("All done------exiting\n");
return 0;
}
Example2、队列实现如下:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <pthread.h>
#define BUFFER_SIZE 16 //表示一次最多可以不间断的生产16个产品
struct prodcons {
int buffer[BUFFER_SIZE]; /* 数据 */
pthread_mutex_t lock; /* 加锁 */
int readpos, writepos; /* 读pos 写位置 */
pthread_cond_t notempty; /* 不空,可以读 */
pthread_cond_t notfull; /* 不满,可以写 */
};
/* 初始化*/
void init(struct prodcons * b)
{
pthread_mutex_init(&b->lock, NULL); //初始化锁
pthread_cond_init(&b->notempty, NULL); //初始化条件变量
pthread_cond_init(&b->notfull, NULL); //初始化条件变量
b->readpos = 0; //初始化读取位置从0开始
b->writepos = 0; //初始化写入位置从0开始
}
/* 销毁操作 */
void destroy(struct prodcons *b)
{
pthread_mutex_destroy(&b->lock);
pthread_cond_destroy(&b->notempty);
pthread_cond_destroy(&b->notfull);
}
void put(struct prodcons * b, int data)//生产者
{
pthread_mutex_lock(&b->lock);
while ((b->writepos + 1) % BUFFER_SIZE == b->readpos) {//判断是不是满了
printf("wait for not full\n");
pthread_cond_wait(&b->notfull, &b->lock); //此时为满,不能生产,等待不满的信号
}
//下面表示还没有满,可以进行生产
b->buffer[b->writepos] = data;
b->writepos++; //写入点向后移一位
if (b->writepos >= BUFFER_SIZE) b->writepos = 0; //如果到达最后,就再转到开头
pthread_cond_signal(&b->notempty); //此时有东西可以消费,发送非空的信号
pthread_mutex_unlock(&b->lock);
}
int get(struct prodcons * b)//消费者
{
pthread_mutex_lock(&b->lock);
while (b->writepos == b->readpos) {//判断是不是空
printf("wait for not empty\n");
pthread_cond_wait(&b->notempty, &b->lock); //此时为空,不能消费,等待非空信号
}
//下面表示还不为空,可以进行消费
int data = b->buffer[b->readpos];
b->readpos++; //读取点向后移一位
if (b->readpos >= BUFFER_SIZE) b->readpos = 0; //如果到达最后,就再转到开头
pthread_cond_signal(&b->notfull); //此时可以进行生产,发送不满的信号
pthread_mutex_unlock(&b->lock);
return data;
}
/*--------------------------------------------------------*/
#define OVER (-1) //定义结束标志
struct prodcons buffer; //定义全局变量
/*--------------------------------------------------------*/
void * producer(void * data)
{
int n = 0;
for (; n < 50; n++) {
printf(" put-->%d\n", n);
put(&buffer, n);
}
put(&buffer, OVER);
printf("producer stopped!\n");
pthread_exit(NULL);
}
/*--------------------------------------------------------*/
void * consumer(void * data)
{
while (1) {
int d = get(&buffer);
if (d == OVER ) break;
printf(" %d-->get\n", d);
}
printf("consumer stopped!\n");
pthread_exit(NULL);
}
/*--------------------------------------------------------*/
int main(void)
{
pthread_t th_a, th_b;
init(&buffer);
pthread_create(&th_a, NULL, producer, 0);
pthread_create(&th_b, NULL, consumer, 0);
pthread_join(th_a, NULL);
pthread_join(th_b, NULL);
destroy(&buffer);
return 0;
}
8 线程的属性(选修)
pthread_create的第二个参数attr是一个结构体指针,结构中的元素分别指定新线程的运行属性,各成员属性为:
__detachstate表示新线程是否与进程中其他线程脱离同步,如果置位则新线程不能用pthread_join()来同步,且在退出时自行释放所占用的资源。缺省为PTHREAD_CREATE_JOINABLE状态。这个属性也可以在线程创建并运行以后用pthread_detach()来设置,而一旦设置为PTHREAD_CREATE_DETACH状态(不论是创建时设置还是运行时设置)则不能再恢复到PTHREAD_CREATE_JOINABLE状态。
__schedpolicy,表示新线程的调度策略,主要包括SCHED_OTHER(正常、非实时)、SCHED_RR(实时、轮转法)和SCHED_FIFO(实时、先入先出)三种,缺省为SCHED_OTHER,后两种调度策略仅对超级用户有效。运行时可以用过pthread_setschedparam()来改变。
__schedparam,一个sched_param结构,目前仅有一个sched_priority整型变量表示线程的运行优先级。这个参数仅当调度策略为实时(即SCHED_RR或SCHED_FIFO)时才有效,并可以在运行时通过pthread_setschedparam()函数来改变,缺省为0。
__inheritsched,有两种值可供选择:PTHREAD_EXPLICIT_SCHED和PTHREAD_INHERIT_SCHED,前者表示新线程使用显式指定调度策略和调度参数(即attr中的值),而后者表示继承调用者线程的值。缺省为PTHREAD_EXPLICIT_SCHED。
__scope,表示线程间竞争CPU的范围,也就是说线程优先级的有效范围。POSIX的标准中定义了两个值:PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS,前者表示与系统中所有线程一起竞争CPU时间,后者表示仅与同进程中的线程竞争CPU。目前Linux仅实现了PTHREAD_SCOPE_SYSTEM一值。
属性设置是由一些函数来完成的,通常调用pthread_attr_init函数进行初始化。设置绑定属性的函数为pthread_attr_setscope,设置分离属性的函数是pthread_attr_setdetachstate,设置线程优先级的相关函数pthread_attr_getscehdparam(获取线程优先级)和pthread_attr_setschedparam(设置线程优先级)。再设置完成属性后,调用pthread_creat函数创建线程。
·线程属性初始化:
int pthread_attr_init (pthread_attr_t *attr);
attr:传出参数,表示线程属性,后面的线程属性设置函数都会用到。
返回值:成功0,错误-1。
·设置绑定属性:
pthread_attr_setscope(pthread_attr_t *attr, init scope);
attr:线程属性
scope:PTHREAD_SCOPE_SYSTEM(绑定) PTHREAD_SCOPE_PRCESS(非绑定)
返回值:成功0,错误-1。
·设置分离属性:
pthread_attr_setdetachstate(pthread_attr_t *attr, init detachstate);
attr:线程属性
detachstate :PTHREAD_CREAT_DETACHED(分离) PTHREAD_CREAT_JOINABLE(非分离)
返回值:成功0,错误-1。
·获取线程优先级:
int pthread_attr_getschedparam(pthread_attr_attr *attr, struct sched_param *param);
attr:线程属性
param:线程优先级
返回值:成功0,错误-1。
·设置线程优先级:
int pthread_attr_setschedparam(pthread_attr_attr *attr, struct sched_param *param);
attr:线程属性
param:线程优先级
返回值:成功0,错误-1。
实例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
void *thread_function(void *arg);
char message[] = "Hello World";
int thread_finished = 0;
int main()
{
int res = 0;
pthread_t a_thread;
void *thread_result;
pthread_attr_t thread_attr; //定义属性
struct sched_param scheduling_value;
res = pthread_attr_init(&thread_attr); //属性初始化
if (res != 0)
{
perror("Attribute creation failed");
exit(EXIT_FAILURE); // EXIT_FAILURE -1
}
//设置调度策略
res = pthread_attr_setschedpolicy(&thread_attr, SCHED_OTHER);
if (res != 0)
{
perror("Setting schedpolicy failed");
exit(EXIT_FAILURE);
}
//设置脱离状态
res = pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED);
//创建线程
res = pthread_create(&a_thread, &thread_attr, thread_function, (void *)message);
if (res != 0) {
perror("Thread creation failed");
exit(EXIT_FAILURE);
}
//获取最大优先级别
int max_priority = sched_get_priority_max(SCHED_OTHER);
//获取最小优先级
int min_priority = sched_get_priority_min(SCHED_OTHER);
//重新设置优先级别
scheduling_value.sched_priority = min_priority + 5;
//设置优先级别
res = pthread_attr_setschedparam(&thread_attr, &scheduling_value);
pthread_attr_destroy(&thread_attr);
while(!thread_finished)
{
printf("Waiting for thread to say it‘s finished...\n");
sleep(1);
}
printf("Other thread finished, bye!\n");
exit(EXIT_SUCCESS);
}
void *thread_function(void *arg)
{
printf("thread_function is running. Argument was %s\n", (char *)arg);
sleep(4);
printf("Second thread setting finished flag, and exiting now\n");
thread_finished = 1;
pthread_exit(NULL);
}