线程库会被加载到进程地址空间中(共享区),tid为线程对象的起始地址。
多线程情况下测试局部变量test_i
#define NUM 5
struct threadData
{
string threadname;
};
string toHex(pthread_t tid)
{
char buffer[128];
snprintf(buffer,sizeof(buffer),"0x%x",tid);
return buffer;
}
void* threadRoutine(void* args)
{
threadData* td = static_cast(args);
int cnt=10;
int test_i=0;
while(cnt--)
{
cout<<"pid:"<threadname<threadname = "thread-" + to_string(number);
}
int main()
{
vector tids;
for(int i=0;i
每个线程的test_i都是独立的,有自己的地址,是在线程各自的栈空间上开辟的。
堆空间是共享的,每个线程分配一块。
int *p = nullptr;定义一个全局的p变量
主线程中可以获取子线程的栈区上的局部变量。
也就是说,线程之间虽然有独立的栈区,但线程之间也是可以做到互相访问的。(在地址空间中)
但实际使用时,规定不能这样使用。
int g_val1=0;
__thread编译选项,运用线程局部存储原理,在共享区上创建一个私有全局变量
只能创建内置类型
应用:可以保存一些需要系统调用的值(获取一些基本属性),提高效率。
__thread int g_val2=0;
int pthread_detach(pthread_t thread);
pthread_detach(pthread_self());
for (auto i : tids)
{
pthread_detach(i);//主线程分离
}
for (int i = 0; i < tids.size(); i++)
{
int n = pthread_join(tids[i], nullptr);
printf("n = %d, who = 0x%x, why: %s\n", n, tids[i], strerror(n));
}
主线程调用pthread_exit只是退出主线程,并不会导致进程的退出
// -----------多线程并发抢票,互斥
#define NUM 4
int tickets = 1000;
class threadData
{
public:
threadData(int number)
{
threadName = "thread-" + to_string(number);
}
public:
string threadName;
};
string toHex(pthread_t tid)
{
char buffer[128];
snprintf(buffer, sizeof(buffer), "0x%x", tid);
return buffer;
}
void *getTicket(void *args)
{
threadData* td = static_cast(args);
while(tickets>0)
{
usleep(10000);
--tickets;
cout<<"threadname:"<threadName<<" tid:"
<threadName< tids;
vector thread_Datas;
for (int i = 1; i <= NUM; ++i)
{
pthread_t tid;
threadData* td = new threadData(i);
thread_Datas.push_back(td);
pthread_create(&tid, nullptr, getTicket, thread_Datas[i-1]);
tids.push_back(tid);
}
for(auto tid:tids)
{
pthread_join(tid,nullptr);
}
for(auto td:thread_Datas)
{
delete td;
}
return 0;
}
tickets--
的操作不是原子性的,而是分为三个步骤:
- 将共享变量tickets从内存加载到寄存器中
- 更新寄存器里面的值 执行-1操作
- 将新值从寄存器写回共享变量tickets的内存地址
假设现在有2个线程thread-1 和 thread-2进行抢票工作,分为上面的1,2,3步。
对于thread-1:执行第一步,从内存中读取到1000,1步完成。此时时间片到了,切换为thread-2
对于thread-2:时间片剩余较多,假设可以完整完成100次抢票工作。最后一次完成3步,此时eax中保存的值是900,最终将其写回内存中。然后切换回thread-1.
对于thread-1:继续第2步,先恢复上下文数据,将1000写到eax中,计算后为999,再进行第三步写回内存,此时内存中的值就变为999了,也就是多了100张票。
即对于thread2来说,tickets值前后不一致,即数据不一致问题。
保存上下文到线程的对象内部,每次轮转到时恢复到CPU内的寄存器中
也就是thread-1认为自己一直在正确地--,实际上保存在上下文的那一份,拷贝回内存时,导致了最终的数据不一致问题。
tickets--的操作不是原子性的,即允许多个执行流同时进入,会互相干扰。
为避免该问题,则需要加锁操作。
判断tickets时,成立进入。
但设置了usleep,为了让多线程都停留在判断进入,但没有--操作,就被切换走了。
此时就会出现tickets为1,但有>1个线程判断成立。
后续的--操作就不判断tickets的值是否>0了
每次--都需要重新读取tickets的值,在这之前tickets可能已经被其它线程修改了。
pthread_mutex是库提供的一种数据类型
全局的mutex不用手动init和destroy
在main函数中创建并init一个lock。
在threadData中加入一个锁的指针,多线程对于一个临界区,共用一把锁。
多线程共享的资源是临界资源,访问临界资源的代码叫临界区。
加锁本质:是用时间换安全。
加锁表现:原来多线程并发执行,对于加锁的临界区的代码变为串行执行。(并发度下降)
加锁原则:临界区的代码越少越好。
lock和unlock之间的代码就是临界区。
把lock和unlock放在while(1)外部会怎么样?
一直是一个线程在抢票(执行while(1)),与逻辑不符。
问题2:
lock失败则会阻塞等待,直到申请成功。
问题3:
tickets为0时,break跳出,不会unlock
在if和else中都要加unlock
上述代码加锁后,不会出现负数情况,但为什么还是一个线程在抢票?
这是由于多个线程对于锁的竞争性不同导致的。
对于lock的线程,unlock后可以直接继续lock,中间间隔很短,lock的概率大。
只要lock成功,即使轮转到别的线程,其它线程也只能阻塞等待。
对于其它线程,线程的切换需要的时间很长,lock的概率就很小。
在抢票完成后usleep,此时持有锁的进程不会立刻下一次lock,而是和其它线程一样进行时间片轮转,多线程直接lock的概率差距就变小了,即对锁的竞争性就差不多了。
联系信号量,PV操作的设计也是原子性(不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成)
锁是为了保护临界资源的,各线程申请一把锁,锁本身也是临界资源。
线程切换时,是持有锁一起切换走的,这期间其它线程不能进入临界区访问临界资源。该持有锁的线程访问临界区的过程,对其它线程来说是原子的。
为了实现互斥锁的操作,大多数体系结构都提供了swap
或exchange
指令,该指令的作用就是把寄存器和内存单元的数据相交换(一条汇编指令完成,即原子性的)
以加锁示例,这是由多条汇编语句执行的,上述
%al
是寄存器,mutex
就是内存中的一个变量。每个线程申请锁时都要执行上述语句,执行步骤如下:
- (
movb $0
,%al
)先将al寄存器中的值清0。该动作可以被多个线程同时执行,因为每个线程都有自己的一组寄存器(上下文信息),执行该动作本质上是将自己的al寄存器清0。注意:凡是在寄存器中的数据,全部都是线程的内部上下文!多个线程看起来同时在访问寄存器,但是互不影响。- (
xchgb %al
,mutex
)然后用此一条指令交换al寄存器和内存中mutex的值,xchgb是体系结构提供的交换指令,该指令可以完成寄存器和内存单元之间数据的交换。- 最后判断al寄存器中的值是否大于0。若大于0则申请锁成功,此时就可以进入临界区访问对应的临界资源;否则申请锁失败需要被挂起等待,直到锁被释放后再次竞争申请锁。
现在线程A要开始加锁,执行上述语句。首先(movb $0
,%al
),线程A把0读进al
寄存器(清0寄存器)
然后执行第二条语句(xchgb %al
,mutex
)将al
寄存器中的值与内存中mutex
的值进行交换。
mutex
的数据先前已经被线程A交换至寄存器,然后保存到线程A的上下文了,现在的mutex
为0,而线程B执行交换动作,拿寄存器al
的0去换内存中mutex
的0。最终A进行lock成功,B被挂起等待。
线程A在执行第一条语句把寄存器清0后就发生了线程切换(切至线程B),线程A保存上下文数据(0),此时线程B执行第一条语句把0写进寄存器,随后线程B执行第二条语句xchgb
交换:
此时线程A执行第三条语句if判断失败,只能被挂起等待,线程A只能把自己的上下文数据保存,重新切换至线程B,也就是说线程B只要不运行,你们其它所有线程都无法申请成功。线程B恢复上下文数据(1)到寄存器,然后执行第三条语句if成功,返回结果。
交换的本质:上述
xchgb
就是申请锁的过程。申请锁是将数据从内存交换到寄存器,本质就是将数据从共享内存变成线程私有。
mutex
就是内存里的全局变量,被所有线程共享,但是一旦用一条汇编语句将内存的mutex
值交换到寄存器,寄存器内部是哪个线程使用,那么此mutex
就是哪个线程的上下文数据,那么就意味着交换成功后,其它任何一个线程都不可能再申请锁成功了,因为mutex
已经独属于某线程私有了。- 这个
mutex = 1
就如同令牌一般,哪个线程先交换拿到1,那么哪个线程就能申请锁成功,所以加锁是原子的。
进行unlock的一般都是是lock成功的那个线程,因此天然具有原子性。
某些情况下,也可以让其它线程进行unlock。
当线程释放锁时,需要执行以下步骤:
mutex
置回1。使得下一个申请锁的线程在执行交换指令后能够得到1,形象地说就是“将锁的钥匙放回去”。Mutex
的线程。唤醒这些因为申请锁失败而被挂起的线程,让它们继续竞争申请锁。al
寄存器中的值清0,这不会造成影响,因为每次线程在申请锁时都会先将自己al
寄存器中的值清0,再执行交换指令。mutex
通过交换指令,原子性的交换到自己的al
寄存器中。class Mutex//封装Lock和Unlock接口
{
public:
Mutex(pthread_mutex_t* lock)
:_lock(lock){}
~Mutex(){}
void Lock()
{
pthread_mutex_lock(_lock);
}
void Unlock()
{
pthread_mutex_unlock(_lock);
}
private:
pthread_mutex_t* _lock;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t* lock)
:_mutex(lock)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.Unlock();
}
private:
Mutex _mutex;
};
使用一个全局的锁,且创建一个LockGuard设计RAII风格的锁
加一个代码块,让LockGuard完成RAII功能。
// int 票数计数器
int tickets = 1000; // 临界资源,可能会因为共同访问,造成数据不一致的问题
pthread_mutex_t Mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t Mutex1=PTHREAD_MUTEX_INITIALIZER;
void *getTickets(void *args)
{
const char *name = static_cast(args);
while (true)
{
pthread_mutex_lock(&Mutex);
sleep(1);
pthread_mutex_lock(&Mutex1);
// 临界区
if (tickets > 0)
{
usleep(100);
cout << name << " 抢到了票, 票的编号: " << tickets << endl;
tickets--;
}else
{
break;
}
pthread_mutex_unlock(&Mutex1);
pthread_mutex_unlock(&Mutex);
}
return nullptr;
}
void *getTickets1(void *args)
{
const char *name = static_cast(args);
while (true)
{
pthread_mutex_lock(&Mutex1);
sleep(1);
pthread_mutex_lock(&Mutex);
// 临界区
if (tickets > 0)
{
usleep(100);
cout << name << " 抢到了票, 票的编号: " << tickets << endl;
tickets--;
}else
{
break;
}
pthread_mutex_unlock(&Mutex);
pthread_mutex_unlock(&Mutex1);
}
return nullptr;
}
破坏4个必要条件中的一个即可。
1、互斥条件(编码):也就是不再使用锁,一般不会改变。
2、变为请求与不保持条件(接口)
例如A和B两个线程,A申请到了a锁,B申请到了b锁,然后A又要申请b锁,B又要申请a锁。
使用pthread_mutex_trylock接口,A申请b锁时,由于B持有锁,所以申请失败直接返回,if成功执行临界区代码,else失败会把a锁释放,然后再重新申请2把锁。
释放a锁后,B线程就可以正常申请到第二把a锁,使用完后释放a锁和b锁。
3、变为剥夺条件(接口)
线程A会因为没申请到b锁而阻塞,此时如果可以让B线程把b锁释放,A线程就可以申请成功了。
4、循环等待条件(编码)
2个线程需要2把锁,按照顺序申请锁,都是先申请a锁,再申请b锁
如:线程A申请到a锁,线程B申请a锁时会阻塞,此时b锁不在线程B上,线程A可以正常拿到b锁,然后执行完代码后依次释放a锁和b锁。
之后线程B才能申请这两把锁。
同步:在保证数据安全(有锁)的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
如果没有锁,新来的线程都会直接访问临界资源,失败了才会排队,但访问期间就会导致线程不安全。
如果没有饥饿问题,只用互斥即可。
有饥饿问题,则需要同步。
让阻塞的线程去排队(按一定顺序)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int cnt=0;
void* Cond(void* args)//condition
{
pthread_detach(pthread_self());
uint64_t number = (uint64_t)args;
cout<<"thread-"<
template
class BlockQueue
{
const static int defaultNum = 20;
private:
queue q_;
int maxcap_;
// 保证生产者和消费者之间是互斥+同步的
pthread_mutex_t mutex_;// 互斥
pthread_cond_t c_cond_;// 同步
pthread_cond_t p_cond_;//分别放在2个不同的条件变量的队列中
int _high_water_ = maxcap_*2/3;//水位线控制策略
int _low_water_ = maxcap_/3;
public:
BlockQueue(int maxcap = defaultNum) : maxcap_(maxcap)
{
pthread_mutex_init(&mutex_, nullptr);
pthread_cond_init(&c_cond_, nullptr);
pthread_cond_init(&p_cond_, nullptr);
}
~BlockQueue()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&c_cond_);
pthread_cond_destroy(&p_cond_);
}
void push(const T &in)
{
pthread_mutex_lock(&mutex_);
// 判断保证可以生产
if (q_.size() == maxcap_) // 判断时也要访问临界资源,需要放在加锁之后
{
pthread_cond_wait(&p_cond_, &mutex_); // 1、调度的时候自动释放锁
}
// 1、队列不满 2、满了后被唤醒,唤醒是因为已经有消费了
q_.push(in);
if(q_.size()>_high_water_)pthread_cond_signal(&c_cond_);
pthread_mutex_unlock(&mutex_);
}
const T pop()
{
pthread_mutex_lock(&mutex_);
// 判断保证可以消费
if (q_.size() == 0)
{
pthread_cond_wait(&c_cond_, &mutex_);
}
T out = q_.front();
q_.pop();
if(q_.size()<_low_water_)pthread_cond_signal(&p_cond_);
pthread_mutex_unlock(&mutex_);
return out;
}
};
#include"BlockQueue.hpp"
void* Productor(void* args)
{
BlockQueue* bq = static_cast*>(args);
int data = 0;
while(1)
{
data++;
bq->push(data);
cout<<"生产了一个数据 "<* bq = static_cast*>(args);
while (1)
{
int data = bq->pop();
cout<<"消费了一个数据 "<* bq = new BlockQueue();
pthread_t c,p;
pthread_create(&c,nullptr,Consumer,bq);
pthread_create(&p,nullptr,Productor,bq);
pthread_join(c,nullptr);
pthread_join(p,nullptr);
delete bq;
return 0;
}