目录
引言:多线程编程一直是服务器编程的一个重点,而且自己在前段时间面试后端开发时多线程编程问的也比较多一点,换句话来说,这是必须掌握的,本文主要列出的是操作系统原生Linux api
,C++11/14/17
和Windows
相关api
在此不作过多叙述。当然可见:C++API参考
这篇博文应当算是对于我的一次知识回顾和知识进阶,以前也写过多线程编程的相关文章:C/C++多线程编程专栏,做一波引流。
GitHub多线程读取文件内容示例代码仓库:GitHub代码仓库
此处IO多路复用使用的是select
模型,epoll
可参考:一个epoll的ET模式下的非阻塞的服务端小用例
希望大家不负韶华,奋力向前,共同进步,创造辉煌,获得自己所想要的,守护自己所拥有的。
1 线程的基本概念及常见问题
1.1 主线程退出,支线程也将退出吗
在Windows
系统中,当一个进程存在多个线程时,如果主线程执行结束,那么这时支线程(工作线程)及时还没有执行完相关代码,也会退出。
在Linux
系统中,如果主线程执行结束,则工作线程一般不会受到影响,还会继续运行,但此时这个进程就会编程僵尸进程(应当避免产生僵尸进程)。
1.2 某个线程崩溃,会导致进程退出吗
有可能
2 线程的基本操作
2.1 创建线程
Linux线程的创建
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
2.2 获取线程ID
pthread_t pthread_self(void);
2.1 等待线程结束
在实际的项目开发中,我们常常有这样一种需求,即一个线程需要等待另一个线程执行完任务退出后再继续执行
pthread_join
用来等待某个线程的退出并接收他的返回值
int pthread_join(pthread_t thread, void **retval);
4 整形变量的原子操作
线程同步技术,当多个线程同时操作某个资源
5 Linux线程同步对象
5.1 Linux互斥体
通过限制多个线程同时执行某段代码来保护资源
创建一个互斥体
pthread_mutex_t mutex;
初始化一个互斥体
int pthread_mutex_init(pthread_mutex_t* restrict mutex,
const pthread_mutexattr_t* restrict attr);
销毁一个互斥体
int pthread_mutex_destroy(pthread_mutex_t* mutex);
对于一个互斥体的加锁和解锁操作
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_trylock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);
对于pthread_mutexattr_t
,如下
/* 初始化一个pthread_mutexattr_t对象 */
int pthread_mutexattr_init(pthread_mutexattr_t* attr);
/* 销毁一个pthread_mutexattr_t对象 */
int pthread_mutexattr_destroy(pthread_mutexattr_t* attr);
设置和获取属性类型
int pthread_mutexattr_settype(pthread_mutexattr_t* attr, int type);
int pthread_mutexattr_gettype(const pthread_mutexattr_t* restrict attr,
int* restrict type);
属性类型
-
PTHREAD_MUTEX_NORMAL
:普通锁。互斥体对象的默认属性,在一个线程对一个普通锁加锁以后,其他线程会阻塞在pthread_mutex_lock
调用处,直到对互斥体加锁的线程释放了锁 -
PTHREAD_MUTEX_ERRORCHECK
:检错锁。如果一个线程使用了pthread_mutex_lock
对已加锁的互斥体对象再次加锁,则pthread_mutex_lock
会返回EDEADLK
-
PTHREAD_MUTEX_RECURSIVE
:可重入锁。该属性允许同一个线程对其持有的互斥体重复加锁,每成功调用pthread_mutex_lock
一次,该互斥体对象的锁引用计数就会增加1
,相反,每成功调用pthread_mutex_unlock
一次,锁引用计数就会减1
5.2 Linux的信号量
5.3 Linux条件变量
条件变量的初始化和销毁
int pthread_cond_init(pthread_cond_t* cond, const pthread_condattr_t* sttr);
int pthread_cond_destroy(pthread_cond_t* cond);
等待条件变量唤醒
int pthread_cond_wait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex);
int pthread_cond_timewait(pthread_cond_t* restrict cond,
pthread_mutex_t* restrict mutex,
const struct timespec* restrict abstime);
因调用pthread_cond_wait
而等待的线程可以被以下API
唤醒
/* 一次唤醒一个线程 */
int pthread_cond_signal(pthread_cond_t* restrict cond);
/* 一次唤醒多个线程 */
int pthread_cond_broadcast(pthread_cond_t* restrict cond);
5.4 Linux读写锁
-
读写锁的应用场景:在很多情况下,线程只是读取共享变量的值,只在极少数情况下才会真正修改共享变量的值,对于这种情况,读请求之间无需同步,他们之间的并发访问是安全的
-
读写锁的应用方法
int pthread_rwlock_init(pthread_rwlock_t* rwlock, const pthread_rwlockattr_t *attr);
int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);
声明一个读写锁
pthread_rwlock_t myrwlock;
三个读锁的系统API
接口
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_timerdlock(pthread_rwlock_t* rwlock, const struct timespec* abstime);
三个写锁的系统API
接口
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_timewrlock(pthread_rwlock_t* rwlock, const struct timespec* abstime);
- 读写锁的属性
/* 设置属性 */
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
/* 获取属性 */
int pthread_rwlockattr_getkind_np(pthread_rwlockattr_t *attr, int* pref);
pref
枚举类型
enum {
/* 读者优先 */
PTHREAD_RWLOCK_PREFER_READER_NP;
/* 读者优先 */
PTHREAD_RWLOCK_PREFER_WRITER_NP;
/* 写者优先 */
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP;
/* 默认情况 */
PTHREAD_RWLOCK_DEFAULT_NP = PTHREAD_RWLOCK_PREFER_READER_NP;
};
初始化属性
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
6 Windows线程同步对象
不做概述
7 C++11/14/17线程同步对象
在C/C++
中直接使用操作系统提供的多线程资源同步API
虽然限制很少,但使用起来毕竟不方便,同样的代码不能兼容Windows
和Linux
两个平台
互斥量 | 作用 |
---|---|
mutex |
基本的互斥量 |
timed_mutex |
有超时机制的互斥量 |
recursive_mutex |
可重入的互斥量 |
recursive_timed_mutex |
结合timed_mutex 和recursive_mutex 特点的互斥量 |
shared_timed_mutex |
具有超时机制的可共享互斥量 |
shared_mutex |
共享的互斥量 |
为了避免死锁,C++
提供了四个互斥量管理机制
互斥量管理 | 作用 |
---|---|
lock_guard |
基于作用域的互斥量管理 |
unique_lock |
更加灵活的互斥量管理 |
shared_lock |
共享的互斥量管理 |
scoped_lock |
多互斥量避免死锁的互斥量管理 |
互斥量的生命周期必须鲳鱼互斥量管理所在函数的周期
8 经验总结
8.1 减少锁的使用次数
使用了锁的代码一般存在如下性能损失
- 加锁和解锁操作,本身有一定的开销
- 临界区的代码不能并发执行
- 上下文切换过多
8.2 减小锁的粒度
减小锁的粒度,指的是尽量减少锁作用的临界区代码范围,临界区的代码范围越小,多个线程排队进入临界区的时间机会越短
原代码
void TaskPool::addTask(Task* task) {
std::loack_guard<std::mutex> guard(m_mutexList);
std::shared_ptr<Task> spTask;
spTask.reset(task);
m_mutexList.push_back(spTask);
m_cv.notify_one();
}
其实4/5/8行代码没必要作为临界区的代码,建议挪到临界区外面
void TaskPool::addTask(Task* task) {
std::shared_ptr<Task> spTask;
spTask.reset(task);
{
std::loack_guard<std::mutex> guard(m_mutexList);
m_mutexList.push_back(spTask);
}
m_cv.notify_one();
}
9 线程局部存储
对于一个存在多个线程的进程来说,有时需要每个线程都自己操作的这份数据。这有点类似于C++
类的实例属性,每个实例对象操作的都是自己的属性,称为线程局部存储(TLS)
/* 函数调用成功,则会为线程局部存储创建一个新的key,用户通过这个key设置和获取数据 */
int pthread_key_create(pthread_key_t* key, void (*destructor)(void*));
int pthread_key_delete(pthread_key_t* key);
/* 设置数据 */
int pthread_setspecific(pthread_key_t* key, const void* value);
/* 获取数据 */
void* pthread_getspecific(pthread_key_t* key);
10 环形队列和IO多路复用代码示例
代码我放在了GitHub,可以参考我的个人仓库获取代码,链接放在了引言结束部分。
此处采用的是循环队列。
这是一个多线程读取文件内容的C
代码,我设置时间为sleep(1)
,如果想过快的读可以取消test.c
中的这一行注释
运行效果