HeadFirstC笔记_12 线程:平行世界

来源:互联网 发布:免费网络视频会议系统 编辑:程序博客网 时间:2024/04/29 04:30
难道每当想要同时做几件事时都得创建进程吗?
不见得,有以下几个原因:
1.创建进程要花时间
有的机器新建进程只要花一丁点时间。虽然时间很短,但还是需要时间。如果你想要执行的任务才用几十毫秒,每次都创建进程就很低效。
2.共享数据不方便
当创建子进程时,子进程会自动包含父进程所有数据的副本。但这些只是副本,如果子进程想把数据发回父进程,就需要借助管道之类的东西。
3.进程真的很难
创建进程需要写很多代码,这会让代码又乱又长。

你需要线程
普通进程一次只做一件事
到目前为止,你写过的所有程序都是单线程,这就好比进程中只有一个人在干活。

多雇几名员工:使用线程
你可以雇多名员工在店里干活,同样,也可以在一个进程
中使用多个线程。所有线程能访问同一段堆存储器,读
写同一个文件,使用同一个网络套接字进行通信。当一个
线程修改了某个全局变量,其他线程马上就能看到。
也就是说,可以为每个线程都分配一个独立的任务,让这
些线程同时执行。

如何创建线程
你可以使用很多线程库,这里我们将使用最流行的一
种:POSIX线程库,也叫 pthread 。可以在Cygwin、Linux
和Mac上使用 pthread 。
假设你想在独立的线程中运行这个函数:
  1. void* does_not(void *a)
  2. {
  3. int i = 0;
  4. for (i = 0; i < 5; i++) {
  5. sleep(1);
  6. puts("Does not!");
  7. }
  8. return NULL;
  9. }
注意:void指针可以指向存储器中任何类型的数据,线程函数的返回类型必须是 void* !

用pthread_create创建线程
每个线程都需
要把信息保存在一个叫 pthread_t 的数据结构中,然后就可以用
pthread_create() 创建并运行线程。
  1. #include <pthread.h>
  2. ...
  3. pthread_t t0; //它保存了线程的所有信息
  4. pthread_create(&t0, NULL, does_not, NULL);
代码将以独立线程运行这两个函数。还没完,如果程序运行完这段代
码就结束了,线程也会随之灭亡,因此必须等待线程结束:
  1. // 函数返回的void指针会保存在这里
  2. void* result;
  3. //pthread_join()函数会等待线程结束。
  4. pthread_join(t0,&result);
pthread_join() 会接收线程函数的返回值,并把它保存在一个void 指针变量中。一旦两个线程都结束了,程序就可以顺利退出了。
代码示例:
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <unistd.h>
  5. #include <errno.h>
  6. #include <pthread.h>
  7. // 打印错误的函数
  8. void error(char *msg) {
  9. fprintf(stderr, "%s: %s\n", msg, strerror(errno));
  10. exit(1);
  11. }
  12. void* does_not(void *a) {
  13. int i = 0;
  14. for (i = 0; i < 5; i++) {
  15. sleep(1);
  16. puts("Does not!");
  17. }
  18. return NULL;
  19. }
  20. void* does_too(void *a) {
  21. int i = 0;
  22. for (i = 0; i < 5; i++) {
  23. sleep(1);
  24. puts("Does too!");
  25. }
  26. return NULL;
  27. }
  28. int main() {
  29. pthread_t t0; //它保存了线程的所有信息
  30. pthread_t t1;
  31. // 创建线程
  32. if (pthread_create(&t0, NULL, does_not, NULL) == -1) //does_not是线程将运行的函数名
  33. error("无法创建线程t0"); //每次都应该检查错误。
  34. if (pthread_create(&t1, NULL, does_too, NULL) == -1) //does_not是线程将运行的函数名
  35. error("无法创建线程t1"); //每次都应该检查错误。
  36. // 函数返回的void指针会保存在这里
  37. void* result;
  38. //pthread_join()函数会等待线程结束。
  39. if(pthread_join(t0,&result)==-1)
  40. error("无法回收线程t0");
  41. if(pthread_join(t1,&result)==-1)
  42. error("无法回收线程t1");
  43. return 0;
  44. }
运行结果:

 如果不能编译,可以在编译时使用 pthread 库:
  1. 命令:gcc dchapter12.c -lpthread -o dchapter12

练习:
派对开始了,倒计数啤酒瓶数。下面这段代码运行了20个线程,总共有200万瓶啤酒。看看你能否找到丢失的代码,搞定以后干杯庆祝一下。

代码示例:
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <unistd.h>
  5. #include <errno.h>
  6. #include <pthread.h>
  7. // 打印错误的函数
  8. void error(char *msg) {
  9. fprintf(stderr, "%s: %s\n", msg, strerror(errno));
  10. exit(1);
  11. }
  12. int beers = 2000000;
  13. void* drink_lots(void* param) {
  14. int i;
  15. for (i = 0; i < 100000; i++) {
  16. beers = beers - 1;
  17. }
  18. return NULL;
  19. }
  20. int main() {
  21. pthread_t threads[20];
  22. int t;
  23. printf("%i bottles of beer on the wall\n%i bottles of beer\n", beers,
  24. beers);
  25. // 创建20个线程来运行该函数
  26. for (t = 0; t < 20; t++) {
  27. //if (pthread_create(&threads[t], NULL, drink_lots, (void*)thread_no) == -1)
  28. if (pthread_create(&threads[t], NULL, drink_lots, NULL) == -1)
  29. error("无法创建线程");
  30. }
  31. // 等待所有线程结束
  32. void* result;
  33. for (t = 0; t < 20; t++) {
  34. if (pthread_join(threads[t], &result) == -1)
  35. error("无法回收线程");
  36. }
  37. printf("There are now %i bottles of beer on the wall\n", beers);
  38. return 0;
  39. }
运行结果:

 结果显示,大多数情况下,代码中两个线程并没有把beers变量减为0!---原因就是:

线程不安全
多个线程同时访问同一个数据,就会导致线程安全问题,数据就会出错!

增设红绿灯---互斥锁
假设两辆车想要驶过一段羊肠小道。为了防止交通事故,你可以增设红绿灯,它可以防止两辆车同时访问共享资源。
如果想防止两个或多个线程访问共享数据资源,也可以采取相同的方法:增设红绿灯。这样两个线程就不能同时读取相同数据,并把它写回。
用来防止线程发生车祸的红绿灯就叫互斥锁,它们是把代码变为线程安全最简单的方法。

用互斥锁来管理交通
  1. // 创建互斥锁
  2. pthread_mutex_t a_lock = PTHREAD_MUTEX_INITIALIZER;
互斥锁必须对所有可能发生冲突的线程可见,也就是说它是一个全局变量
PTHREAD_MUTEX_INITIALIZER 实际上是一个,当编译器看到它,就会插入创建互斥锁的代码。
  1. pthread_mutex_lock(&a_lock);
  2. /* 含有共享数据的代码从这里开始 */
  3. ....这里的代码是线程安全的
  4. /* ... 代码结束了 */
  5. pthread_mutex_unlock(&a_lock);

给线程编号
线程函数可以接收一个void指针作为参数,并返回一个void指针
值。通常你希望把某个整型值传给线程,并让它返回某个整型值,
一种方法是用 long ,因为它的大小和void指针相同,可以把它保
存在void指针变量中。
  1. void* do_stuff(void* param)
  2. {
  3. long thread_no = (long)param; // 线程编号
  4. printf("Thread number %ld\n", thread_no);
  5. return (void*)(thread_no + 1);
  6. }

用互斥锁来修改上面的beers.c代码:
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <unistd.h>
  5. #include <errno.h>
  6. #include <pthread.h>
  7. // 打印错误的函数
  8. void error(char *msg) {
  9. fprintf(stderr, "%s: %s\n", msg, strerror(errno));
  10. exit(1);
  11. }
  12. // 创建互斥锁
  13. pthread_mutex_t a_lock = PTHREAD_MUTEX_INITIALIZER;
  14. int beers = 2000000;
  15. void* drink_lots(void* param) {
  16. // 线程函数可以接收一个void指针类型的参数,这里将参数转化为long类型
  17. long thread_no = (long) param;
  18. int i;
  19. for (i = 0; i < 100000; i++) {
  20. pthread_mutex_lock(&a_lock);
  21. /* 含有共享数据的代码从这里开始 */
  22. beers = beers - 1;
  23. /* ... 同步代码结束了 */
  24. pthread_mutex_unlock(&a_lock);
  25. // 现在线程安全了
  26. }
  27. // 打印线程号
  28. //printf("Thread number %ld :", thread_no);
  29. printf("beers = %i\n", beers);
  30. return NULL;
  31. // 返回时将其类型转化为void指针
  32. //return (void*)(thread_no + 1);
  33. }
  34. int main() {
  35. pthread_t threads[20];
  36. printf("%i bottles of beer on the wall\n%i bottles of beer\n", beers,
  37. beers);
  38. // 创建20个线程来运行该函数
  39. int t;
  40. for (t = 0; t < 20; t++) {
  41. //if (pthread_create(&threads[t], NULL, drink_lots, NULL) == -1)
  42. if (pthread_create(&threads[t], NULL, drink_lots, (void*) t) == -1)
  43. error("无法创建线程");
  44. }
  45. // 等待所有线程结束
  46. void* result;
  47. for (t = 0; t < 20; t++) {
  48. if (pthread_join(threads[t], &result) == -1)
  49. error("无法回收线程");
  50. //printf("Thread %ld returned %ld\n", t, (long)result);
  51. }
  52. printf("There are now %i bottles of beer on the wall\n", beers);
  53. return 0;
  54. }
运行结果:

 可以看到,这下线程安全了!

问: 怎样设计高效的多线程程序?
答: 减少线程需要访问的共享数据的数量。如果线程无需访问很多共享数据,那么多个线程等一个线程的情况就很少出现,速度会大大提高。

问: 线程要比进程快?
答: 通常是这样,因为创建进程要比创建线程花更多时间。

问: 听说互斥锁会引发“死锁”,那是什么玩意儿?
答: 假设你有两个线程,它们都想得到互斥锁A和B。倘若第一个线程得到了A,第二个线程得到了B,这两个线程就会陷入死锁。因为第一个线程无法得到B,第二个线程无法得到A,它俩都停滞不前。


0 0
原创粉丝点击