浅析C++编程当中的线程
线程的概念
C++中的线程的TextSegment和DataSegment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到。除此之外,各线程还共享以下进程资源和环境:
- 文件描述符
- 每种信号的处理方式
- 当前工作目录
- 用户id和组id
但是,有些资源是每个线程各有一份的:
- 线程id
- 上下文,包括各种寄存器的值、程序计数器和栈指针
- 栈空间
- errno变量
- 信号屏蔽字
- 调度优先级
我们将要学习的线程库函数是由POSIX标准定义的,称为POSIXthread或pthread。
线程控制
创建线程
创建线程的函数原型如下:
#include<pthread.h> intpthread_create(pthread_t*thread,constpthread_attr_t*attr,void*(*start_routine)(void*),void*arg);
返回值:成功返回0,失败返回错误号。
在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。start_routine函数接收一个参数,是通过pthread_create的arg参数传递给它的,该参数类型为void*,这个指针按什么类型解释由调用者自己定义。start_routine的返回值类型也是void*,这个指针的含义同样由调用者自己定义。start_routine返回时,这个线程就退出了,其它线程可以调用pthread_join得到start_routine的返回值。
pthread_create成功返回后,新创建的线程的id被填写到thread参数所指向的内存单元。我们知道进程id的类型是pid_t,每个进程的id在整个系统中是唯一的,调用getpid可以得到当前进程的id,是一个正整数值。线程id的类型是thread_t,它只在当前进程中保证是唯一的,在不同的系统中thread_t这个类型有不同的实现,它可能是一个整数值,也可能是一个结构体,也可能是一个地址,所以不能简单的当成整数用printf打印,调用pthread_self可以获取当前线程的id。
我们先来写一个简单的例子:
#include<stdio.h> #include<string.h> #include<stdlib.h> #include<pthread.h> #include<unistd.h> pthread_tntid; voidprintids(constvoid*t) { char*s=(char*)t; pid_tpid; pthread_ttid; pid=getpid(); tid=pthread_self(); printf("%spid%utid%u(0x%x)\n",s,(unsignedint)pid, (unsignedint)tid,(unsignedint)tid); } void*thr_fn(void*arg) { printids(arg); returnNULL; } intmain(void) { interr; err=pthread_create(&ntid,NULL,thr_fn,(void*)"ChildProcess:"); if(err!=0){ fprintf(stderr,"can'tcreatethread:%s\n",strerror(err)); exit(1); } printids("mainthread:"); sleep(1); return0; }
编译执行结果如下:
g++thread.cpp-othread-lpthread ./thread mainthread:pid21046tid3612727104(0xd755d740) ChildProcess:pid21046tid3604444928(0xd6d77700)
从结果可以知道,thread_t类型是一个地址值,属于同一进程的多个线程调用getpid可以得到相同的进程号,而调用pthread_self得到的线程号各不相同。
如果任意一个线程调用了exit或_exit,则整个进程的所有线程都终止,由于从main函数return也相当于调用exit,为了防止新创建的线程还没有得到执行就终止,我们在main函数return之前延时1秒,这只是一种权宜之计,即使主线程等待1秒,内核也不一定会调度新创建的线程执行,接下来,我们学习一下比较好的解决方法。
终止线程
如果需要只终止某个线程而不是终止整个进程,可以有三种方法:
- 从线程函数return。这种方法对主线程不适应,从main函数return相当于调用exit。
- 一个线程可以调用pthread_cancel终止同一个进程中的另一个线程。
- 线程可以调用pthread_exit终止自己。
这里主要介绍pthread_exit和pthread_join的用法。
#include<pthread.h> voidpthread_exit(void*value_ptr);
value_ptr是void*类型,和线程函数返回值的用法一样,其它线程可以调用pthread_join获取这个指针。
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
#include<pthread.h> intpthread_join(pthread_tthread,void**value_ptr);
返回值:成功返回0,失败返回错误号。
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
- 如果thread线程通过return返回,value_ptr所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_cancel异常终止掉,value_ptr所指向的单元存放的是常数PTHREAD_CANCELED。
- 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
如果对thread线程的终止状态不感兴趣,可以传NULL给value_ptr参数。参考代码如下:
#include<stdio.h> #include<stdlib.h> #include<pthread.h> #include<unistd.h> void*thread_function_1(void*arg) { printf("thread1running\n"); return(void*)1; } void*thread_function_2(void*arg) { printf("thread2exiting\n"); pthread_exit((void*)2); } void*thread_function_3(void*arg) { while(1){ printf("thread3writeing\n"); sleep(1); } } intmain(void) { pthread_ttid; void*tret; pthread_create(&tid,NULL,thread_function_1,NULL); pthread_join(tid,&tret); printf("thread1exitcode%d\n",*((int*)(&tret))); pthread_create(&tid,NULL,thread_function_2,NULL); pthread_join(tid,&tret); printf("thread2exitcode%d\n",*((int*)(&tret))); pthread_create(&tid,NULL,thread_function_3,NULL); sleep(3); pthread_cancel(tid); pthread_join(tid,&tret); printf("thread3exitcode%d\n",*((int*)(&tret))); return0; }
运行结果是:
thread1running thread1exitcode1 thread2exiting thread2exitcode2 thread3writeing thread3writeing thread3writeing thread3exitcode-1
可见,Linux的pthread库中常数PTHREAD_CANCELED的值是-1.可以在头文件pthread.h中找到它的定义:
#definePTHREAD_CANCELED((void*)-1)
线程间同步
多个线程同时访问共享数据时可能会冲突,例如两个线程都要把某个全局变量增加1,这个操作在某平台上需要三条指令才能完成:
- 从内存读变量值到寄存器。
- 寄存器值加1.
- 将寄存器的值写回到内存。
这个时候很容易出现两个进程同时操作寄存器变量值的情况,导致最终结果不正确。
解决的办法是引入互斥锁(Mutex,MutualExclusiveLock),获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据,这样,“读-修改-写”的三步操作组成一个原子操作,要不都执行,要不都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。
Mutex用pthread_mutex_t类型的变量表示,可以这样初始化和销毁:
#include<pthread.h> intpthread_mutex_destory(pthread_mutex_t*mutex); intpthread_mutex_int(pthread_mutex_t*mutex,constpthread_mutexattr_t*attr); pthread_mutex_tmutex=PTHEAD_MUTEX_INITIALIZER;
返回值:成功返回0,失败返回错误号。
用pthread_mutex_init函数初始化的Mutex可以用pthread_mutex_destroy销毁。如果Mutex变量是静态分配的(全局变量或static变量),也可以用宏定义PTHREAD_MUTEX_INITIALIZER来初始化,相当于用pthread_mutex_init初始化并且attr参数为NULL。Mutex的加锁和解锁操作可以用下列函数:
#include<pthread.h> intpthread_mutex_lock(pthread_mutex_t*mutex); intpthread_mutex_trylock(pthread_mutex_t*mutex); intpthread_mutex_unlock(pthread_mutex_t*mutex);
返回值:成功返回0,失败返回错误号。
一个线程可以调用pthread_mutex_lock获得Mutex,如果这时另一个线程已经调用pthread_mutex_lock获得了该Mutex,则当前线程需要挂起等待,直到另一个线程调用pthread_mutex_unlock释放Mutex,当前线程被唤醒,才能获得该Mutex并继续执行。
我们用Mutex解决上面说的两个线程同时对全局变量+1可能导致紊乱的问题:
#include<pthread.h> #include<stdio.h> #include<stdlib.h> #defineNLOOP5000 intcounter; pthread_mutex_tcounter_mutex=PTHREAD_MUTEX_INITIALIZER; void*do_add_process(void*vptr) { inti,val; for(i=0;i<NLOOP;i++){ pthread_mutex_lock(&counter_mutex); val=counter; printf("%x:%d\n",(unsignedint)pthread_self(),val+1); counter=val+1; pthread_mutex_unlock(&counter_mutex); } returnNULL; } intmain() { pthread_ttida,tidb; pthread_create(&tida,NULL,do_add_process,NULL); pthread_create(&tidb,NULL,do_add_process,NULL); pthread_join(tida,NULL); pthread_join(tidb,NULL); return0; }
这样,每次运行都能显示到10000。如果去掉锁机制,可能就会有问题。这个机制类似于Java的synchronized块机制。
ConditionVariable
线程间的同步还有这样一种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,就唤醒线程A继续执行。在pthread库中通过条件变量(ConditiionVariable)来阻塞等待一个条件,或者唤醒等待这个条件的线程。ConditionVariable用pthread_cond_t类型的变量表示,可以这样初始化和销毁:
#include<pthread.h> intpthread_cond_destory(pthread_cond_t*cond); intpthread_cond_init(pthead_cond_t*cond,constpthread_condattr_t*attr); pthread_cond_tcond=PTHREAD_COND_INITIALIZER;
返回值:成功返回0,失败返回错误号。
和Mutex的初始化和销毁类似,pthread_cond_init函数初始化一个ConditionVariable,attr参数为NULL则表示缺省属性,pthread_cond_destroy函数销毁一个ConditionVariable。如果ConditionVariable是静态分配的,也可以用宏定义PTHEAD_COND_INITIALIZER初始化,相当于用pthread_cond_init函数初始化并且attr参数为NULL。ConditionVariable的操作可以用下列函数:
#include<pthread.h> intpthread_cond_timedwait(pthread_cond_t*cond,pthread_mutex_t*mutex,conststructtimespec*abstime); intpthread_cond_wait(pthread_cond_t*cond,pthread_mutex_t*mutex); intpthread_cond_broadcast(pthread_cond_t*cond); intpthread_cond_signal(pthread_cond_t*cond);
可见,一个ConditionVariable总是和一个Mutex搭配使用的。一个线程可以调用pthread_cond_wait在一个ConditionVariable上阻塞等待,这个函数做以下三步操作:
- 释放Mutex。
- 阻塞等待。
- 当被唤醒时,重新获得Mutex并返回。
pthread_cond_timedwait函数还有一个额外的参数可以设定等待超时,如果到达了abstime所指定的时刻仍然没有别的线程来唤醒当前线程,就返回ETIMEDOUT。一个线程可以调用pthread_cond_signal唤醒在某个ConditionVariable上等待的另一个线程,也可以调用pthread_cond_broadcast唤醒在这个ConditionVariable上等待的所有线程。
下面的程序演示了一个生产者-消费者的例子,生产者生产一个结构体串在链表的表头上,消费者从表头取走结构体。
#include<stdio.h> #include<stdlib.h> #include<pthread.h> #include<unistd.h> structmsg{ structmsg*next; intnum; }; structmsg*head; pthread_cond_thas_product=PTHREAD_COND_INITIALIZER; pthread_mutex_tlock=PTHREAD_MUTEX_INITIALIZER; void*consumer(void*p) { structmsg*mp; for(;;){ pthread_mutex_lock(&lock); while(head==NULL){ pthread_cond_wait(&has_product,&lock); } mp=head; head=mp->next; pthread_mutex_unlock(&lock); printf("Consume%d\n",mp->num); free(mp); sleep(rand()%5); } } void*producer(void*p) { structmsg*mp; for(;;){ mp=(structmsg*)malloc(sizeof(*mp)); pthread_mutex_lock(&lock); mp->next=head; mp->num=rand()%1000; head=mp; printf("Product%d\n",mp->num); pthread_mutex_unlock(&lock); pthread_cond_signal(&has_product); sleep(rand()%5); } } intmain() { pthread_tpid,cid; srand(time(NULL)); pthread_create(&pid,NULL,producer,NULL); pthread_create(&cid,NULL,consumer,NULL); pthread_join(pid,NULL); pthread_join(cid,NULL); return0; }