文章目录
互斥:对共享数据的任何访问,保证任何时候只有一个执行流访问
同步:按照一定的顺序性获取资源
pthread_mutex_init,初始化互斥量的
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
利用互斥锁实现简易的多线程抢票
#include<iostream>
#include<vector>
#include<unistd.h>
#include<cstring>
#include<pthread.h>
using namespace std ;
#define NUM 4
int tickets = 1000; // 票
class threadData
{
public:
threadData ( int number,pthread_mutex_t * mutex )
{
threadname = "thread-" + to_string(number);
lock =mutex ;
}
public:
string threadname ; //线程名
pthread_mutex_t * lock ;
} ;
void * getTicket (void *args )
{
threadData *td = (threadData * ) args ;
const char * name = td->threadname.c_str() ;
while(true )
{
//加锁
pthread_mutex_lock(td->lock) ;
//在加锁和解锁之间的代码是临界区
// 申请锁成功,才能往后执行,不成功,阻塞等待。
if(tickets > 0)
{
usleep(1000);
printf("who=%s, get a ticket: %d\n", name, tickets);
tickets --;
//解锁
pthread_mutex_unlock(td->lock) ;
}
else
{
//解锁
pthread_mutex_unlock(td->lock) ;
break;
}
}
printf("%s ... quit\n", name);
return nullptr ;
}
int main()
{
//互斥锁
pthread_mutex_t lock ;
pthread_mutex_init(&lock ,nullptr) ;
vector<pthread_t> tids;
vector<threadData *> thread_datas;
//模拟多线程抢票
for (int i = 1; i <= NUM; i++)
{
threadData * td = new threadData (i,&lock);
pthread_t tid ;
thread_datas.push_back(td);
pthread_create(&tid , nullptr ,getTicket , thread_datas[i-1]);
tids.push_back(tid) ;
}
//线程等待
for (auto thread : tids)
{
pthread_join(thread, nullptr);
}
for (auto td : thread_datas)
{
delete td;
}
pthread_mutex_destroy(&lock) ;
return 0 ;
}
利用互斥锁实现简易的多线程抢票 (全局互斥锁)
#include<iostream>
#include<vector>
#include<unistd.h>
#include<cstring>
#include<pthread.h>
using namespace std ;
#define NUM 4
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;//全局的锁,PTHREAD_MUTEX_INITIALIZER是初始化
int tickets = 1000; // 票
class threadData
{
public:
threadData ( int number)
{
threadname = "thread-" + to_string(number);
}
public:
string threadname ; //线程名
} ;
void * getTicket (void *args )
{
threadData *td = (threadData * ) args ;
const char * name = td->threadname.c_str() ;
while(true )
{
//加锁
pthread_mutex_lock(&lock) ;
//在加锁和解锁之间的代码是临界区
// 申请锁成功,才能往后执行,不成功,阻塞等待。
if(tickets > 0)
{
usleep(1000);
printf("who=%s, get a ticket: %d\n", name, tickets);
tickets --;
//解锁
pthread_mutex_unlock(&lock) ;
}
else
{
//解锁
pthread_mutex_unlock(&lock) ;
break;
}
usleep(13);//我们抢到了票,我们会立马抢下一张吗?其实多线程还要执行得到票之后的后续动作。usleep模拟
}
printf("%s ... quit\n", name);
return nullptr ;
}
int main()
{
vector<pthread_t> tids;
vector<threadData *> thread_datas;
//模拟多线程抢票
for (int i = 1; i <= NUM; i++)
{
threadData * td = new threadData (i);
pthread_t tid ;
thread_datas.push_back(td);
pthread_create(&tid , nullptr ,getTicket , thread_datas[i-1]);
tids.push_back(tid) ;
}
//线程等待
for (auto thread : tids)
{
pthread_join(thread, nullptr);
}
for (auto td : thread_datas)
{
delete td;
}
pthread_mutex_destroy(&lock) ;
return 0 ;
}
线程对于锁的竞争能力可能会不同
在临界区中,线程可以被切换吗?
可以切换, 在线程被切出去时,是持有锁被切走的。线程被切出去期间,其他线程不能进入临界区访问临界资源
对于其他线程来讲,一个线程要么没有锁,要么释放锁,当前线程访问临界区的过程,对于其他线程是原子的
在多线程编程中,“原子操作”(Atomic Operation)是指一个操作或者一系列操作,要么完全执行,要么完全不执行,不会出现中间状态。这意味着原子操作在执行过程中不会被其他线程中断或影响
互斥量实现原理
加锁:
线程把内存中的数据,交换到CPU的寄出器中,实际上就是线程把内存中的数据交换到线程的硬件上下文中,线程的硬件上下文是线程私有的
线程把内存中的数据,交换到CPU的寄出器中可以理解为,把一个共享的锁,让一个线程以一条汇编的方式,交换到线程的上下文中,交换到线程的上下文中就表示当前线程持有锁了
临界区内的线程可能进行线程切换吗
- 临界区内的线程完全可能进行线程切换,在这个线程被切出去的时候,这个线程是持有锁被切走的。线程被切出去期间,锁没有被释放,其他线程就无法申请到锁,所以其他线程不能进入临界区访问临界资源,其他想进入该临界区进行资源访问的线程,必须等该线程执行完临界区的代码并释放锁之后,才能申请锁,申请到锁之后才能进入临界区。
锁是否需要被保护?
- 锁需要被保护,通过申请锁的过程是原子的方式来保护锁
如何通过申请锁的过程是原子的方式来保护锁?
- 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用就是把寄存器和内存单元的数据相交换
lock和unlock的伪代码:
当线程申请锁:
AL 是 EAX 寄存器的低8位,假设mutex的初始值为1
- 1、先将al寄存器中的值清0。该动作可以被多个线程同时执行,因为每个线程都有自己的一组寄存器(上下文信息),执行该动作本质上是将自己的al寄存器清0
- 2 交换al寄存器和mutex中的值。xchgb是体系结构提供的交换指令,该指令可以完成寄存器和内存单元之间数据的交换。
- 3、判断al寄存器中的值是否大于0。若大于0则申请锁成功,此时就可以进入临界区访问对应的临界资源;否则申请锁失败需要被挂起等待,直到锁被释放后再次竞争申请锁
第一步:
交换完成后检测该线程的al寄存器中的值为1,则该线程申请锁成功,可以进入临界区对临界资源进行访问
第二步:
线程释放锁:
- 将内存中的mutex置回1。使得下一个申请锁的线程在执行交换指令后能够得到1
- 唤醒等待Mutex的线程。唤醒这些因为申请锁失败而被挂起的线程,让它们继续竞争申请锁
解锁:
锁的封装
可重入VS线程安全
线程安全: 多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现线程安全问题。
重入: 同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则是不可重入函数。
常见的线程不安全的情况:
1、不保护共享变量的函数。
2、函数状态随着被调用,状态发生变化的函数。
3、返回指向静态变量指针的函数。
4、调用线程不安全函数的函数
常见的线程安全的情况:
1、每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。
2、类或者接口对于线程来说都是原子操作。
3、多个线程之间的切换不会导致该接口的执行结果存在二义性
常见的不可重入的情况
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的。
- 调用了标准I/O库函数,标准I/O可以的很多实现都是以不可重入的方式使用全局数据结构。
- 可重入函数体内使用了静态的数据结构。
常见的可重入的情况
- 不使用全局变量或静态变量。
- 不使用malloc或者new开辟出的空间。
- 不调用不可重入函数。
- 不返回静态或全局数据,所有数据都由函数的调用者提供。
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
可重入与线程安全联系
- 函数是可重入的,那就是线程安全的。
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题。
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别 - 可重入函数是线程安全函数的一种。
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
- 如果对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数的锁还未释放则会产生死锁,因此是不可重入的
死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态
同步
同步: 在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,这就叫做同步。
竞态条件: 因为时序问题,而导致程序异常,我们称之为竞态条件
条件变量
条件变量是利用线程间共享的全局变量进行同步的一种机制,条件变量是用来描述某种资源是否就绪的一种数据化描述
条件变量主要包括两个动作
- 一个线程等待条件变量的条件成立而被挂起
- 另一个线程使条件成立后唤醒等待的线程
条件变量必须依赖于锁的使用
pthread_cond_wait
线程在等待时是持有锁的,在调用的时候,自动释放锁,如果线程因为唤醒而返回的时候,此时线程重新持有锁
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
参数说明:
- cond:需要等待的条件变量。
- mutex:当前线程所处临界区对应的互斥锁
return val :
- 函数调用成功返回0,失败返回错误码。
如何理解pthread_cond_wait需要互斥量
- 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
- 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化,所以一定要用互斥锁来保护,没有互斥锁就无法安全的获取和修改共享数据。
- 当线程进入临界区时需要先加锁,然后判断内部资源的情况,若不满足当前线程的执行条件,则需要在该条件变量下进行等待,但此时该线程是拿着锁被挂起的,也就意味着这个锁再也不会被释放了,此时就会发生死锁问题。
- 所以在调用pthread_cond_wait函数时,还需要将对应的互斥锁传入此时当线程因为某些条件不满足需要在该条件变量下进行等待时,就会自动释放该互斥锁
- 当该线程被唤醒时,该线程会接着执行临界区内的代码,此时便要求该线程必须立马获得对应的互斥锁,因此当某一个线程被唤醒时,实际会自动获得对应的互斥锁
总结:
- 等待的时候往往是在临界区内等待的,当该线程进入等待的时候,互斥锁会自动释放,而当该线程被唤醒时,又会自动获得对应的互斥锁。
- 条件变量需要配合互斥锁使用,其中条件变量是用来完成同步的,而互斥锁是用来完成互斥的。
- pthread_cond_wait函数有两个功能,一就是让线程在特定的条件变量下等待,二就是让线程释放对应的互斥锁
pthread_cond_broadcast &&pthread_cond_signal
唤醒等待的函数有以下两个
pthread_cond_signal,唤醒在cond的等待队列中等待的一个线程,默认都是第一个
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
- cond:唤醒在cond条件变量下等待的线程
return val :
- 函数调用成功返回0,失败返回错误码
示例: 创建一批线程 , 并唤醒线程
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#define NUM 5
using namespace std ;
int cnt = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER ; //定义并初始化互斥锁
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//初始化条件变量
void * Count (void *args)
{
pthread_detach(pthread_self()) ;
uint64_t number= (uint64_t)args;
std::cout << "pthread: " << number << " create success" << std::endl;
while(true)
{
//cout << "pthread: "<<pthread_self()<< "pthread: " << number<< endl;
pthread_mutex_lock (&mutex);//加锁
pthread_cond_wait(&cond ,&mutex) ;//pthread_cond_wait让线程等待的时候,会自动释放锁
cout << "pthread: " << number << " , cnt: " << cnt++ << endl;
pthread_mutex_unlock (&mutex);//解锁
}
}
int main()
{
for(uint64_t i = 0 ; i < NUM ; i++)
{
pthread_t tid ;
// uint64_t是unsigned long int ,在64位下是8个字节, void* 在64位下也是8字节,这样(void *)i就不会出现告警
//必须是(void *)i ,不能是(void *)&i,如果是(void *)&i,主线程和新线程就会访问同一个i ,不方便多线程的观察
//(void *)i ,传参是拷贝式的传参 ,这样主线程和新线程就不会共用同一个i ,不会互相影响,以便后续观察主线程和新线程
pthread_create(&tid , nullptr , Count , (void *) i ) ;
usleep(1000);
}
sleep(3); //让新线程执行完对应的代码
std::cout << "main thread ctrl begin: " << std::endl;
while(true)
{
sleep(1) ;
pthread_cond_signal(&cond); //唤醒在cond的等待队列中等待的一个线程,默认都是第一个
std::cout << "signal one thread..." << std::endl;
}
return 0 ;
}
如果我们想每次唤醒都将在该条件变量下等待的所有线程进行唤醒,可以将代码中的pthread_cond_signal函数改为pthread_cond_broadcast函数。
pthread_cond_init
pthread_cond_init,初始化条件变量
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
参数说明:
- cond:需要初始化的条件变量。
- attr:初始化条件变量的属性,一般设置为NULL即可
return val :
- 条件变量初始化成功返回0,失败返回错误码。
调用pthread_cond_init函数初始化条件变量叫做动态分配,我们还可以用静态分配初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_cond_destroy
pthread_cond_destroy,销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
参数说明:
- cond:需要销毁的条件变量。
return val:
- 条件变量销毁成功返回0,失败返回错误码。
注意:使用PTHREAD_COND_INITIALIZER初始化的条件变量不需要销毁
条件变量使用规范
线程等待在加锁和解锁之间
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(&cond, &mutex);
修改条件
pthread_mutex_unlock(&mutex);