Linux中的线程

news/2024/12/14 10:19:13/

目录

线程的概念

进程与线程的关系

线程创建

线程终止

线程等待

线程分离

原生线程库

线程局部存储

自己实现线程封装

线程的优缺点

多线程共享与独占资源

线程互斥

互斥锁

自己实现锁的封装

加锁实现互斥的原理

死锁

线程同步


线程的概念

回顾进程相关概念

● 进程 = 内核数据结构(pcb等) + 内存中的代码/数据

● 进程创建成本较高,需要创建pcb, 进程地址空间,页表,构建页表映射关系,将磁盘的代码和数据加载到内存中等一系列工作

● 一个进程访问的大部分资源都在物理内存中,需要通过进程地址空间+页表获取到,因此可以认为进程地址空间是进程的"资源窗口",因此进程是承担系统资源分配的基本实体

● 创建进程目的是为了让cpu去调度执行进程中的代码,访问相应的数据,完成任务,因此,之前的认知是: 一个进程本质就是一个执行流

线程的概念

● 在一个进程内只创建若干pcb,这些pcb指向同一个进程地址空间,通过同一个页表,映射到同一个内存,看到的是同一份资源

● 目前一个进程内有多个pcb了,本质就是有多个执行流了,多个执行流的地位是对等的,cpu调度时选择任意一个执行流调度即可,每个执行流本质就是一个线程,所以cpu调度的基本单位是线程

● 每个线程都有自己的pcb, 不同的pcb中保存同一个虚拟地址空间的不同起始地址,进而通过页表映射到不同的物理内存区域,相当于多线程瓜分了进程地址空间,从而并发执行同一个进程内的不同代码,共同完成一项任务

● 多个线程由于共用同一个进程地址空间,通过同一个页表映射,看到的是同一份资源,所以资源共享在线程之间显得非常容易,比如全局变量、环境变量、命令行参数等

● 线程是在进程内部执行的一种执行流

线程是更加轻量级的进程/线程是比进程更加轻量化的执行流

a.创建线程更加简单了,因为创建进程时该进程用到的资源都申请好了,一系列工作都已经做好了,创建线程只是在分配资源!!! 

b.创建线程更加简单意味着释放线程也更加容易了!

c.线程调度也更加简单

c.1 因为不同的线程看到的是同一个地址空间,访问的是同一个资源,因此线程间切换时只需要把一个pcb切换成另一个pcb,  把保存线程临时数据的少量寄存器切换,而页表和地址空间不用切换!

c.2 cpu内部集成了高速缓存cache,线程间切换不需要切换cache, 因为cache中保存的是整个进程中高频访问的数据,但是进程间切换需要切换cache, 因为cache中的大部分数据都失效了!!! 这是线程创建更加简单的最主要的原因

● 创建线程时,线程会瓜分进程总体的时间片,因为时间片也是资源!

● 站在cpu角度,cpu不需要区分调度的是线程还是进程,只需要找到pcb,找到进程地址空间,通过页表映射执行代码即可

Linux中并不存在真正的线程,只有"轻量级进程"的概念

一个进程内可能存在多个线程,要不要把所有的线程管理起来呢?? 要管理! 如何管理? 先描述,再组织!  --- 描述结构体叫做 tcb, 而线程也要有自己的各种队列,线程id, 状态,调度算法等,这都是 tcb中的属性字段,最后把所有tcb用链表链接起来!!!

事实上,windows就是这样实现的,而Linux系统中,并没有单独为线程设计tcb,因为线程的大部分属性特征进程也是有的,线程和进程都是执行流,  不必为线程单独设计,反倒会增加程序员的负担,因此Linux中用pcb可以充当tcb, 所有代码在线程级别上复用即可, 一整套调度算法也可以直接复用!

进程与线程的关系

线程创建

● pthread_create 接口

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

功能: 创建一个新线程

参数:

thread: 输出型参数,获取新线程id

attr: 创建线程时设置的线程属性,直接设为nullptr即可

start_routine:新线程执行的函数

arg: 新线程执行函数的参数

返回值: 创建成功,返回0,创建失败,返回错误码

● 创建线程代码示例

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;//新线程
void* ThreadRoutine(void* arg)
{const char* threadName = (const char*)arg;while(true){cout << "I am a new thread "<< threadName << endl;sleep(1);}
}int main()
{pthread_t tid;//主线程pthread_create(&tid, nullptr, ThreadRoutine, (void*)"thread 1");while(true){cout << "I am main thread" << endl;sleep(1);}return 0;
}

● 尽管有主线程和新线程两个线程,但始终只有1个进程,因此打印出的进程pid是一样的

#include <iostream>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
using namespace std;void* ThreadRoutine(void* arg)
{const char* threadName = (const char*)arg;while(true){cout << "I am a new thread" << ", pid: " << getpid() << ", " << threadName << endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, ThreadRoutine, (void*)"thread 1");while(true){cout << "I am main thread" << ", pid: " << getpid() << endl;sleep(1);}return 0;
}

●  ps -aL 指令查看系统内的线程,cpu调度线程依据的是LWP(light weight process), PID和LWP一样,就是主线程,否则是新线程

● 线程之间看到同一份资源是非常容易的,比如定义一个全局变量,线程就都能看到了!

#include <iostream>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
using namespace std;int gcnt = 100;void *ThreadRoutine(void *arg)
{while (true){cout << "I am a new thread, gcnt: "  << gcnt << ", &gcnt : " << &gcnt << endl;gcnt--;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, ThreadRoutine, nullptr);while (true){cout << "I am main thread, gcnt: "  << gcnt << ", &gcnt : " << &gcnt << endl;sleep(1);}return 0;
}

● 创建多线程

#include <iostream>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <functional>
#include <vector>
using namespace std;using func_t = function<void()>;class ThreadData
{
public:ThreadData(const string &name, const uint64_t &ctime, func_t f): threadname(name), createtime(ctime), func(f){}public:string threadname;uint64_t createtime;func_t func;
};void Print()
{cout << "我是线程执行的大任务的一部分" << endl;
}// 新线程
void *ThreadRoutine(void *args)
{ThreadData *td = static_cast<ThreadData *>(args); // 安全强转while (true){cout << "new thread" << ", thread name :" << td->threadname<< ", create time : " << td->createtime << endl;td->func();sleep(1);}
}int main()
{for (int i = 0; i < 3; i++) {char threadname[64];    snprintf(threadname, sizeof(threadname), "%s-%d", "thread", i);ThreadData *td = new ThreadData(threadname, (uint64_t)time(nullptr), Print);pthread_t tid;pthread_create(&tid, nullptr, ThreadRoutine, td);sleep(1);}while (true){cout << "main thread" << endl;sleep(1);}return 0;
}

● pthread_create 接口的最后一个参数类型是void*,可以接收任意数据类型的地址,因此除了给线程执行方法传递常规的数据类型,还可以传递我们自己封装的类对象

● 类对象中可以封装自定义函数,传递给线程执行方法,在线程内部进行回调

线程终止

● pthread_self()接口可以获取调用该接口的线程id,本质是一个地址

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
using namespace std;//十进制数转十六进制数
string ToHex(pthread_t tid)
{char id[64];snprintf(id, sizeof(id), "0x%x", tid);return id;
}void* threadRoutine(void* args)
{string name = static_cast<const char*>(args);usleep(1000);while(true){cout << "new thread is running, thread name: " << name << ", my thread id: " << ToHex(pthread_self()) << endl;sleep(1);} 
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)"thread-1");while(true){cout << "I am main thread, my thread id :" << ToHex(pthread_self()) << endl;sleep(1);}return 0;
}

● 线程终止有很多方法,比如在线程中直接return(终止线程)/exit(本质是终止整个进程),也可以调用pthread_exit()接口终止线程

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
using namespace std;string ToHex(pthread_t tid)
{char id[64];snprintf(id, sizeof(id), "0x%x", tid);return id;
}void* threadRoutine(void* args)
{string name = static_cast<const char*>(args);usleep(1000);int cnt = 5;while(cnt--){cout << "new thread is running, thread name: " << name << ", my thread id: " << ToHex(pthread_self()) << endl;sleep(1);}pthread_exit(nullptr); //终止调用该接口的线程
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)"thread-1");while(true){cout << "I am main thread, my thread id :" << ToHex(pthread_self()) << endl;sleep(1);}return 0;
}

● 主线程中调用pthread_cancle() 可以取消指定的线程

int pthread_cancel(pthread_t thread);

参数: 要取消的线程id

返回值: 成功,返回0; 失败,返回错误码

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <functional>
using namespace std;void* threadRoutine(void* args)
{int cnt = 10;while(cnt--){cout << "thread is running..." << endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr);sleep(3);pthread_cancel(tid); //取消tid线程cout << "我是主线程,取消了新线程" << endl;return 0;
}

● 一个线程异常,整个进程都会终止

#include <iostream>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <functional>
#include <vector>
using namespace std;using func_t = function<void()>;class ThreadData
{
public:ThreadData(const string &name, const uint64_t &ctime, func_t f): threadname(name), createtime(ctime), func(f){}public:string threadname;uint64_t createtime;func_t func;
};void Print()
{cout << "我是线程执行的大任务的一部分" << endl;
}// 新线程
void *ThreadRoutine(void *args)
{int a = 10;ThreadData *td = static_cast<ThreadData *>(args); // 安全强转while (true){cout << "new thread" << ", thread name :" << td->threadname<< ", create time : " << td->createtime << endl;td->func();//异常终止if(td->threadname == "thread-2"){cout << td->threadname << "触发了异常" << endl;a /= 0;}sleep(1);}
}int main()
{for (int i = 0; i < 3; i++){char threadname[64];snprintf(threadname, sizeof(threadname), "%s-%d", "thread", i);ThreadData *td = new ThreadData(threadname, (uint64_t)time(nullptr), Print);pthread_t tid;pthread_create(&tid, nullptr, ThreadRoutine, td);sleep(1);}while (true){cout << "main thread" << endl;sleep(1);}return 0;
}

线程等待

● 线程退出但没有被等待,也会出现和进程类似的僵尸问题

● 新线程退出时需要让主线程等待,从而获取新线程的退出信息

● 当一个新线程出异常了,其他线程也会受到影响,整个进程都终止了,主线程再等待新线程也就没有了意义

● pthread_join 线程等待代码演示

int pthread_join(pthread_t thread, void **retval);

参数:

thread: 被等待的线程id

retval: 输出型参数,根据threadRoutine的返回值可以获取子进程的退出信息,如果不关心新线程的退出信息,该参数直接设置为nullptr即可

返回值: 成功返回0,失败返回错误码

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <functional>
using namespace std;void* threadRoutine(void* args)
{int cnt = 5;while(cnt--){cout << "thread is running..." << endl;sleep(1);}char* ret = "新线程正常退出啦!!!";return ret;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr);sleep(3);void* ret = nullptr;pthread_join(tid, &ret); cout << "main thread join done, thread return: "<< (char*)ret << endl;return 0;
}

● 线程执行方法的返回值是void*,可以返回任意类型的数据,自定义类对象也是可以的!

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
using namespace std;string ToHex(pthread_t tid)
{char id[64];snprintf(id, sizeof(id), "0x%x", tid);return id;
}class ThreadReturn
{
public:ThreadReturn(pthread_t id, const string& info, int code):id_(id),info_(info),code_(code){}public:pthread_t id_;string info_;int code_;
};void *threadRoutine(void *args)
{string name = static_cast<const char *>(args);usleep(1000);int cnt = 5;while (cnt--){cout << "我是新线程, 正在运行噢, 我的线程id是: " << ToHex(pthread_self()) << endl;sleep(1);}ThreadReturn* ret = new ThreadReturn(pthread_self(),"thread quit normal", 10);return ret;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"thread-1");cout << "I am main thread, my thread id :" << ToHex(pthread_self()) << endl;void *ret = nullptr;pthread_join(tid, &ret);ThreadReturn* r = static_cast<ThreadReturn*>(ret);    cout << "main thread get new thread info : " << r->code_  << ", " << ToHex(r->id_)<< ", " << r->info_<< endl;return 0;
}

线程分离

大部分软件,跑起来之后都是死循环,比如用户打开qq, 打开网易云音乐等等,打开后不会自动退出的,直到用户手动关掉。也就是说,新线程大多数是不需要被等待的,主线程创建出新线程之后就让新线程去跑了,主线程就不管了

线程默认是joinable状态的,但如果主线程就是不想等待新线程,不关心新线程的退出状态,  主线程自己直接做其他事情,那么就可以将新线程设置为分离状态

可以在主线程中将新线程设置为分离状态,新线程也可以让自己设置成分离状态

线程分离代码演示

int pthread_detach(pthread_t thread);

参数:分离的线程id

返回值: 成功,返回0,失败,返回错误码

#include <iostream>
#include <pthread.h>
#include <cstring>
#include <unistd.h>
using namespace std;void* threadRoutine(void* args)
{pthread_detach(pthread_self()); //新线程中将自己分离int cnt = 5;while(cnt--){cout << "thread is running..." << endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr);//pthread_detach(tid); //主线程中将tid线程分离 int n = pthread_join(tid, nullptr);cout << n << endl;return 0;
}

● 线程被分离后,可以被取消,但不能被join,取消线程后线程返回值是PTHREAD_CANCELED

#define PTHREAD_CANCELED ((void *) -1)
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <functional>
using namespace std;void* threadRoutine(void* args)
{int cnt = 5;while(cnt--){cout << "thread is running..." << endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr);sleep(1);pthread_detach(tid); int n = pthread_cancel(tid); //取消tid线程cout << "main thread cancel done, " << " n: " << n << endl;void* ret = nullptr;n = pthread_join(tid, &ret); cout << "main thread join done, " << " n: " << n  << ", thread return:  "<< (int64_t)ret << endl;return 0;
}

原生线程库

Linux下没有真线程,只有轻量级进程,所以OS只会提供轻量级进程创建的系统调用,不会提供线程创建的系统调用

但用户只认线程,而且windows下是有真线程的,因此Linux在内核和用户层之间加了一层软件层,也就是pthread原生线程库,对内核的轻量级进程(LWP)接口进行封装,向上提供线程的一系列接口,同时管理多个线程,先描述,再组织,因此pthread库中是包含了一堆描述线程属性的结构体

原生线程库意思是任何一款操作系统都要默认有的,不属于C/C++语言本身, 因此编译时要带-l

作为用户,如果想知道一共创建了几个线程,每个线程的状态,当前有几个线程,一个线程退出了,退出结果是多少等信息,就直接去pthread库中获取即可

线程要有自己的一些独立属性:

1.上下文数据(被OS以轻量级进程形式维护在tcb中)

2.栈结构(栈大小,栈在哪里等信息,都必须在线程库中维护)

但是线程有多个,而地址空间中栈只有1个,如何分配??

clone接口 --- 创建轻量级进程, pthread_create的底层和fork的底层都是clone

第一个参数是线程执行的函数

第二个参数是线程库在堆区new的一段空间的起始地址,作为栈起始地址

第三个参数flags表示是创建轻量级进程还是创建一个真正的子进程

● 进程地址空间中的栈默认由主线程使用

线程库是共享的, 所以线程内部要管理整个系统中, 多个用户启动的多个线程!

而库要管理线程,就要在库中存在管理线程的结构体 --- struct pthread

线性局部存储是存放一些只能被线程自己看见的数据

线程栈就是保存了堆区new出来的一块空间的起始地址

每个线程在库中都是这三部分,可以把多个这部分看成一个数组,因此对线程的管理就转化成了对数组的增删查改

●  当线程退出时,退出结果会保存到库中的struct pthread中,因此主线程只需要去库中的struct pthread拷贝数据,拿到结果即可!

● 结论: pthread_t tid 表示的是线程属性集合在库中的地址!!! LWP是内核的概念!

C++的线程库本质是对pthread的封装, 因为去掉-lpthread选项之后报链接错误

线程局部存储

全局变量本身就是被线程共享的,而如果定义全局变量时带上__thread,会发现全局变量不是只有1份了,而是每个线程都有一份!

__thread修饰全局变量,会把全局变量拷贝到每个线程内的线程局部存储空间中!

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;//__thread是一个编译选项, 编译的时候就会把线程控制块中的空间开辟出来 --- 拷贝到线程局部存储空间中!
__thread int g_val = 100; void* threadRoutine(void* args)
{string name = static_cast<const char* >(args);while(true){cout << "I am new thread" << ", thread name: " << name << ", g_val:" << g_val << ", &g_val: " << &g_val << endl << endl;g_val++;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)"thread-1");while(true){cout << "I am main thread" <<  ", g_val:" << g_val << ", &g_val: " << &g_val << endl << endl;sleep(1);}pthread_join(tid, nullptr); return 0;
}

● 线程局部存储的用途: 定义一个全局变量,用__thread修饰,这样就可以在每个线程内部获取到线程的lwp

#include <iostream>
#include <pthread.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <unistd.h>
using namespace std;__thread pid_t lwp; void* threadRoutine(void* args)
{string name = static_cast<const char* >(args);lwp =  syscall(SYS_gettid); //系统调用获取当前线程的lwpwhile(true){cout << "I am new thread" << ", thread name: " << name << "new thread lwp: " << lwp << endl; sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)"thread-1");lwp =  syscall(SYS_gettid); //系统调用获取当前线程的lwpwhile(true){cout << "I am new thread" << "new thread lwp: " << lwp << endl; sleep(1);}pthread_join(tid, nullptr); return 0;
}

注意:

__thread string threadname;  //err, __thread只能存储内置类型,不能存储一些容器

线程中可以fork, 本质是在创建子进程, 也可以调用execl, 不过替换的是整个进程,会影响其他所有线程,因此不建议在线程中excel,如果要execl,建议先fork, 再execl

自己实现线程封装

Thread.hpp

#pragma once #include <iostream>
#include <string>
#include <pthread.h>
#include <functional>
using namespace std;//设计方的视角template<class T>
using func_t = function<void(T)>;  //返回值为void, 参数为T的类型template<class T>
class Thread
{
public:Thread(const string& threadname, func_t<T> func, T data):_tid(0),_threadname(threadname),_isrunning(false),_func(func),_data(data){}//改为static, 参数就没有this指针了!static void* ThreadRoutine(void* args) //不加static, 类内方法, 默认携带this指针{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;}else {return false;}}//线程等待bool join(){if(!_isrunning) return true;int n = pthread_join(_tid, nullptr);if(n == 0){_isrunning = false;return true;}else{return false;}}string ThreadName(){return _threadname;}bool IsRunning(){return _isrunning;}~Thread(){}private:pthread_t _tid; //线程idstring  _threadname; //线程名bool _isrunning; //线程是否在运行func_t<T> _func; //线程执行方法T _data;
};

main.cc

#include <iostream>
#include <unistd.h>
#include <vector>
#include "thread.hpp"//应用方的视角
string GetThreadName()
{static int number = 1;char name[64];snprintf(name, sizeof(name), "Thread-%d", number++);return name;
}void Print(int num)
{while(num){cout << "hello world: " << num-- << endl;sleep(1);}    
}int main()
{Thread<int> t(GetThreadName(), Print, 10);t.start();t.join();return 0;
}

线程的优缺点

优点:

创建一个新线程的代价要比创建一个新进程小得多

与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

线程占用的资源要比进程少很多

能充分利用多处理器的可并行数量

在等待慢速I/O操作结束的同时程序可执行其他的计算任务

计算密集型应用,为了能在多处理器系统上运行, 将计算分解到多个线程中实现

I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

缺点:

缺乏访问控制

进程是访问控制的基本粒度, 由于大部分资源都是共享的,在一个线程中调用某些OS函数会对整个进程造成影响,而同步和互斥就是在解决这个问题

健壮性/鲁棒性降低

多线程中,一个线程崩溃,整个进程都崩溃,而多进程程序,一个进程崩溃,不影响其他进程,因为进程具有独立性

编程难度提高

编写与调试一个多线程程序比单线程程序困难得多

性能损失

一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

多线程共享与独占资源

多线程之间共享的资源

1. 进程代码段

2. 进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)

3. 进程打开的文件描述符

4. 信号的处理器

5. 进程的当前目录

6. 进程用户ID与进程组ID。

多线程之间独立的资源

1.线程ID

2.寄存器组的值

3.线程的堆栈

4.错误返回码

5.线程的信号屏蔽码

6.线程的优先级

线程互斥

下面是一段模拟多线程抢票的代码

#include <iostream>
#include <unistd.h>
#include <vector>
#include "Thread.hpp"//构造线程名称
string GetThreadName()
{static int number = 1;char name[64];snprintf(name, sizeof(name), "Thread-%d", number++);return name;
}//抢票逻辑
int ticket = 10000; // 全局的共享资源
void GetTicket(string name)
{while (true){if (ticket > 0){// 充当抢票花费的时间usleep(1000);printf("%s get a ticket : %d\n", name.c_str(), ticket);ticket--; }else{break;}// 实际情况, 还有后续的动作}
}int main()
{string name1 = GetThreadName();Thread<string> t1(name1, GetTicket, name1);sleep(2);string name2 = GetThreadName();Thread<string> t2(name2, GetTicket, name2);sleep(2);string name3 = GetThreadName();Thread<string> t3(name3, GetTicket, name3);sleep(2);t1.start();sleep(2);t2.start();sleep(2);t3.start();sleep(2);t1.join();t2.join();t3.join();return 0;
}

运行代码发现最后出现了票数出现了负数,但是我们在if语句中判断票数>0了呀,为啥还会出现票数为负数呢???

显然票数是公共资源,可以被多个执行流同时访问,而多个执行流同时访问公共资源显然出现了问题,因此我们需要把公共资源保护起来,使得任何一个时刻,只允许一个线程正在访问公共资源,此时公共资源就叫做临界资源! 而我们的代码中只有一部分代码会去访问临界资源的,进程中访问临界资源的代码叫做临界区

任何时刻只允许一个执行流进入临界区,使得多个执行流只能串行访问临界资源,叫做互斥!

++/--本质是三条汇编语句, 每一条汇编语句都是原子性的, 而执行每一条汇编语句都有可能被中断, 三条汇编语句过程是 先把内存中的a拷贝到cpup寄存器中,然后在寄存器中对a++, 最后将寄存器的a拷贝会回内存空间中!

多线程同时访问公共资源有什么问题呢??? 举个例子!

比如有A线程和B线程, 公共资源是int a = 10, 两个线程都要进行a++操作, 目前的情况是A线程把汇编的第二步执行完毕,寄存器中a为11, 然后被切换走了,于是A线程的上下文数据中就保存了a为11, 此时线程B被cpu调度,一直将内存空间中的a++到了100,此时被切走了,线程A被调度,接着执行第三条汇编语句,将自己的上下文数据,a=11恢复到寄存器中,然后将寄存器内容写回内存,于是内存空间中的a改为了11,就出现了数据不一致的问题!!

而我们今天的抢票代码最后票出现了负数原因是:

当票数减为1时,多个线程进行了if条件判断,都是成立的,语句进入到了if循环内部,此时某个线程被调度,将内存中的tickets--到了0, 此时其他线程都执行过了if判断, 再对票数--, 就会将内存中的票数--到负数!

互斥锁

● 互斥锁的功能就是用来实现互斥,使得临界资源只能同时被一个执行流访问

● 尽量要给少的代码块加锁 (因为加锁之后,同一时间内只允许一个线程访问临界区资源,如果给大量代码加锁,多线程就没有意义了,效率可能大大降低)

● 一般都是给临界区加锁

● 使用锁的相关接口

定义全局互斥锁并初始化

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

加锁

int pthread_mutex_lock(pthread_mutex_t *mutex);

 解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

使用全局锁实现互斥访问临界资源

//定义全局锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//抢票逻辑
int ticket = 10000; // 全局的共享资源
void GetTicket(string name)
{while (true){pthread_mutex_lock(&mutex); if (ticket > 0){// 充当抢票花费的时间usleep(1000);printf("%s get a ticket : %d\n", name.c_str(), ticket);ticket--; pthread_mutex_unlock(&mutex); }else{pthread_mutex_unlock(&mutex); break;}// 实际情况, 还有后续的动作}
}

注意:

● 为了实现互斥访问临界资源,我们定义了一把全局锁,而全局锁也是全局变量,也是公共资源的,也得保证申请锁是安全的呀!!! 而申请锁本身是原子性操作,是安全的!

● 加锁是由程序员自己保证的,是一种规则,不遵守就是自己写的bug

● 根据互斥的定义, 任何时刻,只允许一个线程申请锁成功! 就注定了会有多个线程申请锁失败,失败的线程默认会在mutex锁上阻塞,阻塞的本质就是等待!

● 一个线程在临界区访问临界资源的时候, 是完全有可能发生线程切换的,但是切换走的线程依旧没有释放锁,可以理解成把锁带走了,其他线程依旧访问不了临界资源

● 加锁的情况下,if里面的代码块也表现出"原子性", 因为这段代码要么不执行,要么执行完,别的线程才能访问

定义局部互斥锁

初始化局部锁(第二个参数可以设置锁属性,传nullptr即可)

int pthread_mutex_init(pthread_mutex_t *restrict mp,const pthread_mutexattr_t *restrict mattr);

释放局部锁

int pthread_mutex_destroy(pthread_mutex_t *mutex)

加锁

int pthread_mutex_lock(pthread_mutex_t *mutex);

解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

 使用局部锁实现互斥访问临界资源

#include <iostream>
#include <unistd.h>
#include <vector>
#include "Thread.hpp"string GetThreadName()
{static int number = 1;char name[64];snprintf(name, sizeof(name), "Thread-%d", number++);return name;
}int ticket = 10000; // 全局的共享资源void GetTicket(pthread_mutex_t* mutex)
{while (true){   pthread_mutex_lock(mutex); if (ticket > 0){// 充当抢票花费的时间usleep(1000);printf("get a ticket : %d\n", ticket);ticket--;pthread_mutex_unlock(mutex);}else{pthread_mutex_unlock(mutex);break;}// 实际情况, 还有后续的动作}
}int main()
{pthread_mutex_t mutex; //定义局部锁pthread_mutex_init(&mutex, nullptr); //初始化局部锁string name1 = GetThreadName();Thread<pthread_mutex_t*> t1(name1, GetTicket, &mutex);string name2 = GetThreadName();Thread<pthread_mutex_t*> t2(name2, GetTicket, &mutex);string name3 = GetThreadName();Thread<pthread_mutex_t*> t3(name3, GetTicket, &mutex);t1.start();t2.start();t3.start();t1.join();t2.join();t3.join();pthread_mutex_destroy(&mutex); //释放局部锁return 0;
}

自己实现锁的封装

LockGuard.hpp

#include <iostream>
#include <string>
#include <pthread.h>
using namespace std;//不定义锁, 默认外部会传入锁对象
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;
};

 main.cc

#include <iostream>
#include <unistd.h>
#include <vector>
#include "Thread.hpp"
#include "LockGuard.hpp"string GetThreadName()
{static int number = 1;char name[64];snprintf(name, sizeof(name), "Thread-%d", number++);return name;
}int ticket = 10000;                                // 全局的共享资源
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 全局锁对象void GetTicket(string name)
{while (true){LockGuard lockguard(&mutex);{if (ticket > 0){// 充当抢票花费的时间usleep(1000);printf("get a ticket : %d\n", ticket);ticket--;}else{break;}}// 实际情况, 还有后续的动作}
}int main()
{string name1 = GetThreadName();Thread<string> t1(name1, GetTicket, name1);string name2 = GetThreadName();Thread<string> t2(name2, GetTicket, name2);string name3 = GetThreadName();Thread<string> t3(name3, GetTicket, name3);t1.start();t2.start();t3.start();t1.join();t2.join();t3.join();return 0;
}

加锁实现互斥的原理

● 大多数体系结构都提供了swap和exchange指令, 作用是把寄存器内容和内存单元的数据进行交换

● 而锁本质是一个结构体,可以简单认为内部有一个变量 int mutex = 1

● A线程加锁时,先将寄存器%al内容置成0,然后交换寄存器%al的内容和内存变量mutex内容,于是%al的内容变为了1, mutex变为了0

此时就算线程A被切走了,会把上下文数据包括%al的内容带走,线程B开始调度运行, 内存内容已经是0了,因此尽管线程B交换寄存器%al的值和内存中的mutex的值,交换完之后还是0,此时进入else分支,挂起等待。因此可以认为,线程切换时把锁带走了!

● 所以交换本质是将一个共享的mutex资源,交换到自己的上下文中,属于线程自己了!

● 解锁就是直接将mutex置为1,其他线程申请锁时就正常执行上述汇编语句即可!

● 加锁和解锁的一般规则: 谁加锁,谁解锁

死锁

● 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资 源而处于的一种永久等待状

死锁的四个必要条件

1. 互斥条件:一个资源每次只能被一个执行流使用

2. 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放

3. 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺

4. 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

避免死锁:

1. 破坏死锁的四个必要条件

2. 加锁顺序一致

3. 避免锁未释放的场景

4. 资源一次性分配

避免死锁算法

1.死锁检测算法

2.银行家算法

线程同步

● 同步本质是让多执行流访问临界资源具有一定的顺序

多线程访问公共资源可能会出现问题,因此我们使用互斥锁保证任何一个时刻只有1个线程访问公共资源,但有些线程竞争锁的能力很强,每次竞争时都会拿到锁,导致其他线程长时间访问不了公共资源,也就是会导致线程饥饿问题

● 互斥能保证访问资源的安全性,但只有安全是不够的,同步能够较为充分高效的使用资源

● 条件变量本质就是实现线程同步的一种机制,是pthread库提供的一个线程向另一个线程通知信息的方式

● 举个例子理解条件变量

一张桌子,两个人,一个人放苹果,另一个蒙着眼睛的人拿苹果,放苹果的时候不能拿,拿的时候不能放,因此要加锁实现互斥!而拿苹果的人不知道放苹果的人什么时候放,因此拿苹果的人不断的申请锁,检测,释放锁,导致了放苹果人的饥饿问题!

于是放了一个铃铛,让放苹果的人放苹果之后,敲一下铃铛,此时拿苹果的人再去拿苹果!

上述的铃铛本质就是条件变量,可以理解成以下结构体:

struct cond
{int flag; //条件是否就绪tcb_queue; //条件不满足,就排队!
}

● 上述例子中只有1个拿苹果的人,实际可以有多个拿苹果的人,可以认为所有拿苹果的人都要排队,拿完苹果之后再去队尾重新排队,这就使得多执行流可以较为均衡地按照一定顺序访问资源

● 条件变量使用接口

在cond条件变量下进行等待

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

唤醒条件变量下等待的一个线程

int pthread_cond_signal(pthread_cond_t *cond);

唤醒条件变量下等待的所有线程

int pthread_cond_broadcast(pthread_cond_t *cond);

● 条件变量使用示例

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstring>
using namespace std;pthread_cond_t cond = PTHREAD_COND_INITIALIZER;    // 定义条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 定义全局锁void *threadRoutine(void *args)
{const char* threadname = static_cast<const char*>(args);while(true){pthread_mutex_lock(&mutex);pthread_cond_wait(&cond, &mutex); //在指定的条件变量下等待cout << "I am a new thread: " << threadname << endl;pthread_mutex_unlock(&mutex);}
}int main()
{pthread_t t1, t2, t3;pthread_create(&t1, nullptr, threadRoutine, (void*)"thread-1");pthread_create(&t2, nullptr, threadRoutine, (void*)"thread-2");pthread_create(&t3, nullptr, threadRoutine, (void*)"thread-3");sleep(5); // 5s之后唤醒线程while(true){pthread_cond_signal(&cond); //每次唤醒一个线程sleep(1);}pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);
}

说明:

● cout是往显示器上打印,多个线程都执行cout语句,访问显示器资源,此时显示器资源也是公共资源,打印会出现混乱的情况,因此显示器资源也需要被保护起来,因此在cout语句前后加锁解锁

● 只加锁,发现线程1、2、3打印没有任何的顺序性,且一个线程一打印就是一批语句,这就是竞争锁的能力不同而导致的

● 为了让线程1、2、3打印具有一定的顺序性,我们引入了条件变量,在加锁和解锁之间使用pthread_cond_wait 接口让线程在条件变量下等待,在主线程中,每隔1s唤醒一个线程,从而使得打印结果具有明显的顺序性

● 如果在主线程中,每隔1s唤醒所有线程,那么所有线程都会去参与锁的竞争,因此最后打印的顺序依旧不确定

抢票代码加入线程同步:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstring>
using namespace std;pthread_cond_t cond = PTHREAD_COND_INITIALIZER;    // 定义条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 定义全局锁int tickets = 5000;void *threadRoutine(void *args)
{string threadname = static_cast<const char *>(args);while (true){pthread_mutex_lock(&mutex);if (tickets > 0){usleep(1000); // 充当抢票花费的时间cout << threadname << ": get a ticket, ticket : " << tickets << endl;tickets--;}else{cout << threadname << ", 没有票了" << endl;pthread_cond_wait(&cond, &mutex); // 没有票了, 就去条件变量下等待}pthread_mutex_unlock(&mutex);}
}int main()
{pthread_t t1, t2, t3;pthread_create(&t1, nullptr, threadRoutine, (void *)"thread-1");pthread_create(&t2, nullptr, threadRoutine, (void *)"thread-2");pthread_create(&t3, nullptr, threadRoutine, (void *)"thread-3");sleep(5); // 5s之后唤醒线程while (true){sleep(6);pthread_mutex_lock(&mutex);tickets += 1000; // 每隔6s, 就再放1000张票pthread_mutex_unlock(&mutex);pthread_cond_signal(&cond); // 唤醒一个线程// pthread_cond_broadcast(&cond); // 唤醒所有线程}pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);return 0;
}

说明:

● 加锁和解锁之间,往往要访问临界资源,但是临界资源不一定是满足条件的,所以我们要判断,条件不满足,就应该让线程去条件变量下等待

● 线程在条件变量下等待的时候, 会自动释放锁

● 线程被唤醒的时候,是在临界区内被唤醒的,当线程被唤醒,在wait函数返回时,要重新申请并持有锁,才能真正被唤醒,这也就是 pthread_cond_wait 的参数中同时有条件变量和锁的原因,而重新申请并持有锁也是要参与锁的竞争的!


http://www.ppmy.cn/news/1554991.html

相关文章

《Django 5 By Example》阅读笔记:p493-p520

《Django 5 By Example》学习第 17 天&#xff0c;p493-p520 总结&#xff0c;总计 28 页。 一、技术总结 1.internationalization(国际化) vs localization(本地化) (1)18n&#xff0c;L10n&#xff0c;g11n 以前总觉得这两个缩写好难记&#xff0c;今天仔细看了下维基百科…

Jmeter如何对UDP协议进行测试?

Jmeter如何对UDP协议进行测试&#xff1f; 1 jmeter-plugins安装2 UDP-Protocol Support安装3 UDP协议测试 1 jmeter-plugins安装 jmeter-plugins是Jmeter的插件管理器&#xff1b;可以组织和管理Jmeter的所有插件&#xff1b;直接进入到如下页面&#xff0c;选择如图的选项进…

【理想汽车中科院】基于模仿学习的端到端自动驾驶数据缩放规律

论文: https://arxiv.org/pdf/2412.02689 项目: https://github.com/ucaszyp/Driving-Scaling-Law 0. 摘要 端到端自动驾驶范式因其可扩展性而最近吸引了大量关注。然而&#xff0c;现有方法受到现实世界数据规模有限的制约&#xff0c;这阻碍了对端到端自动驾驶相关扩展规律…

得物使用AutoMQ构建海量数据处理的新一代可观测性架构

引言 得物作为全球领先的潮流网购社区&#xff0c;日益增长的用户和数据带来了巨大的技术挑战。当前&#xff0c;得物的可观测性平台每天生成数PB级Trace数据和数万亿条Span记录&#xff0c;要求平台具备高效的实时处理能力和低成本的数据存储解决方案。 传统的存算一体架构将…

【蓝桥杯最新板】蓝桥杯嵌入式液晶上实现电子时钟

这几年蓝桥杯比赛比较适合学生技能学习&#xff0c;考虑板子功能&#xff0c;提出完成的任务。 要求在液晶完成如下图效果&#xff1a; 主要是实现液晶显示时钟和数字时钟&#xff0c;具体样式可以依据实际情况微调。 实现过程&#xff1a; 1.需要画圆&#xff08;外圆、内圆…

颜色的基本处理

数码相机能够获取彩色图像&#xff0c;但相机的色彩处理是一个非常复杂的过程&#xff0c;是非常重要的。 此过程生产制造商在细节方面都是不公布的&#xff0c;但是基本的概念是相同的。当相机捕捉一个真实场景时&#xff0c;是怎么还原成人眼所看到的图像呢&#xff1f; 1.R…

k8s中设置annotation的方法总结

k8s中设置annotation的方法总结 annotation是什么 在 Kubernetes 中&#xff0c;Annotations 是一种用于向 Kubernetes 对象附加非标识性元数据的机制。 annotation有什么用 annotation与 Labels 类似&#xff0c;但有一些关键区别和特定用途。 常用于存储与对象相关的配置…

【Hexo】给博客添加宠物挂件

适配安知鱼主题和Solitude主题&#xff0c;我采用的是安知鱼主题&#xff0c;Solitude主题可参考教程 {% link 给你的博客底部添加一排宠物,青桔气球,https://blog.qjqq.cn/posts/f69c.html %} 同时感谢梦爱吃鱼 大佬的耐心指导和帮助&#xff0c;原文可参考 {% link 给你的…