一、线程介绍
1.1 线程基本概念
- 线程是一个轻量级的进程,一个任务创建、调度、消亡的过程 ,用来实现多任务并发。
- 线程是操作系统任务调度的最小单位。
- 线程由某个进程创建。
1.2 线程的创建
线程由所在进程创建,创建时,进程为其分配独立的栈区空间(默认8M)。
同一个进程中的线程,共用所在进程的堆区、数据区(全局变量、静态变量、字符串常量)、文本区。
1.3 线程的调度
与进程调度保持一致,宏观并行,微观串行
1.4 线程消亡
线程结束必须回收线程空间,否则会产生僵尸线程
1.5 进程和线程的区别
|---------------------------|--------------------------|
| 进程 | 线程 |
| 正在执行的程序 | 轻量级的进程 |
| 操作系统资源分配的最小单位 | 操作系统任务调度的最小单位 |
| 资源空间消耗大(0-4G虚拟内存空间) | 资源空间消耗小(栈区独立,其他区域共享) |
| 进程效率低(创建、任务切换) | 线程效率高(创建、任务切换) |
| 进程安全性高(进程空间独立) | 线程安全性低 |
| 进程间通信较复杂,需要用到IPC机制 | 线程间通信简单(全局变量) |
| 在相同资源平台下,多进程的并发量少于多线程 |
1.6 多进程和多线程优缺点
1)运行效率:多线程 > 多进程
多线程在同一进程空间内部任务调度效率更高。
2)安全:多进程 > 多线程
进程空间是独立的,一个进程异常结束,不会影响其余进程。
线程位于进程空间内部,一个线程异常结束,会导致进程结束,其余线程也随之结束。
3)通信实现:多线程 > 多进程
线程位于进程空间内部,共享进程的文本段、数据段(全局变量、静态变量、字符串常
量)、堆区,所以可以通过共享空间实现通信。
进程空间是独立的,没有共享空间,无法通信。
4)通信编程复杂程度:多进程 > 多线程
多线程使用全局变量通信会引入资源竞争问题,需要加锁来解决。
多进程空间独立的,没有共享空间,需要使用第三方机制来完成进程间通信。
二、线程相关接口函数
2.1 pthread_create
cppint pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); 功能:在进程中创建一个线程 参数: thread :存放线程ID空间的首地址 attr :线程的属性(栈空间大小、 优先级、 分离/加入属性),默认属性传NULL start_routine :函数指针,指向线程要执行的函数,线程启动后要执行的任务的函数地址(线程任务函数) arg :传递给线程任务函数的参数 返回值: 成功:0 失败:返回错误码注意:
- 编译和链接时要加-lpthread, 表示链接线程库。
- 线程执行时,线程所在的进程不能先退出。
2.2 pthread_exit
cppvoid pthread_exit(void *retval); 功能:结束线程任务 参数: retval: 线程结束时的状态 返回值: 缺省
2.3 pthread_join
cppint pthread_join(pthread_t thread, void **retval); 功能:回收线程空间 参数: thread:回收线程的TID retval:存放线程结束状态空间的首地址 返回值: 成功返回0 失败返回错误码具有阻塞功能
pthread_join() 函数会等待由 thread 参数指定的线程完成其执行过程。如果该线程已经结束执行,那么 pthread_join() 将立即返回。由 thread 指定的线程必须处于非分离状态。如果 retval 不为 NULL,则 pthread_join() 会将目标线程的退出状态复制到由 retval 所指向的位置。如果目标线程被取消(pthread_cancel),则会在由 retval 所指向的位置放置 PTHREAD_CANCELED (-1) 标志。
2.4 pthread_self
pthread_t pthread_self(void);
功能:返回调用该函数的线程自身的线程ID(TID)
练习1:创建3个线程
cpp#include "head.h" void *thread1fun(void *arg) { printf("线程1(TID:%#lx)开始执行\n", pthread_self()); return NULL; } void *thread2fun(void *arg) { printf("线程2(TID:%#lx)开始执行\n", pthread_self()); return NULL; } void *thread3fun(void *arg) { printf("线程3(TID:%#lx)开始执行\n", pthread_self()); return NULL; } int main(void) { pthread_t tid[3]; int i = 0; void *(*p[3])(void *) = {thread1fun, thread2fun, thread3fun}; for (i = 0; i < 3; i++) { pthread_create(&tid[i], NULL, p[i], NULL); } for (i = 0; i < 3; i++) { pthread_join(tid[i], NULL); } #if 0 pthread_t tid1,tid2,tid3; pthread_create(&tid1, NULL, thread1fun, NULL); pthread_create(&tid2, NULL, thread2fun, NULL); pthread_create(&tid3, NULL, thread3fun, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); pthread_join(tid3, NULL); #endif return 0; }
cpp#include "head.h" #define DEBUG 0 void *thread(void *arg) { #if DEBUG int *pval = arg; printf("线程%d(TID:%#lx)开始执行\n", *pval, pthread_self()); #else int val = (int)arg; printf("线程%d(TID:%#lx)开始执行\n", val+1, pthread_self()); #endif return NULL; } int main(void) { int i = 0; pthread_t tid[3] = {0}; #if DEBUG int val[3] = {1, 2, 3}; #endif for (i = 0; i < 3; i++) { #if DEBUG pthread_create(&tid[i], NULL, thread, &val[i]); #else pthread_create(&tid[i], NULL, thread, (void *)i); #endif } for (i = 0; i < 3; i++) { pthread_join(tid[i], NULL); } return 0; }练习2:生产者与消费者编程思想
创建4线程任务,
线程1开始时打印: 采集线程(TID:)开始执行,然后间隔1s打印 正在采集
线程2开始时打印: 存储线程(TID:)开始执行,然后间隔2s打印 正在存储
线程3开始时打印: 显示线程(TID:)开始执行,然后间隔5s打印 正在显示
线程4开始时打印: 日志线程(TID:)开始执行,然后间隔10s打印 正在日志记录主进程负责从终端接收字符, 如果接收到的是
'a'或者'A' 采集线程停止/开始打印
'b'或者'B' 存储线程停止/开始打印
'c'或者'C' 显示线程停止/开始打印
'd'或者'D' 日志线程停止/开始打印
cpp#include "head.h" typedef struct pthread_info { pthread_t tid; //线程ID号 char threadname[32]; //线程名 int IntervalSec; //间隔秒数 int IsWork; //是否打印标志位 }PthreadInfo_t; void *threadfun(void *arg) { PthreadInfo_t *pthreadinfo = arg; printf("%s (TID:%#lx)开始执行\n", pthreadinfo->threadname, pthreadinfo->tid); while (1) { if (pthreadinfo->IsWork) { printf("%s正在运行\n", pthreadinfo->threadname); sleep(pthreadinfo->IntervalSec); } } return NULL; } int main(void) { char ch = 0; PthreadInfo_t thread[4] = { { .threadname = "采集线程", .IntervalSec = 1, .IsWork = 0xffffffff, }, { .threadname = "存储线程", .IntervalSec = 2, .IsWork = 0xffffffff, }, { .threadname = "显示线程", .IntervalSec = 5, .IsWork = 0xffffffff, }, { .threadname = "日志线程", .IntervalSec = 10, .IsWork = 0xffffffff, }, }; int i = 0; for (i = 0; i < 4; i++) { pthread_create(&thread[i].tid, NULL, threadfun, &thread[i]); } while (1) { scanf(" %c", &ch); switch (ch) { case 'a': case 'A':thread[0].IsWork = ~thread[0].IsWork;break; case 'b': case 'B':thread[1].IsWork = ~thread[1].IsWork;break; case 'c': case 'C':thread[2].IsWork = ~thread[2].IsWork;break; case 'd': case 'D':thread[3].IsWork = ~thread[3].IsWork;break; } } for (i = 0; i < 4; i++) { pthread_join(thread[i].tid, NULL); } return 0; }
三、线程的分离属性
① 分离属性:具有分离属性的线程,在运行结束后由系统自动回收其资源(如线程控制块、栈空间等),无需其他线程调用
pthread_join进行等待。这类线程一旦终止,其资源立即被释放,外界无法感知其结束时刻。② 非分离属性:具有非分离属性的线程,在其终止后,资源不会自动释放,必须由其他线程调用
pthread_join来等待其结束并回收资源。③默认属性:线程默认具有非分离属性。
④ 设置线程分离属性:
cppint pthread_detach(pthread_t thread); 功能:设置线程的分离属性 参数: thread:要设置的线程的线程ID 返回值: 成功:0 失败:!0 int pthread_attr_init(pthread_attr_t *attr); 功能:将 attr 指向的线程属性进行初始化 int pthread_attr_destroy(pthread_attr_t *attr); 功能:将 attr 指向的线程属性销毁 int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); 功能:设置线程属性 分离状态:PTHREAD_CREATE_DETACHED 非分离状态(可连接状态):PTHREAD_CREATE_JOINABLE
cpp#include "head.h" void *thread(void *arg) { printf("线程(TID:%#lx)开始执行\n", pthread_self()); return NULL; } int main(void) { pthread_t tid[3]; int i = 0; pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); for (i = 0; i < 3; i++) { pthread_create(&tid[i], &attr, thread, NULL); } pthread_attr_destroy(&attr); while (1) { } return 0; }
四、线程间通信
① 线程间通信方式:
线程使用全局变量可以实现通信。
多线程同时使用全局变量会引入资源竞争,需要通过互斥锁解决资源竞争问题。
② 概念:
原子操作:不能被CPU任务调度打断的一次最小的操作称为原子操作。
临界代码/临界区:加锁解锁中间的代码,这段代码用户不会被同时执行。
4.1 线程间互斥机制
① 多线程访问临界资源时,存在资源竞争问题。
② 临界资源:多个线程可以同时操作的资源,比如:全局变量,共享内存等。
③ 如何解决资源竞争问题:
线程的互斥机制:让多个线程在访问临界资源时,具有排他性访问的特性④ 互斥机制:互斥锁、信号量、读写锁(写独占,读共享)、自旋锁
⑤ 互斥锁:实现互斥机制
创建互斥锁:pthread_mutex_t
初始化锁:pthread_mutex_init
加锁:pthread_mutex_lock/pthread_mutex_trylock
解锁:pthread_mutex_unlock
销毁锁:pthread_mutex_destroy
cppint pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 功能:初始化互斥锁 参数: mutex :互斥锁空间首地址 attr: 互斥锁的属性, 默认属性NULL 返回值: 成功:0 失败:非0 int pthread_mutex_lock(pthread_mutex_t *mutex); 功能:以阻塞方式等待加锁 int pthread_mutex_trylock(pthread_mutex_t *mutex); 功能:以非阻塞方式加锁,如果该锁已被使用,则返回 int pthread_mutex_unlock(pthread_mutex_t *mutex); 功能:解锁/释放锁 int pthread_mutex_destroy(pthread_mutex_t *mutex); 功能:销毁锁
cpp#include "head.h" pthread_mutex_t lock; int num = 0; void *thread1(void *arg) { while (1) { pthread_mutex_lock(&lock); num = 100; pthread_mutex_unlock(&lock); } return NULL; } void *thread2(void *arg) { while (1) { pthread_mutex_lock(&lock); num = 200; printf("num = %d\n", num); pthread_mutex_unlock(&lock); } return NULL; } int main(void) { pthread_t tid1; pthread_t tid2; pthread_mutex_init(&lock, NULL); pthread_create(&tid1, NULL, thread1, NULL); pthread_create(&tid2, NULL, thread2, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); pthread_mutex_destroy(&lock); return 0; }⑥ 死锁
1)死锁指的是在多线程环境中,每个执行流(线程)都有未释放的资源,且互相请求对方未释放资源,从而导致陷入永久等待状态的情况。(由于多个锁的加锁解锁导致线程无法继续执行的状态称为死锁)
2)产生死锁的四个充分必要条件(四条全中 = 死锁,破掉任意一条 = 解除):
(1) 互斥条件:一个资源同一时刻只能被一个任务占用(一个执行流获取锁后,其它执行流不能再获取该锁)。
(2) 请求与保持条件:一个任务已经持有了至少一个资源,但它又提出了新的资源请求,而该新资源正被其他任务占用。此时,该请求进程会进入阻塞等待状态,但是对自己已经持有的资源又保持不放。
(3) 不可剥夺条件:资源只能由持有者主动释放,在它使用完毕主动释放之前, 系统/其他任务不能强行抢过来(A执行流拿着锁,其它执行流不能释放)。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系(多个执行流拿着对方想要的锁,并且各执行流还去请求对方的锁)。
3)避免产生死锁:
- 使用pthread_mutex_trylock替代pthread_mutex_lock
- 锁一定要成对出现
- 使线程的加解锁顺序一致
- 破坏环路等待条件 :使用非阻塞锁,一旦线程发现请求的锁被使用,就去释放自己拥有的资源
4.2 线程间同步机制
① 线程间同步:让多个线程任务执行某部分任务的时候具有先后顺序。
② 同步方式:信号量
③ 可以将信号量看成一个资源数,当该资源数>0的时候,可以申请到信号量,当资源时为0时,申请信号量阻塞等待。申请完信号量时,该信号量对应的资源数自动-1。释放对应的信号量时,该信号量对应的资源数自动+1。
④ 信号量是一种资源,在程序运行过程中,代码、变量、 CPU、内存都可以看成是一种资源。资源一定是有限的,资源只能被初始化、销毁、申请和释放。⑤ 函数接口:
创建信号量:sem_t
初始化信号量:sem_init()
申请信号量:sem_wait();sem_trywait();
释放信号量:sem_post();
销毁信号量:sem_destroy();
cppint sem_init(sem_t *sem, int pshared, unsigned int value); 功能:初始化信号量 参数: sem:信号量的地址 pshared: 0: 同一进程的线程间共享 非0: 进程间共享 value:初始化的资源数值 返回值: 成功返回0 失败返回-1 int sem_destroy(sem_t *sem); 功能:销毁信号量 参数: sem:信号量的首地址 返回值: 成功返回0 失败返回-1 int sem_wait(sem_t *sem); 功能: 申请信号量, 如果资源数>0则让资源数-1 如果资源数 == 0,阻塞等待直到资源 > 0 才能申请到资源继续向下执行 参数: sem:信号量空间首地址 返回值: 成功返回0 失败返回-1 int sem_post(sem_t *sem); 功能:释放资源, 让资源数+1 参数: sem:信号量空间首地址 返回值: 成功返回0 失败返回-1练习:实现3个线程任务,线程1循环打印A,线程2循环打印B,线程3循环打印C,但是要求打印出来的顺序总是ABC
cpp#include "head.h" sem_t semA; sem_t semB; sem_t semC; void *threadA(void *arg) { while (1) { sem_wait(&semA); printf("A"); sem_post(&semB); } return NULL; } void *threadB(void *arg) { while (1) { sem_wait(&semB); printf("B"); sem_post(&semC); } return NULL; } void *threadC(void *arg) { while (1) { sem_wait(&semC); printf("C"); sem_post(&semA); } return NULL; } int main(void) { pthread_t tidA; pthread_t tidB; pthread_t tidC; sem_init(&semA, 0, 1); sem_init(&semB, 0, 0); sem_init(&semC, 0, 0); pthread_create(&tidA, NULL, threadA, NULL); pthread_create(&tidB, NULL, threadB, NULL); pthread_create(&tidC, NULL, threadC, NULL); pthread_join(tidA, NULL); pthread_join(tidB, NULL); pthread_join(tidC, NULL); sem_destroy(&semA); sem_destroy(&semB); sem_destroy(&semC); return 0; }