首页 > 编程 > C++ > 正文

浅析C++编程当中的线程

2020-05-23 14:17:14
字体:
来源:转载
供稿:网友

这篇文章主要介绍了浅析C++编程当中的线程,线程在每一种编程语言中都是重中之重,需要的朋友可以参考下

线程的概念

C++中的线程的Text Segment和Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到。除此之外,各线程还共享以下进程资源和环境:

文件描述符

每种信号的处理方式

当前工作目录

用户id和组id

但是,有些资源是每个线程各有一份的:

线程id

上下文,包括各种寄存器的值、程序计数器和栈指针

栈空间

errno变量

信号屏蔽字

调度优先级

我们将要学习的线程库函数是由POSIX标准定义的,称为POSIX thread或pthread。

线程控制 创建线程

创建线程的函数原型如下:

 

 
  1. #include <pthread.h> 
  2. int pthread_create(pthread_t *thread, const pthread_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。

我们先来写一个简单的例子:

 

 
  1. #include <stdio.h> 
  2. #include <string.h> 
  3. #include <stdlib.h> 
  4. #include <pthread.h> 
  5. #include <unistd.h> 
  6.  
  7. pthread_t ntid; 
  8.  
  9. void printids(const void *t) 
  10. char *s = (char *)t; 
  11. pid_t pid; 
  12. pthread_t tid; 
  13.  
  14. pid = getpid(); 
  15. tid = pthread_self(); 
  16. printf("%s pid %u tid %u (0x%x)/n", s, (unsigned int)pid, 
  17. (unsigned int)tid, (unsigned int)tid); 
  18.  
  19. void *thr_fn(void *arg) 
  20. printids(arg); 
  21. return NULL; 
  22.  
  23. int main(void
  24. int err; 
  25.  
  26. err = pthread_create(&ntid, NULL, thr_fn, (void *)"Child Process:"); 
  27. if (err != 0) { 
  28. fprintf(stderr, "can't create thread: %s/n", strerror(err)); 
  29. exit(1); 
  30. printids("main thread:"); 
  31. sleep(1); 
  32.  
  33. return 0; 

编译执行结果如下:

 

 
  1. g++ thread.cpp -o thread -lpthread 
  2. ./thread 
  3. main thread: pid 21046 tid 3612727104 (0xd755d740) 
  4. Child Process: pid 21046 tid 3604444928 (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的用法。

 

 
  1. #include <pthread.h> 
  2.  
  3. void pthread_exit(void *value_ptr); 

value_ptr是void*类型,和线程函数返回值的用法一样,其它线程可以调用pthread_join获取这个指针。

需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

 

 
  1. #include <pthread.h> 
  2.  
  3. int pthread_join(pthread_t thread, 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参数。参考代码如下:

 

 
  1. #include <stdio.h> 
  2. #include <stdlib.h> 
  3. #include <pthread.h> 
  4. #include <unistd.h> 
  5.  
  6. void* thread_function_1(void *arg) 
  7. printf("thread 1 running/n"); 
  8. return (void *)1; 
  9.  
  10. void* thread_function_2(void *arg) 
  11. printf("thread 2 exiting/n"); 
  12. pthread_exit((void *) 2); 
  13.  
  14. void* thread_function_3(void* arg) 
  15. while (1) { 
  16. printf("thread 3 writeing/n"); 
  17. sleep(1); 
  18.  
  19.  
  20. int main(void
  21. pthread_t tid; 
  22. void *tret; 
  23.  
  24. pthread_create(&tid, NULL, thread_function_1, NULL); 
  25. pthread_join(tid, &tret); 
  26. printf("thread 1 exit code %d/n", *((int*) (&tret))); 
  27.  
  28. pthread_create(&tid, NULL, thread_function_2, NULL); 
  29. pthread_join(tid, &tret); 
  30. printf("thread 2 exit code %d/n", *((int*) (&tret))); 
  31.  
  32. pthread_create(&tid, NULL, thread_function_3, NULL); 
  33. sleep(3); 
  34. pthread_cancel(tid); 
  35. pthread_join(tid, &tret); 
  36. printf("thread 3 exit code %d/n", *((int*) (&tret))); 
  37.  
  38. return 0; 

运行结果是:

 

 
  1. thread 1 running 
  2. thread 1 exit code 1 
  3. thread 2 exiting 
  4. thread 2 exit code 2 
  5. thread 3 writeing 
  6. thread 3 writeing 
  7. thread 3 writeing 
  8. thread 3 exit code -1 

可见,Linux的pthread库中常数PTHREAD_CANCELED的值是-1.可以在头文件pthread.h中找到它的定义:

 

  
  1. #define PTHREAD_CANCELED ((void *) -1) 

线程间同步

多个线程同时访问共享数据时可能会冲突,例如两个线程都要把某个全局变量增加1,这个操作在某平台上需要三条指令才能完成:

从内存读变量值到寄存器。

寄存器值加1.

将寄存器的值写回到内存。

这个时候很容易出现两个进程同时操作寄存器变量值的情况,导致最终结果不正确。

解决的办法是引入互斥锁(Mutex, Mutual Exclusive Lock),获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据,这样,“读-修改-写”的三步操作组成一个原子操作,要不都执行,要不都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。

Mutex用pthread_mutex_t类型的变量表示,可以这样初始化和销毁:

 

  1. #include <pthread.h> 
  2.  
  3. int pthread_mutex_destory(pthread_mutex_t *mutex); 
  4. int pthread_mutex_int(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); 
  5. pthread_mutex_t mutex = PTHEAD_MUTEX_INITIALIZER; 

返回值:成功返回0,失败返回错误号。

用pthread_mutex_init函数初始化的Mutex可以用pthread_mutex_destroy销毁。如果Mutex变量是静态分配的(全局变量或static变量),也可以用宏定义PTHREAD_MUTEX_INITIALIZER来初始化,相当于用pthread_mutex_init初始化并且attr参数为NULL。Mutex的加锁和解锁操作可以用下列函数:

 

 
  1. #include <pthread.h> 
  2.  
  3. int pthread_mutex_lock(pthread_mutex_t *mutex); 
  4. int pthread_mutex_trylock(pthread_mutex_t *mutex); 
  5. int pthread_mutex_unlock(pthread_mutex_t *mutex); 

返回值:成功返回0,失败返回错误号。

一个线程可以调用pthread_mutex_lock获得Mutex,如果这时另一个线程已经调用pthread_mutex_lock获得了该Mutex,则当前线程需要挂起等待,直到另一个线程调用pthread_mutex_unlock释放Mutex,当前线程被唤醒,才能获得该Mutex并继续执行。

我们用Mutex解决上面说的两个线程同时对全局变量+1可能导致紊乱的问题:

 

 
  1. #include <pthread.h> 
  2. #include <stdio.h> 
  3. #include <stdlib.h> 
  4.  
  5. #define NLOOP 5000 
  6.  
  7. int counter; 
  8. pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER; 
  9.  
  10. void *do_add_process(void *vptr) 
  11. int i, val; 
  12.  
  13. for (i = 0; i < NLOOP; i ++) { 
  14. pthread_mutex_lock(&counter_mutex); 
  15. val = counter; 
  16. printf("%x:%d/n", (unsigned int)pthread_self(), val + 1); 
  17. counter = val + 1; 
  18. pthread_mutex_unlock(&counter_mutex); 
  19.  
  20. return NULL; 
  21.  
  22. int main() 
  23. pthread_t tida, tidb; 
  24.  
  25. pthread_create(&tida, NULL, do_add_process, NULL); 
  26. pthread_create(&tidb, NULL, do_add_process, NULL); 
  27.  
  28. pthread_join(tida, NULL); 
  29. pthread_join(tidb, NULL); 
  30.  
  31. return 0; 

这样,每次运行都能显示到10000。如果去掉锁机制,可能就会有问题。这个机制类似于Java的synchronized块机制。

Condition Variable

线程间的同步还有这样一种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,就唤醒线程A继续执行。在pthread库中通过条件变量(Conditiion Variable)来阻塞等待一个条件,或者唤醒等待这个条件的线程。Condition Variable用pthread_cond_t类型的变量表示,可以这样初始化和销毁:

 

 
  1. #include <pthread.h> 
  2.  
  3. int pthread_cond_destory(pthread_cond_t *cond); 
  4. int pthread_cond_init(pthead_cond_t *cond, const pthread_condattr_t *attr); 
  5. pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 

返回值:成功返回0,失败返回错误号。

和Mutex的初始化和销毁类似,pthread_cond_init函数初始化一个Condition Variable,attr参数为NULL则表示缺省属性,pthread_cond_destroy函数销毁一个Condition Variable。如果Condition Variable是静态分配的,也可以用宏定义PTHEAD_COND_INITIALIZER初始化,相当于用pthread_cond_init函数初始化并且attr参数为NULL。Condition Variable的操作可以用下列函数:

 

 
  1. #include <pthread.h> 
  2.  
  3. int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime); 
  4. int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); 
  5. int pthread_cond_broadcast(pthread_cond_t *cond); 
  6. int pthread_cond_signal(pthread_cond_t *cond); 

可见,一个Condition Variable总是和一个Mutex搭配使用的。一个线程可以调用pthread_cond_wait在一个Condition Variable上阻塞等待,这个函数做以下三步操作:

释放Mutex。

阻塞等待。

当被唤醒时,重新获得Mutex并返回。

pthread_cond_timedwait函数还有一个额外的参数可以设定等待超时,如果到达了abstime所指定的时刻仍然没有别的线程来唤醒当前线程,就返回ETIMEDOUT。一个线程可以调用pthread_cond_signal唤醒在某个Condition Variable上等待的另一个线程,也可以调用pthread_cond_broadcast唤醒在这个Condition Variable上等待的所有线程。

下面的程序演示了一个生产者-消费者的例子,生产者生产一个结构体串在链表的表头上,消费者从表头取走结构体。

 

  1. #include <stdio.h> 
  2. #include <stdlib.h> 
  3. #include <pthread.h> 
  4. #include <unistd.h> 
  5.  
  6. struct msg { 
  7. struct msg *next; 
  8. int num; 
  9. }; 
  10.  
  11. struct msg *head; 
  12. pthread_cond_t has_product = PTHREAD_COND_INITIALIZER; 
  13. pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; 
  14.  
  15. void* consumer(void *p) 
  16. struct msg *mp; 
  17.  
  18. for(;;) { 
  19. pthread_mutex_lock(&lock); 
  20. while (head == NULL) { 
  21. pthread_cond_wait(&has_product, &lock); 
  22. mp = head; 
  23. head = mp->next; 
  24. pthread_mutex_unlock(&lock); 
  25. printf("Consume %d/n", mp->num); 
  26. free(mp); 
  27. sleep(rand() % 5); 
  28.  
  29. void* producer(void *p) 
  30. struct msg *mp; 
  31.  
  32. for(;;) { 
  33. mp = (struct msg *)malloc(sizeof(*mp)); 
  34. pthread_mutex_lock(&lock); 
  35. mp->next = head; 
  36. mp->num = rand() % 1000; 
  37. head = mp; 
  38. printf("Product %d/n", mp->num); 
  39. pthread_mutex_unlock(&lock); 
  40. pthread_cond_signal(&has_product); 
  41. sleep(rand() % 5); 
  42.  
  43. int main() 
  44. pthread_t pid, cid; 
  45. srand(time(NULL)); 
  46.  
  47. pthread_create(&pid, NULL, producer, NULL); 
  48. pthread_create(&cid, NULL, consumer, NULL); 
  49.  
  50. pthread_join(pid, NULL); 
  51. pthread_join(cid, NULL); 
  52.  
  53. return 0; 
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表