• linux篇【11】:linux下的线程<后序>


    目录

    一.线程互斥

    1.三个概念

    2.互斥

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

    (3)抢票场景中的问题

    (4)解决方案

    3.加锁

    (1)加锁介绍

    (2)加锁小例子

    (3)定义/释放 互斥锁

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

    (5)使用锁代码

    4.加锁方式

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

    (2)既传name又传锁

    二.加锁的原理探究 

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

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

    3.加锁是原子的

    (1)xchgb 交换是原子的

    (2)加锁原理

    (3)C++ 加锁

    Makefile

    Lock.hpp

    mythread.cc

    (4)C++ RAII加锁

    4.可重入VS线程安全

    线程安全概念

    5. 死锁

    (1)概念

    小问题:一把锁可以产生死锁吗?——可以的,代码写错就可能产生一把锁死锁

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

    (3)避免死锁

    (4)避免死锁算法

    ①两个锁互相锁死的情景 

    ②一个锁产生死锁的情景 

    (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写入内存,就错误了。

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

    情况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)加锁小例子

    下列操作中,需要执行加锁的操作是()[多选]

    A.x++;

    B.x=y;

    C.++x;

    D.x=1;

    答:D 常量的直接赋值是一个原子操作

    ABC选项中涉及到了数据的运算,则涉及从内存加载数据到寄存器,在寄存器中运算,将寄存器中数据交还内存的过程因此需要加锁保护的操作中,正确选项为:ABC

    (3)定义/释放 互斥锁

    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); 释放锁

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

    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就会一直阻塞等待线程退出,所以会显示进程卡住了。

    (5)使用锁代码

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

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

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include // 仅仅是了解
    7. // __thread int global_value = 100;
    8. // void *startRoutine(void *args)
    9. // {
    10. // // pthread_detach(pthread_self());
    11. // // cout << "线程分离....." << endl;
    12. // while (true)
    13. // {
    14. // // 临界区,不是所有的线程代码都是临界区
    15. // cout << "thread " << pthread_self() << " global_value: "
    16. // << global_value << " &global_value: " << &global_value
    17. // << " Inc: " << global_value++ << " lwp: " << ::syscall(SYS_gettid)<
    18. // sleep(1);
    19. // break;
    20. // }
    21. // // 退出进程,任何一个线程调用exit,都表示整个进程退出
    22. // //exit(1);
    23. // // pthread_exit()
    24. // }
    25. using namespace std;
    26. // int 票数计数器
    27. // 临界资源
    28. int tickets = 10000; // 临界资源,可能会因为共同访问,可能会造成数据不一致问题。
    29. pthread_mutex_t mutex;
    30. void *getTickets(void *args)
    31. {
    32. const char *name = static_cast<const char *>(args);
    33. while (true)
    34. {
    35. // 临界区,只要对临界区加锁,而且加锁的粒度约细越好
    36. // 加锁的本质是让线程执行临界区代码串行化
    37. // 加锁是一套规范,通过临界区对临界资源进行访问的时候,要加就都要加
    38. // 锁保护的是临界区, 任何线程执行临界区代码访问临界资源,都必须先申请锁,前提是都必须先看到锁!
    39. // 这把锁,本身不就也是临界资源吗?锁的设计者早就想到了
    40. // pthread_mutex_lock: 竞争和申请锁的过程,就是原子的!
    41. // 难度在加锁的临界区里面,就没有线程切换了吗????
    42. pthread_mutex_lock(&mutex);
    43. if (tickets > 0)
    44. {
    45. usleep(1000);
    46. cout << name << " 抢到了票, 票的编号: " << tickets << endl;
    47. tickets--;
    48. pthread_mutex_unlock(&mutex);
    49. //other code
    50. usleep(123); //模拟其他业务逻辑的执行
    51. }
    52. else
    53. {
    54. // 票抢到几张,就算没有了呢?0
    55. cout << name << "] 已经放弃抢票了,因为没有了..." << endl;
    56. pthread_mutex_unlock(&mutex);
    57. break;
    58. }
    59. }
    60. return nullptr;
    61. }
    62. // 如何理解exit?
    63. int main()
    64. {
    65. pthread_mutex_init(&mutex, nullptr);
    66. pthread_t tid1;
    67. pthread_t tid2;
    68. pthread_t tid3;
    69. pthread_t tid4;
    70. pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");
    71. pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");
    72. pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");
    73. pthread_create(&tid4, nullptr, getTickets, (void *)"thread 4");
    74. // sleep(1);
    75. // 倾向于:让主线程,分离其他线程
    76. // pthread_detach(tid1);
    77. // pthread_detach(tid2);
    78. // pthread_detach(tid3);
    79. // 1. 立即分离,延后分离 -- 线程活着 -- 意味着,我们不在关心这个线程的死活。4. 线程退出的第四种方式,延后退出
    80. // 2. 新线程分离,但是主线程先退出(进程退出) --- 一般我们分离线程,对应的main thread一般不要退出(常驻内存的进程)
    81. // sleep(1);
    82. int n = pthread_join(tid1, nullptr);
    83. cout << n << ":" << strerror(n) << endl;
    84. n = pthread_join(tid2, nullptr);
    85. cout << n << ":" << strerror(n) << endl;
    86. n = pthread_join(tid3, nullptr);
    87. cout << n << ":" << strerror(n) << endl;
    88. n = pthread_join(tid4, nullptr);
    89. cout << n << ":" << strerror(n) << endl;
    90. pthread_mutex_destroy(&mutex);
    91. return 0;
    92. }

    4.加锁方式

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

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. int tickets = 1000;
    6. void *startRoutine(void *args)
    7. {
    8. pthread_mutex_t* mutex_p= static_cast<pthread_mutex_t*>(args);
    9. while (true)
    10. {
    11. pthread_mutex_lock(mutex_p);//如果申请不到,线程阻塞等待
    12. if (tickets > 0)
    13. {
    14. usleep(1000);
    15. cout << "thread: " << pthread_self() << "get a ticket: " << tickets << endl;
    16. tickets--;
    17. pthread_mutex_unlock(mutex_p);
    18. //做其他的事
    19. usleep(500);
    20. }
    21. else
    22. {
    23. pthread_mutex_unlock(mutex_p);
    24. break;
    25. }
    26. }
    27. return nullptr;
    28. }
    29. int main()
    30. {
    31. pthread_t t1, t2, t3, t4;
    32. static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
    33. pthread_create(&t1, nullptr, startRoutine, (void *)&mutex);
    34. pthread_create(&t2, nullptr, startRoutine, (void *)&mutex);
    35. pthread_create(&t3, nullptr, startRoutine, (void *)&mutex);
    36. pthread_create(&t4, nullptr, startRoutine, (void *)&mutex);
    37. pthread_join(t1, nullptr);
    38. pthread_join(t2, nullptr);
    39. pthread_join(t3, nullptr);
    40. pthread_join(t4, nullptr);
    41. pthread_mutex_destroy(&mutex);
    42. return 0;
    43. }

    (2)既传name又传锁

    完整版:

    1. #include
    2. #include
    3. #include
    4. #include
    5. using namespace std;
    6. int tickets = 1000;
    7. #define NAMESIZE 64
    8. typedef struct threadData
    9. {
    10. char name[NAMESIZE];
    11. pthread_mutex_t* mutexp;
    12. }threadData;
    13. void *startRoutine(void *args)
    14. {
    15. threadData* td= static_cast(args);
    16. while (true)
    17. {
    18. pthread_mutex_lock(td->mutexp);//如果申请不到,线程阻塞等待
    19. if (tickets > 0)
    20. {
    21. usleep(1000);
    22. cout << "thread: " << td->name << "get a ticket: " << tickets << endl;
    23. tickets--;
    24. pthread_mutex_unlock(td->mutexp);
    25. //做其他的事
    26. usleep(500);
    27. }
    28. else
    29. {
    30. pthread_mutex_unlock(td->mutexp);
    31. break;
    32. }
    33. }
    34. return nullptr;
    35. }
    36. int main()
    37. {
    38. static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
    39. pthread_t t1, t2, t3, t4;
    40. threadData *td1=new threadData();
    41. threadData *td2=new threadData();
    42. threadData *td3=new threadData();
    43. threadData *td4=new threadData();
    44. strcpy(td1->name,"thread 1");
    45. strcpy(td2->name,"thread 2");
    46. strcpy(td3->name,"thread 3");
    47. strcpy(td4->name,"thread 4");
    48. td1->mutexp=&mutex;
    49. td2->mutexp=&mutex;
    50. td3->mutexp=&mutex;
    51. td4->mutexp=&mutex;
    52. pthread_create(&t1, nullptr, startRoutine, (void *)td1);
    53. pthread_create(&t2, nullptr, startRoutine, (void *)td2);
    54. pthread_create(&t3, nullptr, startRoutine, (void *)td3);
    55. pthread_create(&t4, nullptr, startRoutine, (void *)td4);
    56. pthread_join(t1, nullptr);
    57. pthread_join(t2, nullptr);
    58. pthread_join(t3, nullptr);
    59. pthread_join(t4, nullptr);
    60. pthread_mutex_destroy(&mutex);
    61. return 0;
    62. }

     

    二.加锁的原理探究 

    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

    1. mythread:mythread.cc
    2. g++ -o $@ $^ -lpthread -std=c++11
    3. .PHONY:clean
    4. clean:
    5. rm -f mythread

    Lock.hpp

    1. #pragma once
    2. #include
    3. #include
    4. class Mutex
    5. {
    6. public:
    7. Mutex()
    8. {
    9. pthread_mutex_init(&lock_, nullptr);
    10. }
    11. void lock()
    12. {
    13. pthread_mutex_lock(&lock_);
    14. }
    15. void unlock()
    16. {
    17. pthread_mutex_unlock(&lock_);
    18. }
    19. ~Mutex()
    20. {
    21. pthread_mutex_destroy(&lock_);
    22. }
    23. private:
    24. pthread_mutex_t lock_;
    25. };
    26. class LockGuard
    27. {
    28. public:
    29. LockGuard(Mutex *mutex) : mutex_(mutex)
    30. {
    31. mutex_->lock();
    32. std::cout << "加锁成功..." << std::endl;
    33. }
    34. ~LockGuard()
    35. {
    36. mutex_->unlock();
    37. std::cout << "解锁成功...." << std::endl;
    38. }
    39. private:
    40. Mutex *mutex_;
    41. };

    mythread.cc

    1. int tickets = 1000;
    2. Mutex mymutex;
    3. // 函数本质是一个代码块, 会被多个线程同时调用执行,该函数被重复进入 - 被重入了
    4. bool getTickets()
    5. {
    6. bool ret = false; // 函数的局部变量,在栈上保存,线程具有独立的栈结构,每个线程各自一份
    7. LockGuard lockGuard(&mymutex); //局部对象的声明周期是随代码块的!
    8. if (tickets > 0)
    9. {
    10. usleep(1001); //线程切换了
    11. cout << "thread: " << pthread_self() << " get a ticket: " << tickets << endl;
    12. tickets--;
    13. ret = true;
    14. }
    15. return ret;
    16. }
    17. void *startRoutine(void *args)
    18. {
    19. const char *name = static_cast<const char *>(args);
    20. while(true)
    21. {
    22. if(!getTickets())
    23. {
    24. break;
    25. }
    26. cout << name << " get tickets success" << endl;
    27. //其他事情要做
    28. sleep(1);
    29. }
    30. }
    31. int cnt = 10000;
    32. int main()
    33. {
    34. pthread_t t1, t2, t3, t4;
    35. pthread_create(&t1, nullptr, startRoutine, (void *)"thread 1");
    36. pthread_create(&t2, nullptr, startRoutine, (void *)"thread 2");
    37. pthread_create(&t3, nullptr, startRoutine, (void *)"thread 3");
    38. pthread_create(&t4, nullptr, startRoutine, (void *)"thread 4");
    39. pthread_join(t1, nullptr);
    40. pthread_join(t2, nullptr);
    41. pthread_join(t3, nullptr);
    42. pthread_join(t4, nullptr);
    43. }

    (4)C++ RAII加锁

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

    1. {
    2. //临界资源
    3. LockGuard LockGuard(&mymutex);
    4. cnt++;
    5. ...
    6. ...
    7. ...
    8. }

    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)死锁四个必要条件(有一个条件不满足,死锁就不成立)

    充分不必要条件如果有事物情况A,则必然有事物情况B;如果有事物情况B不一定有事物情况A,A就是B的充分而不必要的条件,即充分不必要条件。a是b的充分不必要条件←→b是a的必要不充分条件

    即:只要有一个条件不满足,死锁就不成立

    互斥条件:一个资源每次只能被一个执行流使用
    请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。(即:保持着自己的锁,还要要对方的锁)
    不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
    循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系,即:两个线程各自占用一个锁后,还竞争对方的锁,叫环路等待

    (3)避免死锁

    破坏死锁的四个必要条件(互斥条件无法破坏,其他三个均可以破坏)
    破坏循环等待条件建议:多个线程加锁顺序一致:例如线程1和线程2都是先申请A锁后申请B锁
    避免锁未释放的场景:用完锁尽快释放
    资源一次性分配:不要出现一个临界资源就分配一个锁,出现一个临界资源就分配一个锁,这样大概率会产生死锁。应该要集中这些临界资源使用一把锁处理。(如果进程在一次性申请其所需的全部资源成功后才运行,就不会发生死锁。因为: 资源一次性分配,也就不存在请求与保持的情况以及环路等待情况了

    (4)避免死锁算法

    死锁检测算法(了解)
    银行家算法(了解)(银行家算法是最有代表性的死锁避免而并非解除算法,银行家算法的思想在于将系统运行分为两种状态:安全/非安全,有可能出现风险的都属于非安全,银行家算法不是预防死锁,是避免出现死锁; 银行家算法的思想是为了避免出现“环路等待”条件

    ①两个锁互相锁死的情景 

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include "Lock.hpp"
    7. #include
    8. using namespace std;
    9. pthread_mutex_t mutexA = PTHREAD_MUTEX_INITIALIZER;
    10. pthread_mutex_t mutexB = PTHREAD_MUTEX_INITIALIZER;
    11. void *startRoutine1(void *args)
    12. {
    13. while (true)
    14. {
    15. pthread_mutex_lock(&mutexA);
    16. sleep(1);
    17. pthread_mutex_lock(&mutexB);
    18. cout << "我是线程1,我的tid: " << pthread_self() << endl;
    19. pthread_mutex_unlock(&mutexA);
    20. pthread_mutex_unlock(&mutexB);
    21. }
    22. }
    23. void *startRoutine2(void *args)
    24. {
    25. while (true)
    26. {
    27. pthread_mutex_lock(&mutexB);
    28. sleep(1);
    29. pthread_mutex_lock(&mutexA);
    30. cout << "我是线程2, 我的tid: " << pthread_self() << endl;
    31. pthread_mutex_unlock(&mutexB);
    32. pthread_mutex_unlock(&mutexA);
    33. }
    34. }
    35. int main()
    36. {
    37. pthread_t t1, t2;
    38. pthread_create(&t1, nullptr, startRoutine1, nullptr);
    39. pthread_create(&t2, nullptr, startRoutine2, nullptr);
    40. pthread_join(t1, nullptr);
    41. pthread_join(t2, nullptr);
    42. return 0;
    43. }

    ②一个锁产生死锁的情景 

    申请锁申请了两次

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

    (5)死锁的处理方法

    ① 鸵鸟策略 对可能出现的问题采取无视态度,前提是出现概率很低

    ② 预防策略 破坏死锁产生的必要条件

    ③ 避免策略 银行家算法,分配资源前进行风险判断,避免风险的发生

    ④ 检测与解除死锁 分配资源时不采取措施,但是必须提供死锁的检测与解除手段

  • 相关阅读:
    【归并排序】剑指 Offer 51. 数组中的逆序对
    用最简单的方式理解函数重载
    RuoYi-Vue新建模块
    构建 Audio Unit 应用程序
    java93-线程的创建方法二
    7.取消与关闭
    关于web自动化元素定位,你想知道的都在这里
    C#实现堆栈结构的定义、入栈、出栈
    9月15日第壹简报,星期四,农历八月二十
    dlv调试kubelet
  • 原文地址:https://blog.csdn.net/zhang_si_hang/article/details/127993804