linux篇【11】:linux下的线程<中序>
创始人
2024-02-15 10:35:19
0

目录

一.线程互斥

1.三个概念

2.互斥

(1)在执行语句的任何地方,线程可能被切换走

(3)抢票场景中的问题

(4)解决方案

3.加锁

(1)加锁介绍

(2)定义/释放 互斥锁

(3)加锁、解锁(使用锁)

(4)使用锁代码

4.加锁方式

(1)基本的传锁(就传不了线程名字了)

(2)既传name又传锁

二.加锁的原理探究 

1.上锁后的临界区内仍可以进程切换。

2.在我被切走的时候,绝对不会有线程进入临界区!

3.加锁是原子的

(1)xchgb 交换是原子的

(2)加锁原理

(3)C++ 加锁

Makefile

Lock.hpp

mythread.cc

(4)C++ RAII加锁

4.可重入VS线程安全

5. 死锁

①两个锁互相锁死的情景 

②一个锁产生死锁的情景 


一.线程互斥

1.三个概念

1.临界资源:多个执行流都能看到并能访问的资源,临界资源
2.临界区:多个执行流代码中有不同的代码,访问临界资源的代码,我们称之为临界区
3.互斥特性:当我们访问某种资源的时候,任何时刻。都只有一个执行流在进行访问,这个就叫做:互斥特性

4.线程互斥:线程互斥 指的是在多个线程间对临界资源进行争抢访问时有可能会造成数据二义,因此通过保证同一时间只有一个线程能够访问临界资源的方式实现线程对临界资源的访问安全性

2.互斥

没有互斥时,以抢票为例,一抢票 票数减1: int tickets;   tickets--;为例

(1)在执行语句的任何地方,线程可能被切换走

int tickets;
tickets--;tickets--是由3条语句完成的:

tickets--:有三步
① load tickets to reg
② reg-- ;
③ write reg to tickets

(2)CPU内的寄存器是被所有的执行流共享的,但是寄存器里面的数据是属于当前执行流的上下文数据。线程被切换的时候,需要保存上下文;线程被换回的时候,需要恢复上下文。 

(3)抢票场景中的问题

情况1:线程A先抢到一张票时,寄存器中tickets 10000——>9999, 还未写回内存,A的时间片到了就被切走了,开始执行线程B了;线程B也抢票,直接抢了9950张,还剩50张,此时B的时间片到了,又切回线程A,又把9999写入内存,就错误了。

        if (tickets > 0){usleep(1000);cout << name << " 抢到了票, 票的编号: " << tickets << endl;tickets--;usleep(123); //模拟其他业务逻辑的执行}

情况2:或者在抢最后一张时,线程A先抢最后一张票,if (tickets > 0)为真,进入if语句,此时A的时间片到了就被切走了,开始执行线程B了;线程B也抢票,此时显示票数仍是1,if (tickets > 0)为真,进入if语句,并执行tickets--;,tickets变为0,此时B的时间片到了,又切回线程A,线程A又继续执行tickets--;,此时直接把票数减到了负数,就出错了。

(4)解决方案

原子性:一件事要么不做,要么全做完

把tickets--这个临界区设为原子的,使不想被打扰,加锁

3.加锁

(1)加锁介绍

加锁范围:临界区,只要对临界区加锁,而且加锁的力度越细越好

加锁本质:加锁的本质是让线程执行临界区代码串行化

加锁是一套规范,通过临界区对临界资源进行访问的时候,要加就都要加

锁保护的是临界区, 任何线程执行临界区代码访问临界资源,都必须先申请锁,前提是都必须先看到锁!那这把锁,本身不就也是临界资源吗?锁的设计者早就想到了

pthread_mutex_lock: 竞争和申请锁的过程,就是原子的!申请锁的过程不会中断,不会被打扰。

难度在加锁的临界区里面,就没有线程切换了吗????

mutex简单理解就是一个0/1的计数器,用于标记资源访问状态:

  • 0表示已经有执行流加锁成功,资源处于不可访问,
  • 1表示未加锁,资源可访问。

(2)定义/释放 互斥锁

man pthread_mutex_init

① pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 定义全局/静态的互斥锁,可以用这个宏初始化

② int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);         mutex:锁的地址。attr:锁的属性设为空             

③ int pthread_mutex_destroy(pthread_mutex_t *mutex); 释放锁

(3)加锁、解锁(使用锁)

man pthread_mutex_lock

① int pthread_mutex_lock(pthread_mutex_t *mutex);  加阻塞式锁

线程1正在用锁住的代码,那线程2就要阻塞式等待线程1执行完才能使用这个锁(即执行锁住的代码)

② int pthread_mutex_trylock(pthread_mutex_t *mutex); 加非阻塞式锁

线程1正在用这个非阻塞锁(即执行锁住的代码),那线程2就直接返回,只有当没有别的线程用这个锁,自己才能用。

③ int pthread_mutex_unlock(pthread_mutex_t *mutex);  解锁

如果不解锁,比如线程1使用锁后没有解锁就退出了,那么其他线程在竞争使用这个锁时,就会一直处于休眠,等待这个锁被解锁才能继续使用, n = pthread_join(tid4, nullptr); pthread_join就会一直阻塞等待线程退出,所以会显示进程卡住了。

(4)使用锁代码

else那里也要解锁,否则会阻塞:线程1走else使用锁后如果没有解锁就退出了,那么其他线程在竞争使用这个锁时,就会一直处于休眠,等待这个锁被解锁才能继续使用, n = pthread_join(tid4, nullptr); pthread_join就会一直阻塞等待线程退出,所以会显示进程卡住了。

跟上面代码一样(可以忽略):

#include 
#include 
#include 
#include 
#include 
#include  // 仅仅是了解// __thread int global_value = 100;// void *startRoutine(void *args)
// {
//     // pthread_detach(pthread_self());
//     // cout << "线程分离....." << endl;
//     while (true)
//     {
//         // 临界区,不是所有的线程代码都是临界区
//         cout << "thread " << pthread_self() << " global_value: "
//              << global_value << " &global_value: " << &global_value
//              << " Inc: " << global_value++ << " lwp: " << ::syscall(SYS_gettid)<(args);while (true){// 临界区,只要对临界区加锁,而且加锁的粒度约细越好// 加锁的本质是让线程执行临界区代码串行化// 加锁是一套规范,通过临界区对临界资源进行访问的时候,要加就都要加// 锁保护的是临界区, 任何线程执行临界区代码访问临界资源,都必须先申请锁,前提是都必须先看到锁!// 这把锁,本身不就也是临界资源吗?锁的设计者早就想到了// pthread_mutex_lock: 竞争和申请锁的过程,就是原子的!// 难度在加锁的临界区里面,就没有线程切换了吗????pthread_mutex_lock(&mutex);if (tickets > 0){usleep(1000);cout << name << " 抢到了票, 票的编号: " << tickets << endl;tickets--;pthread_mutex_unlock(&mutex);//other codeusleep(123); //模拟其他业务逻辑的执行}else{// 票抢到几张,就算没有了呢?0cout << name << "] 已经放弃抢票了,因为没有了..." << endl;pthread_mutex_unlock(&mutex);break;}}return nullptr;
}// 如何理解exit?
int main()
{pthread_mutex_init(&mutex, nullptr);pthread_t tid1;pthread_t tid2;pthread_t tid3;pthread_t tid4;pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");pthread_create(&tid4, nullptr, getTickets, (void *)"thread 4");// sleep(1);// 倾向于:让主线程,分离其他线程// pthread_detach(tid1);// pthread_detach(tid2);// pthread_detach(tid3);// 1. 立即分离,延后分离 -- 线程活着 -- 意味着,我们不在关心这个线程的死活。4. 线程退出的第四种方式,延后退出// 2. 新线程分离,但是主线程先退出(进程退出) --- 一般我们分离线程,对应的main thread一般不要退出(常驻内存的进程)// sleep(1);int n = pthread_join(tid1, nullptr);cout << n << ":" << strerror(n) << endl;n = pthread_join(tid2, nullptr);cout << n << ":" << strerror(n) << endl;n = pthread_join(tid3, nullptr);cout << n << ":" << strerror(n) << endl;n = pthread_join(tid4, nullptr);cout << n << ":" << strerror(n) << endl;pthread_mutex_destroy(&mutex);return 0;
}

4.加锁方式

(1)基本的传锁(就传不了线程名字了)

#include 
#include 
#include 
using namespace std;
int tickets = 1000;
void *startRoutine(void *args)
{pthread_mutex_t* mutex_p= static_cast(args);while (true){pthread_mutex_lock(mutex_p);//如果申请不到,线程阻塞等待if (tickets > 0){usleep(1000);cout << "thread: " << pthread_self() << "get a ticket: " << tickets << endl;tickets--;pthread_mutex_unlock(mutex_p);//做其他的事usleep(500);}else{pthread_mutex_unlock(mutex_p);break;}}return nullptr;
}int main()
{pthread_t t1, t2, t3, t4;static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;pthread_create(&t1, nullptr, startRoutine, (void *)&mutex);pthread_create(&t2, nullptr, startRoutine, (void *)&mutex);pthread_create(&t3, nullptr, startRoutine, (void *)&mutex);pthread_create(&t4, nullptr, startRoutine, (void *)&mutex);pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);pthread_join(t4, nullptr);pthread_mutex_destroy(&mutex);return 0;
}

(2)既传name又传锁

完整版:

#include 
#include 
#include 
#include 
using namespace std;
int tickets = 1000;
#define NAMESIZE 64
typedef struct threadData
{char name[NAMESIZE];pthread_mutex_t* mutexp;
}threadData;
void *startRoutine(void *args)
{threadData* td= static_cast(args);while (true){pthread_mutex_lock(td->mutexp);//如果申请不到,线程阻塞等待if (tickets > 0){usleep(1000);cout << "thread: " << td->name << "get a ticket: " << tickets << endl;tickets--;pthread_mutex_unlock(td->mutexp);//做其他的事usleep(500);}else{pthread_mutex_unlock(td->mutexp);break;}}return nullptr;
}int main()
{static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;pthread_t t1, t2, t3, t4;threadData *td1=new threadData();threadData *td2=new threadData();threadData *td3=new threadData();threadData *td4=new threadData();strcpy(td1->name,"thread 1");strcpy(td2->name,"thread 2");strcpy(td3->name,"thread 3");strcpy(td4->name,"thread 4");td1->mutexp=&mutex;td2->mutexp=&mutex;td3->mutexp=&mutex;td4->mutexp=&mutex;pthread_create(&t1, nullptr, startRoutine, (void *)td1);pthread_create(&t2, nullptr, startRoutine, (void *)td2);pthread_create(&t3, nullptr, startRoutine, (void *)td3);pthread_create(&t4, nullptr, startRoutine, (void *)td4);pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);pthread_join(t4, nullptr);pthread_mutex_destroy(&mutex);return 0;
}

 

二.加锁的原理探究 

1.上锁后的临界区内仍可以进程切换。

我在临界资源对应的临界区中上锁了,临界区还是多行代码,是多行代码就可以被切换。加锁 不等于 不会被切换。加锁后仍然可以切换进程,因为线程执行的加锁解锁等对应的也是代码,线程在任意代码处都可以被切换,只是线程加锁是原子的——要么你拿到了锁,要么没有

2.在我被切走的时候,绝对不会有线程进入临界区!

——因为每个线程进入临界区都必须先申请锁! !假设当前的锁被A申请走了,即便当前的线程A没有被调度,因为它是被切走的时候是抱着锁走的,其他线程想进入临界区需要先申请锁,但是已经有线程A持有锁了,则其他线程在申请时会被阻塞。即:一旦一个线程持有了锁,该线程根本就不担心任何的切换问题!对于其他线程而言,线程A访问临界区,只有没有进入和使用完毕两种状态
,才对其他线程有意义!即:对于其他线程而言,线程A访问临界区具有一定的原子性
注意:尽量不要在临界区内做耗时的事情!因为只有持有锁的线程能访问,其他线程都会阻塞等待。

3.加锁是原子的

①每一个CPU任何时刻只能有一个线程在跑

②单独的一条汇编代码是具有原子性的

(1)xchgb 交换是原子的

经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题 为了实现互斥锁操作,大多数体系结构(芯片体系结构)都提供了swap或exchange指令,该指令的作用是使用一条汇编代码把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

(2)加锁原理

mutex中的值默认是1 %al :CPU中的寄存器(凡是在寄存器中的数据,全部都是线程的内部上下文! !
mutex :内存中的一个变量

 

加锁原理解释:线程A执行 movb $0,%al  :把0放入寄存器%al中。然后执行xchgb %al, mutex:通过一条汇编代码交换 寄存器%al (值是0) 和 变量mutex (值是1)  的值,交换后 寄存器%al中是1, 变量mutex中是0。还未执行判断,此时突然进程切换,线程A会自动带走%al中的上下文数据1,线程B开始执行:线程B执行 movb $0,%al  :把0放入寄存器%al中。然后执行xchgb %al, mutex:通过一条汇编代码交换 寄存器%al (值是0) 和 变量mutex (值是0)  的值,交换后 寄存器%al 和 变量mutex中都是0。再判断——>因为%al是0,不大于0就挂起。此时线程B挂起,该线程A继续执行,线程A会把自己上下文数据恢复到%al中,此时%al=1,该执行判断了——>因为%al是1,就返回。这样就成功做到:多个线程看起来同时在访问寄存器,但是互不影响

lock和unlock的伪代码:

(3)C++ 加锁

Makefile

mythread:mythread.ccg++ -o $@ $^ -lpthread -std=c++11.PHONY:clean
clean:rm -f mythread

Lock.hpp

#pragma once#include 
#include class Mutex
{
public:Mutex(){pthread_mutex_init(&lock_, nullptr);}void lock(){pthread_mutex_lock(&lock_);}void unlock(){pthread_mutex_unlock(&lock_);}~Mutex(){pthread_mutex_destroy(&lock_);}private:pthread_mutex_t lock_;
};class LockGuard
{
public:LockGuard(Mutex *mutex) : mutex_(mutex){mutex_->lock();std::cout << "加锁成功..." << std::endl;}~LockGuard(){mutex_->unlock();std::cout << "解锁成功...." << std::endl;}private:Mutex *mutex_;
};

mythread.cc

int tickets = 1000;
Mutex mymutex;// 函数本质是一个代码块, 会被多个线程同时调用执行,该函数被重复进入 - 被重入了
bool getTickets()
{bool ret = false; // 函数的局部变量,在栈上保存,线程具有独立的栈结构,每个线程各自一份LockGuard lockGuard(&mymutex); //局部对象的声明周期是随代码块的!if (tickets > 0){usleep(1001); //线程切换了cout << "thread: " << pthread_self() << " get a ticket: " << tickets << endl;tickets--;ret = true;}return ret;
}
void *startRoutine(void *args)
{const char *name = static_cast(args);while(true){if(!getTickets()){break;}cout << name << " get tickets success" << endl;//其他事情要做sleep(1);}
}int cnt = 10000;int main()
{pthread_t t1, t2, t3, t4;pthread_create(&t1, nullptr, startRoutine, (void *)"thread 1");pthread_create(&t2, nullptr, startRoutine, (void *)"thread 2");pthread_create(&t3, nullptr, startRoutine, (void *)"thread 3");pthread_create(&t4, nullptr, startRoutine, (void *)"thread 4");pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);pthread_join(t4, nullptr);
}

(4)C++ RAII加锁

通过RAII思想,创建对象时加锁,出代码块时解锁

    {//临界资源LockGuard LockGuard(&mymutex);cnt++;.........}

4.可重入VS线程安全

(1)概念

线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。(我们写的不加锁的抢票函数就是线程不安全函数,因为可能抢票抢到-1) 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。(90%函数是不可重入函数,带_r是可重入函数,不带_r是不可重入函数)

(2)####常见的线程不安全的情况

不保护共享变量的函数 函数状态随着被调用,状态发生变化的函数 返回指向静态变量指针的函数 调用线程不安全函数的函数 (3)常见的线程安全的情况 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的类或者接口对于线程来说都是原子操作 多个线程之间的切换不会导致该接口的执行结果存在二义性 (4)常见不可重入的情况 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构 可重入函数体内使用了静态的数据结构 (5)常见可重入的情况 不使用全局变量或静态变量 不使用用malloc或者new开辟出的空间 不调用不可重入函数 不返回静态或全局数据,所有数据都有函数的调用者提供 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据 (6)可重入与线程安全联系 函数是可重入的,那就是线程安全的 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。 (7)可重入与线程安全区别 可重入函数是线程安全函数的一种 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

5. 死锁

(1)概念

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

(2)死锁四个必要条件(有一个条件不满足,死锁就不成立)

互斥条件:一个资源每次只能被一个执行流使用 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。(即:保持着自己的锁,还要要对方的锁) 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系 (3)避免死锁 破坏死锁的四个必要条件 加锁顺序一致 避免锁未释放的场景 资源一次性分配 (4)避免死锁算法 死锁检测算法(了解) 银行家算法(了解) 7. Linux线程同步 ###条件变量 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情 况就需要用到条件变量。

①两个锁互相锁死的情景 

#include 
#include 
#include 
#include 
#include 
#include "Lock.hpp"
#include using namespace std;pthread_mutex_t mutexA = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutexB = PTHREAD_MUTEX_INITIALIZER;void *startRoutine1(void *args)
{while (true){pthread_mutex_lock(&mutexA);sleep(1);pthread_mutex_lock(&mutexB);cout << "我是线程1,我的tid: " << pthread_self() << endl;pthread_mutex_unlock(&mutexA);pthread_mutex_unlock(&mutexB);}
}
void *startRoutine2(void *args)
{while (true){pthread_mutex_lock(&mutexB);sleep(1);pthread_mutex_lock(&mutexA);cout << "我是线程2, 我的tid: " << pthread_self() << endl;pthread_mutex_unlock(&mutexB);pthread_mutex_unlock(&mutexA);}
}int main()
{pthread_t t1, t2;pthread_create(&t1, nullptr, startRoutine1, nullptr);pthread_create(&t2, nullptr, startRoutine2, nullptr);pthread_join(t1, nullptr);pthread_join(t2, nullptr);return 0;
}

②一个锁产生死锁的情景 

申请锁申请了两次

请求与保持条件:你拿着这把锁,还要继续要这把锁,因为这个锁不会解锁,就会死锁

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
苏州离哪个飞机场近(苏州离哪个... 本篇文章极速百科小编给大家谈谈苏州离哪个飞机场近,以及苏州离哪个飞机场近点对应的知识点,希望对各位有...
客厅放八骏马摆件可以吗(家里摆... 今天给各位分享客厅放八骏马摆件可以吗的知识,其中也会对家里摆八骏马摆件好吗进行解释,如果能碰巧解决你...