pthread_create创建一个线程,产生一个线程ID存放在第一个参数之中,该线程ID与内核中的LWP并不是一回事。pthread_create函数第一个参数指向一块虚拟内存单元,该内存单元的地址就是新创建线程ID,这个ID是线程库的范畴,而内核中LWP是进程调度的范畴,轻量级进程是OS调度的最小单位,需要一个数值来表示该唯一线程。
Linux并不提供真正的线程,只提供了LWP,但是程序员用户不管LWP,只要线程。所以OS在OS与应用程序之间设计了一个原生线程库,pthread库,系统保存LWP,原生线程库可能存在多个线程,别人可以同时在用。OS只需要对内核执行流LWP进行管理,而提供用户使用的线程接口等其他数据则需要线程库自己来管理。所以线程库需要对线程管理“先描述,在组织”。
线程库实际上就是一个动态库:
进程运行时动态库加载到内存,然后通过页表映射到进程地址空间的共享区,这时候进程的所有线程都是能看到这个动态库的:
每个线程都有自己独立的栈
:主线程采用的栈是进程地址空间中原生的栈,其他线程采用的是共享区中的栈,每个线程都有自己的struct pthread,包含了对应线程的属性,每个线程也有自己的线程局部存储(添加__thread可以将一个内置类型设置为线程局部存储),包含对应的线程被切换时的上下文。每一个新的线程在共享区都有一块区域对其描述,所以我们要找到一个用户级线程只需要找到该线程内存块的起始地址就可以获取到该线程的信息了:
线程函数起始是在库内部对线程属性进行操作,最后将要执行的代码交给对应的内核级LWP去执行。所以线程数据的管理本质在共享区。
线程ID本质就是进程地址空间共享区上的一个虚拟地址:
void* start_routine(void*args)
{
while(true)
{
printf("new thread tid:%p\n",pthread_self());
sleep(2);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,start_routine,nullptr);
while(true)
{
printf("main thread tid:%p\n",pthread_self());
}
return 0;
}
给一个全局变量g_val,让一个线程进行++,其他线程会受到影响:
#include
#include
#include
using namespace std;
int g_val = 100;
void* start_routine(void*args)
{
string name = static_cast(args);
while(true)
{
cout<
此时在给全局变量g_val加上__thread:
从输出结果上看,g_val此时就不再是共享了,每个线程独有。添加__thread
可以将一个内置类型设置为线程局部存储。每个线程都有一份,介于全局变量和局部变量之间线程特有属性。
我们如果想要跟C++一样使用,创建使用线程时,直接构造对象设置回调函数,对线程原生接口可以进行简单的封装:
#include
#include
#include
#include
#include
class Thread;
//上下文
class Context
{
public:
Thread *this_;
void *args_;
public:
Context():this_(nullptr),args_(nullptr)
{}
~Context()
{}
};
class Thread
{
public:
typedef std::function func_t;
const int num = 1024;
public:
Thread(func_t func,void*args,int number):func_(func),args_(args)
{
char buffer[num];
snprintf(buffer,sizeof(buffer),"thread-%d",number);
name_=buffer;
Context*ctx = new Context();
ctx->this_ = this;
ctx->args_=args_;
int n = pthread_create(&tid_,nullptr,start_routine,ctx);
assert(n==0);
(void)n;
}
static void*start_routine(void*args)
{
Context*ctx = static_cast(args);
void*ret = ctx->this_->run(ctx->args_);
delete ctx;
return ret;
}
// void start()
// {
// Context*ctx = new Context();
// ctx->this_ = this;
// ctx->args_=args_;
// int n = pthread_create(&tid_,nullptr,start_routine,ctx);
// assert(n==0);
// (void)n;
// }
void join()
{
int n = pthread_join(tid_,nullptr);
assert(n==0);
(void)n;
}
void*run(void*args)
{
return func_(args);
}
~Thread()
{}
private:
std::string name_;
pthread_t tid_;
func_t func_;
void* args_;
};
main.cc
void* thread_run(void* args)
{
std::string name = static_cast(args);
while(true)
{
cout << name << endl;
sleep(1);
}
}
int main()
{
std::unique_ptr thread1(new Thread(thread_run,(void*)"hellothread",1));
std::unique_ptr thread2(new Thread(thread_run,(void*)"COUTthread",2));
std::unique_ptr thread3(new Thread(thread_run,(void*)"PRINTthread",3));
//thread1->start();
//thread2->start();
//thread3->start();
thread1->join();
thread2->join();
thread3->join();
return 0;
}
全局变量g_val可以被多个线程同时访问,可以被多个线程访问是共享资源,多个线程对其进行操作,可能会出现问题:
下面模拟抢票的过程,多个线程对共享资源tickets做–的过程:
#include "Thread.hpp"
using std::cout;
using std::endl;
//共享资源
int tickets = 1000;
void* get_ticket(void* args)
{
std::string name = static_cast(args);
while(true)
{
if(tickets>0)
{
usleep(1234);
cout< thread1(new Thread(get_ticket,(void*)"hellothread",1));
std::unique_ptr thread2(new Thread(get_ticket,(void*)"COUTthread",2));
std::unique_ptr thread3(new Thread(get_ticket,(void*)"PRINTthread",3));
std::unique_ptr thread4(new Thread(get_ticket,(void*)"TESTthread",4));
thread1->join();
thread2->join();
thread3->join();
thread4->join();
return 0;
}
此时结果出现了负数,在现实生活中,抢票怎么可能出现负数
结果出现负数:
如果需要出现负数的现象:尽可能让多个线程交叉执行,多个线程交叉执行的本质:让调度器尽可能的频繁发生线程调度与切换
线程一般什么时候发生线程切换:
时间片到了或者来了更高优先级的线程或者线程等待的时候
。线程什么时候检测上面的问题:从内核态返回用户态的时候,线程要对调度状态进行检测,如果可以就直接发生线程切换
出现负数的票就是因为多个线程交叉执行,多个线程交叉执行的本质:让调度器尽可能频繁的发生线程的调度与切换
在tickets==1时,所有进程都可以进去,然后在判断:1.读取内存数据cpu内的寄存器中2.进行判断;第一个线程判断是大于0的,此时线程会被切走,寄存器只有一个,寄存器的内容是当前执行流的上下文,会把上下文带走,还没来得及tickets–,其他线程看到的tickets也是1,也要保存自己的上下文…ticket减减前线程都会休眠一会,当一个线程唤醒tickets–
--的本质就是1.读取数据2.更改数据3.写回数据
对一个全局变量进行多线程更改是不安全的:
对变量进行++或者–,在C、C++上看起来只有一条语句,但是汇编之后至少是三条语句:
1.从内存读取数据到CPU寄存器中2.在寄存器中让CPU进行对应的算逻运算3.写回新的结果到内存中变量的位置
现在线程1把数据加载到寄存器中,做–,成为999,到第三步的时候写回到内存的时候很不幸被切走了,把上下文顺便也卷走了:
此时调度线程2,线程2很开心,一直在–,到1tickets变为100的时候,内存中变量的也变为了100,但是当它想继续–的时候,线程2倍切走了,带着自己的上下文走了,现在线程1回来了:恢复上下文,继续之前的第三步,此时线程2好不容易把tickets变为100,但是被线程1改为了999
又变成了999,造成了干扰
由此可知我们定义的全局变量在没有保护的时候,往往是不安全的,像上面的例子,多个线程交替执行时造成数据安全问题,发生了数据不一致问题
。
而解决这种问题的办法就是加锁!
临界资源:多个执行流进行安全访问的共享资源就叫临界资源
临界区:多个执行流进行访问临界资源的代码就是临界区
互斥: 任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
原子性: 不会被任何调度机制打断的操作,该操作只有两态,要么不做,要么做完,这就是原子性。
现在先简单理解原子性:一个资源进行的操作如果只用一条汇编语句就能完成,就是原子性的,反之不是原子的。
对变量++或者–。在C、C++上,看起来只有一条语句,但是汇编之后至少是三条语句:
1.从内存读取数据到CPU寄存器中
2.在寄存器中让CPU进行对应的算逻运算
3.写回新的结果到内存中变量的位置
对一个资源访问的时候,要么不做,要么做完,不是原子性的情况:线程A被切换,没做完,有中间状态,不是原子性。
实际上对变量做–的时候,对应三条汇编语句,未来会对应三条汇编语句!所以很明显,++、–不是原子性的,不是一条语句。
单纯的++或者++都不是原子的,有可能会有数据一致性的问题。
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互
多个线程并发的操作共享变量,会带来问题:数据不一致问题
要解决线程不安全的情况,保护共享资源:
代码必须有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
如果多个线程同时要求执行临界区的代码,并且此时临界区没有线程在执行,那么只能允许一个线程进入该临界区。
如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
实际上就是需要一把锁,Linux提供的这把锁就叫互斥量,如果一个线程持有锁,那么其他的线程就无法进来访问了。
常见的相关接口:
#include
// 初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
// 销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 全局
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//成功返回0,失败返回错误码
即可以定义成局部的,也可以定义成全局的。
pthread_mutex_t是锁的类型,如果我们定义的锁是全局的,就不要用pthread_mutex_int和pthread_mutex_destroy初始化和销毁了。
#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);
// 成功返回0,失败返回错误码
使用全局锁+4个线程的代码:
定义全局锁并初始化PTHREAD_MUTEX_INITIALIZER,同时用pthread_create创建4个线程进行测试,由于此时锁是全局的,我们不需要把锁传给每个线程:
#include
#include
#include
#include
#include
using std::cout;
using std::endl;
#include
#include
#include
#include
#include
using std::cout;
using std::endl;
int tickets = 1000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void* get_ticket(void* args)
{
std::string username = static_cast(args);
while(true)
{
pthread_mutex_lock(&lock);
if(tickets>0)
{
usleep(11111);
cout<
局部锁+for循环创建线程的代码:
此时的锁是局部的,为了把锁传递给每个线程,我们可以定义一个结构体ThreadData,存放着线程名与锁:
#include
#include
#include
#include
#include
using std::cout;
using std::endl;
int tickets = 1000;
class ThreadData
{
public:
ThreadData(const std::string&threadname,pthread_mutex_t *mutex_p)
:threadname_(threadname),mutex_p_(mutex_p)
{}
~ThreadData(){}
public:
std::string threadname_;
pthread_mutex_t *mutex_p_;
};
void* get_ticket(void* args)
{
ThreadData*td = static_cast(args);
while(true)
{
pthread_mutex_lock(td->mutex_p_);
if(tickets>0)
{
usleep(11111);
cout<threadname_<<"正在抢票 : "<mutex_p_);
}
else
{
pthread_mutex_unlock(td->mutex_p_);
break;//注意这里有break
}
}
return nullptr;
}
int main()
{
#define NUM 4
pthread_mutex_t lock;
pthread_mutex_init(&lock,nullptr);
std::vector tids(NUM);
for(int i =0;i
此时的运行结果每次都是能够减到1,但是运行的速度也变慢了。这是因为加锁和加锁的过程是多个线程串行执行的,程序变慢了
同时这里看到每次都是只有一个线程在抢票,这是因为锁只规定互斥访问,并没有规定谁来优先执行所以谁的竞争力强就谁来持有锁。
要想解决这个问题:想想抢完票就结束了吗?实际的生活当中,抢完票后还有一些工作需要完成:比如发送订单
至此解决抢票的问题。
锁本身就是一个共享资源!全局的变量是要被保护的,锁是用来保护全局的资源的,锁本身也是全局资源,锁的安全谁来保护?
pthread_mutex_lock、pthread_mutex_unlock:加锁和解锁的过程必须是安全的!加锁的过程其实是原子的
谁持有锁,谁就进入临界区!
如果申请成功,就继续向后执行,如果申请暂时没有成功,执行流会如何:第一次加锁,然后在加一次锁:结果会怎么样:
此时运行,程序不在执行,执行流会阻塞!
pthread_mutex_trylock:尝试去加锁,如果加锁成功就持有锁,加锁不成功立马出错返回
一般把这种锁称为挂起等待锁:如果申请暂时没有成功,执行流会阻塞,等待锁成功释放!
如果线程1,申请锁成功,进入临界资源,正在访问临界资源期间,其他线程在做:阻塞等待
如果线程1,申请锁成功,进入临界资源,正在访问临界资源期间,我可以被切换!!绝对可以
当持有锁的线程被切走的时候,是抱着锁被切走的,即便自己被切走了,其他线程依旧无法申请锁成功,也便无法先后执行!直到我最终释放这个锁!
所以,对于其他线程而言,有意义的锁的状态,无非两种:1.申请锁前2.释放锁后
站在其他线程的角度,看待当前线程持有锁的过程就是原子的
结论
**未来我们使用锁的时候:一定要尽量保证临界区的粒度要非常小(粒度:锁中间保护代码的多少)**注意:加锁是程序员行为,必须要做到要加就都要加!(公共资源,要么加锁,要么不加锁,这是程序员行为,不要写BUG)!
理加锁和解锁的本质:加锁的过程是原子的!加锁了,未来解锁的一定是一个
执行流。
单纯的i++,++i都不是原子的,会导致数据不一致问题
从汇编谈加锁:为了实现互斥锁操作,大多数体系结构提供了swap和exchange指令,作用是把寄存器和内存单元的数据直接做交换,由于只用一条指令,就可以保证原子性
加锁
这里的xchgb
指令可以把cpu中的数据和内存中的数据直接交换:
解锁
:过程很简单,把寄存器的内容1移动到内存中,直接return,解锁完成
如果我们想简单的使用,该如何进行封装设计 ——做一个简单设计,传入一个锁自动帮我们加锁和解锁,RAII风格加锁
Mutex.hpp
//Mutex.hpp
#pragma once
#include
#include
class Mutex
{
public:
Mutex(pthread_mutex_t *lock_p=nullptr):lock_p_(lock_p)
{}
void lock()
{
if(lock_p_) pthread_mutex_lock(lock_p_);
}
void unlock()
{
if(lock_p_) pthread_mutex_unlock(lock_p_);
}
~Mutex()
{
}
private:
pthread_mutex_t *lock_p_;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t *mutex):mutex_(mutex)
{
mutex_.lock();//在构造函数中加锁
}
~LockGuard()
{
mutex_.unlock();//在析构函数中解锁
}
private:
Mutex mutex_;
};
main.cc
using std::cout;
using std::endl;
int tickets = 1000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *get_ticket(void *args)
{
std::string username = static_cast(args);
while (true)
{
{//代码块,不给usleep加锁
LockGuard lockguard(&lock);
if (tickets > 0)
{
usleep(1111);
cout << username << "正在抢票 : " << tickets << endl;
tickets--;
}
else
{
break;
}
}
usleep(1000);
}
return nullptr;
}
int main()
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, nullptr, get_ticket, (void *)"thread 1");
pthread_create(&t2, nullptr, get_ticket, (void *)"thread 2");
pthread_create(&t3, nullptr, get_ticket, (void *)"thread 3");
pthread_create(&t4, nullptr, get_ticket, (void *)"thread 4");
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
pthread_join(t4, nullptr);
pthread_join(t4, nullptr);
return 0;
}
可重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流在次进入,我们称为重入。
一个函数在重入的情况的下,运行结果不会出现任何不同回或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数
线程安全:多个线程并发同一段代码时,不会出现不同的结果,常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题;线程不安全:如抢票
线程安全不一定是可重入的,而可重入函数则一定是线程安全的
如果对临界资源的访问加上锁,则这个函数是线程安全的,但是如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的
常见的线程安全的情况:
每个线程对全局变量或静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性
常见不可重入的情况
调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
一组执行流(不管进程还是线程)持有自己锁资源的同时,还想要申请对方的锁,锁是不可抢占的(除非自己主动归还),会导致多个执行流互相等待对方的资源,而导致代码无法推进。这就是死锁
ps:一把锁可以造成死锁,在抢票的时候我们就写过,在加一把锁导致死锁。
推导链:为什么会有死锁:一定是你用了锁——锁保证临界资源的安全,多线程访问我们可能出现数据不一致的问题——多线程、全局资源——多线程大部分资源(全局的)是共享的——多线程的特性,解决问题的同时带来了新的问题:死锁,任何技术都有自己的边界,在解决问题的同时一定可能会引入新的问题
死锁四个必要条件:
1.互斥:一个共享资源每次被一个执行流使用
2.请求与保持:一个执行流因请求资源而阻塞,对已有资源保持不放
3.不剥夺:一个执行流获得的资源在未使用完之前,不能强行剥夺
4.环路等待条件:执行流间形成环路问题,循环等待资源
避免死锁,1.破坏死锁的四个必要条件2.加锁顺序一致3.避免锁未释放的场景4.资源一次性分配
避免死锁算法(了解):死锁检测算法、银行家算法
引入一些情景:自习室VIP,先到先得,上厕所时反锁,别人进不去,离资源近竞争力强,一直是你自己,重复放钥匙拿钥匙,造成其他人饥饿状态;再比如抢票系统我们看到一个线程一直连续抢票,造成了其他线程的饥饿,为了解决这个问题:我们在数据安全的情况下让这些线程按照一定的顺序进行访问,这就是线程同步
饥饿状态
:得不到锁资源而无法访问公共资源的线程处于饥饿状态。但是并没有错,但是不合理
竞态条件
:因为时序问题,而导致程序异常,我们称为竞态条件。
线程同步
:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
当一个线程互斥地访问某个变量时,它可能发现在其他线程改变状态之前,它什么也做不了
例如一个线程访问队列时,发现队列为空,它只能等待,直到其他线程将一个节点添加到队列中。这种情况就需要用到条件变量
条件变量通常需要配合互斥锁一起使用。
条件变量的使用:一个线程等待条件变量的条件成立而被挂起;另一个线程使条件成立后唤醒等待的线程。
#include
//初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//销毁
int pthread_cond_destroy(pthread_cond_t *cond);
#include
//特定时间阻塞等待
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
//等待
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
#include
// 唤醒一批线程
int pthread_cond_broadcast(pthread_cond_t *cond);
// 唤醒一个线程
int pthread_cond_signal(pthread_cond_t *cond);
举个例子:公司进行招聘:应聘者要面试,大家不能同时进入房间进行面试,但是没有由于没有组织,上一个人面试完之后,面试官打开门准备面试下一个,一群人在外面等待面试,但是有人抢不过别人,人太多了,面试官记不住谁面试过了,所以有可能一个人面试完之后又去面试了,造成其他人饥饿问题,这时候效率很低
后来hr重新进行管理:设立一个等待区,先排队去等待区进行等待面试,现在每个人都进行排队,都有机会面试了,而这个等待区就是条件变量,如果一个人想面试,先得去排队等待区等待,未来所有应聘者都要去条件变量等
条件不满足的时候,线程必须去某些定义好的条件变量上进行等待
。
条件变量(struct cond,结构体)里面包含状态,队列,而我们定义好的条件变量包含一个队列,不满足条件的线程就链接在这个队列上进行等待。
通过条件变量来控制线程的执行
条件变量本身不具备互斥的功能
,所以条件变量必须配合互斥锁使用:
创建2个线程,通过条件变量一秒唤醒一个线程(或者全部唤醒):
int tickets = 1000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void* start_routine(void* args)
{
string name = static_cast(args);
while(true)
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond,&mutex);
//判断省略
cout< "<
主线程一个一个去叫,按照一定的顺序输出打印。
int tickets = 1000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void* start_routine(void* args)
{
string name = static_cast(args);
while(true)
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond,&mutex);
//判断省略
cout< "<