当前位置:   article > 正文

C++并发锁相关并发_std::unique_lock

std::unique_lock

互斥锁mutex

  1. #include <mutex>
  2. ```
  3. {
  4. std::mutex mtx;
  5. mtx.lock();
  6. // do something
  7. mtx.unlock();
  8. }
  9. ```

  1. mutex成员方法:lock()、try_lock()、unlock()
  2. try_lock
  3. 1)所有线程都没有lock时,调用lock,并返回true
  4. 2)其他线程lock时返回false

lock_guard

建议使用lock_guard替代直接使用mutext.lock()、mutext.unlock(),lock_guard会在构造方法里mutext.lock(),析构方法里mutext.unlock(),主要为了避免中途异常退出导致mutext没有unlock。

  1. std::mutex m_mutex;
  2. ```
  3. {
  4. std::lock_guard lg(m_mutex);
  5. ...
  6. } //出了作用域后自动调用lock_guard的析构方法

{}:打包作用,限定变量的范围

unique_lock

lock_guard只能在析构的时候解锁,不提供unlock接口,不够方便,unique_lock提供lock和unlock接口,更灵活

  1. std::unique_lock<std::mutex> guard(_mu);
  2. //do something 1
  3. guard.unlock();//临时解锁
  4. //do something 2
  5. guard.lock();//继续上锁
  6. // do something
  7. // 结束时析构guard会临时解锁

unique_lock比lock_guard灵活很多,效率上差一点,内存占用多一点。

  1. 在 C++11 中一共提供了四种互斥锁:
  2. std::mutex:独占的互斥锁,不能递归使用
  3. std::timed_mutex:带超时的独占互斥锁,不能递归使用
  4. std::recursive_mutex:递归互斥锁,不带超时功能—不建议使用
  5. std::recursive_timed_mutex:带超时的递归互斥锁—不建议使用

条件锁(条件变量)

条件锁(condition variable)是C++中的同步机制,用于协调多个线程之间的操作。条件锁允许一个线程等待另一个线程满足某个条件前进行等待,而不是不停地轮询条件。当条件满足时,通知等待的线程以继续执行。

条件锁使用场景:

1.线程间等待和通知操作

2.等待某个条件成立,防止忙等

3.输出数据的同步

实现条件锁有以下几个步骤:

1.初始化条件变量

使用std::condition_variable的构造函数来初始化条件变量。

std::condition_variable cond_var;

2.等待条件变量

当线程需要等待条件变量时,它调用wait()函数并同时释放锁,然后进入休眠状态直到另一个线程通知它。wait()函数的具体定义为:

void wait(std::unique_lock<std::mutex>& lock);

3.通知条件变量

当条件变量被满足时,线程可以调用notify_one()或notify_all()函数来通知等待的线程。 

void notify_one();

void notify_all();

4.使用条件变量

在多线程程序中,在设置条件变量之前,必须首先获得互斥锁,以防止条件被修改。通常情况下,设置条件变量和唤醒等待线程是原子性的,即要么都发生,要么都不发生。

  1. #include<condition_variable>
  2. ```
  3. std::unique_lock<std::mutex> lock(mut);
  4. ```
  5. if (my_condition)
  6. {
  7. cond_var.wait(lock);
  8. }
  9. ```

自旋锁

与互斥锁的相比,在获取锁失败的时候不会使得线程阻塞,而是一直自旋尝试获取锁。

自旋锁主要适用于被持有时间短,线程不希望在重新调度上花过多时间的情况。如果在持锁时间很长的场景下使用自旋锁,则会导致CPU在这个线程的时间片用尽之前一直消耗在无意义的忙等上,造成计算资源的浪费。

读写锁

任意读线程可以同时访问关键区域,但是只允许一个线程写入。

读写锁机制:

写者:写者使用写锁,如果当前没有读者,也没有其他写者,写者立即获得写锁;否则写者将等待,直到没有读者和写者。

读者:读者使用读锁,如果当前没有写者,读者立即获得读锁;否则读者等待,直到没有写者。

C++中标准库提供了std::shared_mutex来实现读写锁。它支持两种级别的锁定:

1.共享锁:多个线程同时进行读操作,不阻塞彼此。使用std::shared_lock<std::shared_mutex>实现。

2.独占锁:只有一个线程可以进行写操作,其他线程无法进行读或写操作。使用std::unique_lock<std::shared_mutex>实现。

共享锁和独占锁的使用方法与std::mutex相似。当需要进行读操作时,线程获取共享锁;当需要进行写操作时,线程获取独占锁。

  1. #include <iostream>
  2. #include <thread>
  3. #include <shared_mutex>
  4. std::shared_mutex rw_mutex; // 读写锁
  5. int shared_data = 0;
  6. void writer()
  7. {
  8. std::unique_lock<std::shared_mutex> lock(rw_mutex);
  9. shared_data++; // 写操作
  10. std::cout << "Writer: data updated to " << shared_data << std::endl;
  11. }
  12. void reader()
  13. {
  14. std::shared_lock<std::shared_mutex> lock(rw_mutex);
  15. std::cout << "Reader: current data is " << shared_data << std::endl; // 读操作
  16. }
  17. int main()
  18. {
  19. std::thread t1(reader);
  20. std::thread t2(reader);
  21. std::thread t3(reader);
  22. std::thread t4(writer);
  23. std::thread t5(reader);
  24. t1.join();
  25. t2.join();
  26. t3.join();
  27. t4.join();
  28. t5.join();
  29. return 0;
  30. }

信号量

信号量的使用主要是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)所拥有。

信号量的值为正的时候,说明它空闲。所测试的线程可以锁定而使用它。若为 0,说明它被占用,测试的线程要进入睡眠队列中,等待被唤醒。

信号量的分类

  • 内核信号量,由内核控制路径使用;

  • 用户态进程使用的信号量,这种信号量又分为 POSIX 信号量和 SYSTEM V 信号量;

内核信号量的相关函数

  1. 初始化:

  1. void sema_init (struct semaphore *sem, int val);
  2. void init_MUTEX (struct semaphore *sem); //将 sem 的值置为 1,表示资源空闲
  3. void init_MUTEX_LOCKED (struct semaphore *sem); //将 sem 的值置为 0,表示资源忙
  1. 申请内核信号量所保护的资源:

  1. void down(struct semaphore * sem); // 可引起睡眠
  2. int down_interruptible(struct semaphore * sem); // down_interruptible能被信号打断
  3. int down_trylock(struct semaphore * sem); // 非阻塞函数,不会睡眠。无法锁定资源则马上返回
  1. 释放内核信号量所保护的资源:

void up(struct semaphore * sem);

除了获得信号量之外,down_interruptible() 函数还会在以下情况下返回:

1. 当前进程收到一个可中断信号(例如 SIGINT、SIGTERM、SIGQUIT)时,down_interruptible() 函数会返回 -ERESTARTSYS 错误码。这个错误码表示需要重新启动系统调用,并且可以通过重新调用该函数来重新尝试获取信号量。

2. 当前进程被挂起时,down_interruptible() 函数也会返回 -ERESTARTSYS 错误码。这可能是因为该进程在等待 I/O 完成,或者因为它需要等待某个事件发生(例如信号量的值变为非零)。在这种情况下,也可以通过重新调用该函数来重新尝试获取信号量。

总之,无论何时,down_interruptible() 函数都可以被可中断地中断,也可以在信号量成功获取之前被挂起。在这种情况下,它会返回 -ERESTARTSYS 错误码。

POSIX信号量详解

POSIX 信号量又分为有名信号量和无名信号量。

  • 有名信号量,其值保存在文件中,所以它可以用于线程也可以用于进程间的同步。

  • 无名信号量,其值保存在内存中。

无名信号量常用于多线程间的同步,同时也用于相关进程间的同步。也就是说,无名信号量必须是多个进程(线程)的共享变量,无名信号量要保护的变量也必须是多个进程(线程)的共享变量,这两个条件是缺一不可的。

常见的无名信号量相关函数:

  1. /*
  2. 1)pshared == 0 用于同一多线程的同步;
  3. 2)若 pshared > 0 用于多个相关进程间的同步(即由 fork 产生的)
  4. */
  5. int sem_init(sem_t *sem, int pshared, unsigned int value);

int sem_destroy(sem_t * sem);

  1. /*
  2. 测试所指定信号量的值,它的操作是原子的。
  3. 若 sem > 0,那么它减 1 并立即返回。
  4. 若 sem== 0,则睡眠直到 sem > 0,此时立即减 1,然后返回。
  5. 相当于 P 操作,即申请资源。
  6. */
  7. int sem_wait(sem_t *sem); // 这是一个阻塞的函数

  1. /*
  2. 其他的行为和 sem_wait 一样,除了sem== 0,不是睡眠,而是返回一个错误 EAGAIN;
  3. */
  4. int sem_trywait(sem_t *sem); // 非阻塞的函数

  1. /*
  2. 把指定的信号量 sem 的值加 1;
  3. 呼醒正在等待该信号量的任意线程;
  4. 相当于 V 操作,释放资源;
  5. */
  6. int sem_post(sem_t *sem);

无名信号量在相关进程间的同步

说是相关进程,是因为本程序中共有 2 个进程,其中一个是另外一个的子进程(由 fork 产生)的。本来对于 fork 来说,子进程只继承了父进程的代码副本,mutex 理应在父子进程中是相互独立的两个变量,但由于在初始化 mutex 的时候,由 pshared = 1 指定了 mutex 处于共享内存区域,所以此时 mutex 变成了父子进程共享的一个变量。此时,mutex 就可以用来同步相关进程了。

有名信号量

有名信号量的特点是把信号量的值保存在文件中。

这决定了它的用途非常广:既可以用于线程,也可以用于相关进程间,甚至是不相关进程。

有名信号量能在进程间共享的原因

由于有名信号量的值是保存在文件中的,所以对于相关进程来说,子进程是继承了父进程的文件描述符,那么子进程所继承的文件描述符所指向的文件是和父进程一样的,当然文件里面保存的有名信号量值就共享了。

有名信号量相关函数说明

有名信号量在使用的时候,和无名信号量共享 sem_wait 和 sem_post 函数。

区别是有名信号量使用 sem_open 代替 sem_init,另外在结束的时候要像关闭文件一样去关闭这个有名信号量。

(1)打开一个已存在的有名信号量,或创建并初始化一个有名信号量。一个单一的调用就完
成了信号量的创建、初始化和权限的设置。

sem_t *sem_open(const char *name, int oflag, mode_t mode , int value);
  1. name 是文件的路径名;
  2. Oflag 有 O_CREAT 或 O_CREAT | EXCL 两个取值;
  3. mode_t 控制新的信号量的访问权限;
  4. Value 指定信号量的初始化值。
  5. 注意:
  6. 这里的 name 不能写成 /tmp/aaa.sem 这样的格式,因为在 linux 下,sem 都是创建在 /dev/shm 目录下。你可以将 name 写成“/mysem”或“mysem”,创建出来的文件都是“/dev/shm/sem.mysem”,千万不要写路径。也千万不要写“/tmp/mysem”之类的。
  7. 当 oflag = O_CREAT 时,若 name 指定的信号量不存在时,则会创建一个,而且后面的 modevalue 参数必须有效。若 name 指定的信号量已存在,则直接打开该信号量,同时忽略 modevalue 参数。
  8. 当 oflag = O_CREAT | O_EXCL 时,若 name 指定的信号量已存在,该函数会直接返回 error

一旦你使用了一信号量,销毁它们就变得很重要

在做这个之前,要确定所有对这个有名信号量的引用都已经通过 sem_close() 函数关闭了,然后只需在退出或是退出处理函数中调用 sem_unlink() 去删除系统中的信号量,注意如果有任何的处理器或是线程引用这个信号量,sem_unlink() 函数不会起到任何的作用。

也就是说,必须是最后一个使用该信号量的进程来执行 sem_unlink 才有效。因为每个信号灯有一个引用计数器记录当前的打开次数,sem_unlink 必须等待这个数为 0 时才能把 name 所指的信号灯从文件系统中删除。也就是要等待最后一个 sem_close 发生。

有名信号量在无相关进程间的同步

前面已经说过,有名信号量是位于共享内存区的,那么它要保护的资源也必须是位于共享内存区,只有这样才能被无相关的进程所共享。

在下面这个例子中,服务进程和客户进程都使用 shmget 和 shmat 来获取得一块共享内存资源。然后利用有名信号量来对这块共享内存资源进行互斥保护。

  1. 以下是一个示例程序,它使用信号量同步两个线程之间的访问:
  2. ```c
  3. #include <stdio.h>
  4. #include <unistd.h>
  5. #include <pthread.h>
  6. #include <semaphore.h>
  7. sem_t sem;
  8. void *thread_func(void *arg)
  9. {
  10. int i;
  11. for (i = 0; i < 10; i++) {
  12. sem_wait(&sem); /* 等待信号量 */
  13. printf("Thread %d is working\n", *(int *)arg);
  14. sleep(1);
  15. sem_post(&sem); /* 发送信号量 */
  16. }
  17. pthread_exit(NULL);
  18. }
  19. int main()
  20. {
  21. pthread_t thread1, thread2;
  22. int id1 = 1, id2 = 2;
  23. sem_init(&sem, 0, 1); /* 初始化信号量为1 */
  24. pthread_create(&thread1, NULL, thread_func, &id1);
  25. pthread_create(&thread2, NULL, thread_func, &id2);
  26. pthread_join(thread1, NULL);
  27. pthread_join(thread2, NULL);
  28. sem_destroy(&sem); /* 销毁信号量 */
  29. return 0;
  30. }
  31. ```
  32. 这个程序创建了两个线程,它们都要获取信号量才能工作。每个线程工作一段时间,然后释放信号量,以允许另一个线程工作。请注意,我们使用`sem_wait`等待信号量,使用`sem_post`发送信号量。这个程序还使用了`sem_init`初始化信号量,以及`sem_destroy`销毁信号量。

  1. 以下是一个使用信号量在多个进程之间同步访问的示例程序:
  2. ```c
  3. #include <stdio.h>
  4. #include <unistd.h>
  5. #include <fcntl.h>
  6. #include <semaphore.h>
  7. int main()
  8. {
  9. sem_t *sem;
  10. pid_t pid;
  11. sem = sem_open("/my_semaphore", O_CREAT, 0644, 1);
  12. if (sem == SEM_FAILED) {
  13. perror("Failed to open semaphore");
  14. return 1;
  15. }
  16. pid = fork();
  17. if (pid < 0) {
  18. perror("Failed to fork");
  19. return 1;
  20. }
  21. else if (pid == 0) {
  22. printf("Child process waiting for lock\n");
  23. sem_wait(sem); /* 等待信号量 */
  24. printf("Child process acquired lock\n");
  25. sleep(2);
  26. sem_post(sem); /* 发送信号量 */
  27. printf("Child process released lock\n");
  28. }
  29. else {
  30. printf("Parent process waiting for lock\n");
  31. sem_wait(sem); /* 等待信号量 */
  32. printf("Parent process acquired lock\n");
  33. sleep(2);
  34. sem_post(sem); /* 发送信号量 */
  35. printf("Parent process released lock\n");
  36. }
  37. sem_close(sem); /* 关闭信号量 */
  38. sem_unlink("/my_semaphore"); /* 删除信号量 */
  39. return 0;
  40. }
  41. ```
  42. 这个程序通过使用`sem_open`创建命名信号量,它可以在多个进程之间共享。
  43. 然后,我们使用`fork`创建子进程,并在父子进程之间共享信号量。
  44. 父进程和子进程都要获取信号量,并且要等待一段时间后释放信号量。
  45. 请注意,这里我们使用`sem_wait`等待信号量,用`sem_post`发送信号量,
  46. 使用`sem_cloes`关闭信号量并使用`sem_unlink`删除信号量。

C++ 锁:C++ 锁 - 简书
C++之{}的作用:C++之{}的作用_c++中{}_chen_hui.778的博客-CSDN博客

最全面的 linux 信号量解析最全面的 linux 信号量解析_Ruo_Xiao的博客-CSDN博客

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/153502
推荐阅读
相关标签
  

闽ICP备14008679号