多线程编程与资源同步API和示例

时间:2021-03-19 01:14:56

引言:多线程编程一直是服务器编程的一个重点,而且自己在前段时间面试后端开发时多线程编程问的也比较多一点,换句话来说,这是必须掌握的,本文主要列出的是操作系统原生Linux apiC++11/14/17Windows相关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读写锁

  1. 读写锁的应用场景:在很多情况下,线程只是读取共享变量的值,只在极少数情况下才会真正修改共享变量的值,对于这种情况,读请求之间无需同步,他们之间的并发访问是安全的

  2. 读写锁的应用方法

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);
  1. 读写锁的属性
/* 设置属性 */
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虽然限制很少,但使用起来毕竟不方便,同样的代码不能兼容WindowsLinux两个平台

互斥量 作用
mutex 基本的互斥量
timed_mutex 有超时机制的互斥量
recursive_mutex 可重入的互斥量
recursive_timed_mutex 结合timed_mutexrecursive_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中的这一行注释

运行效果

多线程编程与资源同步API和示例