【Linux的线程篇章 - 线程基础知识储备】

Linux之线程基础知识储备

前言:
前篇开始进行了解学习Linux的文件的基本知识等相关知识内容,接下来学习关于Linux线程知识储备,深入地了解这个强大的开源操作系统。
/知识点汇总/
1.背景知识
a、地址空间的理解

OS进程内存管理,不是以字节为单位的,而是以内存块为单位的,默认大小是4KB
系统和磁盘文件进行I/O的基本单位是4KB(8个扇区)
操作系统对内存的管理工作,就转变成为页框、页帧的管理,基本单位是4KB
整体发生写时拷贝(空间换取实践,局部性原理)

操作系统如何管理页框呢?
“先描述,再组织”
描述:
struct page
{
int flag;//是否被占用,是否是脏页,是否被锁定等
//…
}
组织:数组
struct page memory[1048576];//1MB
每一个page就又有了索引,标记每一个页框的起始地址。

页表:

实现虚拟到物理地址之间的映射,显然之前的页表并不是一对一的关系,否则内存是远远不足的。

那么真实的页表 和 虚拟地址是如何转换成为物理地址的呢?

前十个bit位1024项,作为页目录,其中每一项存放的是每一张页表4KB的地址4byte。
另外,最后的低12位[0,4095]作为页内偏移
起始地址+页内偏移量 = 对应的物理地址

所以任意的虚拟地址&(按位与0xFFF) = 页框号
页表的本质就是搜索到页框,页框再偏移就能找到物理地址空间的任意地址
(提及类型,所以只需要取首地址)

1)理解文件缓冲区
page是带有类型的,字典树数据结构

2)虚拟地址的本质是什么?
函数有地址吗?第一块代码的入口地址。且每一行代码都有地址。
函数是什么?连续的代码地址放在一起,组成的代码块。
意味着一个函数,对应一批连续的虚拟地址
那么是连续的,也就是可以拆分,即划分地址空间。

b、理解代码数据划分的本质
所以虚拟地址本质就是一种资源。

1、线程的概念

定义:

线程是“一个进程内部的控制序列”,一切进程都至少有一个执行线程。线程在进程内部运行,本质是在进程地址空间内运行。

线程概念:在进程内部(PCB)运行,是CPU调度的基本单位。
task_struct多个PCB指向地址空间同一块区域。 – Linux中的线程
–》内核层面的进程定义:承担分配系统资源的基本实体。
Linux中的执行流:就称为轻量级进程。

与进程的关系:

1.进程是资源分配的基本单位,而线程是资源调度的最小单位,也是程序执行的最小单位。
2.进程拥有独立的地址空间,而线程共享进程的地址空间,即它们共享进程中的代码段、数据段和堆栈段等资源。
3.线程之间的切换效率比进程之间的切换要高,因为线程切换时不需要切换地址空间。

2、Linux中线程的创建与使用

接口函数:pthread_create

#include <pthread.h>
int phread_create(pthread_t* thread,const pthread_attr_t* attr, void (start_routine)(void),void arg);
参数:
void (start_routine)(void) --函数指针,返回值void,参数也是void*,指向的函数参数和返回值也是void*
void* arg – 作为start_routine的参数,传入指向的函数
注意执行编译命令时,需要添加第三方库 -lpthread

#include <iostream>
#include <unistd.h>
#include <ctime>

int gval = 100;
// 新线程
// 0x1111~0x2222
void* threadStart(void* args)
{
    while (true)
    {
        sleep(1);

        // int x = rand() % 5;

        std::cout << "new thread running..." << ", pid: " << getpid()
            << ", gval: " << gval << ", &gval: " << &gval << std::endl;
        //测试一个线程异常对于其他线程的影响
        // if(x == 0)
        // {
        //     int *p = nullptr;
        //     *p = 100; // 野指针
        // }
    }
}
// 0x3333~0x4444
int main()
{
    srand(time(nullptr));

    pthread_t tid1;
    pthread_create(&tid1, nullptr, threadStart, (void*)"thread-new");

    pthread_t tid2;
    pthread_create(&tid2, nullptr, threadStart, (void*)"thread-new");

    pthread_t tid3;
    pthread_create(&tid3, nullptr, threadStart, (void*)"thread-new");
    // 主线程
    while (true)
    {
        std::cout << "main thread running..." << ", pid: " << getpid()
            << ", gval: " << gval << ", &gval: " << &gval << std::endl;

        gval++; // 修改!
        //验证主线程修改全局变量,导致其他线程访问该变量时,变量值也受到影响。
        sleep(1);
    }
    return 0;
}

发现两个执行流,使用同一个进程。验证了线程是进程内部运行的。

ps -aL – 查看线程(轻量级进程)

看到执行代码后的两个线程,且PID相同,TTY和TIME也相同,唯独 LWD(light weight process轻量级进程ID)不同
思考,OS调度时,看到的Pid还是LWP呢?
答:LWP,否则无法区分。
第一个LWP为主线程,依次排序

问题:
a.已经有多进程了,为什么要有多线程呢?

答:因为进程的创建是独立的,成本高,需要单独的PCB,页表,地址空间,物理地址空间,内存块等。
而线程共用一个进程PCB,只需要把已有的资源分配给线程调度执行就行。运行效率高,页表不用换,地址也指向同一块区域。删除直接销毁线程即可。
但是线程的缺点,在于什么都共享一旦一个线程出问题,会导致影响其他线程的任务。
联想野指针或非法访问等结合理解。

b.不同系统对于进程和线程的实现方式存在不同。

答:虽然实现方式/算法不同,但是原则原理相同

c、为什么线程调度成本越低?

答:分为两个层面理解:
软件层面:进程的调度需要单独生成,地址空间,页表,PCB等,而线程只需要给它PCB即可。
硬件层面:cache是集成在CPU中的硬件,遵循局部性原理,可以直接缓存数据到该区域这类数据被称为热数据,如果其中正好有CPU需要的数据,则极快的效率完成了数据的交互。
lscpu可查看CPU的一系列信息
所以对于进程B的热数据来说,拿到cache中对于进程A可能用不上,相反A的B也用不上,而对于线程,多个线程共用一个PCB那么拿到需要的热数据概率更高。
因此,调度成本更低。

每一块代码在汇编后都会转换成地址,给各个线程的地址指向同一程序的不同地址就是再划分地址,实现同时跑死循环。
也就是划分地址空间代码段,划分页表的一部分…
那么跟上面提到的呼应,虚拟地址是连续的,也就是可以拆分,即划分地址空间。

呼应内核层面的进程定义:承担分配系统资源的基本实体。

那么一个进程执行的代码就是一块OS呢?
所以验证OS也是进程。

主线程修改全局变量,导致其他线程访问该变量时,变量值也受到影响。
大部分地址空间上的资源,多线程都是共享的。

1.进程是资源分配的基本单位
2.线程是调度的基本单位
3.线程共享进程数据,但也拥有自己的一部分数据:
a、线程ID
b、一组寄存器
c、栈
d、errno
e、信号屏蔽字
f、调度优先级

单独说说线程中私有的部分:
b.一组寄存器:

线程要有自己独立的寄存器,存储硬件数据的上下文。-- 反映线程是属于动态运行的。

c.栈:

每个线程或者为了理解单说函数,会定义各自的临时变量等数据,根据执行到该线程后执行函数调用后,会在栈空间中进行临时数据的处理。
如果使用共同的地址空间栈,则各个线程数据会相互干扰。
所以线程在运行时,会形成各种临时变量,临时变量会被每个线程保存在自己的栈区。

4.共享机制:

进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到。

5.除此之外,各线程还共享以下进程资源和环境:

a、文件描述符表
b、每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
c、当前工作目录
d、用户id和组id

3、线程的控制(创建、终止、等待和分离)

抽象层面:
用户只认识线程,而Linux底层并没有单独去封装设计实现线程,只有进程模拟的轻量级进程,也就是线程了。
那么用户想要使用,那么Linux系统就只给上层用户提供创建轻量级进程的接口。
linux系统不会给接口名,就有了pthread库,对轻量级进程接口进程封装,方便用户使用,按照线程的接口方式交给用户使用。
pthread库,linux自带的原生线程库,所以属于用户级线程
Windows属于内核级线程。

第三方库:-lpthread
g++ -o $@ $^ -std=c++11 -lpthread

#include <pyhread.h>
int pthread_join(pthread_t thread,void **retval);
功能:等待一个线程
参数1:pthread_t thread要等待的线程
参数2void **retval,输出型参数
//线程控制
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <vector>

class ThreadData
{
public:
	int Excute()
	{
		return x + y;
	}
public:
	std::string name;
	//int num;
	//....
	int x;
	int y;
};

class ThreadResult
{
public:
	std::string print()
	{
		return std::to_string(x) + "+" + std::to_string(y) + "=" + std::to_string(result);
	}
public:
	int x;
	int y;
	int result;
};

void* threadRun(void* args)
{
	ThreadData* td = static_cast<ThreadData*>(args);//静态强转,原本正常形式是这样的:(ThreadData*)args
	ThreadResult* Result = new ThreadResult();
	int cnt = 10;
	while (cnt)
	{
		std::_Ptr_cout << td->name << "run ..." << " , cnt: " << cnt-- << std::endl;
		//赋值
		Result->result = td->Excute();
		Result->x = td->x;
		Result->y = td->y;
		sleep(1);
		break;//计算一次就break
	}
	//释放空间
	delete td;
	return (void*)result;
}

std::string PrintToHex(pthread_t& tid)
{
	char buffer[64];
	sprintf(buffermsizeof(buffer), "0x%lx", tid);
	return buffer;
}

int main()
{
	pthread_t tid;//usigned long int

	//返回值类型问题 -- 布置任务
	ThreadData* td = new ThreadData();
	td->name = "thread-1";
	td->x = 10;
	td->y = 20;
	int n = pthread_create(&tid, nullptr, threadRun, td);

	//问题3:tid是什么样子的呢?虚拟地址
	std::string tid_str = PrintToHex(tid);
	std::wcout << "tid : ", "tid_std << std::endl";

	std::cout << "main thread error" << std::endl;

	ThreadData* result = nullptr;
	n = pthread_join(tid, (void**)&result);//z保证能阻塞等待到new线程的退出
	if (n == 0)
	{
		std::cout << "main thread wait success,new thread exit code: " << result->print() << std::endl;
	}
	return 0;
}

问题1:main和new线程谁先运行呢?不能确定
问题4:给回调函数的参数问题。

int a = 100;//传入参数2
可以是任意类型。说明:一旦可以传入结构体等类型,就可以给传递多个参数了,甚至是传递方法。
传入参数3,但是目前这种属于在主线程中开辟的栈区进行访问,本质是多个线程访问主线程的栈区,但上节知识点讲了不建议这么干。数据紊乱
所以为了解决此类冲突问题,往往是开辟指针数组管理每一个独立的栈空间

问题3:tid是什么样子的呢?虚拟地址

问题2:我们期望谁最后退出呢?main 的 thread

如何保证是main先退出呢?pthread_join等待 == wait
如果不使用pthread_join,main先退出,代表进程就退出了,就导致了进程中所有线程结束,可能导致线程未完成工作就退出了,就导致了类似于僵尸进程的情况。
说明通过返回值的拿到的退出信息回到主线程就能知道,让新线程完成的任务的情况了,是完成还是失败等等。

问题5:全面看待线程函数的返回值
1.线程的返回值只能是正确的返回值,各个子线程不能出错,崩溃,否则讲影响整个线程崩溃,比如线程运行出现野指针崩溃,是直接向进程发信号,所以进程崩溃导致所有线程就都崩溃了。
所以线程在退出的时候,只需要考虑正确的返回,不考虑异常和崩溃的情况,因为一旦崩溃这个进程包括PCB就出问题了,包括主线程的所有线程随着进程的崩溃接收的停止信号都会受影响。
2.返回值二级指针传出型参数,也可以是结构体等任意类型

如何创建多线程呢?

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <stdlib.h>


const int num = 10;

void* threadrun(void* args)
{
	std::string name = static_cast<const char*>(args);
	while (true)
	{
		std::cout << name << " is running" << std::endl;//执行后,发现存在一个问题,不是每一个线程名都会被打印出来,而是随机的,原因是线程的调度是不可控的。所以线程名是不断会被覆盖的。
		sleep(1);
		break;
	}

	//问题7:exit属于进程退出
	//exit(1);//任意一个子线程调用了,进程退出的函数,那么整个线程就都退出了。
	//所以引出线程结束函数pthread_exit();
	pthread_exit(args);//专门用于终止一个线程的
}

std::string PrintToHex(pthread_t& tid)
{
	char buffer[64];
	sprintf(buffermsizeof(buffer), "0x%lx", tid);
	return buffer;
}

int main()
{
	std::vector<pthread_t> tids;//线程集

	//问题6:如何创建多线程呢?
	for (int i = 0; i < num ;i++)
	{
		//1.有线程的id
		pthread_t tid;
		//2.线程的名字
		//char name[128];//所以不能简单的这样写,而是使用new
		//snprintf(name, sizeof(name), "thread-%d", i + 1);

		char* name = new char[128];
		snprintf(name, 128, "thread-%d", i + 1);
		pthread_create(&tid, nullptr, threadrun,/*线程的名字*/name);

		//3.保存所有线程的id信息 -- base
		//tids.emplace_back(tid); 
		tids.push_back(tid);
	}

	//多线程的等待 -- join
	for (auto tid : tids)
	{
		//指针变量调用线程名字的返回值
		void* name = nullptr;
		pthread_join(tid, &name);
		std::cout << (const char*)name << " quit..." << std::endl;
		//释放各个线程空间
		delete (const char*)name;
	}
	return 0;
}

问题7:线程如何终止?

1.函数正常结束的return
2.main函数结束,man和thread都结束,表示进程结束;而子进程结束了,不代表整个进程结束。
所以尽量让主线程最后退出。
3.exit属于进程退出
exit(1);//任意一个子线程调用了,进程退出的函数,那么整个线程就都退出了。
所以引出线程结束函数pthread_exit();
#include <pthread.h>
void pthread_exit(void* retval);
4.还有一个线程退出的函数pthread_cancel();
#include <pthread.h>
int pthread_cancel(pthread_t thread);
功能:取消一个线程(前提是该线程是存在的)
返回值:有符号的整数

线程的取消

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <stdlib.h>


const int num = 10;

void* threadrun(void* args)
{
	std::string name = static_cast<const char*>(args);
	while (true)
	{
		std::cout << name << " is running" << std::endl;//执行后,发现存在一个问题,不是每一个线程名都会被打印出来,而是随机的,原因是线程的调度是不可控的。所以线程名是不断会被覆盖的。
		sleep(3);
	}
	pthread_exit(args);//专门用于终止一个线程的
}

std::string PrintToHex(pthread_t& tid)
{
	char buffer[64];
	sprintf(buffermsizeof(buffer), "0x%lx", tid);
	return buffer;
}

int main()
{
	std::vector<pthread_t> tids;//线程集

	//问题6:如何创建多线程呢?
	for (int i = 0; i < num; i++)
	{
		//1.有线程的id
		pthread_t tid;
		//2.线程的名字
		char* name = new char[128];
		snprintf(name, 128, "thread-%d", i + 1);
		pthread_create(&tid, nullptr, threadrun,/*线程的名字*/name);

		//3.保存所有线程的id信息 -- base
		tids.push_back(tid);
	}

	//多线程的等待 -- join
	for (auto tid : tids)
	{
		pthread_cancel(tid);//取消线程
		std::cout << "cancel: " << tid << std::endl;

		//指针变量调用线程名字的返回值
		void* result = nullptr;//发现取消线程的返回值是-1 PTHREAD_CANCELED
		//PTHREAD_CANCELED; ---> 原型:#define PTHREAD_CANCELED ((void*)-1)
		pthread_join(tid, &result);
		
		std::cout << (long long int)result << " quit..." << std::endl;
		//释放各个线程空间
		//delete (const char*)result;
	}
	//sleep(50);//主线程
	return 0;
}

问题7小结:

1.线程函数 return
2.pthread_exit();
3.main thread中调用pthread_cancel,新线程的返回值为-1

题8:可以不可以不join线程呢?让它执行完就退出呢?

可以,使用pthread_detach();
1.一个线程被创建,默认是joinable的,必须是要被join的
2.如果一个线程被分离,线程的工作状态分离状态,是不需要/不能被join的

接口:

man pthread_detach
#include <pthread.h>
int pthread_detach(pthread_t thread);
功能:线程的分离
#include <pthread.h>
pthread_t pthread_self(void);
功能:获取对应线程的线程id == 类似于getpid()

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <stdlib.h>


const int num = 10;

void* threadrun(void* args)
{
	//分离
	pthread_detach(pthread_self());
	std::string name = static_cast<const char*>(args);
	while (true)
	{
		std::cout << name << " is running" << std::endl;//执行后,发现存在一个问题,不是每一个线程名都会被打印出来,而是随机的,原因是线程的调度是不可控的。所以线程名是不断会被覆盖的。
		sleep(3);
		break;
	}
	pthread_exit(args);//专门用于终止一个线程的
}

std::string PrintToHex(pthread_t& tid)
{
	char buffer[64];
	sprintf(buffermsizeof(buffer), "0x%lx", tid);
	return buffer;
}

int main()
{
	std::vector<pthread_t> tids;//线程集

	//问题6:如何创建多线程呢?
	for (int i = 0; i < num; i++)
	{
		//1.有线程的id
		pthread_t tid;
		//2.线程的名字
		char* name = new char[128];
		snprintf(name, 128, "thread-%d", i + 1);
		pthread_create(&tid, nullptr, threadrun,/*线程的名字*/name);

		//3.保存所有线程的id信息 -- base
		tids.emplace_back(tid);
	}

	while (true)
	{
		sleep(50);//主线程做自己的事了,不必再join等待了
	}
	
	return 0;
}

//现象是禁止主线程等待,说明一个线程被分离了就不需要再去join了
//并且子线程出现崩溃,依旧满足线程的性质,一个崩溃导致进程收到崩溃信号,都得崩溃
//也可以由主线程来进行分离操作

1.理解库

库内部也要对线程进行管理
我们要创建线程,前提是把库加载到内存,映射到我进程的地址空间中。

动态库(libpthread.so):
struct_pthread;(struct tcb;)
线程局部存储;(线程在用户级最基本的属性)
线程栈。(独立的栈结构)

库如何做到对线程进行管理呢?(先描述,再组织)
描述:库中创建描述线程的相关结构体字段, 组织:“数组”

未来我们只要找到线程控制块的地址即可;
pthread_t id 就是一个地址

Linux线程 = pthread库中线程的属性集 + LWP(类似于进程的PCB)
有线程自己的系统调用

tid: 线程属性集合的起始虚拟地址 — 在pthread库中维护。

4、Linux线程的实现

1、线程的封装和线程的互斥-- 黄牛抢票

多个线程能够看到的资源 – 共享资源
对共享资源进行保护 – 同步/互斥

#include <iostream>
#include <vector>
#include <cstdio>
#include <unistd.h>
#include "Thread.hpp"

using namespace ThreadMoudle;
 
int tickets = 10000;//共享资源,造成了数据不一致的问题

void route(const std::string& name)
{
    while (true)
    {
        if (tickets > 0)
        {
            // 抢票过程
            usleep(1000); // 1ms -> 抢票花费的时间
            printf("who: %s, get a ticket: %d\n", name.c_str(), tickets);
            tickets--;
        }
        else
        {
            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();
}
#include <iostream>
#include <cstdio>
#include <string>
#include <unistd.h>
#include <pthread.h>

// Linux有效, __thread只能修饰内置类型
__thread int gval = 100;

std::string ToHex(pthread_t tid)
{
    char id[128];
    snprintf(id, sizeof(id), "0x%lx", tid);
    return id;
}

void* threadrun(void* args)
{
    std::string name = static_cast<const char*>(args);
    while (true)
    {
        std::string id = ToHex(pthread_self());
        std::cout << name << " is running, tid: " << id << ", gval: " << gval << ", &gval: " << &gval << std::endl;
        sleep(1);
        gval++;
    }
}

int main()
{
    pthread_t tid; // 线程属性集合的起始虚拟地址--- 在pthread库中维护
    pthread_create(&tid, nullptr, threadrun, (void*)"thread-1");

    while (true)
    {
        std::cout << "main thread, gval: " << gval << ", &gval: " << &gval << std::endl;
        sleep(1);
    }
    pthread_join(tid, nullptr);
    return 0;
}
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>

namespace ThreadMoudle
{
    struct ThreadData
    {
    public:
        ThreadData(const std::string &name,pthread_mutex_t* lock)
            :_name(name)
            ,_lock(lock)
        {}
    public:
        std::string name;
        pthread_mutex_t* lock;
    };

    typedef void (*func_t)(ThreadData* td);

    // 线程要执行的方法,后面我们随时调整
    //typedef void (*func_t)(const std::string& name); // 函数指针类型
    //using func_t = std::binary_function<void()>;
    //typedef std::binary_function<void()> func_t;

    class Thread
    {
    public:
        void Excute()
        {
            std::cout << _name << " is running" << std::endl;
            _isrunning = true;

            _func(_td);
           // _func(_name);
            _isrunning = false;
        }
    public:
        Thread(const std::string& name, func_t func, ThreadData* td)
            :_name(name)
            , _func(func)
            ,_td(td)
        {
            std::cout << "create " << name << " done" << std::endl;
        }
        static void* ThreadRoutine(void* args) // 新线程都会执行该方法!
        {
            Thread* self = static_cast<Thread*>(args); // 获得了当前对象
            self->Excute();
            return nullptr;
        }
        bool Start()
        {
            int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this);
            if (n != 0) return false;
            return true;
        }
        std::string Status()
        {
            if (_isrunning) return "running";
            else return "sleep";
        }
        void Stop()
        {
            if (_isrunning)
            {
                ::pthread_cancel(_tid);
                _isrunning = false;
                std::cout << _name << " Stop" << std::endl;
            }
        }
        void Join()
        {
            ::pthread_join(_tid, nullptr);
            std::cout << _name << " Joined" << std::endl;
            delete _td;
        }
        std::string Name()
        {
            return _name;
        }
        ~Thread()
        {
        }

    private:
        std::string _name;
        pthread_t _tid;
        bool _isrunning;
        func_t _func; // 线程要执行的回调函数
        ThreadData* _td;
    };
}

2.认识和分析为什么会出现抢到负数的问题?多线程的并发问题

a、直接原因
b、扩展的风险问题
计算机的运算类型:1.算数运算 2.逻辑运算
CPU内,寄存器只有一套,但是寄存器里面的数据,可以有多套,属于线程私有,看起来放在了一套公共的寄存器中,但是属于线程私有,当他被切换的时候,
他要带走自己的数据,回来的时候,会恢复。
tickets–:C语言 --》汇编
1.重读数据
2.–数据
3.写回数据

如何解决多线程的并发问题?加锁(先得有锁)

#include <iostream>
#include <vector>
#include <cstdio>
#include <unistd.h>
#include "Thread.hpp"
#include "LockGuard.hpp"

using namespace ThreadMoudle;

int tickets = 10000;//共享资源,造成了数据不一致的问题

//定义全局锁
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;
//优化锁
void route(ThreadData* td)
{

    while (true)
    {
        LockGuard lockguard(td->_lock);//调用构造函数自动加锁,自动释放
        //加锁

        if (tickets > 0)
        {
            // 抢票过程
            usleep(1000); // 1ms -> 抢票花费的时间
            printf("who: %s, get a 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::_String_iterator(i + 1);
        ThreadData* td = new Threaad(name, &mutex);
        threads.emplace_back(name, route, td);
    }

    for (auto& thread : threads)
    {
        thread.Start();
    }

    for (auto& thread : threads)
    {
        thread.join();
    }
    //释放锁
    pthread_mutex_destory(&mutex);
}
#pragma once

class LockGuard
{
public:
	LockGuard(pthread_t* mutex)
		:_mutex(mutex)
	{
		pthread_mutex_lock(_mutex);
	}
	~LockGuard()
	{
		pthread_mutex_unlock(_mutex);
	}
private:
	pthread_mutex_t* mutex;
};

1.认识锁和它的接口

man pthread_mutex_init
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t* mutex);
int pthread_mutex_init(pthread_mutex_t* restrict mutex,const pyhread_mutexattr_t* restrict attr);//而锁是由malloc,new开辟的就需要使用该函数进行初始化, attr一般给NULL
pthread_mutex_t mutex = PTHREAD_MUTEX_INITALTZER;//全局锁初始化,即锁是全局的或者静态的,只需要init即可,不需要destory。
pthread_mutex_t:互斥锁类型
锁:任何时刻只允许一个线程进行资源访问。
有了锁,如何保护临界区呢?加锁
man pthread_mutex_lock
int pthread_mutex_lock(pthread_mutex_t* mutex);//加锁失败,阻塞线程等待锁
int pthread_mutex_trylock(pthread_mutex_t* mutex);//加锁失败出错返回,不阻塞线程
int pthread_mutex_unlock(pthread_mutex_t* mutex);//解锁
被保护的全局资源称为临界资源;
访问临界资源的代码称为临界区;
不属于访问临界资源的代码称为非临界区;
所谓的对临界资源进行保护,就是对临界区代码的保护
所有的资源都是通过代码来访问的;保护资源,本质就是想办法将访问资源的代码进行起来。

加锁和解锁的原则:

1.加锁的范围,尽可能的准确,粒度一定尽可能小。
2.任何线程,要进行抢票,都得先申请锁,原则上,不允许有特例。
3.所有线程都申请锁,前提是所有线程都能看到这把锁,即锁本身也是共享资源。 — 加锁的过程,必须是原子的。
4.原子性:对于一个操作,要么不做,要么就一步到位的做完,没有中间状态就是原子的。
5.如果线程申请锁失败了,我的线程要被阻塞。
6.如果线程申请锁成功了,继续向后运行。
7.如果一个线程申请锁成功了,执行临界区的代码了。

那么执行临界区代码期间,可以切换吗?

可以,但是其他没枪到锁的线程仍然无法进入临界区,因为即便切换走了,回来解锁之前,都是处于阻塞的。

2.原理角度理解这个锁

pthread_mutex_lock(&mutex);

如何理解申请锁成功,允许你进入临界区; – 》申请锁成功,pthread_mutex_lock()函数会返回 —》pthread_mutex_unlock();
如何理解申请锁失败,不允许你进入临界区; – 》申请锁失败(锁没有就绪),pthread_mutex_lock()函数不返回,线程就是阻塞 -->在pthread_mutex_lock()内部被重新唤醒,重新申请锁

结论:所以对于其它线程,要么我没有申请锁,要么我释放了锁,对其他线程才有意义。
即:我访问临界区,对其它线程是原子性的。

3.从实现角度理解锁

为了实现互斥锁操作,大多数体系结构都提供了swap和exchange指令,该指令的作用是把寄存器和内存单元的数据相互交换,由于只有一条指令,从而保证了原子性。
即使是多处理器的平台,访问内存,总线周期也会有先后,一个处理器上的交换指令执行时,另一个处理器的交换指令只能等待总线周期。

1.CPU的寄存器只有一套,被所有线程共享,但是寄存器里面的数据,属于执行流的上下文,属于执行流的私有的数据。
2.CPU在执行过程中,一定要有对应的执行载体 — 线程 && 进程。
3.数据在内存中,是被所有线程共享的。

结论:把数据从内存移动到CPU寄存器中,本质就是把数据从共享,变成线程私有。

了解底层:多个线程只有一个线程能交换到1,其余的交换到0
所谓互斥(加锁/解锁)就是一个申请成功的锁,0交换到1,其余去交换就只能和0交换,知道解锁归还1,让其它线程再次申请交换。

5、线程的优缺点

5.1、线程的优点

1.创建一个新线程的代价要比创建一个新进程小得多
2.与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
3.线程占用的资源要比进程少很多
4.能充分利用多处理器的可并行数量
5.在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
6.计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
7.I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

5.2、线程的缺点

1.性能损失:
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型
线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的
同步和调度开销,而可用的资源不变。
2.健壮性降低:
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了
不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
3.缺乏访问控制:
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
4.编程难度提高:
编写与调试一个多线程程序比单线程程序困难得多

6、线程异常

1.单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
2.线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

7、线程用途

1.合理的使用多线程,能提高CPU密集型程序的执行效率
2.合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值