目录
一. 什么是线程
线程是指在操作系统中,不同执行路径之间进行并发执行的最小单位。在多线程系统中,每个线程都共享同一个进程的内存空间和资源,可以直接访问进程内的数据和资源。线程之间通过共享的内存空间进行数据传输和协作,不需要使用进程间通信机制。通过线程的并发执行,可以提高程序的效率和响应性。
二. 进程与线程的主要区别
资源占用:每个进程都拥有独立的内存空间、文件描述符和其他系统资源,而线程则共享这些资源。因此,创建一个新的进程需要分配独立的资源,而创建线程则相对较轻量。
切换开销:当进程切换时,需要保存和恢复整个进程的上下文,包括内存映像、寄存器状态等,开销较大;而线程的切换只需要保存和恢复线程自己的上下文,开销较小。
并发性:不同进程之间是并发执行的,它们可以在不同的处理器核心上同时执行。而线程是在同一个进程内部进行并发执行,共享同一进程的资源,通过调度器在处理器上轮流执行,实现并发性。
通信方式:进程间通信需要使用特定的机制,如管道、消息队列、共享内存等,才能进行数据交换和协作。而线程之间共享进程内的内存空间,可以直接读写共享变量,实现数据共享和通信。
错误隔离:由于进程之间拥有独立的内存空间,当一个进程出现错误或崩溃时,不会影响其他进程的执行。而线程共享同一进程的资源,一个线程的错误可能导致整个进程崩溃。
安全性:由于进程之间拥有独立的内存空间,进程之间的数据相互独立,因此更加安全;而线程之间共享相同的内存空间,需要注意对临界区的保护,避免数据竞争和死锁等问题。
三. 使用线程的理由
理由一:
与进程相比,它更节省资源。在Linux系统下,启动一个新进程需要分配独立的地址空间和数据表,这是昂贵的操作;而多个线程在同一进程中运行,彼此共享大部分数据,启动一个线程所需的空间远小于启动一个进程的空间,而且线程切换的时间也远远小于进程切换的时间。
理由二:
线程之间方便的通信机制。因为同一进程下的线程之间共享数据空间,一个线程的数据可以直接为其他线程所用,这既快捷又方便。当然,数据共享也会带来其他问题,如多个线程同时修改同一变量时需要注意同步和保护,static变量等在多线程程序中也需要注意。
四. 线程的相关函数
4.1 线程的创建
头文件 #include<pthread.h> 函数原型 int pthread_create(pthread_t *tidp,const pthread_attr_t *attr,
void *(*start_rtn)(void*),void *arg);
参数 tidp:
指向线程标识符的指针。
attr:
用来设置线程属性。通常为NULL。
(*start_rtn)(void*):
线程运行函数的起始地址。
arg:
运行函数的参数。
作用 创建线程 返回值 成功:返回0
失败:出错编号,并且*thread中的内容是未定义的。
注意:
新创建的线程从start_rtn函数的地址开始运行,该函数只有一个无类型指针参数arg。如果需要向start_rtn函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg参数传入。
4.2 线程的退出
头文件 #include<pthread.h> 函数原型 void pthread_exit(void *retval); 参数 retval:
表示线程退出状态,通常传NULL。
作用 终止调用它的线程并返回一个指向某个对象的指针。 返回值 无
线程退出的三种方式:
线程从启动函数中返回:
线程执行完自己的任务后,可以通过从启动函数中返回来退出线程。线程的退出码可以作为返回值传递给父线程或其他线程,供其获取该线程的退出状态。
线程被其他线程取消:
在同一个进程中,一个线程可以通过向目标线程发送取消请求来终止该线程的执行。要注意,线程需要显式地检查取消请求并做出相应的处理,以确保线程能够正确地退出。
线程调用pthread_exit():
线程可以主动调用pthread_exit()函数来退出线程。这个函数接受一个参数作为线程的退出状态。调用pthread_exit()会导致线程立即退出,并通过指定的退出状态传递给父线程或其他等待该线程结束的线程。
注意:
在任何一种方式下,线程退出时应确保释放被它所占用的资源,避免资源泄露。此外,对于使用动态内存分配的线程,还需要注意在退出前释放相应的内存空间。
4.3 线程的等待
头文件 #include<pthread.h> 函数原型 int pthread_join(pthread_t thread, void **retval); 参数 thread:
线程标识符,即线程ID,标识唯一线程。
retval:
用户定义的指针,用来存储被等待线程的返回值。
作用 以阻塞的方式等待thread指定的线程结束 返回值 成功:返回0
失败:返回错误号。
注意:
只能对已经处于joinable状态的线程使用pthread_join()。创建线程时可以通过设置线程属性来指定线程的分离状态(joinable或detached),默认情况下线程是joinable的。
确保目标线程已经退出。如果目标线程尚未退出,调用pthread_join()将会一直阻塞。因此,在调用pthread_join()之前,需要确保目标线程已经执行完毕或被取消。
可能的性能影响。由于pthread_join()会阻塞当前线程,因此在主线程中频繁地调用pthread_join()可能会影响程序的性能。如果不需要等待线程退出,可以将线程设置为分离状态,或者使用其他方式进行线程管理。
如果对线程的返回值不感兴趣,可以把rval_ptr置为NULL。在这种情况下,调用pthread_join函数将等待指定的线程终止,但并不获得线程的终止状态。
4.4 线程自身的ID
头文件 #include<pthread.h> 函数原型 pthread_t pthread_self(void); 参数 无
作用 获得线程自身的ID 返回值 成功:返回当前线程的标识符
4.5 示例代码
#include <stdio.h> #include <pthread.h> void *func(void *argv) { static char *str = "t1 pthread quit!!!"; printf("t1 receive: %s\n",(char *)argv); printf("t1 pthread id is %lu\n",pthread_self()); //线程退出 //void pthread_exit(void *retval); pthread_exit((void *)str); } int main() { //线程创建 //int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); char *str = "hello"; pthread_t t1; int ret = pthread_create(&t1,NULL,func,(void *)str); if(ret != 0) printf("create the pthread is fail\n"); printf("main pthread id is %lu\n",pthread_self()); //线程等待 /*调用这个函数的线程将一直阻塞,直到指定的线程调用pthread_exit, 从启动例程中返回或者被取消。 */ //int pthread_join(pthread_t thread, void **retval); char *msg = NULL; pthread_join(t1,(void **)&msg); printf("t1 pthread quit after returning the value: %s\n",msg); return 0; }
五. 互斥锁
互斥锁(Mutex)是一种用于多线程编程中保护临界区的同步机制,主要用于防止多个线程同时访问共享资源导致的数据不一致和竞态条件问题。
互斥锁提供了对共享资源的互斥访问,确保在任意时刻只有一个线程可以访问被保护的临界区。在使用互斥锁时,需要首先创建并初始化互斥锁,然后通过加锁和解锁操作来保证线程安全。
5.1 相关函数(简易描述)
pthread_mutex_init 初始化一个互斥锁 pthread_mutex_destroy 注销一个互斥锁 pthread_mutex_lock 加锁成功,如果不成功则阻塞等待 pthread_mutex_unlock 解锁操作
#include <stdio.h> #include<pthread.h> #include <unistd.h> pthread_mutex_t mutex; void *func1(void *argv) { //加锁,加完锁之后,要等待此线程执行完,解锁后,别的线程才能够加锁 //int pthread_mutex_lock(pthread_mutex_t *mutex); pthread_mutex_lock(&mutex); printf("t1 pthread creat success!\n"); printf("t1: pthread id is %ld\n",pthread_self()); //解锁 //int pthread_mutex_unlock(pthread_mutex_t mutex); pthread_mutex_unlock(&mutex); } void *func2(void *argv) { //加锁 //int pthread_mutex_lock(pthread_mutex_t *mutex); pthread_mutex_lock(&mutex); printf("t2 pthread creat success!\n"); printf("t2: pthread id is %ld\n",pthread_self()); //解锁 //int pthread_mutex_unlock(pthread_mutex_t mutex); pthread_mutex_unlock(&mutex); } int main() { //认识互斥锁的使用 //创建互斥锁 //int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); pthread_mutex_init(&mutex,NULL); pthread_t t1; pthread_t t2; int value = 100; int ret ; ret = pthread_create(&t1,NULL,func1,(void *)&value); ret = pthread_create(&t2,NULL,func2,(void *)&value); printf("main: pthread id is %ld\n",pthread_self()); pthread_join(t1,NULL); pthread_join(t2,NULL); //销毁锁 //int pthread_mutex_destroy(pthread_mutex_t *mutex); pthread_mutex_destroy(&mutex); return 0; }
5.2 死锁
死锁指的是两个或多个进程(线程)因为互相等待对方持有的资源而无法继续执行的情况。简单来说,死锁是由于资源竞争和互斥访问引发的一种僵局。
死锁通常发生在以下四个条件同时满足时:
- 互斥条件:某些资源一次只能被一个进程(线程)占用,即资源不能共享。
- 请求与保持条件:一个进程(线程)在持有至少一个资源的同时,又请求其他进程(线程)持有的资源。
- 不剥夺条件:已经分配的资源不能被强制性地从拥有者那里夺走,只能由持有者显式地释放。
- 循环等待条件:存在一个进程(线程)的资源申请序列,每个进程(线程)都在等待下一个进程(线程)释放资源,形成了一个循环等待的环路。
示例代码:
#include <stdio.h> #include<pthread.h> #include <unistd.h> //认识死锁(需要两个锁以上),就是线程1要拿我的锁,线程2要拿你的锁,两个线程僵着导致死锁 //创建两把锁 pthread_mutex_t mutex1; pthread_mutex_t mutex2; void *func1(void *argv) { //死锁现象 pthread_mutex_lock(&mutex1); sleep(1); pthread_mutex_lock(&mutex2); printf("t1 pthread creat success!\n"); printf("t1: pthread id is %ld\n",pthread_self()); pthread_mutex_unlock(&mutex1); } void *func2(void *argv) { //死锁现象 pthread_mutex_lock(&mutex2); sleep(1); pthread_mutex_lock(&mutex1); printf("t2 pthread creat success!\n"); printf("t2: pthread id is %ld\n",pthread_self()); pthread_mutex_unlock(&mutex1); } int main() { pthread_mutex_init(&mutex1,NULL); pthread_mutex_init(&mutex2,NULL); pthread_t t1; pthread_t t2; int value = 100; int ret ; ret = pthread_create(&t1,NULL,func1,(void *)&value); ret = pthread_create(&t2,NULL,func2,(void *)&value); printf("main: pthread id is %ld\n",pthread_self()); pthread_join(t1,NULL); pthread_join(t2,NULL); //销毁锁 //int pthread_mutex_destroy(pthread_mutex_t *mutex); pthread_mutex_destroy(&mutex1); pthread_mutex_destroy(&mutex2); return 0; }
5.3 条件锁
条件锁是一种同步机制,是基于互斥锁的扩展。条件锁可以让线程等待某个特定的条件变量满足后再继续执行。
条件变量是线程的一种同步机制,它提供了一个线程等待特定条件发生的方式。条件变量通常与互斥锁一起使用,以无竞争的方式等待特定条件的发生。其本身受到互斥锁的保护。在改变条件状态之前,线程必须先锁住互斥锁,这样其他线程在获取互斥锁之前不会感知到条件的改变,因为需要先锁定互斥锁才能检查条件。
条件变量在使用之前需要进行初始化。可以通过两种方式来初始化pthread_cond_t类型的条件变量。对于静态分配的条件变量,可以将常量PTHREAD_COND_INITIALIZER赋给它;而对于动态分配的条件变量,可以使用pthread_cond_destroy函数进行去初始化。
示例代码:
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> //认识条件与互斥锁的配合使用 int g_val = 0; pthread_mutex_t mutex; pthread_cond_t cond; void *func1(void *argv) { static int cnt = 0; while(1) { //条件等待,阻塞在这里,等待其他线程触发条件 //int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); pthread_cond_wait(&cond,&mutex); sleep(1); printf("t1:val %d\n---------------------\n",g_val); g_val = 0; if(cnt++ == 5) exit(1); } } void *func2(void *argv) { while(1) { pthread_mutex_lock(&mutex); printf("t2:val %d\n",g_val++); pthread_mutex_unlock(&mutex); if(g_val == 3) { //条件触发 //int pthread_cond_signal(pthread_cond_t cond); pthread_cond_signal(&cond); } sleep(1); } } int main() { //条件的创建 //int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); pthread_cond_init(&cond,NULL); pthread_mutex_init(&mutex,NULL); pthread_t t1; pthread_t t2; int value = 100; int ret ; ret = pthread_create(&t1,NULL,func1,(void *)&value); ret = pthread_create(&t2,NULL,func2,(void *)&value); pthread_join(t1,NULL); pthread_join(t2,NULL); //销毁条件 //int pthread_cond_destroy(pthread_cond_t *cond); pthread_cond_destroy(&cond); pthread_mutex_destroy(&mutex); return 0; }