Linux下的多线程编程:原理、工具及应用(1)

                                               🎬慕斯主页修仙—别有洞天

                                              ♈️今日夜电波:Flower of Life—陽花

                                                                0:34━━━━━━️💟──────── 4:46
                                                                    🔄   ◀️   ⏸   ▶️    ☰  

                                      💗关注👍点赞🙌收藏您的每一次鼓励都是对我莫大的支持😍


目录

模拟语言封装Linux下多线程接口

线程互斥

前置知识

解释为什么会产生上述代码错误

如何解决?加锁!

什么是互斥锁?

pthread_mutex_t

pthread_mutex_init

PTHREAD_MUTEX_INITIALIZER

pthread_mutex_lock

pthread_mutex_unlock

pthread_mutex_trylock(不常用)

根据如上的互斥锁来进行操作

通过定义全局的锁

通过定义局部的锁(优雅的解决)


模拟语言封装Linux下多线程接口

        如下是我们以使用C++实现简单封装:

#pragma once
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include <functional>template <class T>
using func_t = std::function<void(T)>;template <class T>
class Thread
{
public:Thread(const std::string threadname,func_t<T> func,T data):_tid(0),_threadname(threadname),_isrunning(false),_func(func),_data(data){}static void* ThreadRoutine(void* args){Thread *ts=static_cast<Thread *>(args);ts->_func(ts->_data);return nullptr;}bool Start(){int n=pthread_create(&_tid,nullptr,ThreadRoutine,this);if(n==0){_isrunning=true;return true;}return false;}bool Join(){if(!_isrunning) return true;int n=pthread_join(_tid,nullptr);if(n==0){_isrunning=false;return true;}return false;}std::string ThreadName(){return _threadname;}bool Isrunning(){return _isrunning;}private:pthread_t _tid;std::string _threadname;bool _isrunning;func_t<T> _func;T _data;
};

        其中成员变量存储了进程线程的ID、线程名、线程运行状态、线程运行的函数以及线程传递的变量。实现了线程的构造函数(根据线程名、传入函数以及传入变量构造),实现了开始运行的操作、等待线程的操作、判断是否运行以及返回线程名的操作

        重点说一下ThreadRoutine这个函数!他实现的是配合运行操作为pthread_create传递函数在执行完后return nullptr结束线程的操作!那他为啥要定义成如下的形式呢?

    static void* ThreadRoutine(void* args){Thread *ts=static_cast<Thread *>(args);ts->_func(ts->_data);return nullptr;}

        这是因为成员函数是默认会带有this指针的,而我们要传入pthread_create中规定了只能传入void* args变量的函数。因此我们使用static让他不具有this指针!当然,这并不是唯一的解决办法,我们也可以将他定义到类外,然后给在类内声明友元函数即可

线程互斥

        我们根据上面所封装的线程库来叙写了如下的代码:

#include "Thread.hpp"std::string GetThreadName()
{static int number = 1;char name[64];snprintf(name, sizeof(name), "Thread-%d", number++);return name;
}void Print(int num)
{while (num){std::cout << "hello world: " << num-- << std::endl;sleep(1);}
}int ticket = 10000; // 全局的共享资源void GetTicket(std::string name)
{while (true){if (ticket > 0) {// 充当抢票花费的时间usleep(1000);printf("%s get a ticket: %d\n", name.c_str(), ticket);ticket--;}else{break;}// 实际情况,还有后续的动作, TODO?}
}int main()
{std::string name1 = GetThreadName();Thread<std::string> t1(name1, GetTicket, name1);std::string name2 = GetThreadName();Thread<std::string> t2(name2, GetTicket, name2);std::string name3 = GetThreadName();Thread<std::string> t3(name3, GetTicket, name3);std::string name4 = GetThreadName();Thread<std::string> t4(name4, GetTicket, name4);t1.Start();t2.Start();t3.Start();t4.Start();t1.Join();t2.Join();t3.Join();t4.Join();return 0;
}

        该代码简单的模拟了一个抢票的场景,其中票数仅为tickets=10000,在tickets减为0的时候就会停止抢票。我们创建4个线程来模拟多人抢票的情况。按照代码的原意,四个线程会依次抢票知道票数为0为止,但是代码真的如我们所想的那样嘛?

        从上面的现象我们很容易的发现我们的共享资源tickets发生了不该发生的操作,出现抢到同一张票以及多抢了的情况,出现了数据不一致的情况!这是为什么呢?下面先了解一些前置知识:

前置知识

        我们将任何一个时刻,只允许一个进程正在访问的资源称为临界资源。把进程中访问临界资源的代码叫做临界区。例如我们上述的代码:

        互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用 。图解如下:

        原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

 

解释为什么会产生上述代码错误

        请先看以下代码:

#include<iostream>int main()
{int a = 10;a++;return 0;
}

        很平常的一段代码,但是这也是我们常出现错误代码中访问临界资源的代码。我们可以从反汇编中看出问题:

        可以看到通过反汇编,我们发现本来是一段代码的操作,在底层居然被翻译成了三条操作。这也就违背了我们的原子性!这三条语句在多线程并发访问的时候都有可能会被中断!大致图解:

        当我们知道实际上访问临界资源时是有三步操作后,我们就可以理解为什么会产生如上的错误了!看完如下的例子就明白了:假设现在我们启动了两个线程,他们都要执行如上的三步。线程1先是访问该临界资源,但是在他执行完几次完整的访问(3步都走完)后,它的时间片用完了,恰好此时该线程卡在第二步的--操作,此时他要保存上下文,eax中存储的值为7,然后轮到下一个进程执行。下一个进程也是执行完整了几步,恰好也是在第二步的时候他的时间片用完了,此时他也要保存上下文,eax中存储的值为3。接着轮到第一个线程,他需要恢复上下文啊,因此,从第三步开始将eax返回内存中,此时!count又变回了7!!!

        看完上面的例子你大概就明白了,为什么上述代码互产生错误的原因。因为多线程并发访问全部int,不是原子的!!!会有数据不一致的并发访问问题!

        看完上述的解释是不是以为完了?当然没有!我们都知道在CPU中我们存在着:算术运算、逻辑运算、处理内外中断、控制单元的操作。在上述出错代码中我们还存在着if的判断语句,这就是一种逻辑运算,底层是需要两步的处理:1、加载如寄存器。2、判断他也不是原子的!也会出现会有数据不一致的并发访问问题!因此,我们根本就不知道每个进程的具体执行状况!这也是为什么该数据会减到负数的原因,因为有可能if判断都认为是符合条件的!但是实际寄存器中的值确是不符合的!

如何解决?加锁!

什么是互斥锁?

        互斥锁是一种同步机制,用于确保在多线程环境中共享资源的安全访问

        互斥锁的核心作用是防止多个线程同时访问和修改共享资源,从而避免数据竞争和不一致的问题。以下是互斥锁的一些关键特性和概念:

  1. 排他性:互斥锁确保在任何时刻,只有一个线程能够持有锁并访问共享资源。当一个线程获得锁时,其他线程必须等待直到锁被释放。
  2. 同步原语:互斥锁是一种同步原语,它通常与条件变量、信号量等其他同步机制一起使用,以实现复杂的线程间协作和通信。
  3. 初始化:在使用互斥锁之前,需要对其进行初始化,这可以通过pthread_mutex_init函数完成,也可以静态地通过PTHREAD_MUTEX_INITIALIZER宏来初始化一个互斥锁变量。
  4. 加锁与解锁:线程在访问共享资源前需要对互斥锁进行加锁(上锁),访问完成后需要释放互斥锁(解锁)。这一过程通常通过pthread_mutex_lockpthread_mutex_unlock函数来实现。
  5. 性能考量:互斥锁可能导致进程睡眠和唤醒,以及上下文切换,这些都会带来一定的性能开销。因此,互斥锁适用于加锁时间较长的场景,以减少频繁的锁争用和上下文切换。
  6. 销毁:当互斥锁不再使用时,应当通过pthread_mutex_destroy函数进行销毁,以避免资源泄漏。

pthread_mutex_t

    pthread_mutex_t是一个数据类型,用于表示互斥锁(Mutex)对象。在多线程编程中,互斥锁是一种同步机制,用于保护共享资源,防止多个线程同时访问和修改这些资源,从而避免数据竞争和不一致的问题。

   pthread_mutex_t类型的变量通常用于声明一个互斥锁对象,并使用pthread_mutex_init函数进行初始化。初始化后,可以使用pthread_mutex_lock函数对互斥锁进行加锁操作,使用pthread_mutex_unlock函数进行解锁操作。

        需要注意的是,在使用完互斥锁后,应该及时销毁它,以避免资源泄漏。

pthread_mutex_init

    pthread_mutex_init是一个用于初始化互斥锁的函数,它是POSIX线程库(Pthreads)中的一部分。互斥锁是一种同步原语,用于保护共享资源,防止多个线程同时访问。

        函数原型:

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

        参数说明:

  • mutex:指向要初始化的互斥锁对象的指针。
  • attr:指向互斥锁属性对象的指针,可以设置为NULL,表示使用默认属性。

        返回值:

  • 成功时,返回0;
  • 失败时,返回一个非零错误码。

PTHREAD_MUTEX_INITIALIZER

    PTHREAD_MUTEX_INITIALIZER是一个宏定义,用于初始化一个互斥锁对象。它通常与静态分配的互斥锁变量一起使用,以确保在多线程环境中对共享资源的访问是安全的。

        该宏定义的作用是将互斥锁对象的值设置为默认状态,以便在程序启动时立即可用。具体来说,它将互斥锁对象的类型设置为pthread_mutex_t,并将其属性设置为默认值(通常是快速互斥锁)。

pthread_mutex_lock

    pthread_mutex_lock是一个函数,用于对互斥锁进行加锁操作。它的作用是确保在多线程环境中,只有一个线程可以访问共享资源,从而避免数据竞争和不一致的问题。

        函数原型:

int pthread_mutex_lock(pthread_mutex_t *mutex);

        其中,mutex参数是一个指向互斥锁对象的指针。

        当一个线程调用pthread_mutex_lock函数时,它会尝试获取互斥锁。如果互斥锁当前未被其他线程持有,则该线程成功获取互斥锁并继续执行后续代码。如果互斥锁已经被其他线程持有,则该线程会被阻塞,直到互斥锁被释放为止。

        一旦线程成功获取互斥锁,其他试图获取该互斥锁的线程将会被阻塞,直到当前持有互斥锁的线程调用pthread_mutex_unlock函数释放互斥锁。

        需要注意的是,在使用完互斥锁后,应该及时调用pthread_mutex_unlock函数来释放互斥锁,以避免死锁或资源泄漏的情况发生。

pthread_mutex_unlock

    pthread_mutex_unlock是一个函数,用于对互斥锁进行解锁操作。它的作用是释放当前线程持有的互斥锁,以便其他线程可以获取该互斥锁并访问共享资源。

        函数原型:

int pthread_mutex_unlock(pthread_mutex_t *mutex);

        其中,mutex参数是一个指向互斥锁对象的指针。

        当一个线程调用pthread_mutex_unlock函数时,它会尝试释放当前线程持有的互斥锁。如果当前线程确实持有该互斥锁,则该函数会成功释放互斥锁并返回0;否则,该函数会返回错误码。

        需要注意的是,在使用完互斥锁后,应该及时调用pthread_mutex_unlock函数来释放互斥锁,以避免死锁或资源泄漏的情况发生。

pthread_mutex_trylock(不常用)

    pthread_mutex_trylock是一个函数,用于尝试对互斥锁进行加锁操作。它的作用是尝试获取互斥锁,如果互斥锁当前未被其他线程持有,则该线程成功获取互斥锁并继续执行后续代码;如果互斥锁已经被其他线程持有,则该线程不会阻塞,而是立即返回错误码。

        函数原型:

int pthread_mutex_trylock(pthread_mutex_t *mutex);

        其中,mutex参数是一个指向互斥锁对象的指针。

        当一个线程调用pthread_mutex_trylock函数时,它会尝试获取互斥锁。如果互斥锁当前未被其他线程持有,则该线程成功获取互斥锁并继续执行后续代码;如果互斥锁已经被其他线程持有,则该线程不会阻塞,而是立即返回错误码。

        需要注意的是,在使用完互斥锁后,应该及时调用pthread_mutex_unlock函数来释放互斥锁,以避免死锁或资源泄漏的情况发生。

根据如上的互斥锁来进行操作

通过定义全局的锁

        如下我们根据互斥锁来解决上述的问题:

#include <iostream>
#include <unistd.h>
#include <vector>
#include <cstdio>
#include "Thread.hpp"// 应用方的视角
std::string GetThreadName()
{static int number = 1;char name[64];snprintf(name, sizeof(name), "Thread-%d", number++);return name;
}void Print(int num)
{while (num){std::cout << "hello world: " << num-- << std::endl;sleep(1);}
}int ticket = 10000; // 全局的共享资源
// 共享资源了
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 锁就有了,被定义并初始化了,这个锁也是全局的哦!!// 加锁:
// 1. 我们要尽可能的给少的代码块加锁
// 2. 一般加锁,都是给临界区加锁
void GetTicket(std::string name)
{while (true){// 2. 是由程序员自己保证的!规则都必须先申请锁// 3. 根据互斥的定义,任何时刻,只允许一个线程申请锁成功!多个线程申请锁失败,失败的线程怎么办?在mutex上进行阻塞,本质就是等待!pthread_mutex_lock(&mutex); // 1. 申请锁本身是安全的,原子的,为什么?if (ticket > 0) // 4. 一个线程在临界区中访问临界资源的时候,可不可能发生切换?可能,完全允许!!{// 充当抢票花费的时间usleep(1000);printf("%s get a ticket: %d\n", name.c_str(), ticket);ticket--;pthread_mutex_unlock(&mutex);}else{pthread_mutex_unlock(&mutex);break;}// 实际情况,还有后续的动作, TODO?}
}int main()
{std::string name1 = GetThreadName();Thread<std::string> t1(name1, GetTicket, name1);std::string name2 = GetThreadName();Thread<std::string> t2(name2, GetTicket, name2);std::string name3 = GetThreadName();Thread<std::string> t3(name3, GetTicket, name3);std::string name4 = GetThreadName();Thread<std::string> t4(name4, GetTicket, name4);t1.Start();t2.Start();t3.Start();t4.Start();t1.Join();t2.Join();t3.Join();t4.Join();return 0;
}

        可以看到我们成功的解决了上述的问题,但是加了互斥锁也让我们的执行速度相对于上面的执行速度变慢了许多。这是因为:当一个线程持有互斥锁时,其他试图访问相同共享资源的线程必须等待,直到锁被释放。这种等待会导致线程阻塞,减少了并行执行的机会。当线程在等待锁的过程中,操作系统可能会将其置于睡眠状态,并在锁可用时再次唤醒它。这种从睡眠到唤醒的过程涉及到上下文切换,这是一种相对耗时的操作。频繁的上下文切换会显著增加程序的执行时间。

通过定义局部的锁(优雅的解决)

        如下我们定义一个LockGuard.hpp的文件,该文件封装了一个可以通过构造以及析构完成对应的加锁以及解锁的操作:

#pragma once#include <pthread.h>// 不定义锁,默认认为外部会给我们传入锁对象
class Mutex
{
public:Mutex(pthread_mutex_t *lock):_lock(lock){}void Lock(){pthread_mutex_lock(_lock);}void Unlock(){pthread_mutex_unlock(_lock);}~Mutex(){}private:pthread_mutex_t *_lock;
};class LockGuard
{
public:LockGuard(pthread_mutex_t *lock): _mutex(lock){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}
private:Mutex _mutex;
};

        主函数:

        可以看到我们在临界区中定义了上述LockGuard.hpp的lockguard变量,我们可以通过上述的构造以及析构进行加锁、解锁需要注意的是:临界区代码中可以发现我们使用了一个{}来括起来,这是表示代码块的意思,可以理解变量同在函数栈帧中一样。我们通过新定义的ThreadData类来传递给之前封装的多线程接口,优雅的实现了如下的代码:

#include <iostream>
#include <string>
#include <unistd.h>
#include <vector>
#include <cstdio>
#include "Thread.hpp"
#include "LockGuard.hpp"// 应用方的视角
std::string GetThreadName()
{static int number = 1;char name[64];snprintf(name, sizeof(name), "Thread-%d", number++);return name;
}void Print(int num)
{while (num){std::cout << "hello world: " << num-- << std::endl;sleep(1);}
}class ThreadData
{
public:ThreadData(const std::string &name, pthread_mutex_t *lock): threadname(name), pmutex(lock){}
public:std::string threadname;pthread_mutex_t *pmutex;
};int ticket = 10000; // 全局的共享资源void GetTicket(ThreadData *td)
{while (true){// 非临界区代码!// 2. 是由程序员自己保证的!规则都必须先申请锁// 3. 根据互斥的定义,任何时刻,只允许一个线程申请锁成功!多个线程申请锁失败,失败的线程怎么办?在mutex上进行阻塞,本质就是等待!{LockGuard lockguard(td->pmutex);if (ticket > 0) // 4. 一个线程在临界区中访问临界资源的时候,可不可能发生切换?可能,完全允许!!{// 充当抢票花费的时间usleep(1000);printf("%s get a ticket: %d\n", td->threadname.c_str(), ticket);ticket--;}else{break;}}// 非临界区代码!// 实际情况,还有后续的动作, TODO?}
}int main()
{pthread_mutex_t mutex;pthread_mutex_init(&mutex, nullptr);std::string name1 = GetThreadName();ThreadData *td = new ThreadData(name1, &mutex);Thread<ThreadData *> t1(name1, GetTicket, td);std::string name2 = GetThreadName();ThreadData *td2 = new ThreadData(name2, &mutex);Thread<ThreadData *> t2(name2, GetTicket, td2);std::string name3 = GetThreadName();ThreadData *td3 = new ThreadData(name3, &mutex);Thread<ThreadData *> t3(name3, GetTicket, td3);std::string name4 = GetThreadName();ThreadData *td4 = new ThreadData(name4, &mutex);Thread<ThreadData *> t4(name4, GetTicket, td4);t1.Start();t2.Start();t3.Start();t4.Start();t1.Join();t2.Join();t3.Join();t4.Join();pthread_mutex_destroy(&mutex);delete td;delete td2;delete td3;delete td4;return 0;
}

        一些知识点汇总:

// 加锁:
// 1. 我们要尽可能的给少的代码块加锁
// 2. 一般加锁,都是给临界区加锁
// 3. 个别系统,抢票代码会出现很多的票被同一个线程抢完了
// 4. 多线程运行,同一份资源,有线程长时间无法拥有,饥饿问题
// 5. 要解决饥饿问题,要让线程执行的时候,具备一定的顺序性 --- 同步

 


                         感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o! 

                                       

                                                                        给个三连再走嘛~  

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/540630.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

搜索:DFS与BFS

DFS与BFS为图论中的概念。在搜索算法中&#xff0c;该词常常指利用递归函数方便地实现暴力枚举的算法&#xff0c;与图论中的 DFS 算法有一定相似之处&#xff0c;但并不完全相同。BFS 在搜索中&#xff0c;将每个状态对应为图中的一个点即可。 DFS&#xff08;搜索&#xff0…

使用IDF和VSCode构建ESP32的开发环境

目录 概述 1 安装ESP32开发环境IDF 1.1 下载软件 1.2 安装软件 1.3 配置环境参数 1.4 VSCode中配置IDF插件 1.5 配置烧写代码方式 2 在使用vscode创建工程 2.1 创建工程模版 2.2 工程目录介绍 3 编译和下载 3.1 认识 VSCode下idf编译环境 3.2 编译代码 3.2.1 认识…

数据结构:静态链表(编程技巧)

链表的元素用数组存储&#xff0c; 用数组的下标模拟指针。 一、理解 如果有些程序设计语言没有指针类型&#xff0c;如何实现链表&#xff1f; 在使用指针类型实现链表时&#xff0c;我们很容易就可以直接在内存中新建一块地址用于创建下一个结点&#xff0c;在逻辑上&#x…

软件无线电系列——模拟无线电、数字无线电、软件无线电

本节目录 一、模拟无线电 二、数字无线电 1、窄带数字无线电 2、宽带数字无线电 三、软件无线电本节内容 一、模拟无线电 20世纪80年代的模拟体制(美国的AMPS/欧洲的TACS)被称为第一代移动通信&#xff0c;简称1G,主要目标是为在大范围内有限的用户提供移动电话服务。最主要的…

【数据结构】二叉搜索树底层刨析

文章目录 1. 二叉搜索树的实现2. 二叉搜索树的应用3. 改造二叉搜索树为 KV 结构4. 二叉搜索树的性能分析 1. 二叉搜索树的实现 namespace key {template<class K>struct BSTreeNode{typedef BSTreeNode<K> Node;Node* _left;Node* _right;K _key;BSTreeNode(const…

pycharm打开文件GBK编码格式

Preferences→File Encodings→Global Encoding(全局编码)或Project Encoding(项目编码)。IIDE Encoding是IDE编码。下面这个roject Encoding 是项目编码&#xff0c;修改项目编码为UTF-8

离线安装数据库 mysql 5.7 linux

离线安装数据库 mysql 5.7 linux 方法一 参考链接Linux(Debian10.2)安装MySQL5.7.24环境 赋予文件执行权限chmod x 文件名 使用root用户sudo su解压文件tar xvf mysql-5.7.42-linux-glibc2.12-x86_64.tar.gz重命名mv mysql-5.7.42-linux-glibc2.12-x86_64 mysql将桌面的mys…

Redis 持久化-AOF

AOF&#xff08;Append Only File&#xff09;&#xff0c;以日志的形式来记录每个写操作&#xff08;增量保存&#xff09;&#xff0c;将Redis执行过的所有写指令记录下来(读操作不记录)&#xff0c; 只许追加文件但不可以改写文件&#xff0c;redis启动之初会读取该文件重新…

企业级项目带你入门Jmeter(不定时更新,请耐心等待,谢谢支持)

企业级项目带你入门Jmeter JDK安装及配置Jmeter安装Jmeter调整界面调整参数调整汉化界面比例BodyData消息体工具栏大小左侧树结构调整后效果展示 测试计划和线程组测试计划线程组 取样器HTTP请求默认配置录制模拟间隔时间统计报表Cookie管理器变量和后置处理器CSV数据文件处理断…

leetcode判断子序列

本题中&#xff0c;我们可以删除原始字符串的一些字符但是不能改变其他字符的位置&#xff0c;这种求子序列的题都可以用动态规划来解决。 首先我们要确定dp数组的定义&#xff0c;这里我们将dp数组定义为dp[i][j] 表示以下标i-1为结尾的字符串s&#xff0c;和以下标j-1为结尾的…

【洛谷 P8637】[蓝桥杯 2016 省 B] 交换瓶子 题解(贪心算法)

[蓝桥杯 2016 省 B] 交换瓶子 题目描述 有 N N N 个瓶子&#xff0c;编号 1 ∼ N 1 \sim N 1∼N&#xff0c;放在架子上。 比如有 5 5 5 个瓶子&#xff1a; 2 , 1 , 3 , 5 , 4 2,1,3,5,4 2,1,3,5,4 要求每次拿起 2 2 2 个瓶子&#xff0c;交换它们的位置。 经过若干次…

WebAssembly探索篇(二)引入第三库的简单demo

文章目录 开发环境demo简单介绍实践出真知各个文件内容CMakeLists.txtmain.cpp cmake 编译结果 遇到问题错误1&#xff1a;both async and sync fetching of the wasm failedvscode安装Preview on Web Server插件 最近因为项目原因&#xff0c;研究了一下WebAssembly。2015年上…