一.进程线程基本概念和常见问题
进程是计算机中实际运行的一个程序。每个进程都有自己的独立空间和上下文堆栈,进程为线程分配资源,线程才是进程中执行的基本单位。
1.主线程推出,支线程也将退出吗?
window系统是这样,Linux不会,主线程退出,子线程会变成僵尸线程。
2.某个线程崩溃,会导致进程推出吗?
每个线程都是独立执行的单位,都有自己的上下文堆栈,一个线程的崩溃不会影响其他线程。但是通常之情况下,线程的崩溃会导致进程的退出,进程退出会导致其他线程退。
二.线程的基本操作
1.线程的创建。
在Linux平台上,使用pthread_create创建
thread是创建成功后分配给它的线程编号,atrr是线程属性,一般指定为0,start_routine是线程运行的函数,arg是传入函数的值。
2.获取线程的ID
虽然可以通过thread知道线程的ID,但是一般情况下我们需要知道当前线程的ID,获取当前线程ID的方法是:
在Linux系统中,我们可以使用pstack查看线程的使用情况,它会显示堆栈。
3.等待线程的结束
在linux下等待线程结束的函数为
pthread_join函数会挂起当前线程等待目标线程退出,直到被等待的线程退出后才会被唤醒继续执行,retval可以获取线程的退出码.
在c++11中提供了join方法等待线程结束,但是前提要求是线程也处于运行状态,如果线程不存在就会报错,c++11提供了joinable方法来判断是否可以join,如果不可以就会返回false.
4.c++将对象实例指针作为线程函数的参数
LINUX的线程函数是固定,它的形式是
如果c++把这个函数封装为类的一个函数,会怎么样?
class Thread{
public:
Thread();
~Thread();
void* threadFunc(void*);
}
c++编译器会把它看作void* threadFunc(Thread* this, void*);
我们在外使用pthread_create就没办法调用它了。
因此我们不能把线程函数作为类的实例函数,但是我们可以把它用作类的静态函数。
但是如果使用c++提供的线程对象,就没有问题了,但是我们必须显式的把线程函数所属的类对象实例指针作为构造函数传递给std::thread。
class Thread{
public:
Thread();
~Thread();
void fun()
{
//线程对象的构造函数第一个参数必须是线程函数地址。
//第二个参数必须显式的传入对象实例指针(this)
thread_ptr = new(std::thread(&Thread::threadFunc,this, 100, 200));
}
//线程函数作为类的内部函数
void ThreadFunc(int a, int b)
{
std::cout << a << " " << b << std::endl;
}
private:
std::shared_ptr<std::thread> thread_ptr;//智能指针
}
为什么这样,而不是
根据上面的解释好好思考就知道。
如果我们把类的静态函数作为线程函数,那么我们就可以在Linux的线程创建函数中使用它了,但是问题是,使用静态函数,我们就无法调用类的其他成员函数,怎么办呢?
我们可以把this指针作为静态函数的参数,然后转换回来。
class Thread{
Thread();
~Thread();
static threadFunc(void* ptr)
{
Thread* pthread = static_cast<Thread*>(ptr);
......
}
}
最后,我们还可以用bind方法把类的实例方法作为线程函数
class Thread{
void* thread(void* arg){
}
Thread(){
std::thread(std::bind(&Thread::thread, this));
}
}
c++还有原子操作,这个我在其他页面说过了。
5.Linux进程服务
1.Linux互斥体
pthread_mutex_t类型表示互斥体对象
有两种初始化方法
(1).静态初始化
(2).动态初始化
如果互斥量动态创建,就需要用到动态初始化
2.销毁一个互斥体
无需销毁静态初始化对象,不能销毁一个已经被加锁的互斥体对象
3.加锁解锁
4.带属性的锁
带属性的锁对象是pthread_mutexattr_t
1.API
2.属性取值
它的属性一般取如下
(1),PTHREAD_MUTEX_NORMAL:普通锁
(2).PTHREAD_MUTEX_ERRORCHECK:检错锁
设置这个属性后,如果重复调用,就会返回一个错误码
(3).PTHREAD_MUTEX_RECURSIVE:可重入锁
运行多个线程对同一把锁加锁,每次增加一个线程对锁lock一次,引用计数就会增加1,unlock一次就减少1
5.信号量
根据资源给定信号量,然后让其他线程前来消费
Linux给定的一系列信号量API如下:
6.条件变量
1.为什么需要条件变量
我们有了锁还有什么不满足的吗?
这是因为我们如果进入临界需要判断某种条件满足,然后进行操作,这个过程之前需要加锁,这个过程之后解锁,那么就显得效率低下。如果我们加锁进入临界区不满足,就要让出锁,岂不是白白浪费了一次获取锁的机会,无缘无故加了一次锁缺什么也没做。
如果我们可以做到这样:我们加锁进入临界区,然后等待条件满足,如果条件满足,则开始运行,如果不满足则进入睡眠,档条件满足的时候,就被唤醒。
这就是条件变量。
2.条件变量要与互斥量一起使用
为什么要一起使用?
因为如果不一起使用,就如下面一段代码
如果正好运行到条件唤醒前面一行,一个线程突然杀出,cpu切换,这个线程唤醒了某个条件,则另一个线程完美错过,那么它将会永远阻塞再这里。所以这个步骤必须是一个原子性的。
3.条件变量的使用
(1).初始化和销毁
(2).等待条件被唤醒
等待的线程可以被唤
我忘了说明前面的返回值了,默认都是返回0代表调用成功,返回负数代表失败,一般还会返回错误码。
3.虚假唤醒
#include<pthread.h>
#include<error.h>
#include<list>
#include<semaphore.h>
#include<unistd.h>
#include<iostream>
using std::list;
using std::cout;
using std::endl;
class Task;
pthread_mutex_t mutex;
list<Task*> tasks;
pthread_cond_t cond;
class Task{
public:
Task(int taskID){ this->taskID = taskID;}
void doTask(){
pthread_t pid = pthread_self();
cout << "task id = "<<taskID << " pid = " << pid << endl;
}
private:
int taskID;
};
void* consumer_thread(void* param){
Task* ptask = NULL;
while(true){
pthread_mutex_lock(&mutex);
while(tasks.empty())//如果队列为空,则等待条件满足,这时线程不会往下继续执行,它会释放锁
pthread_cond_wait(&cond, &mutex);//当条件满足的时候,它会获得锁
ptask = tasks.front();
tasks.pop_front();
pthread_mutex_unlock(&mutex);
if(ptask == NULL)
continue;
ptask->doTask();
ptask = NULL;
}
return NULL;
}
void* producer_thread(void* param)
{
int taskID = 0;
Task* ptask = NULL;
while(true)
{
ptask = new Task(taskID);
pthread_mutex_lock(&mutex);
tasks.push_back(ptask);
cout<<"producer taskID = "<< taskID << " pid = "<< pthread_self() << endl;
pthread_mutex_unlock(&mutex);
//释放信号量,通知条件变量
pthread_cond_signal(&cond);
taskID+=1;
sleep(1);
}
return NULL;
}
int main()
{
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond, NULL);
pthread_t consumer[5];
for(int i = 0; i < 5; ++i)
pthread_create(consumer+i,NULL, consumer_thread, NULL);
pthread_t producer;
pthread_create(&producer,NULL, producer_thread, NULL);
pthread_join(producer, NULL);
for(int i = 0; i < 5; ++i)
pthread_join(*consumer+i, NULL);
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
return 0;
}
生产者-消费者
里面有一段pthread_cond_wait的代码
(1).pthread_cond_wait在阻塞的时候,会释放绑定的互斥体,然后阻塞线程,因此在调用改函数前应该对互斥体有个加锁操作。
(2).pthread_cond_wait在收到信号的时候会解锁,因此需要在后面释放锁。
代码里面有个地方耐人寻味:
为什么不用if呢?
这是因为系统会虚假唤醒,也就是存在没有线程向它发送信号,但是系统却唤醒了它,所以我们要求不仅被唤醒,还要满足条件。这是一个系统bug,系统可能因为某些原因被中断了,然后自行重启,但是这个时候,可能不会再产生唤醒的信号,线程将永远被阻塞,所以与其被阻塞,不如虚假唤醒。
4.条件变量丢失问题
小心!
一个条件变量信号在产生时,如果没有相关线程接受,这个信号就会永久丢失,再次调用pthread_cond_wait就会永久阻塞,所以确保信号被接受。
6.读写锁
其实读取数据在很多情况下是安全的,如果使用各种安全措施,会造成资源浪费,毕竟看一下又不会少一块肉,不会修改值。
1.初始化和销毁
读写锁的类型是pthread_rwlock_t
2.请求读锁的接口
3.请求写锁的接口
读锁用于共享模式也就是读锁形式下可以获取多个读锁,如果是写模式,则会阻塞
写锁用于独占模式,被写锁占据,则其他进程都被阻塞。
4.释放锁
5.读写锁的属性
第二个参数pref设定了属性,取值自己查一查man
6.对读写锁初始化和销毁的函数
创建一个读写锁,获取读锁的可能性比获取写锁的可能性大很多。除非延长读锁的时间或者设置写锁优先。
7.c++多线程实现消息线程池和队列系统
关于c++的多线程这里不做介绍了,可以看看其他文章;
比如线程库<thread>,互斥量metux,操作互斥量的封装lock_guard,unique_lock等等
条件变量condition_variable
这里实现一个线程队列池,实现多线程处理多任务
#include <iostream>
#include<thread>
#include<memory>
#include<mutex>
#include<list>
#include<vector>
#include<condition_variable>
using namespace std;
//任务
class Task{
public:
void DoTask(){
cout << "执行任务" << endl;
}
~Task()
{
cout << "任务执行结束" << endl;
}
};
//线程队列
class PthreadList{
public:
PthreadList(int num = 5)
{
if(num <= 0)
num = 5;
runable = true;
for(int i{0}; i < num; i++)
{
shared_ptr<thread> ptr;
ptr.reset(new thread(bind(PthreadList::threadFunc, this)));
m_threads.push_back(ptr);
}
}
~PthreadList()
{
removeAllTasks();
}
//线程函数
void threadFunc()
{
shared_ptr<Task> ptr_task;
while(true)
{
{
unique_lock<mutex> guard(m_mutexList);//上锁
//条件变量存在的意义,当条件满足的时候被唤醒
while(_taskList.empty()) //检查任务是否为空
{
//如果不允许执行,则跳出
if(!runable)
break;
//等待条件满足,满足则唤醒 ,不满足则在此阻塞
cv.wait(guard);
}
if(!runable)
break;
//获取任务
ptr_task = _taskList.front();
_taskList.pop_front();
}
//执行任务
if(ptr_task == NULL)
continue;
ptr_task->DoTask();
ptr_task.reset();
}
}
//等到所有线程结束
void stop(){
runable = false;
cv.notify_all();
for(auto& ptr : m_threads)
if(ptr->joinable())
ptr->join();
}
//添加任务到队列
void addTask(Task* task)
{
if(task == NULL)
return;
shared_ptr<Task> ptr;
ptr.reset(task);
{
lock_guard<mutex> m(m_mutexList);
_taskList.push_back(ptr);
}
cv.notify_one();
}
void removeAllTasks()
{
lock_guard<mutex> guard(m_mutexList);
for(auto& iter : _taskList)
iter.reset();
_taskList.clear();
}
private:
list<shared_ptr<Task>> _taskList;
mutex m_mutexList;
condition_variable cv;
bool runable{false};
vector<shared_ptr<thread>> m_threads;
};
int main(int argc, char** argv) {
PthreadList plist(5);
Task* ptr_task;
for(int i = 0; i < 10; i++)
plist.addTask(new Task());
plist.stop();
return 0;
}
为什么要一起使用?
可以思考一下:
如果线程运行到解锁,cpu切换,另一个线程处理了某个条件,然后发送信号;信号就会被之前的线程错过,条件会一直等待下去。
所以,加锁和条件要是一个步骤,具有原子性。