本节重点:
1. 了解线程概念,理解线程与进程区别与联系。
2. 学会线程控制,线程创建,线程终止,线程等待。
3. 了解线程分离与线程安全概念。
Linux线程概念
在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序 列”
一切进程至少都有一个执行线程
线程在进程内部运行,本质是在进程地址空间内运行
内核视角:进程是承担分配系统资源的基本实体,线程是CPU调度的基本单位。
虚拟内存决定了进程看到的内存资源,线程中的资源由进程划分。
CPU执行调度线程需要:线程ID,状态,优先级,上下文,栈……(进程与线程有重叠,所以Linux不给线程设计新的数据结构,复用PCB模拟线程——轻量级进程)。
由此可以得出,不同平台底层线程实现不一样。
OS要管理线程——先描述,再组织。
windows TCB (Thread contrl block)
Linux 轻量级进程
从前我们创建的进程只有一个执行流,但可以有多个执行流。
总结一下就是,进程整体申请资源,线程想进程申请资源。
Linux中没有真正意义上的线程。
OS和程序员只认线程,而Linux只能提供创建轻量级进程的接口,Linux通过原生线程库<pthread.h>来解决:
通过原生线程库的封装,用户可以不关心底层,仅仅通过调用线程接口实现调用轻量级进程接口。
Linux线程创建
遵守POSIX线程库标准, 错误码通过返回值返回。
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
要使用这些函数库,要通过引入头文件<pthread.h>
链接这些线程函数库时要使用编译器命令的“-lpthread”选项
功能:创建一个新的线程
原型
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;失败返回错误码
传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通 过返回值返回
pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误, 建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小
#include<pthread.h>
#include<iostream>
#include<unistd.h>
#include<string>
using namespace std;
void* route_run(void* args)
{
while(true)
{
sleep(1);
cout << "this is new thread running ......" << endl;
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, route_run, nullptr);
while(true)
{
sleep(1);
cout << "this is main thread running ......" << endl;
}
return 0;
}
查看线程: ps -aL
LWP (light weight process)
主线程 PID = LWP
线程一旦创建几乎所有资源都是共享的(环境变量、文件描述符表、当前工作目录、用户ID)。
CPU调度以LWP为调度ID标识。
那么什么资源应该是线程私有的呢?
私有栈、PCB、要有一定的私有上下文。
进程切换VS线程切换
由于线程许多资源是共享的,当CPU进行线程切换时,需要改变的数据更少,高速缓存中储存了许多的热点数据,线程切换后这些数据大概率仍然有效(命中率高),不需要从内存中重新读取。
线程的优点
创建一个新线程的代价要比创建一个新进程小得多
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
线程占用的资源要比进程少很多
能充分利用多处理器的可并行数量
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
线程的缺点
性能损失 :一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型 线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的 同步和调度开销,而可用的资源不变。
健壮性降低: 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了 不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制: 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高: 编写与调试一个多线程程序比单线程程序困难得多
进程与线程的关系
进程信号是发给进程的。
线程共享进程数据,但也拥有自己的一部分数据:
线程ID
一组寄存器
栈
errno
信号屏蔽字
调度优先级
线程的私有栈
pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID 不是一回事。
前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要 一个数值来唯一表示该线程。
pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID, 属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:
返回当前线程ID:
pthread_t pthread_self(void);
pthread_t类型的线程ID,本质 就是一个进程地址空间上的一个地址。
void* route_run(void* args)
{
while(true)
{
sleep(1);
char tid[128];
snprintf(tid, sizeof(tid), "%p", (long long)pthread_self());
cout << "this is new thread running : " << string(tid) << endl;
}
return nullptr;
}
线程的栈是私有的,clone接口(用作创建子进程(fork)或者轻量级进程(vfork)),我们用不到。
线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
2. 线程可以调用pthread_ exit终止自己。
3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
不能用exit终止某个子线程,整个进程都会崩溃。
void* route_run(void* args)
{
while(true)
{
sleep(5);
char tid[128];
snprintf(tid, sizeof(tid), "%p", (long long)pthread_self());
cout << "this is new thread running : " << string(tid) << endl;
pthread_exit(nullptr);
}
return nullptr;
}
线程取消
功能:
取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码
线程退出码为PTHREAD_CANCELED(-1)。
线程等待
功能:
等待线程结束
原型
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<pthread.h>
#include<iostream>
#include<stdio.h>
#include<unistd.h>
#include<string>
using namespace std;
void* route_run(void* args)
{
while(true)
{
sleep(5);
char tid[128];
snprintf(tid, sizeof(tid), "%p", (long long)pthread_self());
cout << "this is new thread running : " << string(tid) << endl;
pthread_cancel(pthread_self());
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, route_run, nullptr);
cout << "this is main thread running ......" << endl;
void* ret = nullptr;
pthread_join(tid, &ret);
cout << "ret = " << (long long)(ret) << endl;
return 0;
}
如果不关心线程退出码,也可以:
pthread_join(tid, nullptr);
线程分离
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放 资源,从而造成系统泄漏。 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线 程资源。
分离线程(不想等待,不关心线程退出返回值),退出时自动释放资源。
void* route_run(void* args)
{
while(true)
{
sleep(5);
char tid[128];
snprintf(tid, sizeof(tid), "%p", (long long)pthread_self());
cout << "this is new thread running : " << string(tid) << endl;
pthread_detach(pthread_self());
pthread_cancel(pthread_self());
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, route_run, nullptr);
cout << "this is main thread running ......" << endl;
void* ret = nullptr;
if(0 == pthread_join(tid, &ret))
{
cout << "wait success" << endl;
}
else
{
cout << "wait failed" << endl;
}
cout << "ret = " << (long long)(ret) << endl;
return 0;
}
如果分离线程就不能再等待,但若先一步等待,线程等待仍会成功。
正常使用应该是:
void* route_run(void* args)
{
while(true)
{
sleep(1);
char tid[128];
snprintf(tid, sizeof(tid), "%p", (long long)pthread_self());
cout << "this is new thread running : " << string(tid) << endl;
pthread_detach(pthread_self());
pthread_cancel(pthread_self());
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, route_run, nullptr);
cout << "this is main thread running ......" << endl;
void* ret = nullptr;
sleep(2);
if(0 == pthread_join(tid, &ret))
{
cout << "wait success" << endl;
}
else
{
cout << "wait failed" << endl;
}
cout << "ret = " << (long long)(ret) << endl;
return 0;
}
在全局变量前加__thread,可以将一个内置类型设为线程局部储存(每个线程都有一个独立的变量)。
线程库的简单封装
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cassert>
#include <functional>
#include <pthread.h>
class Thread;
//context传递对象的地址,帮助静态成员函数可以使用类内成员变量
class context
{
public:
context(Thread* t = nullptr, void* args = nullptr)
:_this(t)
,_args(args)
{}
Thread* _this;
void* _args;
};
class Thread
{
public:
Thread(const std::function<void*(void*)>& f, void* args = nullptr, const std::string& name = "")
:_func(f)
,_args(args)
,_name(name)
{
++_num;
if(_name.size() == 0)
{
_name += "thread ";
_name += std::to_string(_num);
}
//创建上下文结构体
context* pcon = new context(this, _args);
int n = pthread_create(&_tid, nullptr, route_start, pcon);
assert(0 == n);
(void)n;
}
static void* route_start(void* pcon)
{
context* cont = static_cast<context*>(pcon);
void* ret = cont->_this->run(cont->_args);
delete cont;
return ret;
}
void* run(void* args)
{
return _func(args);
}
void join()
{
int n = pthread_join(_tid, nullptr);
assert(n == 0);
(void)n;
}
~Thread()
{
--_num;
}
private:
pthread_t _tid;
std::string _name;
void* _args;
std::function<void*(void*)> _func;
static int _num;
};
int Thread::_num = 0;