补充一下上次没有说完的线程终止的内容:
1. 从线程函数return,这种方法对主线程不适用,从main函数return相当于调用exit
2. 线程可以调用pthread_ exit终止自己
3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程
void pthread_exit(void *value_ptr);
参数 value_ptr:value_ptr不要指向一个局部变量
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
tips:pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了
woc好像
线程等待的作用:
已经退出的线程,其空间没有被释放,仍然在进程的地址空间内
创建新的线程不会复用刚才退出线程的地址空间
int pthread_join(pthread_t thread, void **value_ptr);
参数 thread:线程ID value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止,thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值
2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数 PTHREAD_ CANCELED
3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数
4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread1( void *arg )
{
printf("thread 1 returning ... \n");
int *p = (int*)malloc(sizeof(int));
*p = 1;
return (void*)p;
}
void *thread2( void *arg )
{
printf("thread 2 exiting ...\n");
int *p = (int*)malloc(sizeof(int));
*p = 2;
pthread_exit((void*)p);
}
void *thread3( void *arg )
{
while ( 1 )
{
printf("thread 3 is running ...\n");
sleep(1);
}
return NULL;
}
int main( void )
{
pthread_t tid;
void *ret;
// thread 1 return
pthread_create(&tid, NULL, thread1, NULL);
pthread_join(tid, &ret);
printf("thread return, thread id %X, return code:%d\n", tid, *(int*)ret);
free(ret);
// thread 2 exit
pthread_create(&tid, NULL, thread2, NULL);
pthread_join(tid, &ret);
printf("thread return, thread id %X, return code:%d\n", tid, *(int*)ret);
free(ret);
// thread 3 cancel by other
pthread_create(&tid, NULL, thread3, NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &ret);
if ( ret == PTHREAD_CANCELED )
{
printf("thread return, thread id %X, return code:PTHREAD_CANCELED\n", tid);
}
else
{
printf("thread return, thread id %X, return code:NULL\n", tid);
}
}
还是复习下上次学过的:
#include <iostream>
#include<string>
#include<vector>
#include<pthread.h>
#include<thread>
#include<stdlib.h>
#include <unistd.h>
void* threadRun(void *args)
{
std::string name = static_cast<const char*>(args);
while (true)
{
std::cout << name << " is running,tid: " << pthread_self() << std::endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,threadRun,(void*)"thread-1");
std::cout << "new thread tid: " << tid << std::endl;
pthread_join(tid,nullptr);
return 0;
}
给用户提供的线程ID,不是内核中的lwp,而是pthread库维护的一个唯一值
库内部也要承担对线程的管理
为了方便我们观察,都打成十六进制的:
#include <iostream>
#include<string>
#include<cstdio>
#include<vector>
#include<pthread.h>
#include<thread>
#include<stdlib.h>
#include <unistd.h>
std::string ToHex(pthread_t tid)
{
char id[128];
snprintf(id,sizeof(id),"0x%x",tid);
return id;
}
void* threadRun(void *args)
{
std::string name = static_cast<const char*>(args);
while (true)
{
std::cout << name << " is running,tid: " << ToHex(pthread_self()) << std::endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,threadRun,(void*)"thread-1");
std::cout << "new thread tid: " << ToHex(tid) << std::endl;
pthread_join(tid,nullptr);
return 0;
}
tid是一个地址
ls /lib/x86_64-linux-gnu/libpthread.so.0 -l
这是一个炫酷的库 ,pthread库本质是一个文件
我们要创建线程,前提是把库加载到内存,映射到进程的地址空间
有个问题:库是怎么做到对线程进行管理的呢?
和操作系统对进程进行管理差不多
struct pthread存放的是线程在用户级最基本的属性,线程栈是用户级别的独立的栈结构
库如何做到对线程进行管理呢?
还是先描述再组织
库中创建描述线程的相关结构体字段属性,管理只需要找到线程控制块的地址即可
Linux线程 = pthread库中线程的属性集 + LWP
操作系统没有线程,那它势必就要为我们提供LWP的系统调用
#define _GNU_SOURCE
#include <sched.h>
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
/* pid_t *parent_tid, void *tls, pid_t *child_tid */ );
所以总结一下就是,tid是线程属性集合的起始虚拟地址,在pthread库中维护
“他在干什么”
“我也不知道 ”
我们来尝试自己封装一个线程类,这个线程类放在命名空间里
还是那句话,先描述再组织
#include<iostream>
#include<functional>
#include<string>
#include<pthread.h>
namespace ThreadMoudle
{
//线程要执行的方法
//using func_t = std::function<void()>;
typedef void(*func_t)(const std::string &name); //函数指针类型
class Thread
{
public:
Thread()
{
}
void Start()
{
}
void Stop()
{
}
void Join()
{
}
~Thread()
{
}
private:
std::string _name;
pthread_t _tid;
bool _isrunning;
func_t func; //线程要执行的回调函数
};
}
不能执行类内的方法,因为类内的成员函数默认带了一个参数:this指针
这是错误写法:
#pragma once
#include<iostream>
#include<functional>
#include<string>
#include<pthread.h>
namespace ThreadMoudle
{
//线程要执行的方法
//using func_t = std::function<void()>;
typedef void(*func_t)(const std::string &name); //函数指针类型
class Thread
{
public:
Thread()
{
}
void *ThreadRoutine(void* args) //新线程执行的方法
{
_func();
}
bool Start()
{
int n = ::pthread_create(&_tid,nullptr,ThreadRoutine,nullptr); //这个::指用系统提供的
if(n != 0)
{
return false;
}
return true;
}
void Stop()
{
}
void Join()
{
}
~Thread()
{
}
private:
std::string _name;
pthread_t _tid;
bool _isrunning;
func_t _func; //线程要执行的回调函数
};
}
那我们应该怎么办呢?
变成友元?
也行,但是实现起来复杂一点
最通俗的做法是加static,这样这个方法就属于类而不属于对象了,参数里就没有this指针了
#pragma once
#include<iostream>
#include<functional>
#include<string>
#include<pthread.h>
namespace ThreadMoudle
{
//线程要执行的方法
//using func_t = std::function<void()>;
typedef void(*func_t)(const std::string &name); //函数指针类型
class Thread
{
public:
Thread()
{
}
static void *ThreadRoutine(void* args) //新线程执行的方法
{
_func();
}
bool Start()
{
int n = ::pthread_create(&_tid,nullptr,ThreadRoutine,nullptr); //这个::指用系统提供的
if(n != 0)
{
return false;
}
return true;
}
void Stop()
{
}
void Join()
{
}
~Thread()
{
}
private:
std::string _name;
pthread_t _tid;
bool _isrunning;
func_t _func; //线程要执行的回调函数
};
}
然后对于线程内各个接口有对应的实现:
#pragma once
#include<iostream>
#include<functional>
#include<string>
#include<pthread.h>
namespace ThreadMoudle
{
//线程要执行的方法
//using func_t = std::function<void()>;
typedef void(*func_t)(const std::string &name); //函数指针类型
class Thread
{
public:
void Excute()
{
_isrunning = true;
_func(_name);
}
public:
Thread(const std::string &name,func_t func):_name(name),_func(func)
{
}
static void *ThreadRoutine(void* args) //新线程执行的方法
{
Thread *self = static_cast<Thread*>(args); //获得当前对象
self->Excute();
}
bool Start()
{
int n = ::pthread_create(&_tid,nullptr,ThreadRoutine,this); //这个::指用系统提供的
if(n != 0)
{
return false;
}
return true;
}
std::string Status() //线程启动检测下状态
{
if(_isrunning)
{
return "running";
}
return "sleep";
}
void Stop()
{
if(_isrunning)
{
pthread_cancel(_tid);
_isrunning = false;
}
}
void Join()
{
if(!_isrunning)
{
pthread_join(_tid,nullptr);
}
}
std::string Name()
{
return _name;
}
~Thread()
{
Stop();
Join();
}
private:
std::string _name;
pthread_t _tid;
bool _isrunning;
func_t _func; //线程要执行的回调函数
//std::string _result; //返回值,不关心的话也可以不用写
};
}
C++11提供的线程本身也是对原生线程的封装
我们也可以给它传参:
#include<iostream>
#include<unistd.h>
#include<vector>
#include"Thread.hpp"
using namespace ThreadMoudle;
void Print(const std::string &name)
{
int cnt = 1;
while (true)
{
std::cout << name << "is running" << std::endl;
sleep(1);
}
}
const int num = 10;
int main()
{
//构建线程
std::vector<Thread> threads;
for(int i=0;i<num;i++)
{
std::string name = "thread";
threads.emplace_back(name,Print);
}
//统一启动
for(auto &thread:threads)
{
thread.Start();
}
sleep(5);
//统一结束,好像集中营,我们都是
for(auto &thread:threads)
{
thread.Stop();
}
//等待线程
for(auto &thread:threads)
{
thread.Join();
}
// Thread t("thread-1",Print);
// t.Start();
// std::cout << t.Name() << ", status: " << t.Status() << std::endl;
// t.Stop();
// sleep(1);
// std::cout << t.Name() << ", status: " << t.Status() << std::endl;
// sleep(1);
// t.Join();
// std::cout << t.Name() << ", status: " << t.Status() << std::endl;
return 0;
}
对线程的管理变成对vector的管理
线程的互斥
多个线程能够看到的资源是共享资源
那线程间通信比进程间通信容易好多
因为多个线程都能看到一份资源啊,我们需要对这部分共享资源进行保护
这种保护的最简单有效的方式是互斥
多线程访问
代码未写服务器先崩
多线程访问并发资源的时候
会出现抢票抢到负数的情况
把数据移动到寄存器里捏
CPU里寄存器只有一套,但是寄存器里的数据可以有多套
对于tickets--;这一条语句,看上去是只有一句,但是转成汇编之后,会执行三条:重读数据、数据--,写回数据
load :将共享变量ticket从内存加载到寄存器中
update : 更新寄存器里面的值,执行-1操作
store :将新值,从寄存器写回共享变量ticket的内存地址
这就是共享资源造成了数据不一致的问题
怎么解决呢?
做到三点就好勒:
代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区
如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区
如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区
我们需要认识加锁
锁和接口
如果锁是全局的后者静态的,那么只需要init即可(不用destroy)
在使用man查看之前安装一下pthread的开发包:
sudo apt-get install glibc-doc
互斥锁类型:
#include <pthread.h>
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
这是动态和静态分配,而且注意互斥量和锁不是一个东西,上面的是初始化互斥量
关于销毁互斥量也要注意:
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁
对临界区资源进行保护是对临界区代码进行保护捏
所有资源是用代码进行访问的
对所有资源进行访问本质是通过代码进行访问
保护资源本质是想办法把访问资源的代码保护起来
进入临界区之前要加锁,出了临界区要解锁
我们先要有一把全局的锁,而加锁也要有一定的原则,并行改穿行
加锁的范围是要保证粒度一定要小
#include<iostream>
#include<vector>
#include<unistd.h>
#include<cstdio>
#include"Thread.hpp"
using namespace ThreadMoudle;
//抢票要有全部的票数
int tickets = 10000;
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;
void route(const std::string &name)
{
while (true)
{
//加锁
pthread_mutex_lock(&gmutex);
if(tickets > 0)
{
//抢票过程
usleep(1000); // 1ms -> 抢票花费的时间,这里暂时用这个休眠函数模拟
printf("%s get ticket:%d\n",name.c_str(),tickets); //是谁没有抢到票
tickets--;
pthread_mutex_unlock(&gmutex); //解锁
}
else
{
pthread_mutex_unlock(&gmutex); //解锁
break;
}
}
}
int main()
{
Thread t1("thread-1",route);
Thread t2("thread-2",route);
Thread t3("thread-3",route);
Thread t4("thread-4",route);
t1.Start();
t2.Start();
t3.Start();
t4.Start();
t1.Join();
t2.Join();
t3.Join();
t4.Join();
return 0;
}
任何线程要进行抢票都得先申请锁,不应该有例外
调用pthread_lock的时候可能会出现的情况:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量, 那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁
所有线程申请锁前提是他们能看到这把锁,锁本身也是共享资源
加锁的过程必须是原子的(原子性:要么不做,要么做完,没有中间状态)
如果线程申请锁失败了,那么线程要被阻塞
如果线程 申请锁成功了,那就继续向后运行
如果线程申请锁成功就可以执行临界区的代码哩
在执行临界区代码的期间可以切换吗?
可以捏
有点意思,如果我在执行临界区代码中被切走了,那么其他线程可以进来吗?
不可以捏
人不在江湖,江湖依旧流传着我的传说
可以放心的执行完毕,因为没人能打扰我
对于其他线程,要么没有申请锁,要么释放了锁对其他线程才有意义,访问临界区对其他线程是原子的
为了保证封装性再封装一个类:
#include<iostream>
#include<vector>
#include<unistd.h>
#include<cstdio>
#include"Thread.hpp"
using namespace ThreadMoudle;
//抢票要有全部的票数
int tickets = 10000;
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;
void route(ThreadData* td)
{
std::cout << td->_name << ":name, mutex address: " << td->_lock << std::endl;
sleep(1);
}
static int threadnum = 4;
int main()
{
pthread_mutex_t mutex;
pthread_mutex_init(&mutex,nullptr);
std::vector<Thread> threads;
for(int i=0;i<threadnum;i++)
{
std::string name = "thread-" + std::to_string(i+1);
ThreadData *td = new ThreadData(name,&mutex);
threads.emplace_back(name,route,td);
}
for(auto &thread : threads)
{
thread.Start();
}
for(auto &thread : threads)
{
thread.Join();
}
pthread_mutex_destroy(&mutex);
return 0;
}
Thread.hpp:
#pragma once
#include <iostream>
#include <functional>
#include <string>
#include <pthread.h>
namespace ThreadMoudle
{
class ThreadData
{
public:
ThreadData(const std::string &name,pthread_mutex_t *lock):_name(name),_lock(lock)
{
}
public: //正常来说应该是私有但是我不想写接口了凑合看吧
std::string _name;
pthread_mutex_t *_lock;
};
// 线程要执行的方法
// using func_t = std::function<void()>;
typedef void (*func_t)(ThreadData *td); // 函数指针类型
class Thread
{
public:
void Excute()
{
_isrunning = true;
_func(_td);
_isrunning = false;
}
public:
Thread(const std::string &name, func_t func, ThreadData *td) : _name(name), _func(func),_td(td)
{
}
static void *ThreadRoutine(void *args) // 新线程执行的方法
{
Thread *self = static_cast<Thread *>(args); // 获得当前对象
self->Excute();
}
bool Start()
{
int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this); // 这个::指用系统提供的
if (n != 0)
{
return false;
}
return true;
}
std::string Status() // 线程启动检测下状态
{
if (_isrunning)
{
return "running";
}
return "sleep";
}
void Stop()
{
if (_isrunning)
{
::pthread_cancel(_tid);
_isrunning = false;
}
}
void Join()
{
::pthread_join(_tid, nullptr);
std::cout << "join done" << std::endl;
}
std::string Name()
{
return _name;
}
~Thread()
{
Stop();
Join();
}
private:
std::string _name;
pthread_t _tid;
bool _isrunning;
func_t _func; // 线程要执行的回调函数
ThreadData* _td;
// std::string _result; //返回值,不关心的话也可以不用写
};
}
四个线程访问的都是同一把锁:
把我们的锁以参数的方式传递给每一个线程:
#include <iostream>
#include <vector>
#include <unistd.h>
#include <cstdio>
#include "Thread.hpp"
using namespace ThreadMoudle;
// 抢票要有全部的票数
int tickets = 10000;
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;
void route(ThreadData *td)
{
while (true)
{
// 加锁
pthread_mutex_lock(td->_lock);
if (tickets > 0)
{
// 抢票过程
usleep(1000); // 1ms -> 抢票花费的时间,这里暂时用这个休眠函数模拟
printf("%s get ticket:%d\n", td->_name.c_str(), tickets); // 是谁没有抢到票
tickets--;
pthread_mutex_unlock(td->_lock); // 解锁
}
else
{
pthread_mutex_unlock(td->_lock); // 解锁
break;
}
}
}
对于上面的代码封装性还是不算太好
所以我们引入一个LockGuard.hpp:
#pragma once
#include<pthread.h>
class LockGuard
{
public:
LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
{
pthread_mutex_lock(_mutex);
}
~LockGuard()
{
pthread_mutex_unlock(_mutex);
}
private:
pthread_mutex_t *_mutex;
};
这样它就会在创建的时候调用构造函数,在销毁的时候调用析构函数实现自动的加锁解锁了:
#include <iostream>
#include <vector>
#include <unistd.h>
#include <cstdio>
#include "Thread.hpp"
#include"LockGuard.hpp"
using namespace ThreadMoudle;
// 抢票要有全部的票数
int tickets = 10000;
void route(ThreadData *td)
{
while (true)
{
// 加锁
LockGuard lockguard(td->_lock);
if (tickets > 0)
{
// 抢票过程
usleep(1000); // 1ms -> 抢票花费的时间,这里暂时用这个休眠函数模拟
printf("%s get ticket:%d\n", td->_name.c_str(), tickets); // 是谁没有抢到票
tickets--;
}
else
{
break;
}
}
}
static int threadnum = 4;
int main()
{
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, nullptr);
std::vector<Thread> threads;
for (int i = 0; i < threadnum; i++)
{
std::string name = "thread-" + std::to_string(i + 1);
ThreadData *td = new ThreadData(name, &mutex);
threads.emplace_back(name, route, td);
}
for (auto &thread : threads)
{
thread.Start();
}
for (auto &thread : threads)
{
thread.Join();
}
pthread_mutex_destroy(&mutex);
return 0;
}
原理角度理解锁
如何理解申请锁成功就允许进入临界区?申请锁失败就不允许进入临界区
允许进入临界区就是申请锁成功,pthread_mutex_lock()函数会返回
不允许进入临界区就是申请锁失败,pthread_mutex_lock()函数不返回,线程就阻塞了(阻塞之后在pthread_mutex_lock内部被重新唤醒,重新申请锁)
单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一 个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下:
CPU的寄存器只有一套,被所有的线程共享,但是寄存器里面的数据属于执行流的上下文,属于执行流私有的数据
CPU在执行代码的时候一定要有对应的载体:进程或者线程
数据在内存中是被所有线程共享的
把数据从内存移动到CPU寄存器中本质是把数据从共享变成线程私有
这个交换不是拷贝,持有1的就表示持有锁
这个流程要有人道主义,第一个有锁的线程在还完了锁之后不能立马申请(要二次申请必须排队,其他线程也必须排队),也就是说在保证临界资源安全的情况下让访问顺序合理公平
这就是:线程同步!
可以是严格的顺序性,也可以是宏观上具有相对的顺序性