目录
线程相关问题
线程安全
常见的线程安全的情况
常见的线程不安全的情况
可重入函数与不可重入函数
常见不可重入的情况
常见可重入的情况
可重入与线程安全的关系
联系
区别
线程同步与互斥
互斥锁
使用
死锁
死锁的四个必要条件
如何避免死锁
条件变量
同步概念与竞态条件
使用
Posix 信号量
使用
生产者消费者模型
线程池
读者写者问题
线程相关问题
线程安全
线程安全是指多线程环境下,某个函数、代码块或数据结构在被多个线程并发访问时,能够正确地执行并且保持数据的一致性和完整性,不会出现数据污染、数据竞争或死锁等问题。
常见的线程安全的情况
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。
- 类或者接口对于线程来说都是原子操作。
- 多个线程之间的切换不会导致该接口的执行结果存在二义性。
常见的线程不安全的情况
- 不保护共享变量的函数。
- 函数状态随着被调用,状态发生变化的函数。
- 返回指向静态变量指针的函数。
- 调用线程不安全函数的函数。
可重入函数与不可重入函数
若一个函数在调用期间被不同的控制流程调用,在第一次调用未结束前再次调用该函数,这种行为是被允许的,运行结果正确且不会出现任何问题,则称该函数为可重入函数,否则就是不可重入函数。
上图表示链表的多次 insert 操作,在 insert node1后,若在更新 head 前进入信号处理函数并 insert node2,最后的结果就是丢失 node2 的信息,所以 insert 是不可重入函数。
常见不可重入的情况
- 调用了 malloc/free 函数,因为 malloc 函数是用全局链表来管理堆的。
- 调用了标准 I/O 库函数,标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构。
- 可重入函数体内使用了静态的数据结构。
常见可重入的情况
- 没有使用全局变量或静态变量
- 没有使用 malloc 或者 new 开辟出空间
- 没有调用其他不可重入函数
- 返回值不是静态或全局数据,所有数据都由函数的调用者提供
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全的关系
联系
- 函数如果可重入,那么就是线程安全的。
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题。
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
区别
- 可重入函数是线程安全函数的一种。
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
线程同步与互斥
编写代码,创建 4 个线程进行抢票,Thread.hpp 文件为【Linux】线程与线程控制-CSDN博客里模拟实现的线程库。
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
#include "Thread.hpp"using namespace zyh;const int num = 4;
int ticket = 10000;void print(std::string name)
{while (ticket > 0){usleep(1001);td::cout << "I am " << name << ", ticket: " << --ticket << std::endl;// sleep(1);}
}int main()
{std::vector<Thread<int>> threads;int cnt = 10;// 1. 创建线程for (int i = 0; i < num; i++){std::string name = "thread-" + std::to_string(i + 1);threads.emplace_back(print, name, name);}// 2. 启动线程for (auto& thread : threads){thread.start();}sleep(3);// 3. 等待线程for (auto& thread : threads){thread.join();std::cout << "wait thread done, thread is: " << thread.getThreadName() << std::endl;}return 0;
}
运行结果:
可以看到,我们希望原本票数为 0 时,线程就应该无法进行抢票,但实际上,票数为负数时仍有线程在抢票。
原因:当 ticket 为 0,线程 1 执行完 while (ticket > 0),但还未打印剩余票数时,发生线程调度切换到线程 2,此时 ticket 仍为 0,线程 2 会打印剩余票数并使剩余票数减 1,执行完毕后 ticket < 0,切换回线程 1 后继续执行剩余的打印代码,这时打印出来的结果就是负数。
解决方案:出现上述问题,归根结底的原因是 ticket 是临界资源,多线程在对临界资源的修改与访问并不是原子化的,使用互斥锁就可以解决问题。
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
#include "Thread.hpp"using namespace zyh;const int num = 4;
int ticket = 10000;
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;void print(std::string name)
{while (ticket > 0){pthread_mutex_lock(&mtx);//usleep(1001);if (ticket > 0){std::cout << "I am " << name << ", ticket: " << --ticket << std::endl;// sleep(1);}pthread_mutex_unlock(&mtx);}
}int main()
{std::vector<Thread<std::string>> threads;int cnt = 10;// 1. 创建线程for (int i = 0; i < num; i++){std::string name = "thread-" + std::to_string(i + 1);threads.emplace_back(print, name, name);}// 2. 启动线程for (auto& thread : threads){thread.start();}sleep(3);// 3. 等待线程for (auto& thread : threads){thread.join();std::cout << "wait thread done, thread is: " << thread.getThreadName() << std::endl;}return 0;
}
互斥锁
如文章【Linux】进程间通信 —— 管道与 System V 版本通信方式-CSDN博客 信号量部分所述,信号量本质上是一个描述临界资源数量的计数器,对公共局部临界资源的预定机制,用来保护临界资源。如果信号量初始值是 1 呢?代表将临界资源看作整体,来实现互斥,二元信号量就是一把锁。
伪代码如下:
使用
ubuntu 下可能会出现 man 手册查不到 pthread 相关库函数的问题
原因:因为man手册中默认没有安装关于 posix 标准的文档。
解决办法:bash 输入以下内容
sudo apt-get install manpages-posix-dev
创建与销毁
原型
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);int pthread_mutex_destroy(pthread_mutex_t *mutex);
若 mutex 为静态或全局变量,则可以用宏来初始化,后续不用 destroy
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
加锁与解锁
原型
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
一个最简单的死锁代码:
#include <iostream>
#include <pthread.h>pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;int main()
{pthread_mutex_lock(&mtx);std::cout << "Lock once..." << std::endl;pthread_mutex_lock(&mtx);std::cout << "Deadlock generated." << std::endl;pthread_mutex_unlock(&mtx);return 0;
}
另一份死锁代码:
#include <iostream>
#include <pthread.h>pthread_mutex_t mtx1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mtx2 = PTHREAD_MUTEX_INITIALIZER;void* handler1(void* arg)
{pthread_mutex_lock(&mtx1);std::cout << "Lock mtx1." << std::endl;pthread_mutex_lock(&mtx2);std::cout << "Lock mtx1 and mtx2." << std::endl;pthread_mutex_unlock(&mtx2);std::cout << "Unlock mtx2." << std::endl;pthread_mutex_unlock(&mtx1);std::cout << "Unlock mtx1." << std::endl;return nullptr;
}void* handler2(void* arg)
{pthread_mutex_lock(&mtx2);std::cout << "Lock mtx2." << std::endl;pthread_mutex_lock(&mtx1);std::cout << "Lock mtx1 and mtx2." << std::endl;pthread_mutex_unlock(&mtx1);std::cout << "Unlock mtx1." << std::endl;pthread_mutex_unlock(&mtx2);std::cout << "Unlock mtx2." << std::endl;return nullptr;
}int main()
{pthread_t tid1, tid2;pthread_create(&tid1, nullptr, handler1, nullptr);pthread_create(&tid2, nullptr, handler2, nullptr);pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);return 0;
}
死锁的四个必要条件
- 互斥条件:一个资源每次只能被一个执行流使用。
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。
如何避免死锁
- 破坏死锁的四个必要条件之一
- 加锁顺序一致
- 避免锁未释放的场景
- 资源一次性分配
条件变量
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
同步概念与竞态条件
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。
使用
创建与销毁
原型
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);int pthread_cond_destroy(pthread_cond_t *cond);
若 cond 为静态或全局变量,则可以用宏来初始化,后续不用 destroy
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
唤醒与等待
原型
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
Posix 信号量
Posix 信号量和 System V 信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但 Posix 可以用于线程间同步。
相关概念可查看【Linux】进程间通信 —— 管道与 System V 版本通信方式-CSDN博客 信号量部分。
使用
创建与销毁
原型
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
参数:
pshared: 0 表示线程间共享,非 0 表示进程间共享
value:信号量初始值
P 操作
原型
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
V 操作
原型
int sem_post(sem_t *sem);
生产者消费者模型
这里会放一个超链接,待更新。
线程池
这里会放一个超链接,待更新。
读者写者问题
这里会放一个超链接,待更新。