目录
一、Linux线程概念
二、线程的特性
1、线程的优点
2、线程的缺点
3、线程异常
4、线程用途
三、进程与线程
四、Linux线程控制
1、创建线程
2、线程退出
3、等待线程
4、线程取消
5、其他接口
5.1、获取自己的线程id
6、线程分离
五、线程库
六、线程互斥
1、进程线程间的互斥相关背景概念
2、互斥量
3、互斥量实现原理
七、封装
1、线程封装
2、锁的封装
八、线程安全
1、概念
2、常见的线程不安全的情况
3、常见的线程安全的情况
4、常见不可重入的情况
5、常见可重入的情况
6、可重入与线程安全联系
7、可重入与线程安全区别
九、死锁
十、线程同步
1、同步与竞态
2、条件变量
2.1、条件变量接口
2.2、为什么 pthread_cond_wait 需要互斥量
2.3、条件变量使用规范
十一、生产者消费者模型
十二、线程池
1、线程池实现简易代码-版本一
2、自己封装线程和锁实现线程池-版本二
3、自己封装线程和锁实现单例模式线程池-版本三
十三、线程安全的单例模式
1、饿汉方式实现单例模式
2、懒汉方式实现单例模式
3、懒汉方式实现单例模式(线程安全版本)
十四、STL,智能指针和线程安全
1、STL中的容器是否是线程安全的
2、智能指针是否是线程安全的
十五、其他常见的各种锁
十六、读者写者问题
1、读写锁
2、 读写锁接口
在CPU中有多个寄存器,有的寄存器中保存当前进程的PCB,有的寄存器保存当前进程的页表,还有一些寄存器指向当前进程执行的指令、代码、堆栈等等。当切换进程时,只需要把这些寄存器中的内容保存并切换,就可以换到另一个进程。
现在一个进程有多个PCB结构 task_struct ,都指向同一个地址空间。把地址空间中例如代码区的代码划分成多个部分,每一个 task_struct 在未来执行时,都执行同一个地址空间的不同部分的代码。这样一个进程中就存在了多个执行流,我们称每一个 task_struct 都是一个单独的线程。因此线程是一个执行分支,执行粒度比进程更细。
CPU在执行OS的代码切换 task_struct 时,由于这些 task_struct 都指向同一个地址空间,所以就不需要再更新寄存器中保存的地址空间和页表了。CPU内部有运算器、控制器、寄存器、MMU、硬件cache等等。其中 硬件cache 是高速缓存,它会把一部分热点数据预先加载进来,并在接下来的执行中有很大的概率命中这些数据,从而提高整机的效率。所以在多线程执行代码和数据时,cache中会缓存各种数据,当CPU在切换执行流时,由于进程没变,所以缓存内容也不变。如果进程改变,就需要把cache中缓存的数据设置为无效,并重新加载新进程的代码和数据。因此,线程的调度成本更低,主要体现在不用对cache进行切换。
所以现在重新理解进程,进程应该包含一批执行流、地址空间、页表以及代码和数据。进程是承担分配系统资源的基本实体。线程是CPU调度的基本单位。
在Windows操作系统,内核中有真线程,名为TCB :线程控制块。因为 TCB 属于 PCB,所以还需要维护进程与线程之间的调度关系算法,这过于复杂。
在Linux中,由于线程的控制块与进程控制块相似性非常高,所以直接复用了PCB的结构体,用PCB模拟线程的TCB。所以Linux没有真正意义上的线程,而是用进程方案模拟的线程。这样做的好处是复用代码和结构更简单,好维护,效率更高,也更安全。
进程是资源分配的基本单位。线程是调度的基本单位。线程共享进程数据,但也拥有自己的一部分数据:
进程的多个线程共享同一地址空间,因此Text Segment(代码区)、Data Segment(数据区)都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
进程和线程的关系如下图:
在Linux操作系统的视角,Linux下没有真正意义上的线程,而是用进程模拟的线程(LWP)。所以,Linux不会提供直接创建线程的系统调用,而是提供创建轻量级进程的接口。但是由于用户只认线程,所以库会对下将Linux接口封装,对上给用户提供线程控制的接口,这种库被称为用户级线程库。 pthread 库在任何系统都要自带,因此也称原生线程库。
创建线程函数:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
pthread_create 函数不是系统调用接口,而是一个库函数。
pthread_create 函数的参数列表中, thread 是线程id。 attr 是线程属性,一般设置为nullptr。函数指针用来回调式的执行目标函数。 arg 是传递给回调函数的参数。线程创建成功返回0,失败返回-1,并且对全局变量 errno 赋值以指示错误。
创建线程代码:
#include
#include
#include
using namespace std;
void* thread_run(void* args)
{
while(true)
{
cout << "new thread running" << endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t t;
pthread_create(&t, nullptr, thread_run, nullptr);
while(true)
{
cout << "main thread running, new thread id: " << t << endl;
sleep(1);
}
return 0;
}
因为 pthread_create 函数是库函数,所以在编译的时候需要加上库选项:
g++ -o $@ $^ -std=c++11 -lpthread
编译后查看库:
可以看到pthread库存在的位置。
运行程序:
这两个线程的PID是相同的,LWP不同。
如果需要向线程执行的函数传递参数,则可以使用如下方式:
#include
#include
#include
using namespace std;
#define NUM 10
void* thread_run(void* args)
{
char* name = (char*)args;
while(true)
{
cout << "new thread running, my name is: " << name << endl;
sleep(1);
}
delete name;
return nullptr;
}
int main()
{
pthread_t tids[NUM];
for(int i = 0; i < NUM; ++i)
{
char* tname = new char[64];
//char tname[64] //这种写法不行,因为线程传参时,传递的是数组的地址,随着数组被重写,线程读取到的值也被更新了,无法达到预期效果
snprintf(tname, 64, "thread-%d", i + 1);
pthread_create(tids + i, nullptr, thread_run, tname);
}
while(true)
{
cout << "main thread running" << endl;
sleep(1);
}
return 0;
}
运行观察结果:
因为参数 arg 是一个空指针类型,所以不只是可以传递整型、字符型变量,还可以传递类:
class ThreadData
{
public:
ThreadData(const string& name, int id, time_t createTime)
: _name(name)
, _id(id)
, _createTime(createTime)
{}
~ThreadData()
{}
public:
string _name;
int _id;
uint64_t _createTime;
};
void* thread_run(void* args)
{
ThreadData* td = static_cast(args);
while(true)
{
cout << "thread is runing, name is: " << td->_name << " create time: " << td->_createTime << " index: " << td->_id << endl;
sleep(1);
break;
}
delete td;
return nullptr;
}
int main()
{
pthread_t tids[NUM];
for(int i = 0; i < NUM; ++i)
{
char tname[64];
snprintf(tname, 64, "thread-%d", i + 1);
ThreadData* td = new ThreadData(tname, i + 1, time(nullptr));
pthread_create(tids + i, nullptr, thread_run, td);
sleep(1);
}
while(true)
{
sleep(1);
}
return 0;
}
运行观察结果:
如果线程执行 return 指令退出时,只有当前线程会退出。如果线程执行 exit 指令退出时,整个进程都会退出,即全部线程都会退出。
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
线程退出函数:
void pthread_exit(void *retval);
pthread_exit 函数的参数列表中, retval 是一个输出型参数,用于取出新线程退出的相关结果。
pthread_exit((void*)1);
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的,或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
注意:主线程退出,全部线程退出。因此新线程创建出来后,需要被主线程等待,否则会出现类似于僵尸进程的问题。
等待线程函数:
int pthread_join(pthread_t thread, void **retval);
pthread_join 函数的参数列表中, thread 是线程id。 retval 是一个输出型参数,用于取出新线程退出的相关结果。线程等待成功返回0,失败返回错误码 errno 。
等待线程退出代码:
void* ret = nullptr;
for(int i = 0; i < NUM; ++i)
{
int n = pthread_join(tids[i], &ret);
if(n != 0)
cerr << "pthread_join error" << endl;
cout << "thread quit: " << (uint64_t)ret << endl;
}
与等待进程退出不同,waitpid 的参数中,status里不光包含子进程退出的结果,还包含进程信号。而 pthread_join 函数的参数中,只有新线程的退出结果,并不关心信号,是因为当一个线程出现异常接收到信号时,所有的线程都会退出,主线程也会退出,也就等不到信号了。
因为参数 ret 是一个空指针类型,所以不只是可以传递整型、字符型变量,还可以传递类:
#define NUM 10
enum
{
OK=0,
ERROR
};
class ResultData
{
public:
ResultData(int top)
:_status(OK)
,_top(top)
,_result(0)
{}
~ResultData()
{}
public:
int _status;
int _top;
int _result;
};
void* thread_run(void* args)
{
ResultData* rd = static_cast(args);
for(int i = 1; i < rd->_top; ++i)
{
rd->_result += i;
}
cout << " cal done!" << endl;
pthread_exit(rd);
}
int main()
{
pthread_t tids[NUM];
for(int i = 0; i < NUM; ++i)
{
char tname[64];
snprintf(tname, 64, "thread-%d", i + 1);
ResultData* rd = new ResultData(100 + i * 5);
pthread_create(tids + i, nullptr, thread_run, rd);
sleep(1);
}
void* ret = nullptr;
for(int i = 0; i < NUM; ++i)
{
int n = pthread_join(tids[i], &ret);
if(n != 0)
cerr << "pthread_join error" << endl;
ResultData* rd = static_cast(ret);
if(rd->_status == OK)
{
cout << "[1, " << rd->_top << "] 计算的结果是:" << rd->_result << endl;
}
delete rd;
}
cout << "all thread quit..." << endl;
return 0;
}
运行观察结果:
可以通过 retval 参数检测线程运行的结果。
如果thread线程被别的线程调用 pthread_ cancel 异常终掉,value_ ptr所指向的单元里存放的是常数 PTHREAD_ CANCELED 。
线程取消函数:
int pthread_cancel(pthread_t thread);
编写代码:
void* threadRun(void* args)
{
const char* name = (const char*)args;
int cnt = 5;
while(cnt--)
{
cout << name << " is running: " << cnt << endl;
sleep(1);
}
pthread_exit((void*)11);
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
sleep(3);
pthread_cancel(tid);
void* ret = nullptr;
pthread_join(tid, &ret);
cout << " new thread exit: " << (int64_t)ret << endl;
return 0;
}
运行观察结果:
可以看到线程退出码是 -1 , 代表线程被取消。
获取自己的线程id的函数:
pthread_t pthread_self(void);
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
当线程已经被分离,却仍然被join时,则pthread_join函数会调用失败。
线程分离函数:
int pthread_detach(pthread_t thread);
编写代码:
void* threadRoutine(void* args)
{
string name = static_cast(args);
int cnt = 5;
while(cnt)
{
cout << name << " : " << cnt-- << endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
pthread_detach(tid);
int n = pthread_join(tid, nullptr);
if(n != 0)
cerr << "error: " << n << " : " << strerror(n) << endl;
sleep(10);
return 0;
}
在主线程中对新线程进行分离,并在分离后继续join新进程,运行观察结果:
发现pthread_join函数立刻调用失败,返回错误码,并且执行sleep指令暂时不退出。而新线程继续正常执行。如果不在主线程中写sleep指令,则主线程会立刻退出,并连带所有线程退出。
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
pthread_detach(pthread_self());
编写代码:
void* threadRoutine(void* args)
{
pthread_detach(pthread_self());
string name = static_cast(args);
int cnt = 5;
while(cnt)
{
cout << name << " : " << cnt-- << endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
int n = pthread_join(tid, nullptr);
if(n != 0)
cerr << "error: " << n << " : " << strerror(n) << endl;
return 0;
}
编译运行:
发现结果与线程没分离时一致,这是因为线程被创建出来后,谁先调度是由调度器决定的。于是虽然新线程被创建出来了,但还没来的及调度执行分离函数,就被主线程先执行join函数等待了。为了避免这个问题,在创建完新线程后,主线程等待1秒再开始执行:
运行观察结果:
结果符合预期。
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
我们知道,进程中的线程可以随时访问库中的代码和数据。所以关于线程管理的代码,比如线程切换的代码就可以放如库中,由库对线程进行管理。
库对线程的管理也是依照先描述再组织的原则实现的。当创建进程时,OS会在内核中创建相应的数据结构 LWP ,并使用库来封装管理,形成用户层的 TCB 结构。TCB与LWP的关系,可以对照文件系统的 struct FILE 和内核中文件描述符表的关系来理解。
线程库对线程先描述在组织,描述组织的方式如下图所示:
mmap 区域是共享区。动态库中有多个为线程创建的描述结构体,等同于 TCB 结构。结构体中主要有三个字段,其中第一个字段:struct pthread 包括了线程的属性,第二个字段:线程局部存储用于保存用 __thread 修饰的全局变量,第三个字段:线程栈保存本线程产生的临时数据。
为了能够找到这些描述结构体,就把这些结构体的起始地址保存起来,形成 pthread_t 类型的线程ID,用于标识线程相关属性集合的起始地址。
所有线程都要有自己独立的栈结构,主线程用的是进程系统栈,新线程用的是库中提供的栈。
我们创建新线程时,使用的 pthread_create 函数就是线程库的库函数,在库函数中封装了系统调用 clone :
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
在调用 pthread_create 函数时,会把回调函数的指针传递给系统调用 clone ,并且在库中创建新线程的结构体,并把线程栈地址传给clone的参数 child_stack 。 在调用不同的线程时,通过更新寄存器 esp、ebp 的值就可以实现栈的切换,关于栈帧的内容可以参考文章《函数栈帧的创建与销毁》。因此就算多个线程共同调用同一个函数,因为线程栈不同,所创建的临时变量的地址也不同。
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。多个线程并发的操作共享变量,会带来一些问题。
为了避免这种问题,需要做到以下三点:
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
对共享资源进行加锁操作相关函数:
#include
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
函数参数列表中的类型 pthread_mutex_t 称为互斥锁。定义完互斥锁后,使用函数 pthread_mutex_init 对互斥锁初始化,使锁变为可工作状态。锁用完之后,需要使用 pthread_mutex_destroy 函数销毁锁。
初始化锁还有一种静态分配的方法:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁。
#include
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_mutex_lock 函数对相关线程进行加锁操作。返回值:成功返回0,失败返回错误号。一旦加锁成功,就可以继续执行对应的执行流,如果加锁失败,则会把对应执行流阻塞住。执行流完成后,需要使用 pthread_mutex_unlock 函数进行解锁。
具体用法如下:
#include
#include
#include
#include
#include
#include
using namespace std;
int tickets = 1000; //加锁保护
class TData
{
public:
TData(const string &name, pthread_mutex_t* mutex)
:_name(name)
,_pmutex(mutex)
{}
~TData()
{}
public:
string _name;
pthread_mutex_t* _pmutex;
};
void* threadRoutine(void* args)
{
TData* td = static_cast(args);
while(1)
{
pthread_mutex_lock(td->_pmutex); // 所有线程都要遵守这个规则
if(tickets > 0)
{
usleep(2000);//模拟抢票花费的时间
cout << td->_name << " get a tickets: " << tickets-- << endl;
pthread_mutex_unlock(td->_pmutex);
}
else
{
pthread_mutex_unlock(td->_pmutex);
break;
}
usleep(13);
}
return nullptr;
}
int main()
{
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, nullptr);
pthread_t tids[4];
int n = sizeof(tids)/sizeof(tids[0]);
for(int i = 0; i < n; ++i)
{
char name[64];
snprintf(name, 64, "thread-%d", i + 1);
TData* td = new TData(name, &mutex);
pthread_create(tids + i, nullptr, threadRoutine, td);
}
for(int i = 0; i < n; ++i)
{
pthread_join(tids[i], nullptr);
}
pthread_mutex_destroy(&mutex);
return 0;
}
编译运行:
结果符合预期。
加锁操作有几点注意事项:
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下:
加锁代码的 movb 指令本质上是调用线程,向自己的上下文写入 0 。 xchgb 指令本质上是将共享数据交换到自己的私有上下文中,即加锁操作,因为这里是一条汇编指令,就保证了加锁操作的原子性。假设共享资源 mutex 中存放的是 1 ,那么交换操作并没有新增任何的 1 , 1 只会进行流转,保证了锁的唯一性。
使用线程库在语言层面封装线程,代码如下:
//Thread.hpp
#pragma once
#include
#include
#include
#include
using namespace std;
class Thread
{
public:
typedef enum
{
NEW = 0,
RUNNING,
EXITED
}ThreadStatus;
typedef void (*func_t)(void*);
public:
Thread(int num, func_t func, void* args)
:_tid(0)
,_status(NEW)
,_func(func)
,_args(args)
{
char name[128];
snprintf(name, sizeof(name), "thread-%d", num);
_name = name;
}
int status()
{
return _status;
}
string threadname()
{
return _name;
}
pthread_t threadid()
{
return _tid;
}
//因为类的成员函数有this指针,占用了回调函数的void*类型的参数,所以要定义成static类型
static void* runHelper(void* args)
{
Thread* ts = (Thread*)args;
//_func(ts->_args );
//(*ts)(); //使用仿函数的形式调用func函数
ts->_func(ts->_args);
}
void operator()()
{
_func(_args);
}
void run()
{
int n = pthread_create(&_tid, nullptr, runHelper, this); //这里传参传的是 this 指针,为了static类型的回调函数可以访问类属性和其他成员函数
if(n != 0)
exit(1);
_status = RUNNING;
}
void join()
{
int n = pthread_join(_tid, nullptr);
if(n != 0)
{
cerr << "main thread join thread " << _name << " error" << endl;
return;
}
_status = EXITED;
}
~Thread()
{}
private:
pthread_t _tid;
string _name;
func_t _func; //线程未来要执行的回调
void* _args; //回调函数的参数,可以设置成模板
ThreadStatus _status;
};
//ThreadTest.cc
#include
#include
#include
#include "Thread.hpp"
using namespace std;
void threadRun(void* args)
{
string message = static_cast(args);
while(1)
{
cout << "线程已被创建, " << message << endl;
sleep(1);
}
}
int main()
{
Thread t1(1, threadRun, (void*)"hello world");
cout << "thread name: " << t1.threadname() << ", thread id: " << t1.threadid() << ", thread status: " << t1.status() << endl;
t1.run();
cout << "thread name: " << t1.threadname() << ", thread id: " << t1.threadid() << ", thread status: " << t1.status() << endl;
t1.join();
cout << "thread name: " << t1.threadname() << ", thread id: " << t1.threadid() << ", thread status: " << t1.status() << endl;
return 0;
}
如果手动加锁,还需要在用完锁时主动解锁,很容易忘记。所以使用类来封装锁,通过构造与析构函数完成锁的添加和解除。
//LockGuard.hpp
#pragma once
#include
#include
using namespace std;
class Mutex //自己不维护锁,由外部传入
{
public:
Mutex(pthread_mutex_t* mutex)
:_pmutex(mutex)
{}
void lock()
{
pthread_mutex_lock(_pmutex);
}
void unlock()
{
pthread_mutex_unlock(_pmutex);
}
~Mutex()
{}
private:
pthread_mutex_t* _pmutex;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t* mutex)
:_mutex(mutex)
{
_mutex.lock();
}
~LockGuard()
{
_mutex.unlock();
}
private:
Mutex _mutex;
};
//ThreadTest.cc
#include
#include
#include
#include "Thread.hpp"
#include "lockGuard.hpp"
using namespace std;
int tickets = 1000; //加锁保护
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void threadRoutine(void* args)
{
string message = static_cast(args);
while(1)
{
usleep(13);
LockGuard lockguard(&mutex);
if(tickets > 0)
{
usleep(2000);//模拟抢票花费的时间
cout << message << " get a tickets: " << tickets-- << endl;
}
else
{
break;
}
}
}
int main()
{
Thread t1(1, threadRoutine, (void*)"hello world1");
Thread t2(2, threadRoutine, (void*)"hello world2");
Thread t3(3, threadRoutine, (void*)"hello world3");
Thread t4(4, threadRoutine, (void*)"hello world4");
t1.run();
t2.run();
t3.run();
t4.run();
t1.join();
t2.join();
t3.join();
t4.join();
return 0;
}
死锁是指在一组进程中的各个线程均占有不会释放的资源,但因互相申请被其他线程所占用的不会释放的资源而处于的一种永久等待状态。
死锁四个必要条件:
避免死锁:
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。条件变量是一个数据类型,其中有一个队列,供线程等待使用。
条件变量初始化:
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);
参数:
cond:要初始化的条件变量
attr:NULL
销毁:
int pthread_cond_destroy(pthread_cond_t *cond)
等待条件满足:
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量
唤醒等待:
//唤醒在条件队列中等待的所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);
//按照队列顺序逐个唤醒线程
int pthread_cond_signal(pthread_cond_t *cond);
// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
由于解锁和等待不是原子操作。调用解锁之后, pthread_cond_wait 之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么 pthread_cond_wait 将错过这个信号,可能会导致线程永远阻塞在这个 pthread_cond_wait 。所以解锁和等待必须是一个原子操作。
int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t * mutex); 进入该函数后,
会去看条件量等于0不?等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样。
等待条件代码:
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
给条件发送信号代码:
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
有关于生产者消费者模型,由于篇幅过长,专门写了一篇博客《生产者消费者模型》。大家可以点击查看。
线程池:
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
线程池的应用场景:
线程池的种类:
线程池示例:
线程池本质上是一种变形的生产者消费者模型,生产者是用户,消费者是操作系统本身。
//threadPool_V1.hpp
#pragma once
#include
#include
#include
#include
#include
#include
using namespace std;
const static int N = 5;
template
class ThreadPool
{
public:
ThreadPool(int num = N)
:_num(num)
,_threads(num)
{
pthread_mutex_init(&_lock, nullptr);
pthread_cond_init(&_cond, nullptr);
}
void lockQueue()
{
pthread_mutex_lock(&_lock);
}
void unlockQueue()
{
pthread_mutex_unlock(&_lock);
}
void threadWait()
{
pthread_cond_wait(&_cond, &_lock);
}
void threadWakeup()
{
pthread_cond_signal(&_cond);
}
bool isEmpty()
{
return _tasks.empty();
}
T popTask()
{
T t = _tasks.front();
_tasks.pop();
return t;
}
static void* threadRoutine(void* args)
{
pthread_detach(pthread_self()); //进行线程分离
ThreadPool* tp = static_cast*>(args);
while(1)
{
//1、检测有没有任务
//2、有:处理
//3、无:等待
//必须要加锁
tp->lockQueue();
while(tp->isEmpty())
{
//等待,cond
tp->threadWait();
}
T t = tp->popTask(); //从公共区域拿到私有区域
tp->unlockQueue();
//t.run(); //处理任务不应该在临界区中进行
t();
cout << "thread handler done, result: " << t.formatRes() << endl;
}
}
void init()
{
}
void start()
{
for(int i = 0; i < _num; i++)
{
pthread_create(&_threads[i], nullptr, threadRoutine, this);
}
}
void pushTask(const T& t)
{
lockQueue();
_tasks.push(t);
threadWakeup();
unlockQueue();
}
~ThreadPool()
{
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_cond);
}
private:
vector _threads;
int _num;
queue _tasks; //使用stl的自动扩容特性
pthread_mutex_t _lock;
pthread_cond_t _cond;
};
//task.hpp
#pragma once
#include
#include
#include
class Task
{
public:
Task()
{
}
Task(int x, int y, int op)
: _x(x), _y(y), _op(op), _result(0), _exitCode(0)
{
}
void operator()()
{
switch (_op)
{
case '+':
_result = _x + _y;
break;
case '-':
_result = _x - _y;
break;
case '*':
_result = _x * _y;
break;
case '/':
{
if (_y == 0)
_exitCode = -1;
else
_result = _x / _y;
}
break;
case '%':
{
if (_y == 0)
_exitCode = -2;
else
_result = _x % _y;
}
break;
default:
break;
}
usleep(100000);
}
std::string formatArg()
{
return std::to_string(_x) + _op + std::to_string(_y) + "= ?";
}
std::string formatRes()
{
return std::to_string(_result) + "(" + std::to_string(_exitCode) + ")";
}
~Task()
{
}
private:
int _x;
int _y;
char _op;
int _result;
int _exitCode;
};
//main.cc
#include "threadPool_V1.hpp"
#include "task.hpp"
#include
int main()
{
unique_ptr> tp(new ThreadPool());
tp->init();
tp->start();
while(1)
{
//充当生产者,从网络中读取数据,构建成任务,推送给线程池
int x, y;
char op;
cout << "please Enter x> ";
cin >> x;
cout << "please Enter y> ";
cin >> y;
cout << "please Enter op(+-*/%)> ";
cin >> op;
Task t(x, y, op);
tp->pushTask(t);
//sleep(1);
}
return 0;
}
运行观察结果:
//Task.hpp
#pragma once
#include
#include
#include
class Task
{
public:
Task()
{
}
Task(int x, int y, int op)
: _x(x), _y(y), _op(op), _result(0), _exitCode(0)
{
}
void operator()()
{
switch (_op)
{
case '+':
_result = _x + _y;
break;
case '-':
_result = _x - _y;
break;
case '*':
_result = _x * _y;
break;
case '/':
{
if (_y == 0)
_exitCode = -1;
else
_result = _x / _y;
}
break;
case '%':
{
if (_y == 0)
_exitCode = -2;
else
_result = _x % _y;
}
break;
default:
break;
}
usleep(100000);
}
std::string formatArg()
{
return std::to_string(_x) + _op + std::to_string(_y) + "= ?";
}
std::string formatRes()
{
return std::to_string(_result) + "(" + std::to_string(_exitCode) + ")";
}
~Task()
{
}
private:
int _x;
int _y;
char _op;
int _result;
int _exitCode;
};
//Thread.hpp
#pragma once
#include
#include
#include
#include
using namespace std;
class Thread
{
public:
typedef enum
{
NEW = 0,
RUNNING,
EXITED
}ThreadStatus;
typedef void (*func_t)(void*);
public:
Thread(int num, func_t func, void* args)
:_tid(0)
,_status(NEW)
,_func(func)
,_args(args)
{
char name[128];
snprintf(name, sizeof(name), "thread-%d", num);
_name = name;
}
int status()
{
return _status;
}
string threadname()
{
return _name;
}
pthread_t threadid()
{
return _tid;
}
//因为类的成员函数有this指针,占用了回调函数的void*类型的参数,所以要定义成static类型
static void* runHelper(void* args)
{
Thread* ts = (Thread*)args;
//_func(ts->_args );
//(*ts)(); //使用仿函数的形式调用func函数
ts->_func(ts->_args);
}
void operator()()
{
_func(_args);
}
void run()
{
int n = pthread_create(&_tid, nullptr, runHelper, this); //这里传参传的是 this 指针,为了static类型的回调函数可以访问类属性和其他成员函数
if(n != 0)
exit(1);
_status = RUNNING;
}
void join()
{
int n = pthread_join(_tid, nullptr);
if(n != 0)
{
cerr << "main thread join thread " << _name << " error" << endl;
return;
}
_status = EXITED;
}
~Thread()
{}
private:
pthread_t _tid;
string _name;
func_t _func; //线程未来要执行的回调
void* _args; //回调函数的参数,可以设置成模板
ThreadStatus _status;
};
//lockGuard.hpp
#pragma once
#include
#include
using namespace std;
class Mutex //自己不维护锁,由外部传入
{
public:
Mutex(pthread_mutex_t* mutex)
:_pmutex(mutex)
{}
void lock()
{
pthread_mutex_lock(_pmutex);
}
void unlock()
{
pthread_mutex_unlock(_pmutex);
}
~Mutex()
{}
private:
pthread_mutex_t* _pmutex;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t* mutex)
:_mutex(mutex)
{
_mutex.lock();
}
~LockGuard()
{
_mutex.unlock();
}
private:
Mutex _mutex;
};
//ThreadPool_V2.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include "Thread.hpp"
#include "lockGuard.hpp"
using namespace std;
const static int N = 5;
template
class ThreadPool
{
public:
ThreadPool(int num = N)
: _num(num)
{
pthread_mutex_init(&_lock, nullptr);
pthread_cond_init(&_cond, nullptr);
}
pthread_mutex_t* getlock()
{
return &_lock;
}
void threadWait()
{
pthread_cond_wait(&_cond, &_lock);
}
void threadWakeup()
{
pthread_cond_signal(&_cond);
}
bool isEmpty()
{
return _tasks.empty();
}
T popTask()
{
T t = _tasks.front();
_tasks.pop();
return t;
}
static void threadRoutine(void *args)
{
ThreadPool *tp = static_cast *>(args);
while (1)
{
// 1、检测有没有任务
// 2、有:处理
// 3、无:等待
// 必须要加锁
T t;
{
LockGuard lockguard(tp->getlock());
while (tp->isEmpty())
{
// 等待,cond
tp->threadWait();
}
t = tp->popTask(); // 从公共区域拿到私有区域
}
// t.run(); //处理任务不应该在临界区中进行
t();
cout << "thread handler done, result: " << t.formatRes() << endl;
}
}
void init()
{
for (int i = 0; i < _num; i++)
{
_threads.push_back(Thread(i, threadRoutine, this));
}
}
void start()
{
for (auto &t : _threads)
{
t.run();
}
}
void check()
{
for (auto &t : _threads)
{
cout << t.threadname() << " runing... " << endl;
}
}
void pushTask(const T &t)
{
LockGuard lockguard(&_lock);
_tasks.push(t);
threadWakeup();
}
~ThreadPool()
{
for (auto &t : _threads)
{
t.join();
}
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_cond);
}
private:
vector _threads;
int _num;
queue _tasks; // 使用stl的自动扩容特性
pthread_mutex_t _lock;
pthread_cond_t _cond;
};
//main.cc
#include "threadPool_V2.hpp"
#include "task.hpp"
#include
int main()
{
unique_ptr> tp(new ThreadPool());
tp->init();
tp->start();
while(1)
{
//充当生产者,从网络中读取数据,构建成任务,推送给线程池
int x, y;
char op;
cout << "please Enter x> ";
cin >> x;
cout << "please Enter y> ";
cin >> y;
cout << "please Enter op(+-*/%)> ";
cin >> op;
Task t(x, y, op);
tp->pushTask(t);
//sleep(1);
}
return 0;
}
运行观察结果:
//ThreadPool_V4.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include "Thread.hpp"
#include "lockGuard.hpp"
using namespace std;
const static int N = 5;
template
class ThreadPool
{
private:
ThreadPool(int num = N)
: _num(num)
{
pthread_mutex_init(&_lock, nullptr);
pthread_cond_init(&_cond, nullptr);
}
ThreadPool(const ThreadPool& tp) = delete;
void operator=(const ThreadPool& tp) = delete;
public:
static ThreadPool* getinstance()
{
if(instance == nullptr)
{
LockGuard lockguard(&instance_lock);
if(instance == nullptr)
{
instance = new ThreadPool();
instance->init();
instance->start();
}
}
return instance;
}
pthread_mutex_t *getlock()
{
return &_lock;
}
void threadWait()
{
pthread_cond_wait(&_cond, &_lock);
}
void threadWakeup()
{
pthread_cond_signal(&_cond);
}
bool isEmpty()
{
return _tasks.empty();
}
T popTask()
{
T t = _tasks.front();
_tasks.pop();
return t;
}
static void threadRoutine(void *args)
{
ThreadPool *tp = static_cast *>(args);
while (1)
{
// 1、检测有没有任务
// 2、有:处理
// 3、无:等待
// 必须要加锁
T t;
{
LockGuard lockguard(tp->getlock());
while (tp->isEmpty())
{
// 等待,cond
tp->threadWait();
}
t = tp->popTask(); // 从公共区域拿到私有区域
}
// t.run(); //处理任务不应该在临界区中进行
t();
cout << "thread handler done, result: " << t.formatRes() << endl;
}
}
void init()
{
for (int i = 0; i < _num; i++)
{
_threads.push_back(Thread(i, threadRoutine, this));
}
}
void start()
{
for (auto &t : _threads)
{
t.run();
}
}
void check()
{
for (auto &t : _threads)
{
cout << t.threadname() << " runing... " << endl;
}
}
void pushTask(const T &t)
{
LockGuard lockguard(&_lock);
_tasks.push(t);
threadWakeup();
}
~ThreadPool()
{
for (auto &t : _threads)
{
t.join();
}
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_cond);
}
private:
vector _threads;
int _num;
queue _tasks; // 使用stl的自动扩容特性
pthread_mutex_t _lock;
pthread_cond_t _cond;
static ThreadPool* instance;
static pthread_mutex_t instance_lock;
};
template
ThreadPool* ThreadPool::instance = nullptr;
template
pthread_mutex_t ThreadPool::instance_lock = PTHREAD_MUTEX_INITIALIZER;
//main.cc
#include "threadPool_V4.hpp"
#include "task.hpp"
#include
int main()
{
// unique_ptr> tp(new ThreadPool());
// tp->init();
// tp->start();
while(1)
{
//充当生产者,从网络中读取数据,构建成任务,推送给线程池
int x, y;
char op;
cout << "please Enter x> ";
cin >> x;
cout << "please Enter y> ";
cin >> y;
cout << "please Enter op(+-*/%)> ";
cin >> op;
Task t(x, y, op);
ThreadPool::getinstance()->pushTask(t);
//tp->pushTask(t);
//sleep(1);
}
return 0;
}
template
class Singleton {
static T data;
public:
static T* GetInstance()
{
return &data;
}
};
只要通过 Singleton 这个包装类来使用 T 对象, 则一个进程中只有一个 T 对象的实例。
template
class Singleton
{
static T* inst;
public:
static T* GetInstance()
{
if (inst == NULL)
{
inst = new T();
}
return inst;
}
};
存在一个严重的问题:线程不安全。第一次调用 GetInstance 的时候,如果两个线程同时调用,可能会创建出两份 T 对象的实例。
// 懒汉模式, 线程安全
template
class Singleton
{
volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
static std::mutex lock;
public:
static T* GetInstance()
{
if (inst == NULL)
{ // 双重判定空指针, 降低锁冲突的概率, 提高性能.
lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new.
if (inst == NULL)
{
inst = new T();
}
lock.unlock();
}
return inst;
}
};
注意事项:
不是。
原因是:STL 的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。
而且对于不同的容器,加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶)。因此 STL 默认不是线程安全。如果需要在多线程环境下使用,往往需要调用者自行保证线程安全。
对于 unique_ptr,由于只是在当前代码块范围内生效,因此不涉及线程安全问题。
对于 shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证 shared_ptr 能够高效,原子的操作引用计数。
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
注意:写独占,读共享,读锁优先级高
设置读写优先:
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
/*
pref 共有 3 种选择
PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况
PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和
PTHREAD_RWLOCK_PREFER_READER_NP 一致
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁
*/
初始化:
int pthread_rwlock_init(pthread_rwlock_t* restrict rwlock,const pthread_rwlockattr_t* restrict attr);
销毁:
int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);
加锁和解锁:
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); //读者加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); //写者加锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); //释放锁
读写锁案例:
#include
#include
#include
#include
#include
#include
#include
volatile int ticket = 1000;
pthread_rwlock_t rwlock;
void* reader(void* arg)
{
char* id = (char*)arg;
while (1) {
pthread_rwlock_rdlock(&rwlock);
if (ticket <= 0) {
pthread_rwlock_unlock(&rwlock);
break;
}
printf("%s: %d\n", id, ticket);
pthread_rwlock_unlock(&rwlock);
usleep(1);
}
return nullptr;
}
void* writer(void* arg)
{
char* id = (char*)arg;
while (1) {
pthread_rwlock_wrlock(&rwlock);
if (ticket <= 0) {
pthread_rwlock_unlock(&rwlock);
break;
}
printf("%s: %d\n", id, --ticket);
pthread_rwlock_unlock(&rwlock);
usleep(1);
}
return nullptr;
}
struct ThreadAttr
{
pthread_t tid;
std::string id;
};
std::string create_reader_id(std::size_t i)
{
// 利用 ostringstream 进行 string 拼接
std::ostringstream oss("thread reader ", std::ios_base::ate);
oss << i;
return oss.str();
}
std::string create_writer_id(std::size_t i)
{
// 利用 ostringstream 进行 string 拼接
std::ostringstream oss("thread writer ", std::ios_base::ate);
oss << i;
return oss.str();
}
void init_readers(std::vector& vec)
{
for (std::size_t i = 0; i < vec.size(); ++i) {
vec[i].id = create_reader_id(i);
pthread_create(&vec[i].tid, nullptr, reader, (void*)vec[i].id.c_str());
}
}
void init_writers(std::vector& vec)
{
for (std::size_t i = 0; i < vec.size(); ++i) {
vec[i].id = create_writer_id(i);
pthread_create(&vec[i].tid, nullptr, writer, (void*)vec[i].id.c_str());
}
}
void join_threads(std::vector const& vec)
{
// 我们按创建的 逆序 来进行线程的回收
for (std::vector::const_reverse_iterator it = vec.rbegin(); it !=
vec.rend(); ++it) {
pthread_t const& tid = it->tid;
pthread_join(tid, nullptr);
}
}
void init_rwlock()
{
#if 0 // 写优先
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
pthread_rwlock_init(&rwlock, &attr);
pthread_rwlockattr_destroy(&attr);
#else // 读优先,会造成写饥饿
pthread_rwlock_init(&rwlock, nullptr);
#endif
}
int main()
{
// 测试效果不明显的情况下,可以加大 reader_nr
// 但也不能太大,超过一定阈值后系统就调度不了主线程了
const std::size_t reader_nr = 1000;
const std::size_t writer_nr = 2;
std::vector readers(reader_nr);
std::vector writers(writer_nr);
init_rwlock();
init_readers(readers);
init_writers(writers);
join_threads(writers);
join_threads(readers);
pthread_rwlock_destroy(&rwlock);
}