目录
Linux 多线程开发
一, 线程概述
1. 初识线程
与进程类似,线程是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程。
同一个程序中的所有线程均会独立执行不同程序,且共享同一份全局内存区域,其中包括初始化数据段、未初始化数据段,以及堆内存段等。
进程是CPU分配资源的最小单位,线程是操作系统调度执行的最小单位。
线程是轻量级的进程(LWP:Light Weight Process),在Linux环境下线程的本质仍然是进程。
查看指定进程的LWP号命令:
ps -Lf pid
。
示例:使用ps aux
查看浏览器的进程pid=4227,然后使用ps -Lf 4227
查看浏览器的LWP,有如下几个线程,它们的pid都是4227,而LWP都是不同的。说明每一个线程都有它自己对应的线程号。
2. 进程和线程的区别
- 进程间信息难以共享。除去只读代码段,父子进程并未共享内存,因此必须采用一些进程间通信方式(如管道,有名管道,内存映射,信号,共享内存等)在进程间进行信息交换。
- 调用fork()创建进程的代价较高,即便利用写时复制机制,仍然需要复制像内存页表和文件描述符表之类的多种进程属性,这意味这fork()调用在时间上的开销很大。
- 线程之间能够方便、快速地共享信息。只需要将数据复制到共享(全局或堆)变量中即可。
- 创建线程比创建进程通常要快10倍甚至更多。线程间共享虚拟地址空间,无需采用写时复制来复制内存,也无需复制页表。
3. 线程之间共享和非共享的资源
共享的资源 | 非共享的资源 |
---|---|
进程ID和父进程D | 线程ID |
进程组ID和会话ID | 信号掩码 |
用户ID和用户组ID | 线程特有数据 |
文件描述符表 | error变量 |
文件系统的相关信息,如: 文件权限掩码、当前工作目录 |
实时调度策略和优先级 |
虚拟地址空间(除栈、.text代码段外) | 栈、本地变量和函数的调用链接信息 |
4. NPTL
NPTL,Native POSIX Thread Library,是Linux线程的一个实现。线程开发相关的操作函数不是系统提供的函数,而是使用NPTL提供的一系列库函数,所以使用线程相关函数时,在编译或链接时需要加上-pthread
或-lpthread
,建议使用-pthread
。
查看当前NPTL线程库版本:getconf GNU_LIBPTHREAD_VERSION
如下测试当前的NPTL版本为2.27。
二, 线程操作相关函数
在介绍多线程操作相关函数时先了解下主线程和子线程:
一般情况下,main函数所在线程称为主线程(main线程),创建的其余线程称为子线程。
程序中默认只有一个进程,fork()创建子进程后,会有2个进程;
程序中默认只有一个线程,pthread_create()调用后会产生2个线程;
多线程操作常用函数介绍:
函数名 | 说明 |
---|---|
pthread_create() |
创建子线程 |
pthread_exit() |
终止子线程,退出子线程 |
pthread_equal() |
比较两个线程id是否一致 |
pthread_self() |
获取当前线程的线程id |
pthread_join() |
连接已经终止的线程 |
pthread_detach() |
线程分离,不能连接已经分离的线程 |
pthread_cancel() |
取消线程 |
当使用多线程函数pthread_*
时,编译和链接需要使用 -lpthread
或-pthread
。
1. 创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t *sttr, void*(*start_routine) (void*), void *arg); // 创建线程
pthread_create()
函数介绍
项目 | 说明 |
---|---|
头文件 | #include <pthread.h> |
函数声明 | int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); |
函数功能 | 创建一个新的子线程 一般认为main()所在的线程是主线程(main线程),其余创建的线程称为子线程。 |
参数thread |
pthread_t 类型,线程创建成功后,存储子线程的线程ID |
参数atttr |
pthread_attr_t 结构体指针,设置线程的属性,一般使用默认值NULL |
参数start_routine |
函数指针,子线程处理的逻辑代码,是一个回调函数 函数声明: void *callback(void *arg); |
参数arg |
给start_routine 指定的回调函数传递参数 |
返回值 | 成功返回0; 失败返回错误号,获取错误号信息要使用 char *strerror(int errnum) |
使用示例:
/**
* @file create_pthread.c
* @author zoya (2314902703@qq.com)
* @brief 创建子线程示例
* @version 0.1
* @@date: 2022-10-04
*
* @copyright Copyright (c) 2022
*
*/
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// 回调函数
void *callback1(void *arg)
{
printf("tid: 0x%lx, child thread 1..., arg value = %d\n", pthread_self(), *(int *)arg);
return NULL;
}
void *callback2(void *arg)
{
printf("tid: 0x%lx, child thread 2..., arg value = %d\n", pthread_self(), *(int *)arg);
return NULL;
}
int main()
{
// 创建子线程
pthread_t tid1, tid2;
int num = 2;
int ret = pthread_create(&tid1, NULL, callback1, (void *)&num);
if (ret != 0)
{
printf("%s\n", strerror(ret));
exit(-1);
}
num = 5;
ret = pthread_create(&tid2, NULL, callback2, (void *)&num);
for (int i = 0; i < 5; ++i)
{
printf("%d\n", i);
}
sleep(1);
return 0;
}
程序运行:
2. 终止线程
void pthread_exit(void *retval);
pthread_exit()
函数介绍
项目 | 说明 |
---|---|
头文件 | #include <pthread.h> |
函数声明 | void pthread_exit(void *retval); |
函数功能 | 终止线程,在哪个线程中调用就终止哪个线程 |
参数retval |
作为返回值,可以在pthread_join() 中获取到,如果不需要设置为NULL注意:返回值不能是局部变量,因为局部变量是在栈空间,函数运行结束后就自动销毁了,无法返回正确的值 |
返回值 | 无返回值 |
终止线程示例:在主线程中执行pthread_exit()
,那么该语句之后的语句就不会被执行。
/**
* @file exit_pthread.c
* @author zoya (2314902703@qq.com)
* @brief 终止线程示例
* @version 0.1
* @@date: 2022-10-04
*
* `pthread_self()`:获取当前线程的线程ID
*
* @copyright Copyright (c) 2022
*
*/
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// 回调函数
void *callback1(void *arg)
{
printf("tid : 0x%lx, child thread 1..., arg value = %d\n", pthread_self(), *(int *)arg);
return NULL; // 相当于pthread_exit(NULL);
}
int main()
{
// 创建子线程
pthread_t tid1, tid2;
int num = 2;
int ret = pthread_create(&tid1, NULL, callback1, (void *)&num);
if (ret != 0)
{
printf("error : %s\n", strerror(ret));
exit(-1);
}
// 主线程执行代码
for (int i = 0; i < 5; ++i)
{
printf(" %d \n", i);
}
printf("tid : 0x%lx, main thread id : 0x%lx\n",tid1,pthread_self());
// 主线程退出,主线程退出时,不会影响其它正常运行的线程
pthread_exit(NULL);
printf("main thread...\n");
return 0;
}
程序运行,可以看到pthread_exit()
后面的语句没有执行,没有打印main thread...
。
3. 比较两个线程ID
int pthread_equal(pthread_t t1, pthread_t t2);
pthread_equal()
函数介绍
项目 | 说明 |
---|---|
头文件 | #include <pthread.h> |
函数声明 | int pthread_equal(pthread_t t1, pthread_t t2); |
函数功能 | 比较两个线程ID是否相等 不同操作系统pthread_t类型的实现不同,有的是无符号长整型,有的是结构体实现。当pthread_t是结构体实现时,就需要使用 pthread_equal() 函数比较线程id是否相等。 |
参数t1 和t2 |
要比较的线程id |
返回值 | 相等返回非0值,不相等返回0 |
4. 连接已经终止的线程
int pthread_join(pthread_t thread, void **retval);
pthread_join()
函数介绍
项目 | 说明 |
---|---|
头文件 | #include <pthread.h> |
函数声明 | int pthread_join(pthread_t thread, void **retval); |
函数功能 | 和一个已经终止的线程连接,回收子线程的资源 |
参数thread |
要回收的子线程的线程号 |
参数retval |
二级指针,接收子线程退出pthread_exit() 时的返回值 |
返回值 | 成功返回0 失败返回错误号 |
备注 | 是阻塞的,调用一次回收一个子线程; 一般在主线程中使用 |
回收子线程资源示例:
/**
* @file join_thread.c
* @author zoya (2314902703@qq.com)
* @brief 和已经终止的线程连接示例
* @version 0.1
* @@date: 2022-10-04
*
* @copyright Copyright (c) 2022
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
int value = 10;
// 回调函数
void *callback1(void *arg)
{
printf("tid : 0x%lx, child thread 1..., arg value = %d\n", pthread_self(), *(int *)arg);
sleep(3);
pthread_exit((void *)&value); // 子线程退出,返回值为value = return (void*)&value;
// return NULL; // 相当于pthread_exit(NULL);
}
int main()
{
// 创建子线程
pthread_t tid1, tid2;
int num = 2;
int ret = pthread_create(&tid1, NULL, callback1, (void *)&num);
if (ret != 0)
{
printf("error : %s\n", strerror(ret));
exit(-1);
}
// 主线程执行代码
for (int i = 0; i < 5; ++i)
{
printf(" %d \n", i);
}
printf("tid : 0x%lx, main thread id : 0x%lx\n", tid1, pthread_self());
// 主线程调用pthread_join回收子线程资源
#if 0
// 无返回值
ret = pthread_join(tid1, NULL); // 默认阻塞
if (ret != 0)
{
printf("error : %s\n", strerror(ret));
exit(-1);
}
#else
// 有返回值
int *thread_retval;
ret = pthread_join(tid1, (void **)&thread_retval);
if (ret != 0)
{
printf("error : %s\n", strerror(ret));
exit(-1);
}
printf("exit value : %d\n", *thread_retval);
#endif
printf("回收子线程资源成功\n");
// 主线程退出,主线程退出时,不会影响其它正常运行的线程
pthread_exit(NULL);
return 0;
}
程序运行:
5. 线程分离
int pthread_detach(pthread_t thread);
pthread_detach()
函数介绍
项目 | 说明 |
---|---|
头文件 | #include <pthread.h> |
函数声明 | int pthread_detach(pthread_t thread); |
函数功能 | 分离一个线程,被分离的线程在终止时自动释放资源,要注意: 1. 不能多次分离,否则会产生不可预料的行为 2. 不能连接已经分离的线程,否则会报错 |
参数thread |
要分离的线程的线程号 |
返回值 | 成功返回0 失败返回错误号 |
线程分离示例:
/**
* @file detach_pthread.c
* @author zoya (2314902703@qq.com)
* @brief 分离线程
* @version 0.1
* @@date: 2022-10-04
*
* @copyright Copyright (c) 2022
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
void *callback(void *arg)
{
printf("child thread id : 0x%lx\n", pthread_self());
return NULL;
}
int main()
{
// 创建子线程
pthread_t tid;
int ret = pthread_create(&tid, NULL, callback, NULL);
if (ret != 0)
{
printf("pthread_create: %s\n", strerror(ret));
exit(-1);
}
// 输出主线程和子线程id
printf("tid : 0x%lx, main thread id : 0x%lx\n", tid, pthread_self());
// 设置子线程分离,子线程分离后,子线程结束时对应的资源不需要主线程释放
ret = pthread_detach(tid);
if (ret != 0)
{
printf("pthread_detach: %s\n", strerror(ret));
exit(-1);
}
// 设置分离后对子线程连接
ret = pthread_join(tid, NULL);
if (ret != 0)
{
// 输出会打印:pthread_join: Invalid argument
printf("pthread_join: %s\n", strerror(ret));
exit(-1);
}
// 退出主线程
pthread_exit(NULL);
return 0;
}
程序运行:
6. 线程取消
int pthread_cancel(pthread_t thread);
pthread_cancel()
函数介绍
项目 | 说明 |
---|---|
头文件 | #include <pthread.h> |
函数声明 | int pthread_cancel(pthread_t thread); |
函数功能 | 取消线程,让线程终止 默认情况下,并不是立即终止线程,而是子线程执行到一个取消点,线程才会终止 取消点是系统规定好的一些系统调用,可以简单理解取消点是用户区到内核区的切换位置 |
参数thread |
要取消的线程的线程号 |
返回值 | 成功返回0 失败返回错误号 |
线程取消示例:
/**
* @file cancel_pthread.c
* @author zoya (2314902703@qq.com)
* @brief 取消线程
* @version 0.1
* @@date: 2022-10-04
*
* @copyright Copyright (c) 2022
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
void *callback(void *arg)
{
printf("child thread id : 0x%lx\n", pthread_self());
for (int i = 0; i < 5; ++i)
{
printf("child thread , %d\n", i);
}
return NULL;
}
int main()
{
// 创建子线程
pthread_t tid;
int ret = pthread_create(&tid, NULL, callback, NULL);
if (ret != 0)
{
printf("pthread_create: %s\n", strerror(ret));
exit(-1);
}
// 取消线程
ret = pthread_cancel(tid);
if (ret != 0)
{
printf("pthread_cancel: %s\n", strerror(ret));
exit(-1);
}
for (int i = 0; i < 5; ++i)
{
printf("%d\n", i);
}
// 输出主线程和子线程id
printf("tid : 0x%lx, main thread id : 0x%lx\n", tid, pthread_self());
// 退出主线程
pthread_exit(NULL);
return 0;
}
程序运行,第一次运行时,子线程还没运行完,因为触发了取消点,子线程终止了。
多线程开发步骤:
- 创建线程:
pthread_create();
- 回收子线程资源,使用线程分离或者连接线程函数回收子线程资源;
三, 线程属性相关操作
线程属性相关步骤:
- 声明属性变量;
pthread_attr_t attr;
- 初始化;
pthread_attr_init()
- 使用完销毁
pthread_attr_destroy()
属性变量声明:pthread_attr_t attr;
int pthread_attr_init(pthread_attr_t *attr); // 初始化线程属性变量
int pthread_attr_destroy(pthread_attr_t *attr); // 释放线程属性资源
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); // 获取线程分离的状态属性
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate); // 设置线程分离的状态属性
参数
attr
:属性变量;
参数detachstate
:
PTHREAD_CREATE_DETACHED 线程分离
PTHREAD_CREATE_JOINABLE 加入线程,默认
示例:
/**
* @file attr_pthread.c
* @author zoya (2314902703@qq.com)
* @brief 线程属性相关
* @version 0.1
* @@date: 2022-10-04
*
* @copyright Copyright (c) 2022
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
void *callback(void *arg)
{
printf("child thread id : 0x%lx\n", pthread_self());
for (int i = 0; i < 5; i++)
{
printf(" %d child thread, tid : 0x%lx\n", i, pthread_self());
sleep(1);
}
return NULL;
}
int main()
{
// 创建线程属性变量
pthread_attr_t attr;
// 初始化属性变量
pthread_attr_init(&attr);
// 设置属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // 设置线程分离
// 创建子线程
pthread_t tid;
int ret = pthread_create(&tid, &attr, callback, NULL);
if (ret != 0)
{
printf("pthread_create: %s\n", strerror(ret));
exit(-1);
}
// 获取线程栈大小
size_t stacksize;
pthread_attr_getstacksize(&attr, &stacksize);
printf("thread id : 0x%lx get stack size : %ld\n", pthread_self(), stacksize);
for (int i = 0; i < 5; ++i)
{
printf("%d\n", i);
}
// 输出主线程和子线程id
printf("tid : 0x%lx, main thread id : 0x%lx\n", tid, pthread_self());
// 释放属性资源
pthread_attr_destroy(&attr);
printf("释放属性资源成功\n");
// 退出主线程
pthread_exit(NULL);
return 0;
}
程序运行:
四, 线程同步
4.1. 初识线程同步
线程的主要优势在于能够通过全局变量来共享信息,但是这种共享也带来数据安全的问题:必须确保多个线程不会同时修改同一个变量,或者某一个线程不会读取正在由其它线程修改的变量。
临界区是指访问某一共享资源的代码片段,这段代码的执行应该为原子操作,也就是访问同一个共享资源的其它线程不应该中断该片段的执行。
线程同步:当有一个线程在对内存进行操作时,其它线程都不可以对这个内存地址进行操作,直到该线程完成操作,其它线程才能对该内存地址进行操作,而其它线程则处于等待状态。
线程同步会降低线程并发的效率,但线程同步是必须的。
示例:有一个简单售票系统,总共有100张票,有3个窗口售卖,使用多线程方式售票,使用前面提到的多线程开发相关函数,代码参考如下:
/**
* @file selltickets.c
* @author zoya (2314902703@qq.com)
* @brief 使用多线程实现卖票示例
* @version 0.1
* @@date: 2022-10-05
*
总共有100张票,三个窗口售卖
* @copyright Copyright (c) 2022
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
int tickets = 100; // 所有线程共享这一份资源
void *selltickets(void *args)
{
// 售票
while (tickets > 0)
{
usleep(5000);
printf("0x%lx 正在卖第%d张票\n", pthread_self(), tickets);
tickets--;
}
return NULL;
}
int main()
{
printf("**************欢迎访问售票系统******************\n");
// 创建子线程
pthread_t tid1, tid2, tid3;
pthread_create(&tid1, NULL, selltickets, NULL);
pthread_create(&tid2, NULL, selltickets, NULL);
pthread_create(&tid3, NULL, selltickets, NULL);
// 回收子线程资源
#if 1
// 阻塞回收子线程资源
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
#else
// 设置线程分离
pthread_detach(tid1);
pthread_detach(tid2);
pthread_detach(tid3);
#endif
printf("**********************结束访问***********************\n");
pthread_exit(NULL); //退出主线程
return 0;
}
运行以上代码,会发现有问题,有的票卖了多次,还有卖了第0张和第-1张票,这显然是不符合实际的,这也是线程同步需要解决的问题。
4.2. 互斥量实现线程同步
互斥量也称为互斥锁。
为避免线程共享变量时出现问题,可以使用互斥量/互斥锁(mutex,mutual exclusion)来确保同时仅有一个线程可以访问某项共享资源,可以使用互斥量保证对任意共享资源的原子访问。
互斥量有2种状态:已锁定(locked)
和未锁定(unlocked)
。任何时候,最多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁,将可能阻塞线程或者报错失败,具体取决于加锁时使用的方法。
一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁。一般情况下,对每一个共享资源(可能有多个共享变量)会使用不同的互斥量,每一个线程在访问同一资源时将采用如下协议:
- 针对共享资源锁定互斥量;
- 访问共享资源;
- 对互斥量解锁;
如果多个线程试图执行一块代码(临界区),事实上只有一个线程持有该互斥量(其它线程将遭到阻塞),即同时只有一个线程能够进入这段代码区域。
要注意:解锁顺序要按照加锁的逆序。
4.2.1 互斥量操作相关函数
互斥量类型 pthread_mutex_t
int pthread_mutex_init(pthread_mutext_t *restrict mutex,const pthread_mutexattr_t *restrict attr); // 初始化互斥量
int pthread_mutex_destroy(pthread_mutext_t *restrict mutex); // 销毁互斥量
int pthread_mutex_lock(pthread_mutext_t *restrict mutex); // 互斥量加锁
int pthread_mutex_trylock(pthread_mutext_t *restrict mutex); // 尝试加锁
int pthread_mutex_unlock(pthread_mutext_t *restrict mutex); // 解锁
函数名 | 函数功能 | 参数 |
---|---|---|
pthread_mutex_init() |
初始化互斥量 | – mutex :需要初始化的互斥量变量– attr :互斥量相关属性,不使用就设置为NULL |
pthread_mutex_destroy() |
释放互斥量资源 | – mutex :需要释放的互斥量变量 |
pthread_mutex_lock() |
加锁,是阻塞的 如果有一个线程加锁,其它线程只能阻塞等待 |
– mutex :需要加锁的互斥量变量 |
pthread_mutex_trylock() |
尝试加锁,如果加锁失败,不会阻塞,直接返回 | – mutex :尝试加锁的互斥量变量 |
pthread_mutex_unlock() |
解锁 | – mutex :需要解锁的互斥量 |
restrict
是C语言修饰符,被修饰的指针不能由另外的指针进行操作。
互斥量操作步骤:
- 声明互斥量变量:
pthread_mutex_t mutex;
- 初始化互斥量:
pthread_mutex_init();
- 释放互斥量:
pthread_mutex_destroy()
- 在访问共享资源前加锁,访问后解锁;
使用互斥量解决售票问题:
/**
* @file mutex.c
* @author zoya (2314902703@qq.com)
* @brief 使用互斥量实现线程同步示例
* @version 0.1
* @@date: 2022-10-05
*
总共有100张票,三个窗口售卖
* @copyright Copyright (c) 2022
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <time.h>
int tickets = 1000; // 所有线程共享这一份资源
// 创建互斥量
pthread_mutex_t mutex;
void *selltickets(void *args)
{
// 售票
while (1)
{
// 加锁
pthread_mutex_lock(&mutex);
if (tickets > 0)
{
printf("%ld 正在卖第%d张票\n", pthread_self(), tickets);
tickets--;
}
else
{
// 解锁
pthread_mutex_unlock(&mutex);
break;
}
// 解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
printf("**************欢迎访问售票系统******************\n");
// 初始化互斥量
pthread_mutex_init(&mutex, NULL);
// 创建子线程
pthread_t tid1, tid2, tid3;
pthread_create(&tid1, NULL, selltickets, NULL);
pthread_create(&tid2, NULL, selltickets, NULL);
pthread_create(&tid3, NULL, selltickets, NULL);
// 回收子线程资源
#if 1
// 阻塞回收子线程资源
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
#else
// 设置线程分离
pthread_detach(tid1);
pthread_detach(tid2);
pthread_detach(tid3);
#endif
// 释放互斥量资源
pthread_mutex_destroy(&mutex);
printf("**********************结束访问***********************\n");
pthread_exit(NULL); //退出主线程
return 0;
}
程序运行如下:
4.2.2 死锁问题
有时,一个线程需要同时访问两个或更多不同的共享资源,而每个资源又都由不同的互斥量管理,当超过一个线程加锁同一组互斥量时,就有可能发生死锁。
两个或两个以上的线程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,如果无外力作用,它们都将无法推进下去,此时称系统处于死锁状态或系统产生了死锁。
可能产生死锁的几种场景:
- 忘记释放锁;
- 重复加锁;
- 多线程多锁,抢占锁资源;
- 如下,有两个线程A和B,有两个共享变量M和N,在A中先对M加锁,然后对N加锁,在B中先对N加锁,然后对M加锁;那么就可能存在这种情况:A对M加了锁 → 进程调度CPU给到了B,B对N加锁,此时不论是A对N加锁,还是B对M加锁,都会阻塞,
- A对N加锁阻塞,因为B已经对N加了锁,
- B对M加锁阻塞,因为A对M已经加了锁
- 如下,有两个线程A和B,有两个共享变量M和N,在A中先对M加锁,然后对N加锁,在B中先对N加锁,然后对M加锁;那么就可能存在这种情况:A对M加了锁 → 进程调度CPU给到了B,B对N加锁,此时不论是A对N加锁,还是B对M加锁,都会阻塞,
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
// 创建2个互斥量
pthread_mutex_t m1, m2;
void *callbackA(void *arg)
{
printf("cakkback A....\n");
// 加锁
pthread_mutex_lock(&m1);
sleep(1);
pthread_mutex_lock(&m2);
printf("callbackA...\n");
// 解锁
pthread_mutex_unlock(&m2);
pthread_mutex_unlock(&m1);
return NULL;
}
void *callbackB(void *arg)
{
printf("cakkback B....\n");
// 加锁
pthread_mutex_lock(&m2);
sleep(1);
pthread_mutex_lock(&m1);
printf("callbackB...\n");
// 解锁
pthread_mutex_unlock(&m1);
pthread_mutex_unlock(&m2);
return NULL;
}
int main()
{
// 初始化互斥量
pthread_mutex_init(&m1, NULL);
pthread_mutex_init(&m2, NULL);
// 创建子线程
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, callbackA, NULL);
pthread_create(&tid2, NULL, callbackB, NULL);
// 回收子线程资源
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
// 释放互斥量
pthread_mutex_destroy(&m1);
pthread_mutex_destroy(&m2);
return 0;
}
程序运行:
4.3. 读写锁
当有一个线程已经有加锁互斥量,互斥锁将所有试图进入临界区的线程都阻塞。
但是需要考虑一种情况,当前持有互斥锁的线程只是要读访问
共享资源,而同时有其它几个线程也想读取
这个共享资源,但是由于互斥锁的排它性,所有其它的线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题
。
在对数据的读写操作中,更多的是读操作,写操作比较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。
读写锁的特点:
- 如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作;
- 如果有其它线程写数据,则其它线程都不允许读、写操作;
- 写是独占的,写的优先级高;
4.3.1 读写锁相关操作函数
读写锁类型 pthread_rwlock_t
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr); // 初始化读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *restrict rwlock); // 销毁读写锁
int pthread_rwlock_rdlock(pthread_rwlock_t *restrict rwlock); // 读锁加锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *restrict rwlock); // 读锁尝试加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *restrict rwlock); // 写锁加锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *restrict rwlock); // 写锁尝试加锁
int pthread_rwlock_unlock(pthread_rwlock_t *restrict rwlock); // 解锁
- 如果man中没有
pthread_rwlock_t
相关信息,可以先安装sudo apt-get install magpages-posix-dev
;- 使用过程中如果出现
pthread_rwlock_t
识别不到,可以在文件开始加上#define _XOPEN_SOURCE 500
解决。
读写锁示例:
/**
* @file rwlock.c
* @author zoya (2314902703@qq.com)
* @brief 读写锁示例
* @version 0.1
* @@date: 2022-10-05
*
创建多个线程操作同一个全局变量,其中3个线程不定时写全局变量,其余5个不定时线程读全局变量
* @copyright Copyright (c) 2022
*
*/
#define _XOPEN_SOURCE 500
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <unistd.h>
// 全局变量,共享数据
int num = 1;
// 读写锁
pthread_rwlock_t lock;
void *writenum(void *arg)
{
while (1)
{
pthread_rwlock_wrlock(&lock);
num++;
printf("++write, tid : %ld, num : %d\n", pthread_self(), num);
pthread_rwlock_unlock(&lock);
usleep(100);
}
return NULL;
}
void *readnum(void *arg)
{
while (1)
{
pthread_rwlock_rdlock(&lock);
printf("---read, tid : %ld, num : %d\n", pthread_self(), num);
pthread_rwlock_unlock(&lock);
usleep(100);
}
return NULL;
}
int main()
{
// 读写锁初始化
pthread_rwlock_init(&lock, NULL);
pthread_t wtids[3],
rtids[5];
// 创建线程
for (int i = 0; i < 3; ++i)
{
pthread_create(&wtids[i], NULL, writenum, NULL);
}
for (int i = 0; i < 5; ++i)
{
pthread_create(&rtids[i], NULL, readnum, NULL);
}
// 设置线程分离
for (int i = 0; i < 3; ++i)
{
pthread_detach(wtids[i]);
}
for (int i = 0; i < 5; ++i)
{
pthread_detach(rtids[i]);
}
pthread_exit(NULL);
// 释放锁
pthread_rwlock_destroy(&lock);
return 0;
}
4.4 生产者消费者模型
4.4.1 什么是生产者-消费者模型
简单的生产者-消费者模型可以理解为:产品存放在容器中,生产者生产产品后放到容器中,消费者从容器中取出产品。生产者和消费者分别是两个线程,容器相当于内存缓冲区。
从上面描述中可以知道生产者-消费者模型中的对象有生产者、消费者、容器。
在处理过程中可能会出现两个问题:
- 容器满了,生产者就不能再生产; 解决:通知消费者消费
- 容器空了,消费者就不能再消费; 解决:通知生产者生产
生产者-消费者模型特点:
- 保证生产者不会在容器满的时候继续向容器放入数据,消费者不会在容器空的时候从容器中取出数据;
- 当缓冲区满,生产者进入休眠状态(阻塞),当消费者开始消耗容器中的数据时,生产者被唤醒,开始向添加数据;
- 当缓冲区空,消费者进入休眠状态(阻塞),当生产者向容器中放入数据时,消费者被唤醒,开始从容器中取出数据;
当然,可以使用互斥量使用生产者-消费者模型,参考代码如下:
/**
* @file 2prodcust.c
* @author zoya (2314902703@qq.com)
* @brief 生产者/消费者模型, 使用互斥量实现生产者-消费者模型
* @version 0.1
* @@date: 2022-10-05
*
* @copyright Copyright (c) 2022
*
*/
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
struct Node
{
int num;
struct Node *next;
};
struct Node *g_head = NULL;
// 创建互斥量
pthread_mutex_t mutex;
void *producer(void *arg)
{
// 生产者,不断向容器中添加内容
while (1)
{
// 加锁
pthread_mutex_lock(&mutex);
struct Node *newnode = (struct Node *)malloc(sizeof(struct Node)); // 创建新结点
newnode->next = g_head;
g_head = newnode;
newnode->num = rand() % 1000;
printf("add node, num : %d, tid : %ld\n", newnode->num, pthread_self());
// 解锁
pthread_mutex_unlock(&mutex);
usleep(100);
}
return NULL;
}
void *customer(void *arg)
{
// 消费者,不断从容器中取出内容
while (1)
{
// 加锁
pthread_mutex_lock(&mutex);
struct Node *tmp = g_head; // 保存头结点的指针
if (g_head != NULL)
{
g_head = g_head->next;
printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
free(tmp);
// 解锁
pthread_mutex_unlock(&mutex);
usleep(100);
}
else
{
// 解锁
pthread_mutex_unlock(&mutex);
}
}
return NULL;
}
int main()
{
// 互斥量初始化
pthread_mutex_init(&mutex, NULL);
// 创建5个生产者和5个消费者线程,容器使用链表
pthread_t prodtid[5], custtid[5];
for (int i = 0; i < 5; ++i)
{
pthread_create(&prodtid[i], NULL, producer, NULL);
pthread_create(&custtid[i], NULL, customer, NULL);
}
// 线程分离
for (int i = 0; i < 5; ++i)
{
pthread_join(prodtid[i], NULL);
pthread_join(custtid[i], NULL);
}
while (1)
{
sleep(10);
}
// 释放互斥量资源
pthread_mutex_destroy(&mutex);
pthread_exit(NULL);
return 0;
}
4.4.2 条件变量
使用互斥量实现生产者-消费者模型存在一定问题:在customer()
函数中,假设有一个消费者线程拿到了CPU资源开始执行,此时容器中没有数据,那么该线程就会一直重复 加锁→判断是否为空→解锁,直到有生产者线程抢占到CPU资源开始生产数据。
void *customer(void *arg)
{
// 消费者,不断从容器中取出内容
while (1)
{
// 加锁
pthread_mutex_lock(&mutex);
struct Node *tmp = g_head; // 保存头结点的指针
if (g_head != NULL)
{
g_head = g_head->next;
printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
free(tmp);
// 解锁
pthread_mutex_unlock(&mutex);
usleep(100);
}
else
{
// 解锁
pthread_mutex_unlock(&mutex);
}
}
return NULL;
}
我们希望的是:如果消费者发现容器是空的,就立即通知生产者开始生产数据。可以使用条件变量实现这一机制。
条件变量不是锁,不能保证数据混乱的问题,条件变量在满足某个条件后可以引起阻塞线程或者解除线程阻塞。
条件变量类型 pthread_cond_t
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr); // 初始化条件变量
int pthread_cond_destroy(pthread_cond_t *cond); // 释放条件变量
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex); // 等待,阻塞函数,调用后线程会阻塞
int pthread_cond_timewait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,cnost struct timespec *restrict abstime); // 等待一定时间,调用后线程会阻塞,直到指定的时间结束
int pthread_cond_signal(pthread_cond_t *cond); // 唤醒等待的一个或多个线程
int pthread_cond_broadcast(pthread_cond_t* cond); // 唤醒所有等待的线程
调用
pthread_cond_wait()
函数阻塞后,会对互斥量解锁,当被唤醒,会重新加锁互斥量。
使用条件变量实现生产者-消费者模型参考代码:
/**
* @file 3cond.c
* @author zoya (2314902703@qq.com)
* @brief 条件变量实现生产者/消费者模型
* @version 0.1
* @@date: 2022-10-05
*
* @copyright Copyright (c) 2022
*
*/
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
struct Node
{
int num;
struct Node *next;
};
struct Node *g_head = NULL;
// 创建互斥量
pthread_mutex_t mutex;
// 创建条件变量
pthread_cond_t cond;
void *producer(void *arg)
{
// 生产者,不断向容器中添加内容
while (1)
{
// 加锁
pthread_mutex_lock(&mutex);
struct Node *newnode = (struct Node *)malloc(sizeof(struct Node)); // 创建新结点
newnode->next = g_head;
g_head = newnode;
newnode->num = rand() % 1000;
printf("add node, num : %d, tid : %ld\n", newnode->num, pthread_self());
// 只要生产就通知消费者消费
pthread_cond_signal(&cond);
// 解锁
pthread_mutex_unlock(&mutex);
usleep(100);
}
return NULL;
}
void *customer(void *arg)
{
// 消费者,不断从容器中取出内容
while (1)
{
// 加锁
pthread_mutex_lock(&mutex);
struct Node *tmp = g_head; // 保存头结点的指针
if (g_head != NULL)
{
g_head = g_head->next;
printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
free(tmp);
// 解锁
pthread_mutex_unlock(&mutex);
usleep(100);
}
else
{
// 没有数据,则等待生产者产生数据
// 当调用pthread_cond_wait阻塞时,会对互斥锁进行解锁,当不阻塞时,会继续加锁
pthread_cond_wait(&cond, &mutex);
pthread_mutex_unlock(&mutex);
}
}
return NULL;
}
int main()
{
// 互斥量初始化
pthread_mutex_init(&mutex, NULL);
// 初始化条件变量
pthread_cond_init(&cond, NULL);
// 创建5个生产者和5个消费者线程,容器使用链表
pthread_t prodtid[5], custtid[5];
for (int i = 0; i < 5; ++i)
{
pthread_create(&prodtid[i], NULL, producer, NULL);
pthread_create(&custtid[i], NULL, customer, NULL);
}
// 线程分离
for (int i = 0; i < 5; ++i)
{
pthread_join(prodtid[i], NULL);
pthread_join(custtid[i], NULL);
}
while (1)
{
sleep(10);
}
// 释放互斥量资源
pthread_mutex_destroy(&mutex);
// 释放条件变量
pthread_cond_destroy(&cond);
pthread_exit(NULL);
return 0;
}
4.4.3 信号量
信号量主要是用于进程和线程间的同步,信号量保存一个整数值来的控制对资源的访问。
当值>0时,表示资源空闲可以访问;
当值=0时,表示资源分配完毕无法访问;
当值<0时,表示有至少一个线程正在等待资源;
如果信号量的值只为0或1,那么他就是一个二元信号量。
信号量不能保证多线程数据安全,保证多线程数据安全要使用互斥锁。
信号量的P-V操作:
信号量有两种操作:等待和发送信号,分别用P(s)和V(s)表示,
P(s) 如果s的值大于0,那么P(s)操作后s=s-1;如果s=0,那么就阻塞当前线程直到s变为非0值;
V(s) V(s)执行后s=s+1,如果有线程在阻塞等待s变为非零值,那么会唤醒该线程;
P-V操作不可分,因为不能对一个信号量一直加或者一直减。
POSIX信号量函数有以下:
信号量类型 sem_t
int sem_init(sem_t *sem, int pshared, unsigned int value); // 初始化信号量
int sem_destroy(sem_t *sem); // 释放信号量
int sem_wait(sem_t *sem); // 等待 对信号量-1,如果值为0,就阻塞
int sem_trywait(sem_t *sem); // 等待
int sem_timewait(sem_t *restrict cond, const struct timespec * abstime); // 等待一定时间,调用后线程会阻塞,直到指定的时间结束
int sem_post(sem_t *cond); // 对信号量+1
int sem_getvalue(sem_t* sem, int *sval); // 获取信号量值
函数名 | 函数功能 | 参数 |
---|---|---|
sem_init |
初始化信号量 | – sem :需要初始化的信号量– pshared :判断信号量用在进程之间还是线程之间,为0表示在线程之间,如果为非0值表示在进程之间– value :信号量中的初始值 |
sem_destroy |
释信号量 | – sem :需要释放的信号量 |
sem_wait |
如果sem =0,就阻塞,如果sem ≠0,就执行减1操作调用一次对信号量减1 |
– sem :需要操作的信号量 |
sem_trywait |
尝试对信号量执行减1操作,如果不能执行减1操作立即返回 | – sem :操作的信号量 |
sem_timewait |
于sem_wait() 类似,不同在于如果不能执行减1操作,就阻塞指定的时间 |
– sem :需要操作的信号量– abstime :阻塞等待的时间 |
sem_post |
对信号量 执行加1操作, 调用一次对信号量的值加1 |
– sem :需要操作的信号量 |
sem_getvalue |
获取信号量的值 | – sem :需要获取值的信号量– value :获取到的值 |
信号量使用步骤参考:
// 1. 声明信号量
sem_t psem,csem;
// 2. 初始化信号量
init(psem, 0, 8);
init(csem, 0, 0);
// 生产者线程中对信号量操作,
producer()
{
sem_wait(&psem); // psem-1
sem_post(&csem); // csem+1
}
// 消费者线程对信号量操作
customer()
{
sem_wait(&csem); // csem-1
sem_post(&psem); // psem+1
}
如上信号量可以理解为psem关注的是容器中客房产品的空闲的位置有多少,csem关注的是容器中的产品有多少。
在生产者线程中,生产一个产品,空闲位置就少1个,所以psem-1,产品就多1个,所以csem+1;
在消费者线程中,取出一个产品,产品就少1个,所以csem-1,空闲位置就多1个,所以psem+1;
信号量实现生产者-消费者模型参考代码:
/**
* @file 4semaphore.c
* @author zoya (2314902703@qq.com)
* @brief 信号量操作示例
* @version 0.1
* @@date: 2022-10-05
*
* @copyright Copyright (c) 2022
*
*/
#define _XOPEN_SOURCE 500
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <semaphore.h>
struct Node
{
int num;
struct Node *next;
};
struct Node *g_head = NULL;
// 创建互斥量
pthread_mutex_t mutex;
// 创建信号量
sem_t psem, csem;
void *producer(void *arg)
{
// 生产者,不断向容器中添加内容
while (1)
{
// psem-1
sem_wait(&psem);
// 加锁
pthread_mutex_lock(&mutex);
struct Node *newnode = (struct Node *)malloc(sizeof(struct Node)); // 创建新结点
newnode->next = g_head;
g_head = newnode;
newnode->num = rand() % 1000;
printf("add node, num : %d, tid : %ld\n", newnode->num, pthread_self());
// 解锁
pthread_mutex_unlock(&mutex);
sem_post(&csem); // csem+1
}
return NULL;
}
void *customer(void *arg)
{
// 消费者,不断从容器中取出内容
while (1)
{
sem_wait(&csem); // csem-1
// 加锁
pthread_mutex_lock(&mutex);
struct Node *tmp = g_head; // 保存头结点的指针
g_head = g_head->next;
printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
free(tmp);
// 解锁
pthread_mutex_unlock(&mutex);
sem_post(&psem); // psem+1
}
return NULL;
}
int main()
{
// 互斥量初始化
pthread_mutex_init(&mutex, NULL);
// 初始化信号量
sem_init(&psem, 0, 8);
sem_init(&csem, 0, 0);
// 创建5个生产者和5个消费者线程,容器使用链表
pthread_t prodtid[5], custtid[5];
for (int i = 0; i < 5; ++i)
{
pthread_create(&prodtid[i], NULL, producer, NULL);
pthread_create(&custtid[i], NULL, customer, NULL);
}
// 线程分离
for (int i = 0; i < 5; ++i)
{
pthread_join(prodtid[i], NULL);
pthread_join(custtid[i], NULL);
}
while (1)
{
sleep(10);
}
// 释放互斥量资源
pthread_mutex_destroy(&mutex);
sem_destroy(&psem);
sem_destroy(&csem);
pthread_exit(NULL);
return 0;
}
思考一下:生产者和消费者中psem和csem位置能否换换呢?
应该是不行的,试想如果当前容器中没有数据,先执行了sem_post(&csem)
,唤醒消费者线程,此时生产者中还没来得及加锁,CPU资源被消费者拿到了,开始从容器中取数据,但是容器是空的,此时就会产生错误。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/46066.html