【Linux】多线程开发

导读:本篇文章讲解 【Linux】多线程开发,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

Linux 多线程开发

一, 线程概述

1. 初识线程

与进程类似,线程是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程。

同一个程序中的所有线程均会独立执行不同程序,且共享同一份全局内存区域,其中包括初始化数据段、未初始化数据段,以及堆内存段等。

进程是CPU分配资源的最小单位,线程是操作系统调度执行的最小单位。

线程是轻量级的进程(LWP:Light Weight Process),在Linux环境下线程的本质仍然是进程。

查看指定进程的LWP号命令:ps -Lf pid

示例:使用ps aux查看浏览器的进程pid=4227,然后使用ps -Lf 4227查看浏览器的LWP,有如下几个线程,它们的pid都是4227,而LWP都是不同的。说明每一个线程都有它自己对应的线程号。

查看线程id

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。

查看NPTL版本

二, 线程操作相关函数

在介绍多线程操作相关函数时先了解下主线程和子线程:

一般情况下,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是否相等。
参数t1t2 要比较的线程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;
}

程序运行,第一次运行时,子线程还没运行完,因为触发了取消点,子线程终止了。
线程取消

多线程开发步骤:

  1. 创建线程:pthread_create();
  2. 回收子线程资源,使用线程分离或者连接线程函数回收子线程资源;

三, 线程属性相关操作

线程属性相关步骤:

  1. 声明属性变量;pthread_attr_t attr;
  2. 初始化; pthread_attr_init()
  3. 使用完销毁 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语言修饰符,被修饰的指针不能由另外的指针进行操作。

互斥量操作步骤:

  1. 声明互斥量变量:pthread_mutex_t mutex;
  2. 初始化互斥量:pthread_mutex_init();
  3. 释放互斥量:pthread_mutex_destroy()
  4. 在访问共享资源前加锁,访问后解锁;

使用互斥量解决售票问题:

/**
* @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已经加了锁

死锁

代码示例:


#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 什么是生产者-消费者模型

简单的生产者-消费者模型可以理解为:产品存放在容器中,生产者生产产品后放到容器中,消费者从容器中取出产品。生产者和消费者分别是两个线程,容器相当于内存缓冲区。

从上面描述中可以知道生产者-消费者模型中的对象有生产者、消费者、容器。
在处理过程中可能会出现两个问题:

  • 容器满了,生产者就不能再生产; 解决:通知消费者消费
  • 容器空了,消费者就不能再消费; 解决:通知生产者生产

在这里插入图片描述

生产者-消费者模型特点:

  1. 保证生产者不会在容器满的时候继续向容器放入数据,消费者不会在容器空的时候从容器中取出数据;
  2. 当缓冲区满,生产者进入休眠状态(阻塞),当消费者开始消耗容器中的数据时,生产者被唤醒,开始向添加数据;
  3. 当缓冲区空,消费者进入休眠状态(阻塞),当生产者向容器中放入数据时,消费者被唤醒,开始从容器中取出数据;

当然,可以使用互斥量使用生产者-消费者模型,参考代码如下:

/**
 * @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.nowcoder.com/study/live/504

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/46066.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!