Linux学习七(线程)

Linux学习七 : 线程

线程概述

线程是轻量级的进程:在操作系统中,将一个进程划分为多个执行单元,每个单元拥有自己的堆栈、程序计数器和资源使用情况,但共享同一进程的地址空间和文件描述符等资源,这些执行单元就是线程。进程是资源分配的最小单位,线程是系统调度的最小单位。

线程特点:

  • 线程的创建、切换和销毁都更加高效,占用的系统资源少。
  • 线程提供了一种并发执行的机制,使得多个任务可以在同一个进程中并行执行。
  • 不同线程之间可以通过共享内存等机制进行通信和同步,数据共享更加便捷,且避免了进程切换的开销。
  • 线程能够利用多核处理器的并发执行有事,提高系统的吞吐能力,一定程度下可以提升系统性能。CPU有多少个核就可以开多少个线程。
  • 由于线程共享进程的地址空间,因此需要注意线程之间的数据竞争和同步,调度资源并发编程问题,充分考虑线程安全和并发控制,以确保程序的正确性和稳定性。

线程的实现主要通过引入线程组的概念来实现。每个进程都有一个主线程,也就是创建该进程的线程,当创建新的线程时,新的线程将与主线程一起组成一个线程组。

多进程模型:每个进程都有独立的地址空间、文件表信号表等资源。
多线程模型:线程组内线程共享地址内存空间,文件表信号表等资源。

实现多线程的目的:为了更高效的利用CPU资源,线程相比于进程,创建、切换、通信、终止成本降低。线程(任务)的数量要根据具体的处理器数量来决定。假设只有一个处理器,那么划分太多线程可能会适得其反。因为很多时间都花在任务切换上了。
因此,在设计并发系统之前,一方面我们需要做好对于硬件性能的了解,另一方面需要对我们的任务有足够的认识。

线程的缺点

  • 安全性差:线程相互耦合,一个线程出错会影响其他线程。
  • 编程难度大:
  • 调试困难
  • 竞态条件和死锁:需要线程同步管理。

线程操作

创建线程

pthread_create:Linux下用于创建线程的函数,需要给线程一个处理函数,否则线程无法工作。
线程属性:可以在线程创建时指定的一组特性,用于控制线程的行为和特性。比如线程调度优先级,调度策略,线程栈空间大小。
pthread_self():获取线程id。

线程退出:只要调用该函数当前线程就马上退出了,并且不会影响到其他线程的正常运行,不管是在子线程或者主线程中都可以使用。

#include <pthread.h>
void pthread_exit(void *retval);retval是子线程的主线程会得到该数据。如果不需要使用,指定为NULL

线程回收:等待一个线程结束并回收器返回值和线程资源。pthread_join(),这个函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程。

#include <pthread.h>
// 这是一个阻塞函数, 子线程在运行这个函数就阻塞
// 子线程退出, 函数解除阻塞, 回收对应的子线程资源, 类似于回收进程使用的函数 wait()
int pthread_join(pthread_t thread, void **retval);

回收子线程数据的方法:

  • 用子线程栈区是不可以的,因为每个线程都有一个独立的栈区,子线程结束以后,数据不能回传给主线程。
  • 用堆区和全局数据区,是可以的,位于同一虚拟地址空间中的线程,虽然不能共享栈区数据,但是可以共享全局数据区和堆区数据,因此在子线程退出的时候可以将传出数据存储到全局变量、静态变量或者堆内存中。
  • 回传到主线程栈区是可以的,一般情况下主线程栈区都是最后退出的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <unistd.h>
#include <string.h>
#include <iostream>
#include <pthread.h>

using std::cout;
using std::cerr;
using std::endl;

struct Person
{
int id;
std::string name;
int age;
};

struct Person Person_GLOBAL; // 定义全局变量

void* workingThread1(void* arg)
{
struct Person p;
p.age = 27;
p.name= "liubei";
p.id = 1;
pthread_exit(&p);// 该函数的参数将这个地址传递给了主线程的pthread_join()
return NULL; // 代码执行不到这个位置就退出了
}

void* workingThread2(void* arg)
{
Person_GLOBAL.age = 25;
Person_GLOBAL.name= "guanyu";
Person_GLOBAL.id = 2;
pthread_exit(&Person_GLOBAL);
return NULL;
}

void* workingThread3(void* arg)
{
struct Person* p = (struct Person*)arg;
p->age = 24;
p->name= "zhangfei";
p->id = 3;
pthread_exit(&p);
return NULL;
}

int main(){
// 创建子线程
pthread_t tid1;
pthread_create(&tid1, NULL, workingThread1, NULL);
void* ptr1 = NULL;
// ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存
// 这个内存地址就是pthread_exit() 参数指向的内存
pthread_join(tid1, &ptr1);
struct Person* p1 = (struct Person*)ptr1;
cout<<"线程栈返回数据:"<<p1->id<<" "<<p1->age<<" "<<p1->name<<endl;

pthread_t tid2;
pthread_create(&tid2, NULL, workingThread2, NULL);
void* ptr2 = NULL;
pthread_join(tid2, &ptr2);
struct Person* p2 = (struct Person*)ptr2;
cout<<"全局变量返回数据:"<<p2->id<<" "<<p2->age<<" "<<p2->name<<endl;

pthread_t tid3;
struct Person p;
pthread_create(&tid3, NULL, workingThread3, &p);
void* ptr3 = NULL;
pthread_join(tid3, &ptr3);
cout<<"主线程栈返回数据:"<<p.id<<" "<<p.age<<" "<<p.name<<endl;
}

最终运行的结果中子线程栈空间的数据丢失。
在这里插入图片描述

线程分离:将一个线程从它的创建线程中分离出来,使得该线程不会成为“僵尸线程”,从而避免资源泄露和内存占用的问题。不需要其他线程调用pthread_join()函数来等待其终止,但是在终止后不能再重连。

pthread_detach(tid);线程分离。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <unistd.h>
#include <string.h>
#include <iostream>
#include <pthread.h>

using std::cout;
using std::cerr;
using std::endl;

void* working(void* arg)
{
cout<<"子线程ID: "<<pthread_self()<<endl;
cout<<"参数: "<<(char*)arg<<endl;
return NULL;
}

void create(){
pthread_t tid;
char* arg= "hello";
int ret = pthread_create(&tid, NULL, working, arg);
if(ret == -1){
cerr<<"pthread_create"<<endl;
return -1;
}

// 设置子线程和主线程分离
pthread_detach(tid);
// 让主线程自己退出即可
pthread_exit(NULL);
}

int main()
{
create();
return 0;
}

线程终止

  • 自然终止
  • 调用exit()函数:立即终止整个进程,包括所有线程。
  • pthread_cancel()函数:线程取消
  • pthread_kill()函数:杀死线程。
  • pthread_exit()函数:显式退出线程

线程取消:在一个线程中杀死另一个线程,需要分两步:

  1. 在线程A中调用线程取消函数pthread_cancel,指定杀死线程B,这时候线程B是死不了的。
  2. 在线程B中进程一次系统调用(从用户区切换到内核区),否则线程B可以一直运行。

线程取消的类型:

  • 异步取消:立即取消线程,但是可能会出现无法清理,资源泄露的情况。
  • 延迟取消:在取消点检查是否请求取消。如IO操作,线程等待函数等。

pthread_cancel()函数:线程取消。
pthread_setcanceltype()函数:设置线程取消的取消类型。

C++线程类

C++11中提供的线程类为std::thread,基于这个类创建一个新的线程非常的简单,只需要提供线程函数或者函数对象即可,并且可以同时指定线程函数的参数。作为一个类,类的构造函数,移动构造都有,但是不允许拷贝线程对象。

std::thread t1(); 可以传线程执行函数,以及函数所需参数。
std::thread::id get_id() const noexcept; 获取线程ID。
void join(); 回收线程
void detch(); 线程分离
bool joinable() const noexcept; 用于判断主线程和子线程是否处理关联(连接)状态。
// move (1)
thread& operator= (thread&& other) noexcept;
// copy [deleted] (2)
thread& operator= (const other&) = delete; 线程中的资源是不能被复制的,因此通过=操作符进行赋值操作最终并不会得到两个完全相同的对象。只能进行线程资源的转移,直接拷贝赋值是不被允许的。
static unsigned hardware_concurrency() noexcept; 获取当前计算机的CPU核数,根据这个结果在程序中创建出数量相等的线程,每个线程独自占有一个CPU核心,这些线程就不用分时复用CPU时间片,此时程序的并发效率是最高的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include<iostream>
#include<thread>
#include<memory>

using std::cout;
using std::endl;
using std::thread;
using std::string;

class A{
public:
void func(){
cout<<"hello func"<<endl;
}
};


void printHW(string msg){
cout<<msg<<endl;
}

int main(){
int num = thread::hardware_concurrency();
cout << "CPU number: " << num << endl;
thread t1(printHW,"Happy new year");
if(t1.joinable()){
t1.join();// 等待t1执行完成
}

std::shared_ptr<A> a = std::make_shared<A>();
thread t2(&A::func, a);
if(t2.joinable()){
t2.join();// 等待t1执行完成
}
return 0;
}

参考列表
https://subingwen.cn/linux/thread/
https://www.bilibili.com/video/BV1Xb4y1u7gR/
https://www.bilibili.com/video/BV1d841117SH


Linux学习七(线程)
https://cauccliu.github.io/2024/03/26/Linux学习七(线程)/
Author
Liuchang
Posted on
March 26, 2024
Licensed under