线程有自己的pcb
线程共享资源:1.文件描述符 2.当前工作目录,内存地址空间
线程非共享资源 1.线程id,2.内核栈 3.独立的栈空间 4.errno变量 5.信号屏蔽字 6.调度优先级
char* strerror(int errnum) //获取错误码对应的错误消息
功能:创建一个新的线程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
参数
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数指针,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
//文件名:first_pthread.c
#include
#include
void* thr(void* arg)
{
int i=*(int*)arg;
printf("agr:%d",i);
printf("I am a thread! pid=%d\n",getpid());
return NULL;
}
int main()
{
pthread_t tid;
int* p=new int(4);
pthread_create(&tid,NULL,thr,p);
printf("I am main pthread,pid=%d\n",getpid());
sleep(1);
return 0;
}
gcc first_pthread.c -o first_pthread -g -lpthread
查看线程id
//返回当前线程id
#include
pthread_t pthread_self(void);
改进上面的程序
void* thr(void* arg)
{
printf("I am a thread! pid=%d,tid=%lu\n",getpid(),pthread_self());
return NULL;
}
在前面的进程中,我们知道创建一个子进程之后,父进程是需要等待的,否则子进程成为僵尸进程,父进程都不知道,后面也就无法处理,造成内存泄漏等问题,所以子线程同样需要等待。等待原因总结如下:
线程等待原因:
已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
创建新的线程不会复用刚才退出线程的地址空间。
功能:等待线程结束后回收(阻塞等待)
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
- 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_CANCELED。
- 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传pthread_exit的参数。
- 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数
void* thr(void* args)
{
int cnt=0;
while(cnt<3)
{
sleep(1);
cout<<"thread id:"<<pthread_self()<<endl;
sleep(1);
cnt++;
}
cout<<"thread quit"<<endl;
return NULL;
}
int main()
{
pthread_t main_thread;
main_thread=pthread_self();
pthread_t tid[2];
int j;
for(j=0;j<2;j++)
{
pthread_create(tid+j,NULL,thr,NULL);
cout<<"tid="<<tid[j]<<endl;
}
int i;
for(i=0;i<2;i++)
{
if(0==pthread_join(tid[i],NULL))
{
cout<<"thread"<< i <<" tid: "<<tid[i]<<" join sucess "<<endl;
}
}
return 0;
}
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码
using namespace std;
void *Rountine(void* args)
{
int cnt = 5;
while(cnt--)
{
printf("thread %d: %lu is running !\n", cnt, pthread_self());
}
return (void*)-1;
}
int main()
{
pthread_t tids[5];
for(auto i = 0 ; i < 5; ++i)
{
pthread_create(&tids[i], nullptr, Rountine, nullptr);
}
sleep(3);
for(auto i = 2; i< 4; ++i)
{
pthread_cancel(tids[i]);
printf("thread %p is cancel\n", tids[i]);
}
void* ret=nullptr;
for(auto i = 0; i < 5; ++i)
{
if(pthread_join(tids[i], &ret) == 0)
{
printf("thread %d: %lu join sucess!!! | exit code: %d\n", i, tids[i],ret);
}
}
return 0;
}
对应线程函数的执行过程没有执行,就直接返回-1了,说明我们取消线程成功了。
线程分离状态,指定该状态,线程主动与主控线程断开练习。线程结束后,其退出状态不由其他线程获取,而由自己主动释放。常用于网络,多线程服务器。
进程若有该机制,就不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源倍释放,一部分资源残留仍然存于系统中,导致内核认为该进程仍然存在。
int pthread_detach(pthread_t thread);
//也可以分离自己
pthread_detach(pthread_self());
void* thr(void*agrs)
{
printf("I am a thread,self=%lu\n",pthread_self());
sleep(4);
printf("I am a thread,self=%lu\n",pthread_self());
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,thr,NULL);
//线程分离
pthread_detach(tid);
sleep(5);
int ret=0;
if((ret=pthread_join(tid,NULL))>0)
{
printf("join err %d,%s\n",ret,strerror(ret));
}
return 0;
}
当一个线程同时调用pthread_join和pthread_detach时,就会报非法参数的错误。
比较两个线程ID是否相等。
int pthread_equal(pthread_t t1,pthread_t t2);
(cpu核数 * 2,cpu核数 * 2+2)
临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,常对临界资源起保护作用
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完。
部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
多个线程并发的操作共享变量,会带来一些问题
观察下面的这个程序
// buy_ticets.c
int num=5;
int tickets=50;
void* Route(void* agrs)
{
printf("begin buy tickets......\n");
while(1)
{
if(tickets>0)
{
unsleep(2000);
printf("0x%x: get a tickets:%d\n",(int)pthread_self(),tickts);
tickets--;
}
else
{
break;
}
}
}
int main()
{
pthread_t nums[num];
for(int i=0;i<num;i++)
{
pthread_create(nums+i,NULL,Route,NULL);
}
//回收资源
for(int i=0;i<num;i++)
{
pthread_join(nums[i],NULL);
}
return 0;
}
在抢票的过程中,出现了两个需要注意的点
原因
原因是tickets是一个全局变量,属于是临界资源,被所有线程共享。在多线程中,当多个线程对一个全局变量进行操作时,该操作不是原子的(安全的)。
比如上述的tickets–操作,一共设计三条汇编指令。
为什么会出现负数的情况?
要解决以上的问题,需要以下的解决方法:
代码直接必须有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
如果多个线程同时要求执行临界区代码,并且临界区没有线程在执行,那么只能允许一个线程进入。
如果线程不在临界区执行,那么该线程不能阻止其他线程进入临界区。
就像是一把锁,锁住了临界区,linux上提供的这把锁叫做互斥量
初始化互斥量的方式:
方法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法2,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t*restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL
销毁互斥量 销毁互斥量需要注意的点:
静态分配的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁。
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);
返回值:
成功返回0,失败返沪错误码
调用pthread_lock时,注意事项:
对程序中的临界区加上锁
int num=5;
int tickets=50;
pthread_mutex_t lock;
void* Route(void* agrs)
{
printf("begin buy tickets......\n");
while(1)
{
pthread_mutex_lock(&lock);
if(tickets>0)
{
unsleep(2000);
printf("0x%x: get a tickets:%d\n",(int)pthread_self(),tickts);
tickets--;
pthread_mutex_unlock(&lock);
}
else
{
pthread_mutex_unlock(&lock);
break;
}
}
}
int main()
{
pthread_t nums[num];
for(int i=0;i<num;i++)
{
pthread_create(nums+i,NULL,Route,NULL);
}
//回收资源
for(int i=0;i<num;i++)
{
pthread_join(nums[i],NULL);
}
//销毁锁
pthread_mutex_destory(&lock)l
return 0;
}
加上锁后,票数没有再减至为负数。并且票数减少是从50逐渐减1到0。
需要注意的知识点:
死锁:
死锁的必要条件:
饥饿问题: 我们在创建锁的过程中,可能会存在一个优先级高的线程,每次都是该线程优先申请到锁。该线程一直申锁,执行程序,释放锁。导致其他线程没有机会得到这把锁。
条件变量(cond):条件变量是线程库提供的一个描述临界资源状态的对象的变量。不用频繁申请或者释放锁的方式,也能达到检测临界资源的目的。
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件
初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:
cond:要初始化的条件变量
attr:NULL
销毁
int pthread_cond_destroy(pthread_cond_t *cond)
阻塞等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量
等待唤醒
//唤醒所有等待的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
//唤醒在指定条件下等待的一个线程
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_t cond; //条件变量,用来控制同步
pthread_mutex_t mutex; //互斥量
void* Route1(void* argc)
{
while(1)
{
pthread_cond_wait(&cond,&mutex);
printf("%s线程正在运行......\n",(char*)argc)
}
return NULL;
}
void* Route2(void* agrc)
{
while(1)
{
pthread_cond_broadcast(&cond);
//pthread_cond_signal(&cond); //给在等待队列的一个线程发送信号
sleep(1);
}
}
int main()
{
pthread_cond_init(&cond,NULL); //初始化条件变量
pthread_mutex_init(&mutex,NULL); //锁也进行初始化
//使用tid4控制其他的线程
pthread_t tid1,tid2,tid3,tid4;
pthread_create(&tid1,NULL,Route1,(void*)"thread 1");
pthread_create(&tid2,NULL,Route1,(void*)"thread 2");
pthread_create(&tid3,NULL,Route1,(void*)"thread 3");
pthread_create(&tid4,NULL,Route2,(void*)"thread 4");
//回收资源
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_join(tid3,NULL);
pthread_join(tid4,NULL);
//销毁条件变量和互斥量
pthread_cond_destroy(&cond);
pthread_mutex_destory(&mutex);
return 0;
}
为什么pthread_cond_wait需要互斥量?
特点:
BlockingQueue的概念:
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于:
单消费者和单生产者的生产消费模型
使用STL中的queue进行模拟
#pragma once
#include
#include
#include
#include
//单生产者、单消费者
//类模板,
template <class T>
class BlockQueue
{
public:
//构造函数初始化
BlockQueue(int _cap):cap(_cap)
{
//条件变量、互斥锁初始化
pthread_mutex_init(&lock, nullptr);
pthread_cond_init(&have_space, nullptr);//生产者
pthread_cond_init(&have_data, nullptr);//消费者
}
~BlockQueue()
{
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&have_space);
pthread_cond_destroy(&have_data);
}
void put(const T& in)
{
pthread_mutex_lock(&lock);//加锁
//生产者不应该在生在,而应该等待
//为什么用while,防止伪唤醒,只有一个数据,可能唤醒了多个消费者,存在数据错误,所以用while持续判断
// if(is_full())
while(is_full())
{
//若阻塞队列已经满了,那么此时该生产者线程就需要进行等待,直到阻塞队列中有空间时再将其唤醒。
//这里前面条件变量讲到过,可以去看看前面
pthread_cond_wait(&have_space, &lock);//为什么要抱着锁,等待自动解锁,唤醒时自动获取锁
//为了避免死锁问题,在调用pthread_cond_wait函数时就需要传入当前线程手中的互斥锁,此时当该线程被挂起时就会自动释放手中的互斥锁,而当该线程被唤醒时又会自动获取到该互斥锁。
}
bq.push(in);
//有一半以上的数据,我们才去通知消费者进行消费,实现同步过程
if(bq.size() >= cap / 2)
pthread_cond_signal(&have_data);//给消费者发送信号,有数据了,需要你过来进行消费
pthread_mutex_unlock(&lock);
}
void get(T* out)
{
pthread_mutex_lock(&lock);
// if(is_empty())
//若阻塞队列为空,那么此时该消费者线程就需要进行等待,直到阻塞队列中有新的数据时再将其唤醒
while(is_empty())
{
pthread_cond_wait(&have_data, &lock);
}
*out = bq.front();
bq.pop();
pthread_cond_signal(&have_space);
pthread_mutex_unlock(&lock);
}
private:
bool is_empty()
{
return bq.size() == 0 ? true : false;
}
//是否满
bool is_full()
{
return bq.size() == cap ? true : false;
}
private:
int cap;//队列容量
pthread_mutex_t lock;//条件变量需搭配互斥锁使用
pthread_cond_t have_space;//生产者
pthread_cond_t have_data;//消费者
std::queue<T> bq;//本身就是临界资源,也需要加锁保护
};
为了避免出现上述情况,我们就要让线程被唤醒后再次进行判断,确认是否真的满足生产消费条件,因此这里必须要用while进行判断。
#include "block_queue.hpp"
// #include "task.hpp"
#define NUM 10
//消费者
void *consumer(void *c)
{
// BlockQueue *bq = (BlockQueue*)c;
BlockQueue<int> *bq = (BlockQueue<int>*)c;
int out = 0;
while(true)
{
sleep(1);
int t;
bq->get(&t);
std::cout << "consumer have cost: " << t << std::endl;
}
return nullptr;
}
void *producter(void *p)
{
// BlockQueue *bq = (BlockQueue *)p;
BlockQueue<int> *bq = (BlockQueue<int> *)p;
//int in = 1;
while(true)
{
int top = rand()%5 + 1;
bq->put(top);
std::cout << "producer produce: " << top << std::endl;
}
}
int main()
{
//这里处理的int这种商品
//类型具体可以变化,这里可以安排任务给消费者,消费者去消费任务
BlockQueue<int> *bq = new BlockQueue<int>(NUM);
pthread_t c, p;
//两个线程,一个生产者,一个消费者
pthread_create(&c, nullptr, consumer, bq);
pthread_create(&p, nullptr, producter, bq);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
delete bq;//在堆上开辟的记得释放
return 0;
}
信号量本质是一个计数器,用来描述临界资源数目的计数器。
信号量是一种特殊的变量,它只能取自然数值并且只支持两种操作:等待(wait)和信号(signal)。但是在Linux/UNIX中,“等待”和“信号”都已经具有特殊的含义。所以我们用的称呼是P、V操作。
最常用的、最简单的信号量是二进制信号量,它只能取0和1这两个值。
当关键代码段可用时,二进制信号量SV的值为1,进程A和B都有机会进入关键代码段。如果此时进程A执行了P(SV)操作将SV减1,则进程B若再执行P(SV)操作就会被挂起。直到进程A离开关键代码段,并执行V(SV)操作将SV加1,关键代码段才重新变得可用。如果此时进程B因为等待SV而处于挂起状态,则它将被唤醒,并进入关键代码段。同样,这时进程A如果再执行P(SV)操作,则也只能被操作系统挂起以等待进程B退出关键代码段。
初始化信号量
#include
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值
销毁信号量
int sem_destroy(sem_t *sem);
等待信号量
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem);
发布信号量
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);