下面选取32位系统举例。
通常我们创建线程是为了执行程序的一部分代码,所以执行粒度一定比进程更细,我们知道进程=内核数据结构+代码和数据。引入线程之后,这个概念就应该修正为进程=许多内核数据结构+代码和数据。这些数据结构指向同一个程序地址空间,如图:
在操作系统的概念中,这样的数据结构叫做线程控制块(TCB),但由于创建新的数据结构还要设计新的调度逻辑,所以Linux在内核中并没有设计这样的数据结构,而是复用进程的数据结构PCB。故此在Linux中并没有真正的线程,而是用进程模拟的线程,线程在Linux中叫轻量级进程(LWP)。
文件系统指出磁盘和内存的数据交换是以块(4KB)为单位,因此内存的管理也要以4KB为单位,在内存中这样的结构叫做页。
如果页表只有一张,并且页表保存的是字节间的映射关系,那么一个页表需要保存232行,这样肯定是不行的,一是查找速度慢,二是内存可能不够。因此,Linux中采用多级页表,并且将32虚拟地址拆分成3部分使用:前10位用来在页目录中使用,中间10位在页表项中使用,最后12位做为页内偏移地址使用。(212 = 4KB)
页目录共有1024行,每个页表项也有1024行,共可以映射2^20页地址,这恰好是内存中页的数量,页表映射出物理页号,物理页号和虚拟地址中的12位页内偏移组合,构成物理字节地址。
由于程序不可能使用整个内存,所以页表不会一次全部创建,而是创建一部分。
在Linux中,没有真正意思上线程,所以系统不会提供线程相关的接口,但是有控制轻量级进程的相关接口。可是用户只认线程,所以有了用户级线程库pthread
,这个库底层封装了轻量级进程的相关接口,给上层提供了线程相关的接口。pthread库在任何一个Linux系统上都自带了,因此pthread库也叫做原生线程库。由于pthread库属于第三方库,所以在使用gcc/g++编译时,需要指定库名称。gcc xxxx -lpthread
代码示例:
#include
#include
#include
using namespace std;
void* threadRoutine(void* s)
{
const char* str = (const char*)s;
cout << str << endl;
}
int main()
{
pthread_t p1;
pthread_create(&p1, nullptr, threadRoutine, (void*)"thread 1");
sleep(3);
return 0;
}
通过命令
ps -aL
可以查看当前用户创建的线程,线程不分父子,分主次:主线程的PID=LWP。操作系统判断是进程间切换还是线程间切换通常使用PID来区分的,看两个结构体的PID是否相同,相同即是线程间切换。
- 如果主线程退出,当前进程的所有线程立即退出。
- 如果任一线程收到信号,那么所有线程都会收到信号。
如果主线程不等待新线程,那么新线程会产生类似僵尸进程的情况,导致内存泄漏。
代码示例:
#include
#include
#include
#include
using namespace std;
void* threadRoutine(void* s)
{
const char* str = (const char*)s;
cout << str << endl;
return (void*)1;
}
int main()
{
pthread_t p1;
pthread_create(&p1, nullptr, threadRoutine, (void*)"thread 1");
void *retval = nullptr;
int n = pthread_join(p1, &retval);
if (n != 0)
{
cerr << "errno: " << errno << strerror(errno) <<endl;
}
// 当前环境是64位的,int是32位的,如果强转为32位会报错。
cout << "wait successful!" << " retval: " << (int64_t)retval << endl;
return 0;
}
如果新线程不用被等待获取返回值,可以在主线程中分离该线程
#include
#include
#include
#include
using namespace std;
void* threadRoutine(void* s)
{
const char* str = (const char*)s;
cout << str << endl;
}
int main()
{
pthread_t p1;
pthread_create(&p1, nullptr, threadRoutine, (void*)"thread 1");
pthread_detach(p1);
sleep(3);
return 0;
}
线程退出有三种方式:
PTHREAD_CANCELED(-1)
#include
#include
#include
#include
using namespace std;
void* threadRoutine(void* s)
{
const char* str = (const char*)s;
cout << str << endl;
}
int main()
{
pthread_t p1;
pthread_create(&p1, nullptr, threadRoutine, (void*)"thread 1");
pthread_detach(p1);
cout << pthread_self() << endl;
sleep(3);
return 0;
}
pthread库是共享库,也会被映射到进程地址空间中的共享区。在pthread库中创建的所有线程会被组织成数组。而线程id就是数组中线程的地址。LWP是轻量级进程使用的,被封装进struct pthread
字段中。这种结构类似文件,struct FILE
中封装了文件描述符fd。
__thread
,然后每个线程都会在自己的局部存储里面创建这个全局变量。其生命周期是全局的。#include
#include
#include
#include
using namespace std;
__thread int _gval = 0;
void* threadRoutine(void* s)
{
const char* str = (const char*)s;
cout << str << "_gval: " << _gval << " &_gval: " << &_gval << endl;
}
void* threadRoutine1(void* s)
{
sleep(3);
const char* str = (const char*)s;
cout << str << "_gval: " << _gval << " &_gval: " << &_gval << endl;
}
int main()
{
pthread_t p1, p2;
pthread_create(&p1, nullptr, threadRoutine, (void*)"thread 1");
pthread_create(&p2, nullptr, threadRoutine1, (void*)"thread 2");
sleep(4);
return 0;
}
在多线程的场景下,不同线程访问临界资源会引发线程安全的问题,例如:对临界资源做自增操作。
#include
#include
#include
#include
int _gval = 0;
void* threadRoutine(void* s)
{
int cnt = 0;
while (cnt < 100)
{
cnt++;
}
}
int main()
{
pthread_t p1, p2;
pthread_create(&p1, nullptr, threadRoutine, nullptr);
pthread_create(&p2, nullptr, threadRoutine1, nullptr);
sleep(4);
return 0;
}
如果一个线程执行到第二步的时候时间片到了,切换为另一个线程,然后再切换回第一个线程,那么第二个线程对cnt的操作就会被覆盖。如果要保证对cnt的操作是线程安全的,即没有并发访问问题,就需要对该临界区加锁,让临界区任何时刻只能有一个线程进入。
锁的接口定义在pthread.h头文件中。
pthread_mutex_t mutex;
如果定义的锁是全局变量,可以使用PTHREAD_MUTEX_INITIALIZER初始化,不需要调用
pthread_mutex_init() 和 pthread_mutex_destroy()
#include
#include
#include
#include
int _gval = 0;
// 写法一;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* threadRoutine(void* s)
{
int cnt = 0;
pthread_mutex_lock(&mutex);
while (cnt < 100)
{
cnt++;
pthread_mutex_unlock(&mutex);
}
}
int main()
{
pthread_t p1, p2;
pthread_create(&p1, nullptr, threadRoutine, nullptr);
pthread_create(&p2, nullptr, threadRoutine1, nullptr);
sleep(4);
return 0;
}
// 写法二:
void* threadRoutine(void* s)
{
pthread_mutex_t* mutex = (pthread_mutex_t*)s;
int cnt = 0;
pthread_mutex_lock(mutex);
while (cnt < 100)
{
cnt++;
pthread_mutex_unlock(mutex);
}
}
int main()
{
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, nullptr);
pthread_t p1, p2;
pthread_create(&p1, nullptr, threadRoutine, (void*)&mutex);
pthread_create(&p2, nullptr, threadRoutine1, (void*)&mutex);
pthread_mutex_destroy(&mutex);
sleep(4);
return 0;
}
锁也是共享资源,访问共享资源就会有线程安全问题。为了防止这种套娃情况的发生,加锁和解锁操作具有原子性,大多数体系结构都提供了swap或exchange指令,这两条指令都保证了加锁的原子性。
加锁: 伪代码
lock:
movb $0, %al
xchgb %al, mutex ;这条指令是核心,将锁转移到当前线程的上下文中,这也就相当于线程取得了锁资源
if al > 0 then return 0
else 挂起等待
goto lock
解锁:
movb $1 ,mutex
唤醒等待mutex的线程
return 0;
在上面代码中,1不会增多,只会在不同线程之间流转,从而保证了多个线程只有一个锁资源
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
**四个条件之一:**
预防死锁:破坏四个条件之一
如果一个线程释放锁之后又立即申请锁,那么可能会导致其他线程一直申请不到锁,这样就会形成饥饿问题。这样明显是不合理的,我们可以使用条件变量来防止发生饥饿问题。一般条件变量是配合互斥锁使用的。条件变量的接口定义在pthread.h头文件中。
pthread_cond_t
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 POSIX可以用于线程间同步。信号量是资源的一种预定机制,将共享资源看作多份。信号量相关的接口在semaphore.h头文件中。
sem_t
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
如果我们想要维护好生产者消费者模型,就要研究3种关系,两种角色,一个缓冲区
生产者消费者模型的优点:
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)。
生产者可以生产一系列任务让消费者执行任务。下面是模拟的一个计算器任务。
#pragma once
#include
#include
#include
#include
class Task
{
public:
Task() = default;
Task(int x, int y, char op) : _x(x), _y(y), _op(1, op)
{ }
void run()
{
std::unordered_map<std::string, std::function<void(void)>> cal = {
{"+", [this]
{ this->_result = this->_x + this->_y; }},
{"-", [this]
{ this->_result = this->_x - this->_y; }},
{"*", [this]
{ this->_result = this->_x * this->_y; }},
{"/", [this]
{ if (this->_y == 0) this->_exitStatus = 2; this->_result = this->_x/this->_y; }},
{"%", [this]
{ if (this->_y == 0) this->_exitStatus = 1; this->_x%this->_y; }}};
cal[_op]();
}
void formatExpress()
{
std::cout << "productor : " << _x << " " << _op << " " << _y << " = ?" << std::endl;
}
void formatRes()
{
std::cout << "consumer : " << _x << " " << _op << " " << _y << " = " << _result << std::endl;
}
int getExitCode()
{
return _exitStatus;
}
int getRes()
{
return _result;
}
private:
int _x;
int _y;
std::string _op;
int _result;
int _exitStatus;
};
#pragma once
#include
#include
#include
#include
#include
#include
using std::cout;
using std::endl;
const int defaultCapacity = 5;
template <class T>
class BlockQueue
{
public:
BlockQueue(int cap = defaultCapacity)
: _cap(cap)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_consumerCond, nullptr);
pthread_cond_init(&_productorCond, nullptr);
}
bool isFull()
{
return _bq.size() == _cap;
}
void push(const T &in)
{
// 生产者生产数据
pthread_mutex_lock(&_mutex);
// 如果缓冲区满,则生产者阻塞
while (isFull())
pthread_cond_wait(&_productorCond, &_mutex);
_bq.push(in);
/// 可以采用一定策略唤醒消费者消费资源
pthread_cond_signal(&_consumerCond);
pthread_mutex_unlock(&_mutex);
}
bool isempty()
{
return _bq.empty();
}
void pop(T* out)
{
pthread_mutex_lock(&_mutex);
while (isempty())
pthread_cond_wait(&_consumerCond, &_mutex);
*out = _bq.front();
_bq.pop();
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_productorCond);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_consumerCond);
pthread_cond_destroy(&_productorCond);
}
private:
// 缓冲区
std::queue<T> _bq;
// 缓冲区容量
int _cap;
// 互斥锁
pthread_mutex_t _mutex;
// 条件变量:当缓冲区满的时候,生产者阻塞,当缓冲区空的时候,消费者阻塞
pthread_cond_t _consumerCond;
pthread_cond_t _productorCond;
};
#include "bQueue.hpp"
#include "Task.hpp"
#include
void *consume(void *args)
{
BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);
while (true)
{
// 从缓存区拿资源
Task t;
bq->pop(&t);
// 使用资源
t.run();
t.formatRes();
sleep(1);
}
return nullptr;
}
void *product(void *args)
{
BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);
const char* s = "+-*/%";
while (true)
{
sleep(1);
// 生产资源
int x = rand()%10;
int y = rand()%10;
char op = s[rand()%strlen(s)];
Task t(x, y, op);
t.formatExpress();
// 将资源放到缓冲区中
bq->push(t);
}
return nullptr;
}
int main()
{
srand(time(nullptr));
int n = 5;
BlockQueue<Task> *bq = new BlockQueue<Task>(n);
std::vector<pthread_t> consumers(3);
std::vector<pthread_t> productors(4);
for (auto &pth : consumers)
{
pthread_create(&pth, nullptr, consume, static_cast<void *>(bq));
}
for (auto &pth : productors)
{
pthread_create(&pth, nullptr, product, static_cast<void *>(bq));
}
for (auto tid : consumers)
pthread_join(tid, nullptr);
for (auto tid : productors)
pthread_join(tid, nullptr);
return 0;
}
下面实现一种基于环形队列的生产者消费者模型,采用数组模型环形队列。
在阻塞队列中,我们将缓冲区看作一个整体使用,因此也只定义了一个互斥锁就可以保证线程安全。但在环形队列中,我们将缓冲区看成n个小空间使用,因此使用信号量,只要消费者和生产者不指向同一个小空间,它们就可以并发执行。
消费者关心的是数据个数,生产者关心的是剩余空间个数,因此我们可以定义两个信号量,当剩余空间为0时,阻塞生产者;当数据个数为0时,阻塞消费者。
生产者与生产者之间互斥关系要靠一个互斥锁,消费者与消费者互斥关系也要靠一个互斥锁,故需要定义两个互斥锁。不能设置成一个互斥锁,因为生产者与消费者之间除非所占空间相同,不然没有互斥关系。
#pragma once
#include
#include
#include
#include
#include
#include
#include
using std::cout;
using std::endl;
const int N = 5;
template <class T>
class RingQueue
{
public:
RingQueue(int cap = N) : _cap(cap), _consumerIndex(0), _productorIndex(0), _vRingQueue(cap)
{
pthread_mutex_init(&_consumerMutex, nullptr);
pthread_mutex_init(&_productorMutex, nullptr);
sem_init(&_spaceSem, 0, _cap);
sem_init(& _dataSem, 0, 0);
}
void push(const T &in)
{
// 生产者生产数据
P(_spaceSem);
lock(_productorMutex);
_vRingQueue[_productorIndex++] = in;
_productorIndex %= _cap;
unlock(_productorMutex);
V(_dataSem);
}
void pop(T *out)
{
P(_dataSem);
lock(_consumerMutex);
*out = _vRingQueue[_consumerIndex++];
_consumerIndex %= _cap;
unlock(_consumerMutex);
V(_spaceSem);
}
~RingQueue()
{
pthread_mutex_destroy(&_consumerMutex);
pthread_mutex_destroy(&_productorMutex);
sem_destroy(&_spaceSem);
sem_destroy(&_dataSem);
}
private:
void lock(pthread_mutex_t &mutex)
{
pthread_mutex_lock(&mutex);
}
void unlock(pthread_mutex_t &mutex)
{ pthread_mutex_unlock(&mutex);}
void P(sem_t &sem){ sem_wait(&sem);}
void V(sem_t &sem){sem_post(&sem);}
private:
std::vector<T> _vRingQueue;
int _cap; // 环形队列容量
sem_t _spaceSem; // 生产者关系的是空间资源
sem_t _dataSem; // 消费者关心的是数据资源
pthread_mutex_t _consumerMutex;
pthread_mutex_t _productorMutex;
// 消费者访问空间
int _consumerIndex;
// 生产者访问空间
int _productorIndex;
};