文章目录
前言
记录学习多线程的知识重点与难点,若涉及版权问题请联系本人删除!
一、多线程
1. 概述
- 线程是轻量级的进程,一个进程可以涵盖多个线程。
- 线程是操作系统调度的最小单位,进程是资源分配的最小单位。
- 多个线程有不同的“地位”:进程的虚拟地址空间的生命周期默认和主线程一样,与创建的子线程无关。当主线程执行完毕,虚拟地址空间就会被释放。
- 每个线程都有唯一的线程ID(无符号的长整型数),类型为pthread_t。调用pthread_self函数就能得到当前线程的ID。
#include <pthread.h>
pthread_t pthread_self(void);
//使用pthread库,链接时需要加上选项-lpthread来指定动态库
进程与线程的区别:
- 进程有独立的地址空间,线程共用同一个地址空间。每个线程都有自己的栈区和寄存器,多个线程共享代码段、堆区、全局数据区和文件描述符表。
- 线程的上下文切换比进程快得多。
上下文切换:进程/线程分时复用CPU时间片,在切换前会保存当前状态,下次执行该任务时加载保存的状态。
控制多个线程个数:
- 针对文件IO:线程个数=2*CPU核心数。
- 针对复杂算法:线程个数=CPU核心数。
2. 创建线程
调用pthread_create函数:
【1】头文件:#include <pthread.h>
【2】函数原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
【3】参数说明:
- thread:传出的参数,保存创建的线程ID。
- attr:线程的属性,一般为NULL表示默认的属性。
- start_routine:函数指针,线程的执行函数。
- arg:作为实参传递到start_routine指向的函数内部。
【4】返回值:成功返回0,失败返回错误码。
程序实例:创建子线程,并在主线程内打印子线程和主线程的tid。同时,在子线程执行函数中打印信息。
#include <stdio.h> #include <pthread.h> #include <unistd.h> void* doing(void *arg) { printf("我是子线程, ID: %ld\n", pthread_self()); return NULL; } int main(int argc, char ** argv) { /* 创建子线程 */ pthread_t tid; if (!pthread_create(&tid, NULL, doing, NULL)) { printf("子线程创建成功,其ID为:%ld\n", tid); } /* 主线程才会执行 */ printf("主线程的ID:%ld\n", pthread_self()); while (1) { sleep(1); } return 0; }
3. 线程退出
调用pthread_exit函数,调用后线程就退出了。只有当所有线程都退出了,虚拟地址空间才会释放。
【1】头文件:#include <pthread.h>
【2】函数原型:void pthread_exit(void *retval);
【3】参数说明:retval表示子线程退出后返回主线程的数据。不需要时使用NULL。
程序实例:创建子线程,在子线程的执行函数中睡眠3秒后打印信息,主进程创建完子线程后调用pthread_exit函数退出。
#include <stdio.h> #include <pthread.h> #include <unistd.h> /* 子线程执行函数 */ void* doing(void *arg) { sleep(3);//延迟三秒,确定主线程是否会退出 for (int i = 0; i < 9; ++i) { printf("我是子线程!\n"); if (i == 3) { pthread_exit(NULL); } } return NULL; } int main(int argc, char **argv) { /* 创建子线程 */ pthread_t tid; if (!pthread_create(&tid, NULL, doing, NULL)) { printf("子线程创建成功,ID: %ld\n", tid); } /* 主线程:调用pthread_exit不会释放虚拟地址空间 */ pthread_exit(NULL); return 0; }
4. 线程回收
主线程调用pthread_join函数来阻塞式回收子线程。若子线程还在运行,那么该函数就会阻塞。该函数只能回收一个子线程,若想回收多个可以考虑采用循环。
【1】头文件:#include <pthread.h>
【2】函数原型:int pthread_join(pthread_t thread, void **retval);
【3】参数说明:
- thread:要回收的子线程ID。
- retval:接收子线程通过pthread_exit传出的数据。如果目标线程被 pthread_cancel函数取消, 则将PTHREAD_CANCELED(就是(void*)-1)放在*retval 中。
【4】返回值:回收成功返回0,失败返回错误号。
【5】注意:
- 若子线程返回的数据位于子线程的栈区中,那么当子线程退出后其栈区就会被释放,主线程获取的数据就是无效的。
- 由于多个线程共用堆区和全局数据区,可以将子线程的数据保存于全局变量中。
- 由于主线程要回收子线程,一般都是最后退出。因此,可以将主线程的栈区变量传入子线程,在子线程中进行修改。
程序实例:借助主线程的栈区变量,将其传入子线程中,子线程将数据保存至该变量中,最后通过pthread_exit函数返回主线程,主线程调用pthread_join函数回收子线程并获取子线程返回的数据。
#include <stdio.h> #include <pthread.h> void* doing(void *arg) { int *i = (int *)arg; *i = 666666; printf("子线程将参数修改为: %d\n", *i); pthread_exit(i); return NULL; } int main(int argc, char **argv) { /* 创建子线程 */ pthread_t tid; int variable = 0;//主线程栈区变量 pthread_create(&tid, NULL, doing, &variable); /* 主线程获取子线程退出时的数据 */ void *ret = NULL; pthread_join(tid, &ret);//回收子线程 printf("主线程获取子线程数据: %d\n", *(int*)ret); return 0; }
5. 线程分离
如果总是让主线程来回收子线程,那么可能会出现子线程一直运行,而主线程阻塞在pthread_join函数,无法执行其他任务。因此,可以调用pthread_detach函数将子线程分离。当子线程退出时,其内核资源就会被系统其他进程接管并回收。
【1】头文件:#include <pthread.h>
【2】函数原型:int pthread_detach(pthread_t thread);
【3】参数说明:thread是要分离的线程ID。
【4】返回值:成功返回0,失败返回错误号。
程序实例:主线程与子线程分离,主线程执行完任务后就退出。
#include <stdio.h> #include <pthread.h> void* doing(void *arg) { for (int i = 0; i < 9; ++i) { printf("Hello, Can! %d\n", i); if (i == 4) { pthread_exit(NULL); } } return NULL; } int main(int argc, char **argv) { /* 创建子线程 */ pthread_t tid; int retCreate = pthread_create(&tid, NULL, doing, NULL); if (retCreate != 0) { perror("pthread_create error!"); return -1; } /* 主线程与子线程分离 */ int retDetach = pthread_detach(tid); if (retDetach != 0) { perror("pthread_detach error!"); pthread_join(tid, NULL); } /* 主线程执行其他任务 */ for (int i = 0; i < 5; i++) { printf("我是主线程, %d\n", i); } pthread_exit(NULL); return 0; }
6. 线程取消
线程取消就是一个线程杀死另一个线程。线程A杀死线程B需要两个条件:①线程A调用pthread_cancel函数;②线程B进行一次系统调用(取消点),从用户态切换回内核态。
【1】头文件:#include <pthread.h>
【2】函数原型:int pthread_cancel(pthread_t thread);
【3】参数说明:thread是要杀死的线程ID。
【4】返回值:成功返回0,失败返回错误号。
设置线程不能被取消:int pthread_setcancelstate(int state, int *oldstate);
- state:①PTHREAD_CANCEL_ENABLE能被取消(默认);②PTHREAD_CANCEL_DISABLE
不能被取消。- oldstate:线程之前的取消性状态。不关心则为NULL。
设置线程的取消类型:int pthread_setcanceltype(int type, int *oldtype);
- type:①PTHREAD_CANCEL_DEFERRED线程继续运行,直到到达某个取消点(默认);②PTHREAD_CANCEL_ASYNCHRONOUS(应用场景很少)。
产生取消点:void pthread_testcancel(void);若某个线程已有被取下的需求,那么在执行该函数时,线程就会终止。
程序实例:主线程杀死子线程。当子线程执行pthread_self函数时就会被杀死,因为该函数间接调用了系统调用函数。
#include <stdio.h> #include <pthread.h> #include <unistd.h> void* doing(void *arg) { printf("子线程: 我要调用pthread_self了!\n"); printf("子线程: %ld\n", pthread_self()); sleep(2);//确保杀死子线程时间足够 printf("子线程: 我还活着吗?\n"); return NULL; } int main(int argc, char **argv) { /* 创建子线程 */ pthread_t tid; if (pthread_create(&tid, NULL, doing, NULL) != 0) { perror("pthread_create error!"); return -1; } /* 主线程杀死子线程 */ if (pthread_cancel(tid) != 0) { perror("pthread_cancel error!"); } /* 主线程退出 */ pthread_exit(NULL); return 0; }
7. 线程的ID比较
调用pthread_equal函数来比较两个线程的ID是否相等。
【1】头文件:#include <pthread.h>
【2】函数原型:int pthread_equal(pthread_t t1, pthread_t t2);
【3】参数说明:t1和t2就是两个线程的ID。
【4】返回值:相同返回非0值,不同返回0.
程序实例:主线程创建子线程,然后判断主线程和子线程的ID是否相同。
#include <stdio.h> #include <pthread.h> void* doing(void *arg) { printf("我是子线程\n"); return NULL; } int main(int argc, char **argv) { /* 创建子线程 */ pthread_t tid; if (pthread_create(&tid, NULL, doing, NULL) != 0) { perror("create error!"); return -1; } /* 比较线程ID是否相等*/ pthread_t tidMain = pthread_self(); if (pthread_equal(tidMain, tid) > 0) { printf("主线程ID: %ld, 子线程ID: %ld, 二者相等\n", tidMain, tid); } else { printf("主线程ID: %ld, 子线程ID: %ld, 二者不相等\n", tidMain, tid); } return 0; }
8. 注册线程清理函数
当线程退出时,可以去执行相关的处理函数,这类函数被称为线程清理函数。与进程的atexit函数类似,可以登记/注册多个,当执行时也是按照栈的方式来执行。
【1】通过pthread_cleanup_push和pthread_cleanup_pop函数分别负责向调用线程的清理函数栈中添加和移除清理函数:
void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);
【2】注意:
- pthread_cleanup_pop函数中的execute参数可以为0也可以为非0。若为0,只是移除最顶层的清理函数,而不会调用;若不为0,除了移除外还会调用。
- 只有执行以下动作才会执行清理函数:①调用pthread_exit函数;②线程响应取消请求;③用非0参数调用pthread_cleanup_pop函数。
- pthread_cleanup_push和 pthread_cleanup_pop通过宏来实现的,因此必须一一对应,否则编译出错。
程序实例:
#include <stdio.h> #include <pthread.h> /* 线程清理函数 */ void cleanup(void* arg) { printf("cleanup: %s\n", (char*)arg); } /* 线程执行函数 */ void* doing(void* arg) { //注册线程清理函数 pthread_cleanup_push(cleanup, "First"); pthread_cleanup_push(cleanup, "Sencond"); pthread_cleanup_push(cleanup, "Third"); //手动执行清理函数 pthread_cleanup_pop(-1); //执行其它代码 printf("-------------------------\n"); pthread_exit((void*)0); //为了与pthread_cleanup_push对应 pthread_cleanup_pop(0); pthread_cleanup_pop(0); return NULL; } /* 主函数 */ int main(int argc, char** argv) { //创建子线程 pthread_t tid; if (pthread_create(&tid, NULL, doing, NULL) != 0) { perror("pthread_create error"); return -1; } //阻塞回收子线程 void* retval = NULL; int ret = pthread_join(tid, &retval); if (ret) { perror("pthread_join error"); return -1; } printf("thread exit: %d\n", (int)retval); return 0; }
9. 线程属性
在通过pthread_create函数来创建线程时,需要指定线程的属性,之前都是采用NULL来指定为默认属性,当然可以自定义线程的属性。
注意:以下函数的返回值都是:成功返回0,失败返回错误号。
【1】初始化和销毁线程属性变量:
#include <pthread.h>
pthread_attr_t attr;//创建线程属性变量
int pthread_attr_init(pthread_attr_t *attr);//初始化为默认值
int pthread_attr_destroy(pthread_attr_t *attr);//销毁
【2】获取/设置线程栈的属性:(系统默认的栈大小为8MB)当设置的栈大小<2MB时,系统还是给8MB的栈空间大小。
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
//单独设置与获取
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);
int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stackaddr);
【3】获取/设置线程分离的属性:设置该属性后,线程一开始运行就是分离的,后续无需再调用pthread_detach函数。
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
//detachstate: 有两个取值
//PTHREAD_CREATE_DETACHED: 分离启动,线程最后由操作系统回收
//PTHREAD_CREATE_JOINABLE: (默认)正常启动线程
程序实例1:创建新线程,将线程的栈大小设置为3MB(24576bytes)。
#include <stdio.h> #include <pthread.h> /* 线程执行函数 */ void* doing(void* arg) { printf("Hello, Can!\n"); return NULL; } /* 主函数 */ int main(int argc, char** argv) { //初始化线程属性 pthread_attr_t attr; pthread_attr_init(&attr); //设置线程栈大小为3MB pthread_attr_setstacksize(&attr, 24576); //查看线程属性中栈的大小 size_t attr_size; pthread_attr_getstacksize(&attr, &attr_size); printf("stack size: %lu\n", attr_size); //创建子线程 pthread_t tid; if (pthread_create(&tid, &attr, doing, NULL)) { perror("pthread_create error"); return -1; } //阻塞回收子线程 if (pthread_join(tid, NULL)) { perror("pthread_join error"); return -1; } //销毁线程属性 pthread_attr_destroy(&attr); return 0; }
程序实例2:设置线程为分离启动。
#include <stdio.h> #include <pthread.h> #include <unistd.h> /* 线程执行函数 */ void* doing(void* arg) { printf("Hello, Can!\n"); return NULL; } /* 主函数 */ int main(int argc, char** argv) { //初始化线程属性 pthread_attr_t attr; pthread_attr_init(&attr); //设置线程为分离启动 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); //创建子线程 pthread_t tid; if (pthread_create(&tid, &attr, doing, NULL)) { perror("pthread_create error"); return -1; } sleep(2); //销毁线程属性 pthread_attr_destroy(&attr); return 0; }
10. 可重入函数
【1】概念:多个执行流执行同一个函数,并且能得到预期效果,那么该函数就是可重入函数。
【2】场景:
- 多线程执行同一个函数。
- 程序中涉及信号处理,信号处理函数也被其它线程调用。
【3】可重入函数举例:由于i都是位于线程栈内部的,多个线程之间互不影响,其执行效果是能预期获得的。
int add()
{
int i = 0;
return i;
}
【4】不可重入函数举例:由于涉及临界区资源count,多线程来执行该函数可能会造成数据混乱,无法得到预期效果。
int count = 0;
void add()
{
int tmp = count;
tmp++;
count = tmp;
}
二、线程同步
1. 概述
- 当有多个线程访问同一个共享资源(临界资源)时,且不允许同时访问,那么就需要线程同步。
- 常见的线程同步方式:互斥锁、读写锁、条件变量、信号量。
2. 互斥锁
互斥锁的方式可以简单概括为:锁定操作临界资源的代码片段,锁定后每次只能由一个线程来进行操作。这样能够解决多个线程同时访问临界资源造成的数据混乱,但是降低了执行效率(因为并行操作变成了串行操作)。
【1】互斥锁类型:pthread_mutex_t。创建一个该类型的变量就得到一把互斥锁。该变量中保存了锁的状态(打开还是锁定),若为锁定则保存了加锁的线程ID。锁定时其他线程将会阻塞,直到这个互斥锁被打开。
以下函数的返回值:成功返回0,失败返回错误号。
【2】初始化互斥锁:
//restrict:是一个修饰指针的关键字,该关键字修饰的指针可以访问指向的内存地址,其他指针不行
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
//mutex: 互斥锁地址
//attr: 互斥锁属性,一般为NULL默认属性
【3】释放互斥锁资源:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
【4】加锁(阻塞):
int pthread_mutex_lock(pthread_mutex_t *mutex);
//若锁是打开的,那么加锁成功,锁会记录线程ID。
//若锁是锁定的,那么加锁失败,线程阻塞在此,直到上一个线程解锁。
【5】加锁(非阻塞):
int pthread_mutex_trylock(pthread_mutex_t *mutex);
//若加锁失败,则不会阻塞,而是直接返回错误号。
//锁被占用时,返回的是EBUSY(整数16)
【6】解锁:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//解锁还需加锁人,哪个线程加的锁就得哪个线程来解锁。
【7】互斥锁属性:pthread_mutexattr_t类型
①创建互斥锁属性变量:pthread_mutexattr_t attr;
②初始化互斥锁属性变量:int pthread_mutexattr_init(pthread_mutexattr_t *attr);
③销毁互斥锁属性变量:int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
互斥锁类型如下:
- PTHREAD_MUTEX_NORMAL:不做任何的错误检测。
- PTHREAD_MUTEX_ERRORCHECK:会做错误检测,效率不高,可以用于调试。
- PTHREAD_MUTEX_RECURSIVE:允许多次加锁,维护加锁次数,解锁次数和加锁次数相同时才会解锁。
- PTHREAD_MUTEX_DEFAULT:默认属性,与PTHREAD_MUTEX_NORMAL类似。
④获取互斥锁的类型:int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
⑤修改互斥锁的类型:int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
程序实例:创建两个子线程对全局变量number进行+1操作。若不使用互斥锁,就会造成数据混乱。使用了互斥锁,运行正常。
#include <stdio.h> #include <pthread.h> #include <unistd.h> pthread_mutex_t mutex; //全局的互斥锁 int number = 0; //全局变量 //线程t1执行函数 void* funA(void *arg) { for (int i = 0; i < 50; i++) { pthread_mutex_lock(&mutex); int cur = number; cur++; usleep(10); number = cur; printf("线程1的ID: %ld, number: %d\n", pthread_self(), number); pthread_mutex_unlock(&mutex); } return NULL; } //线程t2执行函数 void* funB(void *arg) { for (int i = 0; i < 50; i++) { pthread_mutex_lock(&mutex); int cur = number; cur++; number = cur; printf("线程2的ID: %ld, number: %d\n", pthread_self(), number); usleep(5); pthread_mutex_unlock(&mutex); } return NULL; } //主函数 int main(int argc, char **argv) { /* 初始化互斥锁 */ pthread_mutex_init(&mutex, NULL); /* 创建两个子线程 */ pthread_t t1, t2; pthread_create(&t1, NULL, funA, NULL); pthread_create(&t2, NULL, funB, NULL); /* 阻塞回收两个子线程*/ pthread_join(t1, NULL); pthread_join(t2, NULL); /* 销毁互斥锁 */ pthread_mutex_destroy(&mutex); return 0; }
3. 死锁
- 互斥锁若使用不当,就会造成死锁。一旦多线程造成死锁,就会使得所有线程处于阻塞状态,且无法解除。
- 常见的死锁场景:①加锁后忘记解锁;②重复加锁;③存在多个共享资源,随意加锁。
- 避免死锁:①多检查代码;②对共享资源访问完毕后解锁,或者在加锁时使用trylock非阻塞;③引入一些专门用于死锁检测的模块;④如果程序中有多把锁, 可以控制对锁的访问顺序。另外,在加其它锁之前先释放拥有的互斥锁。
4. 自旋锁
【1】介绍:
- 自旋锁与互斥锁类似,但是自旋锁在试图上锁时是不断“自旋”查看锁是否被释放,CPU资源占用多。因此,使用自旋锁的效率不高,在用户程序中很少使用自旋锁,更多的是在内核中使用、针对执行时间少的代码。
- 互斥锁是基于自旋锁实现的,所以自旋锁更加底层。
【2】自旋锁类型:pthread_spinlock_t。创建一个该类型的变量就得到一把自旋锁。
以下函数的返回值:成功返回0,失败返回错误号。
【3】初始化/销毁自旋锁:
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);//初始化
//lock: 自旋锁的地址
//pshared: 表示自旋锁的进程共享属性
//PTHREAD_PROCESS_SHARED: 共享自旋锁。该自旋锁可以在多个进程中的线程之间共享
//PTHREAD_PROCESS_PRIVATE: 私有自旋锁。只有本进程内的线程才能够使用该自旋锁
int pthread_spin_destroy(pthread_spinlock_t *lock);//销毁
【4】自旋锁的加锁和解锁:
int pthread_spin_lock(pthread_spinlock_t *lock); //加锁(一直自旋)
int pthread_spin_trylock(pthread_spinlock_t *lock); //加锁(返回错误,错误码EBUSY)
int pthread_spin_unlock(pthread_spinlock_t *lock); //解锁
程序实例:创建两个子线程来对全局变量+1(每个线程循环10000次)。
#include <stdio.h> #include <pthread.h> #include <stdlib.h> int count = 0;//全局变量(临界区资源) pthread_spinlock_t locker;//自旋锁 /* 线程执行函数 */ void* doing(void* arg) { for (int i = 0; i < 10000; ++i) { //自旋锁加锁 pthread_spin_lock(&locker); //操作临界区资源 int tmp = count; tmp++; count = tmp; //自旋锁解锁 pthread_spin_unlock(&locker); } return NULL; } /* 主函数 */ int main(int argc, char** argv) { //初始化自旋锁 pthread_spin_init(&locker, PTHREAD_PROCESS_PRIVATE); //创建两个子线程 pthread_t tid[2]; for (int i = 0; i < 2; ++i) { if (pthread_create(&tid[i], NULL, doing, NULL)) { perror("pthread_create error"); exit(-1); } } //阻塞回收子线程 for (int i = 0; i < 2; ++i) { if (pthread_join(tid[i], NULL)) { perror("pthread_join error"); exit(-1); } } //打印全局变量 printf("count = %d\n", count); //销毁自旋锁 pthread_spin_destroy(&locker); return 0; }
5. 读写锁
读写锁可以视为互斥锁的升级版,可以指定锁定的是读操作还是写操作,且同一时间内只能锁定其中一个操作。读写锁的使用方式与互斥锁相同。读写锁非常适合于对共享数据读的次数远大于写的次数的情况。
【1】读写锁类型:pthread_rwlock_t。创建一个该类型的变量就得到一把读写锁。读写锁中保存了以下信息:①锁的状态(打开还是锁定);②锁的是哪个操作(读/写);③哪个线程锁定了这把锁。
【2】读写锁特点:
- 使用读写锁的读锁锁定了临界区,那么读操作是并行的。
- 使用读写锁的写锁锁定了临界区,那么写操作是串行的。
- 使用读写锁的读锁和写锁分别锁定了两个临界区,那么访问写锁的线程优先进入。因为写锁的优先级高于读锁。
以下函数的返回值:成功返回0,失败返回错误号。
【3】初始化读写锁:
//restrict:是一个修饰指针的关键字,该关键字修饰的指针可以访问指向的内存地址,其他指针不行
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
//rwlock: 读写锁地址
//attr: 读写锁属性,一般为NULL默认属性
【3】释放读写锁资源:
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
【4】锁定读操作(阻塞):
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
//若读写锁均打开,那么锁定读操作成功。
//若读写锁中读锁锁定,那么锁定读操作成功。因为读锁是共享的。
//若读写锁中写锁锁定,那么会阻塞。
【5】锁定读操作(非阻塞):
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
//若读写锁均打开,那么锁定读操作成功。
//若读写锁中读锁锁定,那么锁定读操作成功。因为读锁是共享的。
//若读写锁中写锁锁定,那么会返回错误号,而不会阻塞。
【6】锁定写操作(阻塞):
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
//若读写锁均打开,锁定写操作成功。
//若读写锁中读锁或写锁锁定了,那么就会阻塞。
【7】锁定写操作(非阻塞):
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
//若读写锁均打开,锁定写操作成功。
//若读写锁中读锁或写锁锁定了,那么返回错误号,而不会阻塞。
【8】解锁:
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
//不管锁定的是读锁还是写锁,都能解锁。
程序实例:创建3个子线程进行写操作,创建5个子线程进行读操作。它们都针对一个全局变量。
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> int number = 0; //全局变量(临界资源) pthread_rwlock_t rwlocker; //全局的读写锁 /* 写线程执行函数 */ void* writeNum(void *arg) { while (1) { pthread_rwlock_wrlock(&rwlocker); int cur = number; cur++; number = cur; printf("写操作完毕!number = %d, tid: %ld\n", number, pthread_self()); pthread_rwlock_unlock(&rwlocker); usleep(rand() % 100);//让子线程交替写 } return NULL; } /* 读线程执行函数 */ void* readNum(void *arg) { while (1) { pthread_rwlock_rdlock(&rwlocker); printf("读操作完毕!number = %d, tid: %ld\n", number, pthread_self()); pthread_rwlock_unlock(&rwlocker); usleep(rand() % 100); } return NULL; } /* 主函数 */ int main(int argc, char **argv) { //初始化读写锁 pthread_rwlock_init(&rwlocker, NULL); //创建8个线程,3个为写线程,5个为读线程 pthread_t wtid[3]; pthread_t rtid[5]; for (int i = 0; i < 3; i++) { pthread_create(&wtid[i], NULL, writeNum, NULL); } for (int i = 0; i < 5; i++) { pthread_create(&rtid[i], NULL, readNum, NULL); } //主线程回收8个线程 for (int i = 0; i < 3; i++) { pthread_join(wtid[i], NULL); } for (int i = 0; i < 5; i++) { pthread_join(rtid[i], NULL); } //释放读写锁 pthread_rwlock_destroy(&rwlocker); return 0; }
6. 条件变量
条件变量的作用是进行线程的阻塞,而不是线程同步。当满足某个特定条件时才会阻塞线程。一般用于生产者-消费者模型,且和互斥锁相互配合。条件检测需要在互斥锁的保护内进行。
【1】条件变量类型:pthread_cond_t。被条件变量阻塞的线程的信息会被记录到该类型的变量中,以便在解除阻塞时使用。
【2】初始化条件变量:
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
//cond:条件变量的地址
//attr:条件变量的属性,一般为NULL。
【3】释放条件变量资源:调用pthread_cond_destroy销毁条件变量之后,可以调用pthread_cond_init再次初始化。
int pthread_cond_destroy(pthread_cond_t *cond);
【4】阻塞线程:
// 线程阻塞函数, 哪个线程调用这个函数, 哪个线程就会被阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
//mutex是互斥锁,用于线程同步。
//当阻塞线程时,若线程已经对互斥锁mutex上锁,那么会将这把锁打开,这样做是为了避免死锁
//当线程解除阻塞时,函数内部会帮助这个线程再次将这个mutex锁上,继续向下访问临界区
【5】阻塞线程(时间到解除):
// 将线程阻塞一定的时间长度, 时间到达之后, 线程就解除阻塞了
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
// 以下结构体表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds [0 .. 999999999] */
};
//使用示例
time_t mytim = time(NULL); // 1970.1.1 0:0:0 到当前的总秒数
struct timespec tmsp;
tmsp.tv_nsec = 0;
tmsp.tv_sec = time(NULL) + 100; // 线程阻塞100s
【6】解除阻塞(至少一个线程):只有一个处于等待状态的线程,使用 pthread_cond_signal更好
int pthread_cond_signal(pthread_cond_t *cond);
【7】解除阻塞(全部线程):
int pthread_cond_broadcast(pthread_cond_t *cond);
生产者-消费者模型:
①若干个生产者线程:生产商品放入任务队列中,若任务队列满则阻塞,可以使用一个生产者条件变量来控制是否阻塞。
②若干个消费者线程:消费者从任务队列中拿走商品,若任务队列空则阻塞,可以使用一个消费者条件变量来控制是否阻塞。
③任务队列:可以是数组、链表、stl容器等等。
程序实例:使用条件变量实现生产者-消费者模型,生产者线程有5个,往链表头部添加节点;消费者线程也有5个,删除链表头部节点。
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> /* 单链表定义 */ typedef struct LNode { int number; struct LNode *next; } LNode; /* 全局变量及锁 */ pthread_mutex_t mutex; //互斥锁 pthread_cond_t cond; //条件变量 LNode *head = NULL; //临界资源,单链表 /* 生产者执行函数 */ void* produce(void *arg) { while (1) { //生产商品,往链表头部添加节点 pthread_mutex_lock(&mutex); LNode *tmp = (LNode*)malloc(sizeof(LNode));//创建新节点 tmp->number = rand() % 100; tmp->next = head; head = tmp; printf("生产完毕! 新节点number: %d, 线程ID: %ld\n", tmp->number, pthread_self()); pthread_mutex_unlock(&mutex); //通知消费者拿走商品 pthread_cond_broadcast(&cond); sleep(rand() % 3);//生产慢一点 } return NULL; } /* 消费者执行函数 */ void* consume(void *arg) { while (1) { pthread_mutex_lock(&mutex); //无商品则等待 //这里不能用if,可能出现段错误。 //通过while循环判断,当阻塞解除后也会再次判断。 while (head == NULL) { pthread_cond_wait(&cond, &mutex); } //拿走商品,删除链表头部节点 LNode *tmp = head; printf("消费完毕! 节点number: %d, 线程ID: %ld\n", tmp->number, pthread_self()); head = head->next; free(tmp); pthread_mutex_unlock(&mutex); sleep(rand() % 3); } return NULL; } /* 主函数 */ int main(int argc, char **argv) { //初始化锁和条件变量 pthread_mutex_init(&mutex, NULL); pthread_cond_init(&cond, NULL); //创建5个生产者线程和5个消费者线程 pthread_t ptid[5]; pthread_t ctid[5]; for (int i = 0; i < 5; i++) { pthread_create(&ptid[i], NULL, produce, NULL); } for (int i = 0; i < 5; i++) { pthread_create(&ctid[i], NULL, consume, NULL); } //主线程回收10个线程 for (int i = 0; i < 5; i++) { pthread_join(ptid[i], NULL); } for (int i = 0; i < 5; i++) { pthread_join(ctid[i], NULL); } //释放互斥锁和条件变量 pthread_mutex_destroy(&mutex); pthread_cond_destroy(&cond); return 0; }
7. 信号量
信号量(信号灯)用在多线程的多任务同步中,一个线程完成了某个任务就通过信号量告诉其它线程,其它线程再进行相关操作。信号量也是用于阻塞线程,要保证线程安全,需要信号量与互斥锁一起使用。
【1】信号量类型:sem_t。需要添加头文件<semaphore.h>.
【2】初始化信号量:
int sem_init(sem_t *sem, int pshared, unsigned int value);
//sem: 信号量地址
//pshared: 0表示线程同步,非0表示进程同步
//value: 初始化当前信号量拥有的资源数>=0, 若资源数为0则阻塞
【3】释放信号量资源:
int sem_destroy(sem_t *sem);
//sem: 信号量地址
【4】消耗资源函数(阻塞):
//sem: 信号量地址
//函数被调用,sem中的资源就会被消耗1个
//当资源数为0时,线程就会阻塞
int sem_wait(sem_t *sem);
【5】消耗资源函数(非阻塞):
//sem: 信号量地址
//函数被调用,sem中的资源就会被消耗1个
//当资源数为0时,线程就会返回错误号,而不会阻塞
int sem_trywait(sem_t *sem);
【6】时间到解除阻塞:
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
//sem: 信号量地址
//abs_timeout: 与pthread_cond_timedwait中的参数一样
【7】增加资源函数:
//给sem中的资源数+1
//若资源数从0加到1,那么阻塞的线程就会解除阻塞
int sem_post(sem_t *sem);
【8】查看资源数:
//查看信号量sem中的整形数值, 这个值会被写到sval指针对应的内存中
int sem_getvalue(sem_t *sem, int *sval);
程序实例:使用信号量实现生产者-消费者模型,生产者线程有5个,往链表头部添加节点;消费者线程也有5个,删除链表头部节点。同时,限定了平台容纳商品的最大容量为6个。我们可以设置两个信号量,分别来代表生产者线程生产的商品数量、平台中现有的商品数量(用于给消费者线程拿走)。
#include <stdio.h> #include <pthread.h> #include <semaphore.h> #include <stdlib.h> #include <unistd.h> /* 单链表节点声明 */ typedef struct LNode{ int number; struct LNode *next; } LNode; /* 全局变量 */ sem_t psem; //生产者生产商品的信号量 sem_t csem; //消费者拿走时的信号量 pthread_mutex_t mutex; //互斥锁 LNode *head = NULL; //单链表(临界区资源) /* 生产者线程执行函数 */ void* produce(void *arg) { while (1) { //生产者信号量-1 sem_wait(&psem); //生产者生产商品 pthread_mutex_lock(&mutex); LNode *tmp = (LNode*)malloc(sizeof(LNode)); tmp->number = rand() % 100; tmp->next = head; head = tmp; printf("生产完毕!新节点number: %d, 线程ID: %ld\n", tmp->number, pthread_self()); pthread_mutex_unlock(&mutex); //消费者信号量+1 sem_post(&csem); sleep(rand() % 3); } return NULL; } /* 消费者线程执行函数 */ void* consume(void *arg) { while (1) { //消费者信号量-1 sem_wait(&csem); //消费者拿走商品 pthread_mutex_lock(&mutex); LNode *tmp = head; head = head->next; printf("消费完毕!节点number: %d, 线程ID: %ld\n", tmp->number, pthread_self()); free(tmp); pthread_mutex_unlock(&mutex); //生产者信号量+1 sem_post(&psem); sleep(rand() % 3); } return NULL; } /* 主函数 */ int main(int argc, char **argv) { //初始化信号量和互斥锁 sem_init(&psem, 0, 6);//平台最多容纳的商品数 sem_init(&csem, 0, 0);//平台最初没有商品 pthread_mutex_init(&mutex, NULL); //创建5个生产者线程,5个消费者线程 pthread_t ptid[5]; pthread_t ctid[5]; for (int i = 0; i < 5; i++) { pthread_create(&ptid[i], NULL, produce, NULL); } for (int i = 0; i < 5; i++) { pthread_create(&ctid[i], NULL, consume, NULL); } //释放线程资源 for (int i = 0; i < 5; i++) { pthread_join(ptid[i], NULL); } for (int i = 0; i < 5; i++) { pthread_join(ctid[i], NULL); } //释放其它资源 sem_destroy(&psem); sem_destroy(&csem); pthread_mutex_destroy(&mutex); return 0; }
三、线程池
1. 作用
线程池内部维护了多个工作线程,每个工作线程都会去任务队列中拿取任务并执行,当执行完一个任务后不是马上销毁,而是继续保留执行其它任务。显然,线程池提高了多线程的复用率,减少了创建和销毁线程的时间。
2. 实现原理
线程池内部由任务队列、工作线程和管理者线程组成。
任务队列:存储需要处理的任务。每个任务其实就是具体的函数,在任务队列中存储函数指针和对应的实参。当工作线程获取任务后,就能根据函数指针来调用指定的函数。其实现可以是数组、链表、STL容器等。
工作线程:有N个工作线程,每个工作线程会去任务队列中拿取任务,然后执行具体的任务。当任务被处理后,任务队列中就不再有该任务了。当任务队列中没有任务时,工作线程就会阻塞。
管理者线程:周期性检测忙碌的工作线程数量和任务数量。当任务较多线程不够用时,管理者线程就会多创建几个工作线程来加快处理(不会超过工作线程数量的上限)。当任务较少线程空闲多时,管理者线程就会销毁几个工作线程来减少内存占用(不会低于工作线程数量的下限)。
注意:线程池中没有维护“生产者线程”,所谓的“生产者线程”就是往任务队列中添加任务的线程。
3. 手撕线程池
参考来源:爱编程的大丙。
【1】threadpool.c:
#include "threadpool.h"
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define NUMBER 2//增加或销毁的工作线程数量
//任务结构体
typedef struct Task
{
void (*fun)(void* arg);
void* arg;
} Task;
//线程池结构体
struct ThreadPool
{
//任务队列(视为循环队列)
Task* taskQ;
int queueCapacity; //最大容量
int queueSize; //当前任务数量
int queueFront; //队头 -> 取任务
int queueRear; //队尾 -> 加任务
//线程相关
pthread_t managerID; //管理者线程
pthread_t* workerIDs; //工作线程
int minNum; //工作线程最小数量
int maxNum; //工作线程最大数量
int liveNum; //工作线程存活数量
int busyNum; //工作线程繁忙数量
int exitNum; //工作线程退出数量
//互斥锁和互斥量
pthread_mutex_t mutexPool;//锁整个线程池
pthread_mutex_t mutexBusy;//锁busyNum(提升效率)
pthread_cond_t condWorker;//阻塞/唤醒工作线程
pthread_cond_t condProducer;//阻塞/唤醒生产者线程
//线程池销毁标志
int shutFlag; //1表示销毁,0表示非销毁
};
/***************************************************************
* 函 数: threadPoolCreate
* 功 能: 创建线程池并初始化
* 参 数: min---工作线程的最小数量
* max---工作线程的最大数量
* capacity---任务队列的最大容量
* 返回值: 创建的线程池的地址
**************************************************************/
ThreadPool* threadPoolCreate(int min, int max, int capacity)
{
//创建线程池
ThreadPool* pool = (ThreadPool*)malloc(sizeof(ThreadPool));
if (!pool)
return NULL;
//初始化任务队列
pool->taskQ = (Task*)malloc(sizeof(Task) * capacity);
if (!pool->taskQ) {
free(pool);
return NULL;
}
pool->queueCapacity = capacity;
pool->queueSize = 0;
pool->queueFront = 0;
pool->queueRear = 0;
//初始化互斥锁和条件变量
if (pthread_mutex_init(&pool->mutexBusy, NULL) != 0 &&
pthread_mutex_init(&pool->mutexPool, NULL) != 0 &&
pthread_cond_init(&pool->condProducer, NULL) != 0 &&
pthread_cond_init(&pool->condWorker, NULL) != 0)
{
perror("mutex or cond init error");
free(pool->taskQ);
free(pool);
return NULL;
}
//初始化销毁标志
pool->shutFlag = 0;
//初始化线程
pool->minNum = min;
pool->maxNum = max;
pool->liveNum = min;//设置默认存活的工作线程数为最小
pool->busyNum = 0;
pool->exitNum = 0;
pool->workerIDs = (pthread_t*)malloc(sizeof(pthread_t) * max);
if (!pool->workerIDs) {
free(pool->taskQ);
free(pool);
return NULL;
}
memset(pool->workerIDs, 0, sizeof(pthread_t) * max);//初始化所有工作线程ID为0
pthread_create(&pool->managerID, NULL, manager, pool);
for (int i = 0; i < min; ++i) {
pthread_create(&pool->workerIDs[i], NULL, worker, pool);
}
return pool;
}
/***************************************************************
* 函 数: threadPoolDestroy
* 功 能: 销毁线程池
* 参 数: pool---要销毁的线程池
* 返回值: 0表示销毁成功,-1表示销毁失败
**************************************************************/
int threadPoolDestroy(ThreadPool* pool)
{
if (!pool)
return -1;
//修改线程池销毁标志
pool->shutFlag = 1;
//阻塞回收管理者线程
if (pthread_join(pool->managerID, NULL)) {
printf("manager thread destroy success!\n");
}
//唤醒所有工作线程,使其销毁
for (int i = 0; i < pool->liveNum; i++) {
pthread_cond_signal(&pool->condWorker);
}
sleep(3);//保证工作线程全部销毁
//销毁互斥锁和条件变量
pthread_mutex_destroy(&pool->mutexBusy);
pthread_mutex_destroy(&pool->mutexPool);
pthread_cond_destroy(&pool->condProducer);
pthread_cond_destroy(&pool->condWorker);
//回收堆空间资源
if (pool->taskQ) {
free(pool->taskQ);
pool->taskQ = NULL;
}
if (pool->workerIDs) {
free(pool->workerIDs);
pool->workerIDs = NULL;
}
free(pool);
pool = NULL;
return 0;
}
/***************************************************************
* 函 数: threadPoolAdd
* 功 能: 生产者往线程池的任务队列中添加任务
* 参 数: pool---线程池
* task---函数指针,要执行的任务地址
* arg---func指向的函数的实参地址
* 返回值: 无
**************************************************************/
void threadPoolAdd(ThreadPool* pool, void (*task)(void* arg), void* arg)
{
if (!pool) return;
pthread_mutex_lock(&pool->mutexPool);
while (pool->queueSize == pool->queueCapacity && !pool->shutFlag) {
pthread_cond_wait(&pool->condProducer, &pool->mutexPool);
}
if (pool->shutFlag) {
pthread_mutex_unlock(&pool->mutexPool);
return;
}
pool->taskQ[pool->queueRear].fun = task;
pool->taskQ[pool->queueRear].arg = arg;
pool->queueSize++;
pool->queueRear = (pool->queueRear + 1) % pool->queueCapacity;
pthread_cond_signal(&pool->condWorker);
pthread_mutex_unlock(&pool->mutexPool);
}
/***************************************************************
* 函 数: worker
* 功 能: 工作线程的执行函数
* 参 数: arg---实参传入,这里传入的是线程池
* 返回值: 空指针(实则不会返回,而是调用threadExit退出)
**************************************************************/
void* worker(void* arg)
{
ThreadPool* pool = (ThreadPool*)arg;
while (1) {
/* 1.获取任务队列中的任务 */
pthread_mutex_lock(&pool->mutexPool);
//任务队列无任务则阻塞等待
while (pool->queueSize == 0 && !pool->shutFlag) {
pthread_cond_wait(&pool->condWorker, &pool->mutexPool);
//被唤醒情况:管理者线程让工作线程销毁
if (pool->exitNum > 0) {
pool->exitNum--;
if (pool->liveNum > pool->minNum) {//存活>最小时才销毁线程
pthread_mutex_unlock(&pool->mutexPool);
printf("manager inform destroy: ");
threadExit(pool);
}
}
}
//被唤醒情况:线程池关闭
if (pool->shutFlag) {
pthread_mutex_unlock(&pool->mutexPool);
threadExit(pool);
}
//被唤醒情况:任务队列有任务
Task task;
task.fun = pool->taskQ[pool->queueFront].fun;
task.arg = pool->taskQ[pool->queueFront].arg;
pool->queueFront = (pool->queueFront + 1) % pool->queueCapacity;
pool->queueSize--;
pthread_cond_signal(&pool->condProducer);//通知生产者添加任务
pthread_mutex_unlock(&pool->mutexPool);
/* 2.执行获取的任务 */
pthread_mutex_lock(&pool->mutexBusy);
pool->busyNum++;
pthread_mutex_unlock(&pool->mutexBusy);
printf("thread %ld start working...\n", pthread_self());
task.fun(task.arg);
free(task.arg);//由于arg是堆中的资源,执行完释放
task.arg = NULL;
printf("thread %ld end working...\n", pthread_self());
pthread_mutex_lock(&pool->mutexBusy);
pool->busyNum--;
pthread_mutex_unlock(&pool->mutexBusy);
}
return NULL;
}
/***************************************************************
* 函 数: manager
* 功 能: 管理者线程的执行函数
* 参 数: arg---实参传入,这里传入的是线程池
* 返回值: 空指针,当线程池关闭后就执行完毕
**************************************************************/
void* manager(void* arg)
{
ThreadPool* pool = (ThreadPool*)arg;
while (!pool->shutFlag) {
//周期性检测: 每3秒
sleep(3);
//功能一:增加工作线程
pthread_mutex_lock(&pool->mutexPool);
if (pool->liveNum < pool->queueSize && pool->liveNum < pool->maxNum) {
int count = 0;
for (int i = 0; i < pool->maxNum && count < NUMBER
&& pool->liveNum < pool->maxNum; ++i)
{
if (pool->workerIDs[i] == 0) {
count++;
pthread_create(&pool->workerIDs[i], NULL, worker, pool);
pool->liveNum++;
printf("create thread\n");
}
}
}
pthread_mutex_unlock(&pool->mutexPool);
//功能二:销毁工作线程
pthread_mutex_lock(&pool->mutexPool);
if (pool->busyNum * 2 < pool->liveNum && pool->liveNum > pool->minNum) {
pool->exitNum = NUMBER;
for (int i = 0; i < NUMBER; i++) {
pthread_cond_signal(&pool->condWorker);
}
}
pthread_mutex_unlock(&pool->mutexPool);
}
return NULL;
}
/***************************************************************
* 函 数: threadExit
* 功 能: 工作线程退出函,将工作线程的ID置为0,然后liveNum--,再退出
* 参 数: pool---线程池
* 返回值: 无
**************************************************************/
void threadExit(ThreadPool* pool)
{
//修改pool->workerIDs里对应的ID为0
pthread_t tid = pthread_self();
pthread_mutex_lock(&pool->mutexPool);
for (int i = 0; i < pool->maxNum; ++i) {
if (tid == pool->workerIDs[i]) {
pool->workerIDs[i] = 0;
pool->liveNum--;
printf("threadExit() called, %ld exit... liveNum = %d\n", tid, pool->liveNum);
break;
}
}
pthread_mutex_unlock(&pool->mutexPool);
//工作线程退出
pthread_exit(NULL);
}
【2】threadpool.h:
#ifndef _THREADPOOL_H
#define _THREADPOOL_H
typedef struct ThreadPool ThreadPool;
//创建并初始化线程池
ThreadPool* threadPoolCreate(int min, int max, int capacity);
//销毁线程池
int threadPoolDestroy(ThreadPool* pool);
//向线程池中添加任务
void threadPoolAdd(ThreadPool* pool, void (*task)(void* arg), void* arg);
/*************其它函数*******************/
void* worker(void* arg);//工作线程的执行函数
void* manager(void* arg);//管理者线程的执行函数
void threadExit(ThreadPool* pool);//工作线程退出函数
#endif
【3】main.c:
#include <stdio.h>
#include "threadpool.h"
#include <pthread.h>
#include <unistd.h>
//任务函数,所有线程都执行此任务
void testTask(void* arg)
{
int* num = (int*)arg;
printf("threadID: %ld, num: %d\n", pthread_self(), *num);
sleep(1);
}
int main(int argc, char** argv)
{
//创建线程池: 最少3个工作线程,最多10个,任务队列容量为50
ThreadPool* pool = threadPoolCreate(3, 10, 50);
//添加任务到任务队列中
for (int i = 1; i <= 50; ++i) {
int* num = (int*)malloc(sizeof(int));
*num = i;
threadPoolAdd(pool, testTask, num);
}
//销毁线程池
sleep(25);//保证所有的工作线程执行完毕
threadPoolDestroy(pool);
return 0;
}
【4】运行结果:
......