一、什么是线程
线程(Thread)是计算机中的基本执行单元,是操作系统调度的最小单位。线程是进程内的一个独立执行流程,一个进程可以包含多个线程,这些线程共享进程的资源,但每个线程都有自己的独立栈空间以及程序计数器。
二、线程与进程的优缺点
1、线程的优点
(1)、线程创建和销毁的开销比进程小,因为线程共享进程中的地址空间和其他资源。
(2)、线程可以同时执行多个任务,提高了系统的并发性能。
(3)、线程之间的通信和同步比进程之间的通信和同步更快捷和简单,因为线程共享同一进程的内存。
(4)、线程可用于执行GUI等交互性任务而不会卡住整个应用程序。
2、线程的缺点
(1)、 多线程访问共享数据时,需要使用同步技术,否则会导致不可预期的结果。
(2)、 线程的调试和bug定位比较困难,因为多个线程共享进程的执行环境。
(3)、 如果线程中出现了异常,可能会影响整个进程。
3、进程的优点
(1)、 进程相互独立,不会相互影响,因此更加健壮和安全。
(2)、进程可以在不同的硬件和操作系统上运行,更具有可移植性。
(3)、 进程使用管道等IPC(进程间通信)机制可以方便的实现进程之间的通信和同步。
4、进程的缺点
(1)、进程创建和销毁的开销比较大,因为每个进程都需要独立的地址空间和系统资源。
(2)、进程之间通信和同步需要使用IPC技术,比较繁琐和复杂。
(3)、进程的并发性能比较差,不能同时执行多个任务。
三、线程的使用场景
1、多任务处理:多线程可以同时处理多个任务,提高程序的执行效率和响应速度。
2、并发访问:当多个线程同时访问共享资源时,需要使用线程控制技术,避免出现竞态条件和死锁等问题。
3、异步编程:线程可以在后台执行一些耗时的操作,不会阻塞主线程,提高程序的用户体验。
4、服务器编程:服务器一般要同时处理多个客户端请求,使用多线程可以提高服务器的并发处理能力。
5、图形界面编程:图形界面程序中需要使用线程避免阻塞用户界面,实现异步更新UI界面。
6、大数据处理:对于大数据处理和分析,多线程可以提高数据处理的效率和速度。
7、游戏开发:游戏开发中需要实时更新游戏画面和处理用户输入,需要使用多线程技术实现。
四、与线程有关的函数API
1、线程的创建
创建一条POSIX线程非常简单,只需要指定线程的执行函数即可
// 创建一条线程 int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);// 接口说明:返回值:成功返回0,失败返回一个错误码参数thread:新线程的TID参数attr:线程属性,若创建标准线程则该参数可设置为NULL参数start_routine:线程函数,是一个回调函数,跟信号的例程函数有点像参数arg:线程函数的参数
2、线程的退出
与进程类似,当一条线程执行完毕其任务时,可以使用接口来退出
// 线程的退出 void pthread_exit(void *retval);// 接口说明参数retval:线程的返回值,若线程没有数据可返回则可写成NULLpthread_exit()与exit()的区别 pthread_exit():退出当前线程 exit():退出当前进程(即退出进程中的所有进程)
3、线程的结合
与进程类似,线程退出后不会立即释放其所占有的系统资源,而会成为一个僵尸线程。其他线程可使用pthread_join()来释放僵尸线程的资源,并可获得其退出时返回的退出值,该函数接口被称为线程的接合函数:
// 阻塞等待指定线程退出 int pthread_join(pthread_t tid, void **val);// 非阻塞接合指定线程退出 int pthread_tryjoin_np(pthread_t tid, void **retval);// 在指定时间内阻塞接合指定线程的退出 int pthread_timedjoin_np(pthread_t tid, void **retval, const struct timespec *ashtime);// 接口说明(1)若指定tid的线程尚未退出,那么该函数将持续阻塞(2)若只想阻塞等待指定线程tid退出,而不想要其退出值,那么val可置为NULL(3)若指定tid的线程处于分离状态,或者不存在,则该函数会出错返回
4、获取线程TID
// 获取线程TID pthread_t pthread_self(void);// 接口说明返回值:线程TID该接口类似进程管理中的getpid(),但是进程的PID是系统全局资源,而线程的TID仅限于进程内部的线程间有效。当我们要对某条线程执行发送信号,取消,阻塞接合等操作时,需要用到线程的TID。
5、线程错误码
线程函数对系统错误码的处理跟标准C库函数的处理方式有很大不同,标准C库函数会对全局错误码errno进行设置,而线程函数发生错误时会直接返回错误码。以线程接合为例,若要判定接合是否成功,成功的情况下输出僵尸线程的退出值,失败的情况下输出失败的原因,实现代码应该这么写
#include <error.h> // 头文件中定义了errno变量void *val;errno = pthread_join(tid, &val);if(errno == 0) {printf("成功接合线程,其退出值为:%d\n", (int)val; } else {perror("接合线程失败"); }
6、函数单例
许多时候,我们希望某个函数只被严格执行一次,这种需求在一些初始化功能模块中尤为常见,但是如果某个进程中内含多条线程,无法预先知晓哪条线程会先执行,那么初始化就会被执行多次,但如果使用函数单例就会只执行一次。
// 函数单例启动接口 int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));// 接口说明参数once_control:用来关联某个函数单例,被关联的函数单例只会被执行一遍参数init_routine:函数指针指向的函数就是只执行一遍的函数单例// 通常参数once_control指定为函数单例控制pthread_once_t once_control = PTHREAD_ONCE_INIT;
五、案例
// 线程的案例#include <stdio.h> #include <pthread.h> #include <errno.h> #include <string.h>int flag = 0; // 简单的标志位来控制同步 char data[100];// 线程1的例程函数,用来接收数据 void *recv_routine(void *arg) {printf("I am recv_routine, my tid = %ld\n", pthread_self());while(1){if(flag){printf("pthread1 read data: %s\n", data);memset(data, 0, sizeof(data));flag = 0;}} }// 线程2的例程函数,用来发送数据 void *send_routine(void *arg) {printf("I am send_routine, my tid = %ld\n", pthread_self());while(1){printf("please input data:\n");fgets(data, 100, stdin);printf("pthread2 send data\n");flag = 1;} }int main(int argc, char *argv[]) {pthread_t tid1, tid2;// 创建线程1,用来接收数据errno = pthread_create(&tid1, NULL, recv_routine, NULL);if(errno == 0){printf("pthread create recv_routine success, tid = %ld\n", tid1);}else{perror("pthread create recv_routine fail\n");}// 创建线程2,用来发送数据errno = pthread_create(&tid2, NULL, send_routine, NULL);if(errno == 0){printf("pthread create send_routine success, tid = %ld\n", tid2);}else{perror("pthread create send_routine fail\n");}// 一定要加这个,否则主函数直接退出,相当于进程退出,所有线程也退出// 或者加上while(1)等让主函数不退出pthread_exit(0);return 0; }
六、总结
线程可以提供系统的并发性,开销比进程更加小,但是不如进程健壮,移植性好。线程有自己的属性,下一篇博客将讲解。