8.1 线程概念

一个程序如何同时(宏观)完成多个任务?

  • 使用fork和exec同时运行多个程序
  • 使用线程在同一个程序中同时运行多个程序

一个进程至少包含一个线程,此外可以创建多个线程,线程之间彼此平等,称原来的线程为主线程。

image.png

进程是资源分配的最小单位,而线程是计算机中独立运行、CPU调度的最小单元。(只携带一个栈)
同一进程中的线程共享整个进程空间。
线程有时也被称为轻量级进程。

多进程与多线程的选择:

  • 需要频繁创建和销毁,选择多线程
  • 需要大量计算,选择多线程
  • 在通信方面,线程间通信更加方便高效,并发处理间的相关性比较强的,选择多线程,可提高应用程序响应速度
  • 对可靠性有一定要求,多进程更加安全可靠,因为多线程共享进程的地址空间,对资源进行同步互斥访问容易出现错误
  • 若编程与调试都相对较简单的程序,选择多进程。

image.png

  1. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/1054933/1636462357078-bea18d0c-f501-4ecb-9891-26ee4578e78f.png#clientId=ub89e08c0-1aff-4&from=paste&height=452&id=u4ec32156&margin=%5Bobject%20Object%5D&name=image.png&originHeight=602&originWidth=1120&originalType=binary&ratio=1&size=73993&status=done&style=none&taskId=ud06fae8e-3926-43ba-89df-75192ca6c32&width=840)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/1054933/1636463548773-7360ef3c-d995-4b74-9ef8-8ecf39377282.png#clientId=ub89e08c0-1aff-4&from=paste&height=307&id=uf2199895&margin=%5Bobject%20Object%5D&name=image.png&originHeight=614&originWidth=1076&originalType=binary&ratio=1&size=62655&status=done&style=none&taskId=u244dc93f-e4e5-4bd8-b561-af1713bed82&width=538)<br />linux系统内核级的线程实现机制符合POSIX(Portable Operating System Interface of UNIX/LINUX,可移植的操作系统接口)规范,对于用户级的多线程编程接口也遵循POSIX标准,称为Pthread(POSIX Thread)。<br />linux采用Pthread线程库实现对线程的访问与控制。linux下编写多线程应用程序需要使用头文件pthread.h,链接时也需要库libpthread.a,所以编译时需要给出-lpthread选项。<br />gcc -o thread thread.c -lpthread<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/1054933/1636463572258-0edbdc11-c45d-4311-bec9-6f2094852c54.png#clientId=ub89e08c0-1aff-4&from=paste&height=315&id=u24a3d99b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=630&originWidth=980&originalType=binary&ratio=1&size=85458&status=done&style=none&taskId=uff9a9d65-df19-4e8e-8d17-fe8930960ba&width=490)

8.2 线程基本操作

8.2.1 线程创建

  1. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/1054933/1636463752364-ce8fdcf5-97e6-47bb-aaaf-564385c383e3.png#clientId=ub89e08c0-1aff-4&from=paste&height=350&id=u4de18bfc&margin=%5Bobject%20Object%5D&name=image.png&originHeight=699&originWidth=1073&originalType=binary&ratio=1&size=98410&status=done&style=none&taskId=u734927b6-dae2-4527-a17a-a708140c4a4&width=536.5)<br />start_routine:线程创建后调用的函数,也称线程函数的起始地址。

image.png

8.2.2 线程的退出与等待

线程结束方式有三种:正常结束;中途退出(pthread_exit);被其他线程强制退出(pthread_cancel)

  1. //thread_create.c
  2. //线程创建
  3. #include <pthread.h>
  4. #include <stdio.h>
  5. #include <stdlib.h>
  6. void *thread_fun();
  7. int main(int argc, char **argv)
  8. {
  9. int rtn;
  10. pthread_t thread_id;
  11. rtn = pthread_create(&thread_id, NULL, &thread_fun, NULL);
  12. if(rtn != 0)
  13. {
  14. perror("pthread_create error !");
  15. exit(1);
  16. }
  17. sleep(1);//给创建的子进程执行时间1s,也是让主线程休息1s,等待新创建的线程结束后再退出;
  18. //否则主线程返回或调用exit()退出,整个进程将会终止,进程中所有线程也会终止。
  19. return 0;
  20. }
  21. void *thread_fun()
  22. {
  23. pthread_t new_thid;
  24. new_thid = pthread_self();
  25. printf("This is a new thread, thread ID is %u\n", new_thid);
  26. printf("-----end-----\n");
  27. }

线程可隐式退出(执行函数结束),也可显式调用pthread_exit();
void pthread_exit(void *value_ptr);
参数:指向返回状态的指针,可置为NULL。

线程内调用pthread_exit与exit的区别:

  • pthread_exit结束当前主线程,进程不会结束,进程中的其他线程也不会终止
  • exit结束当前进程,当前进程内的其他线程也被结束

有时一个线程为了等待某个线程执行结束,需要使用pthread_join挂起当前线程等待另一个线程结束。
默认线程退出后资源不主动释放,需要调用pthread_join等待并释放资源。
image.png

  1. //thread_join.c
  2. //线程等待
  3. #include <pthread.h>
  4. #include <stdio.h>
  5. #include <stdlib.h>
  6. void *thread_fun(void *ptr);
  7. int main(int argc, char **argv)
  8. {
  9. int rtn1, rtn2;
  10. pthread_t thread_id1;
  11. pthread_t thread_id2;
  12. char *message1 = "new_thread1";
  13. char *message2 = "new_thread2";
  14. rtn1 = pthread_create(&thread_id1, NULL, &thread_fun, (void*)message1);
  15. if(rtn1 != 0)
  16. {
  17. perror("pthread_create error !");
  18. exit(1);
  19. }
  20. rtn2 = pthread_create(&thread_id2, NULL, (void*)thread_fun, (void*)message2);
  21. if(rtn2 != 0)
  22. {
  23. perror("pthread_create error !");
  24. exit(1);
  25. }
  26. pthread_join(thread_id1, NULL);
  27. pthread_join(thread_id2, NULL);
  28. printf("thread1 return %d\n", rtn1);
  29. printf("thread2 return %d\n", rtn2);
  30. return 0;
  31. }
  32. void *thread_fun(void *ptr)
  33. {
  34. pthread_t new_thid;
  35. char *message;
  36. message = (char*)ptr;
  37. new_thid = pthread_self();
  38. printf("This is a new thread, thread ID is %u, message: %s\n", new_thid, message);
  39. sleep(2);
  40. printf("-----end-----\n");
  41. }
  1. 虽然新创建的子线程sleep()休眠了一段时间,但在主线程中调用了pthread_join来等待新线程结束,所以主线程不会过早退出。

8.2.3 线程的取消

image.png
image.png

8.3 线程间通信

线程间共享进程的地址空间,因此线程间通信的难点在于对共享资源访问时的同步与互斥。线程间的同步互斥问题可以使用互斥锁、条件变量、信号量和读写锁来解决。
image.png
互斥锁使用前应先初始化,可使用函数pthread_mutex_init动态初始化,也可以使用静态初始化:
pthread_mutex_t mutex= PTHREAD_MUTEX_INITIALIZER;

  1. //thread_mutex.c
  2. //用互斥锁实现多线程的同步互斥
  3. #include <pthread.h>
  4. #include <stdio.h>
  5. #include <stdlib.h>
  6. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  7. int count = 0;
  8. void *thread_fun();
  9. int main(int argc, char **argv)
  10. {
  11. int rtn1, rtn2;
  12. pthread_t thread_id1;
  13. pthread_t thread_id2;
  14. rtn1 = pthread_create(&thread_id1, NULL, &thread_fun, NULL);
  15. rtn2 = pthread_create(&thread_id2, NULL, &thread_fun, NULL);
  16. pthread_join(thread_id1, NULL);
  17. pthread_join(thread_id2, NULL);
  18. pthread_exit(0);
  19. }
  20. void *thread_fun()
  21. {
  22. pthread_mutex_lock(&mutex);
  23. count++;
  24. sleep(1);
  25. printf("count = %d\n", count);
  26. pthread_mutex_unlock(&mutex);
  27. }

上述程序实现了对共享的全局变量count进行互斥访问。
image.png
当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞.
当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进行加锁, 它必须直到所有的线程释放锁.
通常, 当读写锁处于读模式锁住状态时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁长期占用, 而等待的写模式锁请求长期阻塞.
读写锁适合于对数据结构的读次数比写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁.