Misaki`s blog

学习是一种态度


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

【Cpp】第二章-类和对象-下

发表于 2019-05-28 | 分类于 Cpp
字数统计: 2.3k

类和对象

初始化列表

  我们之前想要在类创建时就对类进行初始化时使用构造函数直接在构造函数中对成员变量进行赋值。但是这种方法并非是最好的方法,并且有一些情况比如说常成员函数我们就无法在构造函数中初始化,因此有了新的对成员进行初始化的方法——初始化列表。

使用

  初始化列表是和构造函数写在一起的,会在执行成员函数函数体之前优先利用初始化列表对成员变量赋予初值。

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
#include <iostream>
using std::cout;
using std::endl;
class A
{
public:
A(int x = 4, int y = 5):_x(x), _y(y)
{

}
int GetX()
{
return _x;
}
int GetY()
{
return _y;
}
private:
int _x;
int _y;
};
class Test
{
public:
Test(int a, int& b, int c, A d):_a(a), _b(b), _c(c), _d(d)
{
cout << _a << "\t" << _b << "\t" << _c << "\t" << _d.GetX() << "\t" << d.GetY() << endl;
cout << "building seccess" << endl;
}
private:
const int _a;
int& _b;
int _c;
A _d;

};
int main()
{
int b = 2;
A d;
Test(1, b, 3, d);
}

  在初始化列表中进行成员的初始化是最优的,因为对于自定义的类一定会先用初始化列表进行初始化。

特性

  1、有几种一定需要在初始化列表中才能进行初始化的成员变量:常成员,引用,没有默认构造函数的自定义类类型成员。

  2、类类型成员不论是否在初始化列表中是否显示初始化都会自动在初始化列表中进行构造函数的调用。

  3、每个成员在初始化列表中只能出现一次。

  4、初始化顺序与初始化列表无关,只与成员在类中声明先后有关。

  5、优先在初始化列表中初始化所有成员,尤其是类类型成员。

explicit关键字

  我们在对对象进行初始化的时候可以进行这样的构造。

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
#include <iostream>               
using std::cout;
using std::endl;
class A
{
public:
A(int x, int y = 5):_x(x), _y(y)
{

}
int GetX()
{
return _x;
}
int GetY()
{
return _y;
}
private:
int _x;
int _y;
};
int main()
{
A a = 4;//等同A a(4);
cout << a.GetX() << "\t" << a.GetY() << endl;
}

  这种构造函数的调用仅适用于调用只有一个参数的构造函数,这种情况下可以达到隐式转换的效果。但是如果我们不想支持这种隐式转换呢?我们就可以在构造函数前加上explicit关键字,这样就可以仅用构造函数的隐式转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
explicit A(int x, int y = 5):_x(x), _y(y)
{

}



[misaki@localhost 第二章-类和对象]$ make
g++ -g Init.cpp -o Init
Init.cpp: 在函数‘int main()’中:
Init.cpp:40:9: 错误:请求从‘int’转换到非标量类型‘A’
A a = 4;
^
make: *** [Init] 错误 1

static静态成员

静态成员变量和静态成员函数

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
#include <iostream>
using namespace std;
class Test
{
public:
Test()
{
_num++;
}
static int GetNum()
{
return _num;
}
private:
static int _num;//静态成员变量
};
int Test::_num = 0;//在类外进行初始化
int main()
{
Test test[4];
cout << Test::GetNum() << endl;//用类+域限定符调用静态成员函数
}


[misaki@localhost 第二章-类和对象]$ ./static
4

  静态成员是属于整个类的成员,同时也是属于所有类的实例化对象的成员,它们在类被定义后即被初始化,静态成员可以被所有的实例化对象操作。

  以上这个代码我们用静态成计算类的构造函数调用次数。

特性

  1、静态成员变量必须在类外进行初始化。

  2、静态成员函数中不含this指针因此不可以使用非静态成员。

  3、静态成员属于这个类和所有实例对象因此用类或者对象都可以调用静态成员,前提其是公有。

  4、非静态成员函数也可以调用静态成员。

static作用

  这里我们又了解了static的新的作用,我们总结一下static一共有哪些作用。
  1、修饰局部变量,改变生命周期。

  2、修饰全局变量,改变链接属性,使其只在当前文件可见。

  3、修饰成员函数,将其变为静态,无this指针。

  4、修饰成员变量,将其变为静态成员变量。

为成员变量设置默认值(C++11)

  这个语法只有在支持11版本的编译器上才可以使用,这种语法相当于给成员变量设定了默认值,十分类似于参数缺省。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;
class Test
{
public:
Test(){}
int _a = 1;
int _b = 2;
};
int main()
{
Test t;
cout << t._a << "\t" << t._b << endl;
}


[misaki@localhost 第二章-类和对象]$ ./default
1 2

  这种设置默认值的语法如果我们并没有在构造函数中给成员变量赋值则会将其赋为这里的默认值。

友元

  有的时候我们想要在外部使用一个对象的成员变量但是又不想破坏其封装的时候就需要借助友元的语法,如果一个函数或者类是另一个类的友元,则这个函数或者类就可以自由使用另一个类中的私有成员。

友元函数

  例如我们要书写一个<<的重载函数让其可以按照我们的规则打印类中的成员,这个函数如果写在类中则左操作数永远被我们的类本身占据我们想要让ostream的对象作为左操作数的时候就只能定义在类外,看这样以来就无法访问类中的私有成员,这是我们就可以借助友元。

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
#include <iostream>
using namespace std;
class Test
{
//将其声明为友元函数
friend ostream& operator<<(ostream& _cout, const Test& test);
private:
int _a;
int _b;
public:
Test(int a = 1, int b = 2):_a(a), _b(b)
{

}
};
ostream& operator<<(ostream& _cout, const Test& test)
{
_cout << test._a << "\t" << test._b << endl;
return _cout;
}
int main()
{
Test test;
cout << test;
}


[misaki@localhost 第二章-类和对象]$ ./friend
1 2

友元类

  友元类和友元函数一样,一旦一个类是另一个类的友元,他就可以访问其的私有成员。

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
#include <iostream>                                           
using namespace std;
class B;//前置声明
class A
{
//将其声明为友元类
friend class B;
//将其声明为友元函数
friend ostream& operator<<(ostream& _cout, const B& test);
private:
int _a;
int _b;
public:
A(int a = 1, int b = 2):_a(a), _b(b)
{

}
};
class B
{
friend ostream& operator<<(ostream& _cout, const B& test);
private:
int _a;
int _b;
A _classA;
public:
B(int a = 3, int b = 4):_a(a), _b(b)
{
//直接访问A类的私有成员
_classA._a = a;
_classA._b = b;
}
};
ostream& operator<<(ostream& _cout, const B& test)
{
_cout << test._a << "\t" << test._b << endl;
_cout << test._classA._a << "\t" << test._classA._b << endl;
return _cout;
}
int main()
{
B b;
cout << b;
}


[misaki@localhost 第二章-类和对象]$ ./friend
3 4
3 4

  以上这个例子综合使用了友元类和友元函数,友元类让B可以随意访问A的成员,但是要注意友元的关系是单向的,B是A的友元,但是A并不是B的友元,因此A不能访问B的成员。

内部类

  内部类是在一个类内部所定义的类。

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
#include <iostream>                                         
using namespace std;
class A
{
private:
int _a;
int _b;
static int num;
public:
A(int a = 1, int b = 2):_a(a), _b(b)
{
num++;
}
//内部类
class B
{
private:
int _a;
int _b;
public:
B(int a = 3, int b = 4):_a(a), _b(b)
{

}
void Test(A classA)
{
cout << classA._a << "\t" << classA._b << endl;
cout << _a << "\t" << _b << endl;
cout << num << endl;
}
};
};
int A::num = 0;
int main()
{
A::B b;//内部类的实例化
A a;
b.Test(a);
}

[misaki@localhost 第二章-类和对象]$ ./insideclass
1 2
3 4
1

特性

  1、内部类是外部类的友元类。

  2、外部类与内部类独立,不能用外部类访问内部类。

  3、对外部类取大小与内部类无关

  4、内部类可以不用加访问限定符就可以访问外部类的静态、枚举成员。# 类和对象

【Linux】第八章-多线程

发表于 2019-05-19 | 分类于 Linux
字数统计: 17.8k

第八章 多线程

线程概念

  多进程任务处理是同时通过多个进程进行任务,多个pcb拥有多个虚拟地址空间,分别执行不同的代码,之间互不关联。而多线程是通过多个pcb共用一个虚拟地址空间,分别执行虚拟地址空间上所对应的多个不同的物理内存中的代码。即一个虚拟地址空间对应多个物理内存。
  之前我们说linux下pcb是一个进程,但其实linux下线程以进程pcb模拟实现线程,因此linux下pcb是线程;因此linux线程也叫轻量级进程。一个进程可能拥有多个线程,而每个进程势必有一个主线程,我们在主线程中创建其他线程。那么一个进程可以理解为一堆线程的集合,我们称其为线程组,而进程的pid为了不冲突则规定是主线程的pid。
  因为linux线程是pcb——因此线程是cpu的基本单位。因为进程是线程组,程序运行起来,资源是分配给整个线程组的,因此进程是资源分配的基本单位。

进程与线程的对比

  一个进程中的线程共用同一个虚拟地址空间,因此线程间通信更加方便;线程的创建/销毁成本更低;线程间切换调度成本更低;线程的执行粒度更细。
  线程之间缺乏访问控制——系统调用,异常针对的是整个进程,健壮性低。
  vfork创建一个子进程共用同一个虚拟地址空间,怕出现调用栈混乱,因此子进程运行完毕或程序替换后父进程才开始运行。而线程也共用同一个虚拟地址空间却不会发生调用栈混乱的情况,因为每个线程都会有一些独立的信息,会为每个线程在虚拟地址空间中单独分配一块内存用来存储这些独立的信息:栈,寄存器,errno,信号屏蔽字,调度优先级。同时线程间也有共享的数据:代码段,数据段,文件描述符表,信号处理方式,用户和组,当前工作目录。
  多线程相比多进程的优点:
  1、通信更加方便,灵活。
  2、创建/销毁成本更低。
  3、切换调度成本更低。
  多线程相比多进程的缺点:
  1、缺乏访问控制并且一些系统调用以及错误针对整个进程,健壮性/稳定性更低。

多进程/多线程进行多任务处理的优势

cpu密集型程序

  对于读写操作比较少,更多的则是计算方面的操作,这类程序尽量少用多线程/进程,因为cpu调度线程/进程会浪费cpu资源。

io密集型程序

  对于读写操作较多,cpu计算操作较少的程序则应该多使用多进程/线程进行io操作,由此来并行执行程序,减少执行时间。

线程控制

线程创建

  操作系统并没有为用户提供直接创建线程的系统调用接口,但是有人自己封装了一套线程库实现线程控制。

pthread_create

  由于pthread_create所在的库pthread并不在gcc默认的链接库中,因此我们在编译时要加参数-pthread或者-lpthread让其连接到这个库中。

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
/**
* 线程创建
**/
/**
* int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
* void *(*start_routine) (void *), void *arg);
* thread:输出型参数,获取新创建的线程id
* attr: 设置线程属性,通常置空
* start_routine: 线程入口函数
* arg:通过线程入口函数传递给线程的参数
**/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void* thr_start(void* arg)
{
while(1)
{
//pthread_self查看此线程的tid
printf("i am child---%d\n",pthread_self());
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, (void*)"Misaki");
printf("%d\n",tid);
if(ret != 0)//0为成功
{
printf("thread vreate errno!\n");
return -1;
}
while(1)
{
//thread_self查看自己的线程id
printf("Misaki!%d\n",getpid());
sleep(1);
}
}

[misaki@localhost 第八章-多线程]$ ./create
-1544186112
Misaki!5429
i am child----1544186112
i am child----1544186112
Misaki!5429
i am child----1544186112
Misaki!5429
i am child----1544186112
Misaki!5429
i am child----1544186112
Misaki!5429
i am child----1544186112

  这个创建线程的函数中的返回值tid为线程在虚拟地址空间上所分配的属于自己的独立空间的首地址,我们以后要靠这个参数来控制线程。一个tid唯一的表示一个线程。

线程终止

在线程入口函数中return

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void* thr_start(void* arg)
{
while(1)
{
printf("i am child\n");
reutrn NULL;
}
return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, (void*)"Misaki");
printf("%d\n",tid);
if(ret != 0)
{
printf("thread vreate errno!\n");
return -1;
}
while(1)
{
//thread_self查看自己的线程id
printf("Misaki!%d\n",getpid());
sleep(1);
return 0;
}
}
[misaki@localhost 第八章-多线程]$ ./exit
2052687616
Misaki!5710

  在线程入口函数中return会让线程退出。当在主函数中使用return退出主函数的时候这时会导致进程终止,由此进程中的所有线程都会终止。

pthread_exit()

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
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void* thr_start(void* arg)
{
while(1)
{
printf("i am child---%s\n", arg);
sleep(1);
//退出调用这个函数的线程
pthread_exit(0);
}

return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, (void*)"Misaki");
while(1)
{
printf("i am main!\n");
sleep(1);
}
}


[misaki@localhost 第八章-多线程]$ ./exit
i am main!
i am child---Misaki
i am main!
i am main!
i am main!

  可以看出我们自己创建的线程在执行pthread_exit()后退出了。如果我们的主线程调用这个函数会怎样呢?

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
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void* thr_start(void* arg)
{
while(1)
{
printf("i am child---%s\n", arg);
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, (void*)"Misaki");
if(ret != 0)
{
printf("thread create error\n");
return -1;
}
while(1)
{
printf("i am main!\n");
sleep(1);
//退出调用这个函数的线程
pthread_exit(0);
}
}


[misaki@localhost 第八章-多线程]$ ./exit
i am main!
i am child---Misaki
i am child---Misaki
i am child---Misaki
i am child---Misaki
i am child---Misaki

  可以看出我们虽然在主线程中调用了退出函数,主线程也确实退出了,但是进程却并没有退出,这说明,主线程终止并不会让进程终止。但是我们要注意线程退出也会成为僵尸线程,但是普通线程退出并不会有过于明显大的影响。

pthread_cancel

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
#include <pthread.h>                                               
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void* thr_start(void* arg)
{
while(1)
{
printf("i am child---%s\n", arg);
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, (void*)"Misaki");
if(ret != 0)
{
printf("thread create error\n");
return -1;
}
while(1)
{
printf("i am main!\n");
sleep(1);
//退出id = tid的线程
pthread_cancel(tid);
}
}


[misaki@localhost 第八章-多线程]$ ./exit
i am main!
i am child---Misaki
i am child---Misaki
i am main!
i am main!
i am main!

线程等待

  线程等待是为了获取指定线程的返回值,和进程等待一样为了让系统可以释放资源,因为一个线程运行起来,默认有一个属性:joinable。这个属性决定了线程退出后,必须被等待,否则线程资源无法完全释放,成为僵尸线程,因此我们必须进行线程等待,获取线程返回值,允许系统释放资源。当然线程等待也有一个前提,线程能够被等待,即joinable属性。

pthread_join()

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
/**
* int pthread_join(pthread_t thread, void **retval);
* 线程等待,获取线程退出返回值。
* thread:要等待的线程id
* retval:输出型参数,用于获取退出线程的返回值
**/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void* thr_start(void* arg)
{
sleep(3);
return (void*)"Misaki";
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, NULL);
if(ret != 0)
{
printf("thread create error\n");
return -1;
}
char* ptr;
pthread_join(tid, (void**)&ptr);
printf("%s\n", ptr);
}


[misaki@localhost 第八章-多线程]$ ./join
Misaki

  如果一个线程是被取消,则返回值是一个宏:PTHREAD_CANCELED,它的值是-1。线程等待pthread_join是阻塞函数,一个一个线程没有推出则会一直等待。

线程分离

  将线程的一个属性从joinable设置为detach属性。属于detach属性的线程,退出后资源直接自动被回收,这类线程不能被等待。

pthread_detach()

  如果用户对一个线程的返回值不关心,则可以在线程入口函数对线程进行分离。

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
/**                                                            
* int pthread_detach(pthread_t thread);
* 线程分离。
* thread:要分离的线程id
**/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
void* thr_start(void* arg)
{

//分离自己这个线程
//线程的分离对于一个线程来说,任意线程在任意位置调用都可以
// pthread_detach(pthread_self());
return (void*)"Misaki";
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, NULL);
if(ret != 0)
{
printf("thread create error\n");
return -1;
}
//分离这个线程
pthread_detach(tid);
char* ptr;
ret = pthread_join(tid, (void**)&ptr);
//如果一个进程无法被等待则返回值为一个宏EINVAL
if(ret == EINVAL)
{
printf("this thread can not be wait!!\n");
return -1;
}
printf("%s\t%d\n", ptr, ret);
}


[misaki@localhost 第八章-多线程]$ ./join
this thread can not be wait!!

  会发现我们已经分离了我们自己创建的线程,这个线程已经无法被等待了,并且我们无法接收到线程的返回值。

线程安全

  多个线程同时操作临界资源而不会出现数据二义性就说这个线程是安全的。如果在线程中进行了非原子性操作就可能会导致线程不安全,这些非原子性操作也叫做不可重入函数,即多个执行流中同时进入函数运行会出现问题的函数。

  如何实现线程安全?这就要靠同步与互斥。同步指临界资源的合理访问,互斥指临界资源同一时间唯一访问。

互斥

  同步和互斥要如何实现呢?我们先从互斥开始讨论。为了保证操作的原子性,在C语言中互斥锁可以帮助我们保证互斥,使我们的函数变为可重入函数。

互斥锁

  互斥锁的值只能为0或1。1表示可以加锁,加锁后值-1,操作结束后就会解锁,解锁就会将值+1。如果一个操作已经加锁则值为0,因此当锁值为0时其他线程则不能加锁,不能加锁线程就会陷入等待。

  互斥锁操作步骤:

  1、定义互斥锁变量:pthread_mutex_t。

  2、初始化互斥锁变量:pthread_mutex_init。

  3、加锁:pthread_mutex_lock。

  4、解锁:pthread_mutex_unlock。

  5、删除锁:pthread_mutex_destroy。
  接下来我用互斥锁将一个不可重入的函数使它可重入从而使多个线程同时运行函数时变得安全。

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
/*实现互斥锁的基本使用以及线程安全的基本认识*/                                    
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
int ticket = 100;
//互斥锁变量不一定非要全局变量,使用的线程都能访问到就行
//互斥锁变量
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int ticket = 100;
pthread_mutex_t mutex;
void* ticket_scalper(void* arg)
{
int id = (int)arg;
while(1)
{
//加锁要在临界资源访问之前
//int pthread_mutex_lock(pthread_mutex_t* mutex);阻塞加锁
//int pthread_mutex_trylock(pthread_mutex_t* mutex);非阻塞加锁,加不上锁就返回
pthread_mutex_lock(&mutex);
if(ticket > 0)
{
printf("scalper:%d--get a ticket:%d\n", id, ticket);
ticket--;
usleep(1000);
}
else
{
//解锁
pthread_mutex_unlock(&mutex);
pthread_exit(0);
}
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
int i = 0;
int ret;
pthread_t tid[4];
//初始化互斥锁
//int pthread_mutex_init(pthread_mutex_t *restrict mutex,
// const pthread_mutexattr_t *restrict attr);
//
pthread_mutex_init(&mutex, NULL);
//创建线程
for(i = 0; i < 4; i++)
{
ret = pthread_create(&tid[i], NULL, ticket_scalper, (void*)i);
if(ret != 0)
{
perror("thread creat error:");
return -1;
}
}
for(i = 0; i < 4; i++)
{
pthread_join(tid[i], NULL);
}
//销毁互斥锁
//int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_destroy(&mutex);
}



[misaki@localhost thread_2019_9_2_class45]$ ./main
scalper:2--get a ticket:100
scalper:2--get a ticket:99
scalper:2--get a ticket:98
scalper:2--get a ticket:97
scalper:2--get a ticket:96
scalper:2--get a ticket:95
scalper:3--get a ticket:94
scalper:3--get a ticket:93
scalper:3--get a ticket:92
scalper:3--get a ticket:91
scalper:3--get a ticket:90
scalper:3--get a ticket:89
scalper:3--get a ticket:88
scalper:3--get a ticket:87
scalper:3--get a ticket:86
scalper:3--get a ticket:85
scalper:3--get a ticket:84
scalper:3--get a ticket:83
scalper:3--get a ticket:82
scalper:3--get a ticket:81
scalper:3--get a ticket:80
scalper:3--get a ticket:79
scalper:3--get a ticket:78
scalper:3--get a ticket:77
scalper:3--get a ticket:76
scalper:3--get a ticket:75
scalper:3--get a ticket:74
scalper:3--get a ticket:73
scalper:3--get a ticket:72
scalper:3--get a ticket:71
scalper:3--get a ticket:70
scalper:3--get a ticket:69
scalper:3--get a ticket:68
scalper:3--get a ticket:67
scalper:3--get a ticket:66
scalper:3--get a ticket:65
scalper:3--get a ticket:64
scalper:3--get a ticket:63
scalper:3--get a ticket:62
scalper:3--get a ticket:61
scalper:3--get a ticket:60
scalper:3--get a ticket:59
scalper:3--get a ticket:58
scalper:3--get a ticket:57
scalper:3--get a ticket:56
scalper:3--get a ticket:55
scalper:3--get a ticket:54
scalper:3--get a ticket:53
scalper:3--get a ticket:52
scalper:3--get a ticket:51
scalper:3--get a ticket:50
scalper:3--get a ticket:49
scalper:3--get a ticket:48
scalper:3--get a ticket:47
scalper:3--get a ticket:46
scalper:3--get a ticket:45
scalper:3--get a ticket:44
scalper:3--get a ticket:43
scalper:3--get a ticket:42
scalper:3--get a ticket:41
scalper:3--get a ticket:40
scalper:3--get a ticket:39
scalper:3--get a ticket:38
scalper:3--get a ticket:37
scalper:3--get a ticket:36
scalper:3--get a ticket:35
scalper:3--get a ticket:34
scalper:3--get a ticket:33
scalper:3--get a ticket:32
scalper:3--get a ticket:31
scalper:3--get a ticket:30
scalper:3--get a ticket:29
scalper:3--get a ticket:28
scalper:3--get a ticket:27
scalper:3--get a ticket:26
scalper:3--get a ticket:25
scalper:3--get a ticket:24
scalper:3--get a ticket:23
scalper:3--get a ticket:22
scalper:3--get a ticket:21
scalper:3--get a ticket:20
scalper:3--get a ticket:19
scalper:3--get a ticket:18
scalper:3--get a ticket:17
scalper:3--get a ticket:16
scalper:3--get a ticket:15
scalper:3--get a ticket:14
scalper:3--get a ticket:13
scalper:3--get a ticket:12
scalper:3--get a ticket:11
scalper:3--get a ticket:10
scalper:3--get a ticket:9
scalper:3--get a ticket:8
scalper:3--get a ticket:7
scalper:3--get a ticket:6
scalper:3--get a ticket:5
scalper:3--get a ticket:4
scalper:3--get a ticket:3
scalper:3--get a ticket:2
scalper:3--get a ticket:1

  这样就达成了互斥,在一个线程操作临界资源时,其他线程不会同时干涉。

死锁

  死锁是指因为对一些无法加锁的锁进行加锁操作而导致程序卡死。死锁是我们一定要在使用锁时要注意和避免的

  死锁产生的四个必要条件:

  1、互斥条件。一个线程操作时其他线程不能操作。

  2、不可剥夺条件。一个线程加的锁别的线程不能释放。

  3、请求与保持条件。一个线程已经有了锁却还在请求其他的锁,但是其他的锁请求不到第一个锁也不释放。

  4、环路等待条件。

  死锁产生往往是因为加锁解锁的顺序不同。要想避免死锁就要避免死锁产生的四个必要条件——死锁检测算法,银行家算法。

同步

  通过对当前是否满足对临界资源的操作条件来判断线程是否该等待或唤醒这种方式实现对临界资源访问的合理性。资源产生后才能进行使用,没有资源则等待资源产生,生产资源后则唤醒等待,这样则达成同步。然而互斥锁虽然可以帮助我们完成等待但是无法判断何时将我们唤醒,不能在合适的事件唤醒,因此便要借助新的东西——条件变量。

条件变量

  条件变量的使用流程:

  1、定义条件变量:pthread_cond_t。

  2、初始化条件变量:pthread_cond_init。

  3、等待或者唤醒:pthread_cond_wait/pthread_cond_signal。

  4、销毁条件变量:pthread_cond_destroy。

  pthread_cond_wait中一共有三个操作,首先它要让让当前线程等待,但是此时有一点,此时的互斥量还处于加锁状态其他线程无法操作临界资源,呢又怎么做到让临界资源达到要求呢?因此他在让线程等待前要先解除了互斥量的加锁状态,并且这两部操作为原子操作。为什么要是原子操作?因为如果不是原子操作有可能在解锁后已经条件满足而此时线程还未进行等待可能会忽略唤醒。之后在线程被唤醒后pthread_cond_wait还会再加锁保证互斥。这就是三部操作:解锁->等待->唤醒后加锁。
  在每一个条件变量内部都有一个等待队列,将所有等待的线程排列在上面,如果有其他线程唤醒则逐一唤醒。
  接下来我们用互斥锁加条件变量模拟实现一个顾客去餐馆吃饭的情景,但是在这个情境中为了符合设计要注意两个顾客不能同时吃一碗饭,并且只有一个锅因此两个厨师不能同时做饭。如果没饭了2个厨师中其中一个做饭,又犯了2个顾客其中一个吃饭。

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
/*实现条件变量的基本使用*/                                           
/*吃面前提有人吃面,如果没有线程的面,等待老板做出来
* 老板做出来面就要唤醒顾客
* 老板不会做太多的面,老板只会提前做一碗面
* 如果已经有面做出来,但是没人吃,不会再做(等待)
* 顾客吃完面后,老板再来一碗(唤醒老板的等待)*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
//是否右面
int have_noodle = 1;
//为了让客人与厨师间的不同的同步性,需要定义多个条件变量
pthread_cond_t customer;
pthread_cond_t boss;
pthread_mutex_t mutex;
//老板做面
void* thr_boss(void* arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
//由于多个顾客,为了避免两个顾客吃一碗面的情况这里要循环判断
while(have_noodle == 1)//有面
{
//等待
//int pthread_cond_timedwait(pthread_cond_t *restrict cond,
// pthread_mutex_t *restrict mutex,
// const struct timespec *restrict abstime);
//限时等待
//cond:条件变量
//mutex:互斥锁
//abstime:限时等待时长
//时间到后返回时间超市,停止阻塞
//int pthread_cond_wait(pthread_cond_t *restrict cond,
// pthread_mutex_t *restrict mutex);
//cond:条件变量
//mutex:互斥锁
//pthread_cond_wait 集合了解锁后挂起的操作(原子操作,不可被打断)
//有可能还没来得及挂起就已经有人唤醒,白唤醒,导致死等
//因此这里的wait将三个操作进行了原子性封装不让其中断
//解锁 -》 等待 -》 被唤醒后加锁
pthread_cond_wait(&boss, &mutex);
}
//面没了,要再做
printf("拉面 + 1\n");
have_noodle = 1;
//面好了,唤醒顾客
pthread_cond_signal(&customer);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
//顾客吃面
void* thr_customer(void* arg)
{
while(1)
{
while(have_noodle == 0)
{
//若没有现成的面等老板做好
//等待
pthread_cond_wait(&customer, &mutex);
}
//有面了
printf("真好吃!\n");
have_noodle -= 1;
//唤醒厨师再做一碗
pthread_cond_signal(&boss);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t tid1, tid2;
int ret;
//条件变量初始化
//int pthread_cond_init(pthread_cond_t *restrict cond,
// const pthread_condattr_t *restrict attr);
pthread_cond_init(&boss, NULL);
pthread_cond_init(&customer, NULL);
pthread_mutex_init(&mutex, NULL);
//各建立两个线程同时工作,相当于两个厨师两个客人
//客人间具有互斥性,厨师间也有互斥性,客人与厨师间有同步与互斥性
for(int i = 0; i < 2; i++)
{
ret = pthread_create(&tid1, NULL, thr_boss, NULL);
if(ret != 0)
{
printf("boss error");
return -1;
}
}
for(int i = 0; i < 2; i++)
{
ret = pthread_create(&tid2, NULL, thr_customer, NULL);
if(ret != 0)
{
printf("customer error");
return -1;
}
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
//销毁条件变量
//int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_destroy(&customer);
pthread_cond_destroy(&boss);
//销毁锁
pthread_mutex_destroy(&mutex);
}


真好吃!
拉面 + 1
真好吃!
拉面 + 1
真好吃!
拉面 + 1
真好吃!
拉面 + 1
真好吃!
拉面 + 1
真好吃!
拉面 + 1
真好吃!
拉面 + 1
^C真好吃!

  在以上这个例子中要注意几个点:
  1、用户对条件判断需要使用循环进行判断(防止角色不符合条件被唤醒之后因为不循环判断直接操作临界资源)。这个问题也被称为虚假唤醒问题。在多核处理器下,pthread_cond_signal可能会激活多于一个线程(阻塞在条件变量上的线程)。结果就是,当一个线程调用pthread_cond_signal()后,多个调用pthread_cond_wait()或其他等待在队列上的线程返回。这种效应就会造成虚假唤醒。
  2、不同角色的线程因该等待在不同的条件变量上。(防止角色的误唤醒,导致程序阻塞)
  但是要注意条件变量并不保证安全,因此往往使用条件变量的时候会与互斥锁共同使用。面生产一碗顾客吃一碗没有出现异常,因此实现是成功的。这种在多线程情况下有人生产数据有人消费数据利用同步与互斥达到合理与安全的模式十分经典,因此产生了一种固定的设计模型,这就是生产者消费者模型。

生产者与消费者模型

  生产者与消费者模型中有两种角色:生产者与消费者,同时包含三种关系:生产者与生产者之间互斥,消费者与消费者之间互斥,生产者与消费者之间同步与互斥。他们工作在一个场景中,这个场景通常是一个队列,用来保存数据。

实现

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/**                                                   
* 基于互斥锁与条件变量实现一个线程安全的队列
* 实现生产者与消费者模型
**/
#include <iostream>
#include <queue>
#include <pthread.h>
#define MAXQ 10
class BlockQueue
{
public:
BlockQueue(int maxq = MAXQ)
:_capacity(maxq)
{
pthread_mutex_init(&_mutex, NULL);
pthread_cond_init(&_cond_consumer, NULL);
pthread_cond_init(&_cond_productor, NULL);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond_consumer);
pthread_cond_destroy(&_cond_productor);
}
bool QueuePush(int data)
{
pthread_mutex_lock(&_mutex);
while(_queue.size() == _capacity)
{
pthread_cond_wait(&_cond_productor, &_mutex);
}
_queue.push(data);
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_cond_consumer);
return true;
}
bool QueuePop(int &data)
{
pthread_mutex_lock(&_mutex);
while(_queue.empty())
{
pthread_cond_wait(&_cond_consumer, &_mutex);
}
data = _queue.front();
_queue.pop();
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_cond_productor);
return true;
}
private:
std::queue<int> _queue;
int _capacity;
pthread_mutex_t _mutex;
pthread_cond_t _cond_productor;
pthread_cond_t _cond_consumer;
};
void* thr_consumer(void* arg)
{
BlockQueue* q = (BlockQueue*)arg;
int data;
while(1)
{
//消费者一直获取数据进行打印
q->QueuePop(data);
std::cout << "consumer gets a piece of data--" << data << std::endl;
}
}
void* thr_productor(void* arg)
{
BlockQueue* q = (BlockQueue*)arg;
int data = 0;
while(1)
{
//生产者一直添加数据
q->QueuePush(data);
std::cout << "producer produces a data--" << (data++) << std::endl;
}
return NULL;
}
int main()
{
pthread_t ctid[4], ptid[4];
int ret, i;
BlockQueue q;
for(i = 0; i < 4; i++)
{
ret = pthread_create(&ctid[i], NULL, thr_consumer, (void*)&q);
if(ret != 0)
{
std::cerr << "create thread error" << std::endl;
}
}
for(i = 0; i < 4; i++)
{
ret = pthread_create(&ptid[i], NULL, thr_productor, (void*)&q);
if(ret != 0)
{
std::cerr << "create thread error" << std::endl;
}
}
for(i = 0; i < 4; i++)
{
pthread_join(ctid[i], NULL);
pthread_join(ptid[i], NULL);
}
}


consumer gets a piece of data--2111
consumer gets a piece of data--2112
consumer gets a piece of data--2113
consumer gets a piece of data--2114
consumer gets a piece of data--2115
consumer gets a piece of data--2116
consumer gets a piece of data--2117
consumer gets a piece of data--2118
producer produces a data--2119
producer produces a data--2120
producer produces a data--2121
producer produces a data--2122
producer produces a data--2123
producer produces a data--2124

  这里打印之所以看上去乱是因为xshell的显示跟不上虚拟机计算的速度。

优点

  生产者与消费者模型有三个优点:
  1、解耦合
  2、支持忙闲不均
  3、支持并发
  一个场所,两种角色,三种关系。

posix标准信号量

  system V是内核中的计数器,posix是线程间的全局计数器。它也有着实现进程/进程间同步与互斥的贡藕功能。

与条件变量的区别

  条件变量是通过等待、唤醒操作来让线程等待在等待队列上来完成同步,这需要用户自己进行外部条件判断并且要搭配互斥锁一起使用。
  信号量是通过自身内部技术实现条件的判断,不需要搭配互斥锁,自身已经保证了原子操作。

信号量的工作原理

  信号量通过一个计数器实现对资源的计数,并且通过这个计数来判断当前线程/进程能否对临界资源进行访问,对临界资源进行访问之前先发起调用访问信号量进行判断是否能够访问。
  信号量实现同步:首先资源计数-1,若此时资源计数>=0,则可以直接进行访问,调用直接返回,若信号量内部计数器<0表示没有资源无法访问,调用阻塞(挂起线程);若其他线程生产了一个资源则发起调用,首先资源计数+1,如果此时计数器<=0则唤醒等待队列上的线程,若此时计数器>0则什么都不做。
  信号量实现互斥:计数只有0/1,资源只有一个,同一时间只有 一个线程可以访问。首先信号量-1,若此时信号量<0则调用阻塞,若>0,则调用返回,对临界资源进行访问,访问完毕,进行计数+1,唤醒所有线程,所有线程继续进行抢夺。
  同时如果信号量小于0则表示当前阻塞在等待队列上的线程/进程数,等于0表示资源刚好完全分配,大于0则表示多余资源数。

接口

1
2
3
4
5
6
7
8
9
sem_t//定义信号量。
sem_init(sem_t* sem, int flag, int initval);//初始化,
//flag:0-线程间,!0-进程间
//initval:用于设置初值
sem_wait(sem_t* sem);//,进行判断是否有资源,<=0则阻塞,>0则-1并调用返回并。
sem_trywait(sem_t* sem);//非阻塞,没有资源直接报错返回。
sem_timedwait(sem_t* sem);//限时阻塞,等待一段时间,若一直没有资源则超时报错返回
sem_post(sem_t* sem);//计数+1,并且唤醒等待的线程
sem_destroy(sem_t* sem);//销毁信号量

应用

  用信号量实现线程安全的环形队列。

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/**
* 利用信号量完成线程安全的环形队列
**/
#include <iostream>
#include <vector>
#include <semaphore.h>
#include <thread>
#define MAXQ 10
class RingQueue
{

public:
RingQueue(int maxq = MAXQ)
:_capacity(maxq)
,_queue(maxq)
,_step_read(0)
,_step_write(0)
{
sem_init(&_lock, 0, 1);
sem_init(&_idle_space, 0, maxq);
sem_init(&_data_space, 0, 0);
}
~RingQueue()
{
sem_destroy(&_lock);
sem_destroy(&_idle_space);
sem_destroy(&_data_space);
}
bool QueuePush(int data)
{
//没有空闲空间则阻塞
sem_wait(&_idle_space);
//加锁
sem_wait(&_lock);
_queue[_step_write] = data;
_step_write = (_step_write + 1) % _capacity;
//解锁
sem_post(&_lock);
//唤醒消费者
sem_post(&_data_space);
return true;
}
bool QueuePop(int& data)
{
sem_wait(&_data_space);
sem_wait(&_lock);
data = _queue[_step_read];
_step_read = (_step_read + 1) % _capacity;
sem_post(&_lock);
sem_post(&_idle_space);
return true;
}
private:
std::vector<int> _queue;//用vector实现环形队列
int _capacity;//容量
int _step_read;//读指针
int _step_write;//写指针

sem_t _lock;//初始计数=1,负责完成互斥

//也需要有两个等待队列,分别完成两个角色间的同步
sem_t _idle_space;//空闲空间节点个数,生产者等待在这里,完成同步
sem_t _data_space;//数据节点个数,初始=0,消费者等待在这里,完成同步
};
void thr_producer(RingQueue* q)
{
int data = 0;
while(1)
{
q->QueuePush(data);
std::cout << "push data ----" << data++ << std::endl;
}
}
void thr_consumer(RingQueue* q)
{
int data = 0;
while(1)
{
q->QueuePop(data);
std::cout << "get data ----" << data << std::endl;
}
}

int main()
{
RingQueue q;
std::vector<std::thread> list_con(4);
std::vector<std::thread> list_pro(4);
for(int i = 0; i < 4; i++)
{
list_pro[i] = (std::thread(thr_producer, &q));
}
for(int i = 0; i < 4; i++)
{
list_con[i] = (std::thread(thr_consumer, &q));
}
for(int i = 0; i < 4; i++)
{
list_con[i].join();
list_pro[i].join();
}
}


push data ----4028
get data ----push data ----4102
41004029
3996push data ----4030
push data ----4031
push data ----4032
push data ----push data ----4033
push data ----4034
push data ----4101
push data ----4102
get data ----
push data ----4103
push data ----4104
40974102
get data ----4101

push data ----4103

push data ----get data ----4029
4095
get data ----4031
get data ----4032
get data ----4033
get data ----4034
get data ----4102
get data ----get data ----40304103
push data ----get data ----4104
get data ----4103
get data ----4105
get data ----4104
push data ----3997
push data ----3998
push data ----3999
push data ----4000
push data ----4001
4104get data ----
push data ----4105
push data ----4106
push data ----4107
push data ----4108
push data ----push data ----4035
3997
get data ----3999
get data ----4000

读写锁

  读写锁在数据库中就有着极为重要的应用,这样才得以让数据得到共享修改得到合理的保存而不出现数据的二义性。

特点,原理及应用

  读写锁有着自己的特点:写互斥,读共享,一个用户写时所有其他所有用户都不能读和写;一个用户读时其他所有用户都可以读但不能写,因此适用于多读少写的应用场景,保证数据不会出现二义性并且保证读取和写入的效率。
  读写锁内部有两个计数器,读者计数与写者计数。加写锁时对两个技术进行判断如果任意一个计数器>0,都无法加写锁需要等待。加读锁时对写者计数进行判断,若大于0,则无法加读锁需要进行等待。
  读写锁通过自旋锁实现,不满足条件时自旋等待。自旋锁的特点是:等待中不停循环对条件进行判断,因此可以及时响应,但是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
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
/**                                                                             
* 线程池由两个部分构成一个是一个任务类
* 另一个部分是一个线程安全的队列,由此构成任务队列,
* 再用一组线程从任务队列中获取任务来执行
* 由此构成线程池
**/
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <queue>
#include <thread>
#include <string>
#include <time.h>
#include <stdlib.h>
#include <sstream>
#define MAX_THREAD 5
#define MAX_QUEUE 10
typedef void(*handler_t)(int val);
//任务类
//1、决定线程处理的任务,处理什么数据,怎么处理都由用户传入
class Task
{
private:
int _data;//数据
handler_t _handler;//处理数据的方法,函数指针,用于传入线程中给线程下达命令
public:
Task(int data, handler_t handler)
:_data(data)
,_handler(handler)
{

}
void SetTask(int data, handler_t handler)
{
_data = data;
_handler = handler;
}
void Run()
{
return _handler(_data);
}

};
//线程池类
class ThreadPool
{
private:
std::queue<Task> _queue;//任务队列
int _capacity;//线程池最大任务数量
pthread_mutex_t _mutex;//锁,完成互斥,类似于生产者消费者模型
pthread_cond_t _cond_pro;//条件变量,完成
pthread_cond_t _cond_con;
int _thr_max;//线程池拥有的总线程数
std::vector<std::thread> _thr_list;//线程组,存储线程操作句柄
bool _quit_flag;//用于控制线程是否退出
int _thr_cur;//线程的数量,线程退出时,判断当前线程数量
void thr_start()
{
while(1)
{
pthread_mutex_lock(&_mutex);
while(_queue.empty())
{
if(_quit_flag == true)
{
std::cout << "thread exit " << pthread_self() << std::endl;
pthread_mutex_unlock(&_mutex);
_thr_cur--;
return;
}
pthread_cond_wait(&_cond_con, &_mutex);
}
Task tt = _queue.front();
_queue.pop();
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_cond_pro);
//任务处理放到锁外,防止线程处理任务时间过长,一直加锁导致其他线程无法处理其他任务
tt.Run();
}
}
public:
//初始化线程池
ThreadPool(int maxq = MAX_QUEUE, int maxt = MAX_THREAD)
:_capacity(maxq)
,_thr_max(maxt)
,_thr_list(maxt)
,_thr_cur(0)
{
pthread_mutex_init(&_mutex, NULL);
pthread_cond_init(&_cond_pro, NULL);
pthread_cond_init(&_cond_con, NULL);
}
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond_pro);
pthread_cond_destroy(&_cond_con);
}
//初始化线程组
bool PoolInit()
{
for(int i = 0; i < _thr_max; i++)
{
_thr_list[i] = std::thread(&ThreadPool::thr_start, this);
_thr_list[i].detach();
_thr_cur++;
}
return true;
}
//添加任务
bool AddTask(Task& tt)
{
pthread_mutex_lock(&_mutex);
while(_queue.size() == _capacity)
{
pthread_cond_wait(&_cond_pro, &_mutex);
}
_queue.push(tt);
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_cond_con);
return true;
}
//销毁线程池,停止工作
bool PoolStop()
{
pthread_mutex_lock(&_mutex);
_quit_flag = true;
pthread_mutex_unlock(&_mutex);
while(_thr_cur > 0)
{
//std::cout << "cont:" << _thr_cur << std::endl;
pthread_cond_broadcast(&_cond_con);
usleep(1000);
}
//for(int i = 0; i < _thr_max; i++)
//{
// _thr_list[i].join();
//}
return true;
}
};
void test(int data)
{
srand(time(NULL));
int nsec = rand() % 5;
std::stringstream ss;
ss << "thread:" << pthread_self() << " processint data ";
ss << data << " and sleep " << nsec << " sec" << std::endl;
std:: cout << ss.str();
sleep(nsec);
return;
}
int main()
{
ThreadPool pool;
pool.PoolInit();
for(int i = 0; i < 10; i++)
{
Task tt(i, test);
pool.AddTask(tt);
}
pool.PoolStop();
}


[misaki@localhost thread]$ ./threadpool
thread:139941165012736 processint data 0 and sleep 4 sec
thread:139941156620032 processint data 1 and sleep 4 sec
thread:139941190190848 processint data 2 and sleep 4 sec
thread:139941181798144 processint data 3 and sleep 4 sec
thread:139941173405440 processint data 4 and sleep 4 sec
thread:139941190190848 processint data 6 and sleep 4 sec
thread:139941156620032 processint data 7 and sleep 4 sec
thread:139941181798144 processint data 8 and sleep 4 sec
thread:139941165012736 processint data 5 and sleep 4 sec
thread:139941173405440 processint data 9 and sleep 4 sec
thread exit 139941156620032
thread exit 139941190190848
thread exit 139941181798144
thread exit 139941165012736
thread exit 139941173405440

设计模式

单例模式

  单例模式是一种常见设计模式,之前已经多次介绍,Cpp章节中也有实现。单例模式使用场景是在一个资源只能被加载分配一次,一个类只能实例化一个对象的情况。

饿汉模式

  资源一次性加载分配完,对象在程序初始化阶段实例化完毕,这种实现是线程安全的,程序运行起来比较流畅,但是启动加载时间可能过长。

懒汉模式

  资源使用时再加载分配,对象再使用的时候再去实例化,这种实现加载快,同一时间消耗资源少,但是运行中可能卡顿。这种实现是线程不安全的,因此我们要加锁判断类是否实例化过,如果没有则实例化。# 第八章 多线程

线程概念

  多进程任务处理是同时通过多个进程进行任务,多个pcb拥有多个虚拟地址空间,分别执行不同的代码,之间互不关联。而多线程是通过多个pcb共用一个虚拟地址空间,分别执行虚拟地址空间上所对应的多个不同的物理内存中的代码。即一个虚拟地址空间对应多个物理内存。
  之前我们说linux下pcb是一个进程,但其实linux下线程以进程pcb模拟实现线程,因此linux下pcb是线程;因此linux线程也叫轻量级进程。一个进程可能拥有多个线程,而每个进程势必有一个主线程,我们在主线程中创建其他线程。那么一个进程可以理解为一堆线程的集合,我们称其为线程组,而进程的pid为了不冲突则规定是主线程的pid。
  因为linux线程是pcb——因此线程是cpu的基本单位。因为进程是线程组,程序运行起来,资源是分配给整个线程组的,因此进程是资源分配的基本单位。

进程与线程的对比

  一个进程中的线程共用同一个虚拟地址空间,因此线程间通信更加方便;线程的创建/销毁成本更低;线程间切换调度成本更低;线程的执行粒度更细。
  线程之间缺乏访问控制——系统调用,异常针对的是整个进程,健壮性低。
  vfork创建一个子进程共用同一个虚拟地址空间,怕出现调用栈混乱,因此子进程运行完毕或程序替换后父进程才开始运行。而线程也共用同一个虚拟地址空间却不会发生调用栈混乱的情况,因为每个线程都会有一些独立的信息,会为每个线程在虚拟地址空间中单独分配一块内存用来存储这些独立的信息:栈,寄存器,errno,信号屏蔽字,调度优先级。同时线程间也有共享的数据:代码段,数据段,文件描述符表,信号处理方式,用户和组,当前工作目录。
  多线程相比多进程的优点:
  1、通信更加方便,灵活。
  2、创建/销毁成本更低。
  3、切换调度成本更低。
  多线程相比多进程的缺点:
  1、缺乏访问控制并且一些系统调用以及错误针对整个进程,健壮性/稳定性更低。

多进程/多线程进行多任务处理的优势

cpu密集型程序

  对于读写操作比较少,更多的则是计算方面的操作,这类程序尽量少用多线程/进程,因为cpu调度线程/进程会浪费cpu资源。

io密集型程序

  对于读写操作较多,cpu计算操作较少的程序则应该多使用多进程/线程进行io操作,由此来并行执行程序,减少执行时间。

线程控制

线程创建

  操作系统并没有为用户提供直接创建线程的系统调用接口,但是有人自己封装了一套线程库实现线程控制。

pthread_create

  由于pthread_create所在的库pthread并不在gcc默认的链接库中,因此我们在编译时要加参数-pthread或者-lpthread让其连接到这个库中。

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
/**
* 线程创建
**/
/**
* int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
* void *(*start_routine) (void *), void *arg);
* thread:输出型参数,获取新创建的线程id
* attr: 设置线程属性,通常置空
* start_routine: 线程入口函数
* arg:通过线程入口函数传递给线程的参数
**/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void* thr_start(void* arg)
{
while(1)
{
//pthread_self查看此线程的tid
printf("i am child---%d\n",pthread_self());
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, (void*)"Misaki");
printf("%d\n",tid);
if(ret != 0)//0为成功
{
printf("thread vreate errno!\n");
return -1;
}
while(1)
{
//thread_self查看自己的线程id
printf("Misaki!%d\n",getpid());
sleep(1);
}
}

[misaki@localhost 第八章-多线程]$ ./create
-1544186112
Misaki!5429
i am child----1544186112
i am child----1544186112
Misaki!5429
i am child----1544186112
Misaki!5429
i am child----1544186112
Misaki!5429
i am child----1544186112
Misaki!5429
i am child----1544186112

  这个创建线程的函数中的返回值tid为线程在虚拟地址空间上所分配的属于自己的独立空间的首地址,我们以后要靠这个参数来控制线程。一个tid唯一的表示一个线程。

线程终止

在线程入口函数中return

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void* thr_start(void* arg)
{
while(1)
{
printf("i am child\n");
reutrn NULL;
}
return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, (void*)"Misaki");
printf("%d\n",tid);
if(ret != 0)
{
printf("thread vreate errno!\n");
return -1;
}
while(1)
{
//thread_self查看自己的线程id
printf("Misaki!%d\n",getpid());
sleep(1);
return 0;
}
}
[misaki@localhost 第八章-多线程]$ ./exit
2052687616
Misaki!5710

  在线程入口函数中return会让线程退出。当在主函数中使用return退出主函数的时候这时会导致进程终止,由此进程中的所有线程都会终止。

pthread_exit()

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
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void* thr_start(void* arg)
{
while(1)
{
printf("i am child---%s\n", arg);
sleep(1);
//退出调用这个函数的线程
pthread_exit(0);
}

return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, (void*)"Misaki");
while(1)
{
printf("i am main!\n");
sleep(1);
}
}


[misaki@localhost 第八章-多线程]$ ./exit
i am main!
i am child---Misaki
i am main!
i am main!
i am main!

  可以看出我们自己创建的线程在执行pthread_exit()后退出了。如果我们的主线程调用这个函数会怎样呢?

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
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void* thr_start(void* arg)
{
while(1)
{
printf("i am child---%s\n", arg);
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, (void*)"Misaki");
if(ret != 0)
{
printf("thread create error\n");
return -1;
}
while(1)
{
printf("i am main!\n");
sleep(1);
//退出调用这个函数的线程
pthread_exit(0);
}
}


[misaki@localhost 第八章-多线程]$ ./exit
i am main!
i am child---Misaki
i am child---Misaki
i am child---Misaki
i am child---Misaki
i am child---Misaki

  可以看出我们虽然在主线程中调用了退出函数,主线程也确实退出了,但是进程却并没有退出,这说明,主线程终止并不会让进程终止。但是我们要注意线程退出也会成为僵尸线程,但是普通线程退出并不会有过于明显大的影响。

pthread_cancel

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
#include <pthread.h>                                               
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void* thr_start(void* arg)
{
while(1)
{
printf("i am child---%s\n", arg);
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, (void*)"Misaki");
if(ret != 0)
{
printf("thread create error\n");
return -1;
}
while(1)
{
printf("i am main!\n");
sleep(1);
//退出id = tid的线程
pthread_cancel(tid);
}
}


[misaki@localhost 第八章-多线程]$ ./exit
i am main!
i am child---Misaki
i am child---Misaki
i am main!
i am main!
i am main!

线程等待

  线程等待是为了获取指定线程的返回值,和进程等待一样为了让系统可以释放资源,因为一个线程运行起来,默认有一个属性:joinable。这个属性决定了线程退出后,必须被等待,否则线程资源无法完全释放,成为僵尸线程,因此我们必须进行线程等待,获取线程返回值,允许系统释放资源。当然线程等待也有一个前提,线程能够被等待,即joinable属性。

pthread_join()

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
/**
* int pthread_join(pthread_t thread, void **retval);
* 线程等待,获取线程退出返回值。
* thread:要等待的线程id
* retval:输出型参数,用于获取退出线程的返回值
**/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void* thr_start(void* arg)
{
sleep(3);
return (void*)"Misaki";
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, NULL);
if(ret != 0)
{
printf("thread create error\n");
return -1;
}
char* ptr;
pthread_join(tid, (void**)&ptr);
printf("%s\n", ptr);
}


[misaki@localhost 第八章-多线程]$ ./join
Misaki

  如果一个线程是被取消,则返回值是一个宏:PTHREAD_CANCELED,它的值是-1。线程等待pthread_join是阻塞函数,一个一个线程没有推出则会一直等待。

线程分离

  将线程的一个属性从joinable设置为detach属性。属于detach属性的线程,退出后资源直接自动被回收,这类线程不能被等待。

pthread_detach()

  如果用户对一个线程的返回值不关心,则可以在线程入口函数对线程进行分离。

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
/**                                                            
* int pthread_detach(pthread_t thread);
* 线程分离。
* thread:要分离的线程id
**/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
void* thr_start(void* arg)
{

//分离自己这个线程
//线程的分离对于一个线程来说,任意线程在任意位置调用都可以
// pthread_detach(pthread_self());
return (void*)"Misaki";
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, NULL);
if(ret != 0)
{
printf("thread create error\n");
return -1;
}
//分离这个线程
pthread_detach(tid);
char* ptr;
ret = pthread_join(tid, (void**)&ptr);
//如果一个线程无法被等待则返回值为一个宏EINVAL
if(ret == EINVAL)
{
printf("this thread can not be wait!!\n");
return -1;
}
printf("%s\t%d\n", ptr, ret);
}


[misaki@localhost 第八章-多线程]$ ./join
this thread can not be wait!!

  会发现我们已经分离了我们自己创建的线程,这个线程已经无法被等待了,并且我们无法接收到线程的返回值。

线程安全

  多个线程同时操作临界资源而不会出现数据二义性就说这个线程是安全的。如果在线程中进行了非原子性操作就可能会导致线程不安全,这些非原子性操作也叫做不可重入函数,即多个执行流中同时进入函数运行会出现问题的函数。

  如何实现线程安全?这就要靠同步与互斥。同步指临界资源的合理访问,互斥指临界资源同一时间唯一访问。

互斥

  同步和互斥要如何实现呢?我们先从互斥开始讨论。为了保证操作的原子性,在C语言中互斥锁可以帮助我们保证互斥,使我们的函数变为可重入函数。

互斥锁

  互斥锁的值只能为0或1。1表示可以加锁,加锁后值-1,操作结束后就会解锁,解锁就会将值+1。如果一个操作已经加锁则值为0,因此当锁值为0时其他线程则不能加锁,不能加锁线程就会陷入等待。

  互斥锁操作步骤:

  1、定义互斥锁变量:pthread_mutex_t。

  2、初始化互斥锁变量:pthread_mutex_init。

  3、加锁:pthread_mutex_lock。

  4、解锁:pthread_mutex_unlock。

  5、删除锁:pthread_mutex_destroy。
  接下来我用互斥锁将一个不可重入的函数使它可重入从而使多个线程同时运行函数时变得安全。

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
/*实现互斥锁的基本使用以及线程安全的基本认识*/                                    
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
int ticket = 100;
//互斥锁变量不一定非要全局变量,使用的线程都能访问到就行
//互斥锁变量
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int ticket = 100;
pthread_mutex_t mutex;
void* ticket_scalper(void* arg)
{
int id = (int)arg;
while(1)
{
//加锁要在临界资源访问之前
//int pthread_mutex_lock(pthread_mutex_t* mutex);阻塞加锁
//int pthread_mutex_trylock(pthread_mutex_t* mutex);非阻塞加锁,加不上锁就返回
pthread_mutex_lock(&mutex);
if(ticket > 0)
{
printf("scalper:%d--get a ticket:%d\n", id, ticket);
ticket--;
usleep(1000);
}
else
{
//解锁
pthread_mutex_unlock(&mutex);
pthread_exit(0);
}
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
int i = 0;
int ret;
pthread_t tid[4];
//初始化互斥锁
//int pthread_mutex_init(pthread_mutex_t *restrict mutex,
// const pthread_mutexattr_t *restrict attr);
//
pthread_mutex_init(&mutex, NULL);
//创建线程
for(i = 0; i < 4; i++)
{
ret = pthread_create(&tid[i], NULL, ticket_scalper, (void*)i);
if(ret != 0)
{
perror("thread creat error:");
return -1;
}
}
for(i = 0; i < 4; i++)
{
pthread_join(tid[i], NULL);
}
//销毁互斥锁
//int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_destroy(&mutex);
}



[misaki@localhost thread_2019_9_2_class45]$ ./main
scalper:2--get a ticket:100
scalper:2--get a ticket:99
scalper:2--get a ticket:98
scalper:2--get a ticket:97
scalper:2--get a ticket:96
scalper:2--get a ticket:95
scalper:3--get a ticket:94
scalper:3--get a ticket:93
scalper:3--get a ticket:92
scalper:3--get a ticket:91
scalper:3--get a ticket:90
scalper:3--get a ticket:89
scalper:3--get a ticket:88
scalper:3--get a ticket:87
scalper:3--get a ticket:86
scalper:3--get a ticket:85
scalper:3--get a ticket:84
scalper:3--get a ticket:83
scalper:3--get a ticket:82
scalper:3--get a ticket:81
scalper:3--get a ticket:80
scalper:3--get a ticket:79
scalper:3--get a ticket:78
scalper:3--get a ticket:77
scalper:3--get a ticket:76
scalper:3--get a ticket:75
scalper:3--get a ticket:74
scalper:3--get a ticket:73
scalper:3--get a ticket:72
scalper:3--get a ticket:71
scalper:3--get a ticket:70
scalper:3--get a ticket:69
scalper:3--get a ticket:68
scalper:3--get a ticket:67
scalper:3--get a ticket:66
scalper:3--get a ticket:65
scalper:3--get a ticket:64
scalper:3--get a ticket:63
scalper:3--get a ticket:62
scalper:3--get a ticket:61
scalper:3--get a ticket:60
scalper:3--get a ticket:59
scalper:3--get a ticket:58
scalper:3--get a ticket:57
scalper:3--get a ticket:56
scalper:3--get a ticket:55
scalper:3--get a ticket:54
scalper:3--get a ticket:53
scalper:3--get a ticket:52
scalper:3--get a ticket:51
scalper:3--get a ticket:50
scalper:3--get a ticket:49
scalper:3--get a ticket:48
scalper:3--get a ticket:47
scalper:3--get a ticket:46
scalper:3--get a ticket:45
scalper:3--get a ticket:44
scalper:3--get a ticket:43
scalper:3--get a ticket:42
scalper:3--get a ticket:41
scalper:3--get a ticket:40
scalper:3--get a ticket:39
scalper:3--get a ticket:38
scalper:3--get a ticket:37
scalper:3--get a ticket:36
scalper:3--get a ticket:35
scalper:3--get a ticket:34
scalper:3--get a ticket:33
scalper:3--get a ticket:32
scalper:3--get a ticket:31
scalper:3--get a ticket:30
scalper:3--get a ticket:29
scalper:3--get a ticket:28
scalper:3--get a ticket:27
scalper:3--get a ticket:26
scalper:3--get a ticket:25
scalper:3--get a ticket:24
scalper:3--get a ticket:23
scalper:3--get a ticket:22
scalper:3--get a ticket:21
scalper:3--get a ticket:20
scalper:3--get a ticket:19
scalper:3--get a ticket:18
scalper:3--get a ticket:17
scalper:3--get a ticket:16
scalper:3--get a ticket:15
scalper:3--get a ticket:14
scalper:3--get a ticket:13
scalper:3--get a ticket:12
scalper:3--get a ticket:11
scalper:3--get a ticket:10
scalper:3--get a ticket:9
scalper:3--get a ticket:8
scalper:3--get a ticket:7
scalper:3--get a ticket:6
scalper:3--get a ticket:5
scalper:3--get a ticket:4
scalper:3--get a ticket:3
scalper:3--get a ticket:2
scalper:3--get a ticket:1

  这样就达成了互斥,在一个线程操作临界资源时,其他线程不会同时干涉。

死锁

  死锁是指因为对一些无法加锁的锁进行加锁操作而导致程序卡死。死锁是我们一定要在使用锁时要注意和避免的

  死锁产生的四个必要条件:

  1、互斥条件。一个线程操作时其他线程不能操作。

  2、不可剥夺条件。一个线程加的锁别的线程不能释放。

  3、请求与保持条件。一个线程已经有了锁却还在请求其他的锁,但是其他的锁请求不到第一个锁也不释放。

  4、环路等待条件。

  死锁产生往往是因为加锁解锁的顺序不同。要想避免死锁就要避免死锁产生的四个必要条件——死锁检测算法,银行家算法。

同步

  通过对当前是否满足对临界资源的操作条件来判断线程是否该等待或唤醒这种方式实现对临界资源访问的合理性。资源产生后才能进行使用,没有资源则等待资源产生,生产资源后则唤醒等待,这样则达成同步。然而互斥锁虽然可以帮助我们完成等待但是无法判断何时将我们唤醒,不能在合适的事件唤醒,因此便要借助新的东西——条件变量。

条件变量

  条件变量的使用流程:

  1、定义条件变量:pthread_cond_t。

  2、初始化条件变量:pthread_cond_init。

  3、等待或者唤醒:pthread_cond_wait/pthread_cond_signal。

  4、销毁条件变量:pthread_cond_destroy。

  pthread_cond_wait中一共有三个操作,首先它要让让当前线程等待,但是此时有一点,此时的互斥量还处于加锁状态其他线程无法操作临界资源,呢又怎么做到让临界资源达到要求呢?因此他在让线程等待前要先解除了互斥量的加锁状态,并且这两部操作为原子操作。为什么要是原子操作?因为如果不是原子操作有可能在解锁后已经条件满足而此时线程还未进行等待可能会忽略唤醒。之后在线程被唤醒后pthread_cond_wait还会再加锁保证互斥。这就是三部操作:解锁->等待->唤醒后加锁。
  在每一个条件变量内部都有一个等待队列,将所有等待的线程排列在上面,如果有其他线程唤醒则逐一唤醒。
  接下来我们用互斥锁加条件变量模拟实现一个顾客去餐馆吃饭的情景,但是在这个情境中为了符合设计要注意两个顾客不能同时吃一碗饭,并且只有一个锅因此两个厨师不能同时做饭。如果没饭了2个厨师中其中一个做饭,又犯了2个顾客其中一个吃饭。

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
/*实现条件变量的基本使用*/                                           
/*吃面前提有人吃面,如果没有线程的面,等待老板做出来
* 老板做出来面就要唤醒顾客
* 老板不会做太多的面,老板只会提前做一碗面
* 如果已经有面做出来,但是没人吃,不会再做(等待)
* 顾客吃完面后,老板再来一碗(唤醒老板的等待)*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
//是否右面
int have_noodle = 1;
//为了让客人与厨师间的不同的同步性,需要定义多个条件变量
pthread_cond_t customer;
pthread_cond_t boss;
pthread_mutex_t mutex;
//老板做面
void* thr_boss(void* arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
//由于多个顾客,为了避免两个顾客吃一碗面的情况这里要循环判断
while(have_noodle == 1)//有面
{
//等待
//int pthread_cond_timedwait(pthread_cond_t *restrict cond,
// pthread_mutex_t *restrict mutex,
// const struct timespec *restrict abstime);
//限时等待
//cond:条件变量
//mutex:互斥锁
//abstime:限时等待时长
//时间到后返回时间超市,停止阻塞
//int pthread_cond_wait(pthread_cond_t *restrict cond,
// pthread_mutex_t *restrict mutex);
//cond:条件变量
//mutex:互斥锁
//pthread_cond_wait 集合了解锁后挂起的操作(原子操作,不可被打断)
//有可能还没来得及挂起就已经有人唤醒,白唤醒,导致死等
//因此这里的wait将三个操作进行了原子性封装不让其中断
//解锁 -》 等待 -》 被唤醒后加锁
pthread_cond_wait(&boss, &mutex);
}
//面没了,要再做
printf("拉面 + 1\n");
have_noodle = 1;
//面好了,唤醒顾客
pthread_cond_signal(&customer);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
//顾客吃面
void* thr_customer(void* arg)
{
while(1)
{
while(have_noodle == 0)
{
//若没有现成的面等老板做好
//等待
pthread_cond_wait(&customer, &mutex);
}
//有面了
printf("真好吃!\n");
have_noodle -= 1;
//唤醒厨师再做一碗
pthread_cond_signal(&boss);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t tid1, tid2;
int ret;
//条件变量初始化
//int pthread_cond_init(pthread_cond_t *restrict cond,
// const pthread_condattr_t *restrict attr);
pthread_cond_init(&boss, NULL);
pthread_cond_init(&customer, NULL);
pthread_mutex_init(&mutex, NULL);
//各建立两个线程同时工作,相当于两个厨师两个客人
//客人间具有互斥性,厨师间也有互斥性,客人与厨师间有同步与互斥性
for(int i = 0; i < 2; i++)
{
ret = pthread_create(&tid1, NULL, thr_boss, NULL);
if(ret != 0)
{
printf("boss error");
return -1;
}
}
for(int i = 0; i < 2; i++)
{
ret = pthread_create(&tid2, NULL, thr_customer, NULL);
if(ret != 0)
{
printf("customer error");
return -1;
}
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
//销毁条件变量
//int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_destroy(&customer);
pthread_cond_destroy(&boss);
//销毁锁
pthread_mutex_destroy(&mutex);
}


真好吃!
拉面 + 1
真好吃!
拉面 + 1
真好吃!
拉面 + 1
真好吃!
拉面 + 1
真好吃!
拉面 + 1
真好吃!
拉面 + 1
真好吃!
拉面 + 1
^C真好吃!

  在以上这个例子中要注意几个点:
  1、用户对条件判断需要使用循环进行判断(防止角色不符合条件被唤醒之后因为不循环判断直接操作临界资源)。这个问题也被称为虚假唤醒问题。在多核处理器下,pthread_cond_signal可能会激活多于一个线程(阻塞在条件变量上的线程)。结果就是,当一个线程调用pthread_cond_signal()后,多个调用pthread_cond_wait()或其他等待在队列上的线程返回。这种效应就会造成虚假唤醒。
  2、不同角色的线程因该等待在不同的条件变量上。(防止角色的误唤醒,导致程序阻塞)
  但是要注意条件变量并不保证安全,因此往往使用条件变量的时候会与互斥锁共同使用。面生产一碗顾客吃一碗没有出现异常,因此实现是成功的。这种在多线程情况下有人生产数据有人消费数据利用同步与互斥达到合理与安全的模式十分经典,因此产生了一种固定的设计模型,这就是生产者消费者模型。

生产者与消费者模型

  生产者与消费者模型中有两种角色:生产者与消费者,同时包含三种关系:生产者与生产者之间互斥,消费者与消费者之间互斥,生产者与消费者之间同步与互斥。他们工作在一个场景中,这个场景通常是一个队列,用来保存数据。

实现

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/**                                                   
* 基于互斥锁与条件变量实现一个线程安全的队列
* 实现生产者与消费者模型
**/
#include <iostream>
#include <queue>
#include <pthread.h>
#define MAXQ 10
class BlockQueue
{
public:
BlockQueue(int maxq = MAXQ)
:_capacity(maxq)
{
pthread_mutex_init(&_mutex, NULL);
pthread_cond_init(&_cond_consumer, NULL);
pthread_cond_init(&_cond_productor, NULL);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond_consumer);
pthread_cond_destroy(&_cond_productor);
}
bool QueuePush(int data)
{
pthread_mutex_lock(&_mutex);
while(_queue.size() == _capacity)
{
pthread_cond_wait(&_cond_productor, &_mutex);
}
_queue.push(data);
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_cond_consumer);
return true;
}
bool QueuePop(int &data)
{
pthread_mutex_lock(&_mutex);
while(_queue.empty())
{
pthread_cond_wait(&_cond_consumer, &_mutex);
}
data = _queue.front();
_queue.pop();
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_cond_productor);
return true;
}
private:
std::queue<int> _queue;
int _capacity;
pthread_mutex_t _mutex;
pthread_cond_t _cond_productor;
pthread_cond_t _cond_consumer;
};
void* thr_consumer(void* arg)
{
BlockQueue* q = (BlockQueue*)arg;
int data;
while(1)
{
//消费者一直获取数据进行打印
q->QueuePop(data);
std::cout << "consumer gets a piece of data--" << data << std::endl;
}
}
void* thr_productor(void* arg)
{
BlockQueue* q = (BlockQueue*)arg;
int data = 0;
while(1)
{
//生产者一直添加数据
q->QueuePush(data);
std::cout << "producer produces a data--" << (data++) << std::endl;
}
return NULL;
}
int main()
{
pthread_t ctid[4], ptid[4];
int ret, i;
BlockQueue q;
for(i = 0; i < 4; i++)
{
ret = pthread_create(&ctid[i], NULL, thr_consumer, (void*)&q);
if(ret != 0)
{
std::cerr << "create thread error" << std::endl;
}
}
for(i = 0; i < 4; i++)
{
ret = pthread_create(&ptid[i], NULL, thr_productor, (void*)&q);
if(ret != 0)
{
std::cerr << "create thread error" << std::endl;
}
}
for(i = 0; i < 4; i++)
{
pthread_join(ctid[i], NULL);
pthread_join(ptid[i], NULL);
}
}


consumer gets a piece of data--2111
consumer gets a piece of data--2112
consumer gets a piece of data--2113
consumer gets a piece of data--2114
consumer gets a piece of data--2115
consumer gets a piece of data--2116
consumer gets a piece of data--2117
consumer gets a piece of data--2118
producer produces a data--2119
producer produces a data--2120
producer produces a data--2121
producer produces a data--2122
producer produces a data--2123
producer produces a data--2124

  这里打印之所以看上去乱是因为xshell的显示跟不上虚拟机计算的速度。

优点

  生产者与消费者模型有三个优点:
  1、解耦合
  2、支持忙闲不均
  3、支持并发
  一个场所,两种角色,三种关系。

posix标准信号量

  system V是内核中的计数器,posix是线程间的全局计数器。它也有着实现进程/进程间同步与互斥的贡藕功能。

与条件变量的区别

  条件变量是通过等待、唤醒操作来让线程等待在等待队列上来完成同步,这需要用户自己进行外部条件判断并且要搭配互斥锁一起使用。
  信号量是通过自身内部技术实现条件的判断,不需要搭配互斥锁,自身已经保证了原子操作。

信号量的工作原理

  信号量通过一个计数器实现对资源的计数,并且通过这个计数来判断当前线程/进程能否对临界资源进行访问,对临界资源进行访问之前先发起调用访问信号量进行判断是否能够访问。
  信号量实现同步:首先资源计数-1,若此时资源计数>=0,则可以直接进行访问,调用直接返回,若信号量内部计数器<0表示没有资源无法访问,调用阻塞(挂起线程);若其他线程生产了一个资源则发起调用,首先资源计数+1,如果此时计数器<=0则唤醒等待队列上的线程,若此时计数器>0则什么都不做。
  信号量实现互斥:计数只有0/1,资源只有一个,同一时间只有 一个线程可以访问。首先信号量-1,若此时信号量<0则调用阻塞,若>0,则调用返回,对临界资源进行访问,访问完毕,进行计数+1,唤醒所有线程,所有线程继续进行抢夺。
 同时如果信号量小于0则表示当前阻塞在等待队列上的线程/进程数,等于0表示资源刚好完全分配,大于0则表示多余资源数。

接口

1
2
3
4
5
6
7
8
9
sem_t//定义信号量。
sem_init(sem_t* sem, int flag, int initval);//初始化,
//flag:0-线程间,!0-进程间
//initval:用于设置初值
sem_wait(sem_t* sem);//,进行判断是否有资源,<=0则阻塞,>0则-1并调用返回并。
sem_trywait(sem_t* sem);//非阻塞,没有资源直接报错返回。
sem_timedwait(sem_t* sem);//限时阻塞,等待一段时间,若一直没有资源则超时报错返回
sem_post(sem_t* sem);//计数+1,并且唤醒等待的线程
sem_destroy(sem_t* sem);//销毁信号量

应用

  用信号量实现线程安全的环形队列。

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/**
* 利用信号量完成线程安全的环形队列
**/
#include <iostream>
#include <vector>
#include <semaphore.h>
#include <thread>
#define MAXQ 10
class RingQueue
{

public:
RingQueue(int maxq = MAXQ)
:_capacity(maxq)
,_queue(maxq)
,_step_read(0)
,_step_write(0)
{
sem_init(&_lock, 0, 1);
sem_init(&_idle_space, 0, maxq);
sem_init(&_data_space, 0, 0);
}
~RingQueue()
{
sem_destroy(&_lock);
sem_destroy(&_idle_space);
sem_destroy(&_data_space);
}
bool QueuePush(int data)
{
//没有空闲空间则阻塞
sem_wait(&_idle_space);
//加锁
sem_wait(&_lock);
_queue[_step_write] = data;
_step_write = (_step_write + 1) % _capacity;
//解锁
sem_post(&_lock);
//唤醒消费者
sem_post(&_data_space);
return true;
}
bool QueuePop(int& data)
{
sem_wait(&_data_space);
sem_wait(&_lock);
data = _queue[_step_read];
_step_read = (_step_read + 1) % _capacity;
sem_post(&_lock);
sem_post(&_idle_space);
return true;
}
private:
std::vector<int> _queue;//用vector实现环形队列
int _capacity;//容量
int _step_read;//读指针
int _step_write;//写指针

sem_t _lock;//初始计数=1,负责完成互斥

//也需要有两个等待队列,分别完成两个角色间的同步
sem_t _idle_space;//空闲空间节点个数,生产者等待在这里,完成同步
sem_t _data_space;//数据节点个数,初始=0,消费者等待在这里,完成同步
};
void thr_producer(RingQueue* q)
{
int data = 0;
while(1)
{
q->QueuePush(data);
std::cout << "push data ----" << data++ << std::endl;
}
}
void thr_consumer(RingQueue* q)
{
int data = 0;
while(1)
{
q->QueuePop(data);
std::cout << "get data ----" << data << std::endl;
}
}

int main()
{
RingQueue q;
std::vector<std::thread> list_con(4);
std::vector<std::thread> list_pro(4);
for(int i = 0; i < 4; i++)
{
list_pro[i] = (std::thread(thr_producer, &q));
}
for(int i = 0; i < 4; i++)
{
list_con[i] = (std::thread(thr_consumer, &q));
}
for(int i = 0; i < 4; i++)
{
list_con[i].join();
list_pro[i].join();
}
}


push data ----4028
get data ----push data ----4102
41004029
3996push data ----4030
push data ----4031
push data ----4032
push data ----push data ----4033
push data ----4034
push data ----4101
push data ----4102
get data ----
push data ----4103
push data ----4104
40974102
get data ----4101

push data ----4103

push data ----get data ----4029
4095
get data ----4031
get data ----4032
get data ----4033
get data ----4034
get data ----4102
get data ----get data ----40304103
push data ----get data ----4104
get data ----4103
get data ----4105
get data ----4104
push data ----3997
push data ----3998
push data ----3999
push data ----4000
push data ----4001
4104get data ----
push data ----4105
push data ----4106
push data ----4107
push data ----4108
push data ----push data ----4035
3997
get data ----3999
get data ----4000

读写锁

  读写锁在数据库中就有着极为重要的应用,这样才得以让数据得到共享修改得到合理的保存而不出现数据的二义性。

特点,原理及应用

  读写锁有着自己的特点:写互斥,读共享,一个用户写时所有其他所有用户都不能读和写;一个用户读时其他所有用户都可以读但不能写,因此适用于多读少写的应用场景,保证数据不会出现二义性并且保证读取和写入的效率。
  读写锁内部有两个计数器,读者计数与写者计数。加写锁时对两个技术进行判断如果任意一个计数器>0,都无法加写锁需要等待。加读锁时对写者计数进行判断,若大于0,则无法加读锁需要进行等待。
  读写锁通过自旋锁实现,不满足条件时自旋等待。自旋锁的特点是:等待中不停循环对条件进行判断,因此可以及时响应,但是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
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
/**                                                                             
* 线程池由两个部分构成一个是一个任务类
* 另一个部分是一个线程安全的队列,由此构成任务队列,
* 再用一组线程从任务队列中获取任务来执行
* 由此构成线程池
**/
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <queue>
#include <thread>
#include <string>
#include <time.h>
#include <stdlib.h>
#include <sstream>
#define MAX_THREAD 5
#define MAX_QUEUE 10
typedef void(*handler_t)(int val);
//任务类
//1、决定线程处理的任务,处理什么数据,怎么处理都由用户传入
class Task
{
private:
int _data;//数据
handler_t _handler;//处理数据的方法,函数指针,用于传入线程中给线程下达命令
public:
Task(int data, handler_t handler)
:_data(data)
,_handler(handler)
{

}
void SetTask(int data, handler_t handler)
{
_data = data;
_handler = handler;
}
void Run()
{
return _handler(_data);
}

};
//线程池类
class ThreadPool
{
private:
std::queue<Task> _queue;//任务队列
int _capacity;//线程池最大任务数量
pthread_mutex_t _mutex;//锁,完成互斥,类似于生产者消费者模型
pthread_cond_t _cond_pro;//条件变量,完成
pthread_cond_t _cond_con;
int _thr_max;//线程池拥有的总线程数
std::vector<std::thread> _thr_list;//线程组,存储线程操作句柄
bool _quit_flag;//用于控制线程是否退出
int _thr_cur;//线程的数量,线程退出时,判断当前线程数量
void thr_start()
{
while(1)
{
pthread_mutex_lock(&_mutex);
while(_queue.empty())
{
if(_quit_flag == true)
{
std::cout << "thread exit " << pthread_self() << std::endl;
pthread_mutex_unlock(&_mutex);
_thr_cur--;
return;
}
pthread_cond_wait(&_cond_con, &_mutex);
}
Task tt = _queue.front();
_queue.pop();
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_cond_pro);
//任务处理放到锁外,防止线程处理任务时间过长,一直加锁导致其他线程无法处理其他任务
tt.Run();
}
}
public:
//初始化线程池
ThreadPool(int maxq = MAX_QUEUE, int maxt = MAX_THREAD)
:_capacity(maxq)
,_thr_max(maxt)
,_thr_list(maxt)
,_thr_cur(0)
{
pthread_mutex_init(&_mutex, NULL);
pthread_cond_init(&_cond_pro, NULL);
pthread_cond_init(&_cond_con, NULL);
}
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond_pro);
pthread_cond_destroy(&_cond_con);
}
//初始化线程组
bool PoolInit()
{
for(int i = 0; i < _thr_max; i++)
{
_thr_list[i] = std::thread(&ThreadPool::thr_start, this);
_thr_list[i].detach();
_thr_cur++;
}
return true;
}
//添加任务
bool AddTask(Task& tt)
{
pthread_mutex_lock(&_mutex);
while(_queue.size() == _capacity)
{
pthread_cond_wait(&_cond_pro, &_mutex);
}
_queue.push(tt);
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_cond_con);
return true;
}
//销毁线程池,停止工作
bool PoolStop()
{
pthread_mutex_lock(&_mutex);
_quit_flag = true;
pthread_mutex_unlock(&_mutex);
while(_thr_cur > 0)
{
//std::cout << "cont:" << _thr_cur << std::endl;
pthread_cond_broadcast(&_cond_con);
usleep(1000);
}
//for(int i = 0; i < _thr_max; i++)
//{
// _thr_list[i].join();
//}
return true;
}
};
void test(int data)
{
srand(time(NULL));
int nsec = rand() % 5;
std::stringstream ss;
ss << "thread:" << pthread_self() << " processint data ";
ss << data << " and sleep " << nsec << " sec" << std::endl;
std:: cout << ss.str();
sleep(nsec);
return;
}
int main()
{
ThreadPool pool;
pool.PoolInit();
for(int i = 0; i < 10; i++)
{
Task tt(i, test);
pool.AddTask(tt);
}
pool.PoolStop();
}


[misaki@localhost thread]$ ./threadpool
thread:139941165012736 processint data 0 and sleep 4 sec
thread:139941156620032 processint data 1 and sleep 4 sec
thread:139941190190848 processint data 2 and sleep 4 sec
thread:139941181798144 processint data 3 and sleep 4 sec
thread:139941173405440 processint data 4 and sleep 4 sec
thread:139941190190848 processint data 6 and sleep 4 sec
thread:139941156620032 processint data 7 and sleep 4 sec
thread:139941181798144 processint data 8 and sleep 4 sec
thread:139941165012736 processint data 5 and sleep 4 sec
thread:139941173405440 processint data 9 and sleep 4 sec
thread exit 139941156620032
thread exit 139941190190848
thread exit 139941181798144
thread exit 139941165012736
thread exit 139941173405440

设计模式

单例模式

  单例模式是一种常见设计模式,之前已经多次介绍,Cpp章节中也有实现。单例模式使用场景是在一个资源只能被加载分配一次,一个类只能实例化一个对象的情况。

饿汉模式

  资源一次性加载分配完,对象在程序初始化阶段实例化完毕,这种实现是线程安全的,程序运行起来比较流畅,但是启动加载时间可能过长。

懒汉模式

  资源使用时再加载分配,对象再使用的时候再去实例化,这种实现加载快,同一时间消耗资源少,但是运行中可能卡顿。这种实现是线程不安全的,因此我们要加锁判断类是否实例化过,如果没有则实例化。

【Cpp】第二章-类和对象-中

发表于 2019-05-15 | 分类于 Cpp
字数统计: 3.2k

类和对象

类的六个默认成员函数

  在我们构建一个类之后即使我们在其中不写任何的成员函数,在其中也会有6个默认编译器自动生成的成员函数,这些函数构成了类的基本功能包括初始化,销毁后的清理工作等。当然这些自动生成的成员函数功能有限有时候或许无法达到预期的效果,因此我们可以对其进行重载让其能够达到我们需要的功能。

  1、构造函数
  2、析构函数
  3、拷贝构造函数
  4、赋值运算符重载函数
  5、对普通对象取地址运算符重载
  6、对常对象取地址运算符重载
  以上这6个函数中前四个我们都有极大可能性会对其重载因此需要重点学习,而后两个则很少需要自己实现。

构造函数

  构造函数时在类刚刚实例化创建出对象时自动进行调用的,主要负责类的初始化。因此我们就要注意一点,我们在调用构造函数的时候只是对对象进行初始化并不为其分配空间,因此构造函数并不是在构造对象,而是在初始化对象。

特性

  构造函数具有以下特性
  1、与类名相同
  2、实例化自动调用
  3、无返回值
  4、支持重载

默认构造函数

  所有的无参或是全缺省的构造函数都可以看作是默认的构造函数,并不一定非要是默认生成的构造函数,因此默认构造函数大致分为三种。
  1、自定义无参构造函数
  2、自定义全缺省构造函数
  3、自动生成无参构造函数
  如果类中没有构造函数,则会默认生成一个无参构造函数,但是如果我们定义了构造函数则不会再默认生成。但是默认生成的无参构造函数不会进行成员变量的初始化,如果成员中有其他自定义类型的成员变量会调用它的构造函数。

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
#include <iostream>                                                                      
using std::cout;
using std::endl;
using std::string;
class Student
{
private:
int _num;//学号
string _name;//姓名
int _classId;//班级编号
public:
//一个类中默认的构造函数只能有一个,否则会产生歧义
//默认构造函数(自动生成的构造函数与它相同)
//Student(){}
//构造函数重载
//全缺省构造函数,也可以视为默认构造函数
//如果有默认值的情况下我们最好在类中将默认构造函数定义为全缺省
Student(int num = 1, string name = "李狗蛋", int classId = 1)
{
_num = num;
_name = name;
_classId = classId;
}
void Print()
{
cout << "学号:" << _num << "\t姓名:" << _name << "\t班级:" << _classId << endl;
}
};
int main()
{
//如果使用无参构造则不需要加括号,否则会和函数声明产生歧义
Student student1;//默认构造,全部用默认值
Student student2(2, "张三", 2);
student1.Print();
student2.Print();
}


[misaki@localhost 第二章-类和对象]$ ./DefaultFun
学号:1 姓名:李狗蛋 班级:1
学号:2 姓名:张三 班级:2

  这里需要注意的就是为了避免歧义一类中只能出现一个默认构造函数。同时自动生成的构造函数功能十分优先,不会进行成员变量初始化,所以我们往往需要重载。

析构函数

  与构造函数向对立的就是析构函数,析构函数是在对象销毁时自动调用的,主要时为了帮助我们清理我们创建对象时分配的空间,清空指针之类的,析构函数在如果成员变量中没有指针或者没有动态分配内存亦或者没有需要关闭的文件的情况下往往时不需要自己进行定义的。

特性

  1、定义在类名前加~
  2、无参数无返回值,意味着析构函数无法重载,只能存在一个
  3、有且只有一个析构函数,若未显式定义则会生成默认析构
  4、对象销毁时自动调用析构函数
  析构函数特性与构造十分类似,最为主要的就是一个类中只会唯一存在一个析构函数。

默认析构函数

  析构函数和构造函数一样如果我们并未定义则会自动生成一个,同样自动生成的析构函数不会做任何处理,析构函数在处理自定义类型时同样也会自动调用其对应的析构函数。

1
2
//析构函数,默认生成的析构函数与它相同   
~Student(){}

拷贝构造

  拷贝构造是用来实例化与某一对象相同的对象的,他会将该对象的数据完全复制一份相同的出来,拷贝构造是构造函数的一个重载。

默认拷贝构造

  同样如果我们并未显式定义拷贝构造则会自动生成一个,但是要注意默认生成的拷贝构造只能完成简单的浅拷贝,因此如果我们想要对复杂的如链表类进行拷贝需要自行定义。

1
2
3
4
5
6
7
//默认拷贝构造  
Student(const Student& student)
{
_num = student._num;
_name = student._name;
_classId = student._classId;
}

注意

  拷贝构造在很多情况下都会调用,比如说我们将对象传给一个函数时,由于副本传参会复制一份副本进行传入,这个时候就已经调用了拷贝构造。那么我们在调用拷贝构造的时候就会将这个对象当作参数传入,如果我们这个时候调用了拷贝构造就是在拷贝构造中调用了拷贝构造会发生无限递归的情况,这该怎么解决呢?——传引用即可。因此拷贝构造的参数必定是本类对象的引用,不然就会报错。

1
2
3
4
5
6
7
8
9
10
11
12
//拷贝构造  
Student(Student student)
{
_num = student._num;
_name = student._name;
_classId = student._classId;
}

报错:
DefaultFun.cpp:25:26: 错误:无效的构造函数:您要的可能是‘Student (const Student&)’
Student(Student student)
^

  在编写函数的时候建议多用引用类型,尤其是在面对占用空间大的自定义类型时,使用引用会提高效率,不会发生拷贝构造,如果我们并不希望修改传入的参数,在前面加上const来避免修改,这是很好的代码风格。这里极度推荐一定要传入常引用,不然可能会引发赋值运算符重载连续赋值时调用拷贝构造传参产生常引用参数类型不服无法调用的情况。

赋值运算符重载

  赋值运算符重载是类似于拷贝构造的默认构造函数,不过是将拷贝构造通过=来使用,不过我们得先了解运算符重载。

运算符重载

  在C++中支持对运算符进行重载,使得给我们带来的很大的遍历,由此我们可以通过预算符来指定操作操作我们的自定义类型。

重载规则

  1、不能创建新的操作符
  2、必须有一个自定义类型或枚举类型操作数。
  3、内置类型操作符不能改变其含义
  4、在类内定义时会多一个默认形参this指针
  5、.*、::、sizeof、.、?:这五个操作符不能重载。

类外重载

  在类外进行操作符重载定义的成为类外重载,往往有两个参数,其中要求至少有一个是自定义类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool operator==(const Student& stu1, const Student& stu2)
{
return stu1._num == stu2._num && stu1._name == stu2._name && stu1._classId == stu2._classId;
}
int main()
{
//如果使用无参构造则不需要加括号,否则会和函数声明产生歧义
Student student1(2, "张三", 2);//默认构造,全部用默认值
Student student2(2, "张三", 2);
cout << (student1 == student2) << endl;
//实际上会转换为operator==(student1, student2);
}

[misaki@localhost 第二章-类和对象]$ ./DefaultFun
1

  但是类外构造有一个致命的缺点,就是在类外我们往往无法访问到类内的很多成员变量,为了实现重载将其改为公有有点以小失大,因此就有了类内构造。

类内重载

  类内重载则能解决成员在类外无法调用的问题。定义的类则自动被视为左操作数,参数则为右操作数。不过要注意类内重载会默认有一个形参this。

1
2
3
4
5
6
7
8
9
10
11
12
13
//类内重载                                                                     
bool operator==(const Student& stu2)
{
return _num == stu2._num && _name == stu2._name && _classId == stu2._classId;
}
int main()
{
//如果使用无参构造则不需要加括号,否则会和函数声明产生歧义
Student student1(2, "张三", 2);//默认构造,全部用默认值
Student student2(2, "张三", 2);
cout << (student1 == student2) << endl;
//实际调用:student1.operator==(&student1, student2);
}

赋值运算符重载

  赋值运算符重载不过是将拷贝构造的内容通过赋值运算符重载来表示,这样更加直观,同样的如果我们未显式定义则会默认生成一个。同时这个运算符重载是有返回值的,是为了用于实现连续赋值。这里推荐一定传入引用类型的参数,是为了防止连续赋值时调用拷贝构造出现问题,同时返回值也要返回引用类型,防止调用拷贝构造影响效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//赋值运算符重载,自动生成的重载与他相同
Student& operator=(const Student& stu)
{
_num = stu._num;
_name = stu._name;
_classId = stu._classId;
return *this;
}
int main()
{
Student student1(2, "张三", 2);//默认构造,全部用默认值
Student student2 = student1;
student2.Print();
}



[misaki@localhost 第二章-类和对象]$ ./DefaultFun
学号:2 姓名:张三 班级:2

  自动生成的运算符重载也只能完成浅拷贝。

const成员

  我们有时会定义常对象,即不可改变的对象,这种对象在调用成员函数如果调用的普通成员函数会出现问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
int main()
{
const Student student1(2, "张三", 2);
student1.Print();
}

[misaki@localhost 第二章-类和对象]$ make
g++ -g DefaultFun.cpp -o DefaultFun
DefaultFun.cpp: 在函数‘int main()’中:
DefaultFun.cpp:61:18: 错误:将‘const Student’作为‘void Student::Print()’的‘this’实参时丢弃了类型限定 [-fpermissive]
student1.Print();
^
make: *** [DefaultFun] 错误 1

  这里主要问题所在就在于调用函数由于我们的对象是const,那么对象本身不可改,在传给隐式this的时候这里的this的类型也应该为const *,否则就会出现这样的问题。那么我们怎么样解决呢?这里就要用到常成员函数。

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
void Print() const                                                                  
{
cout << "学号:" << _num << "\t姓名:" << _name << "\t班级:" << _classId << endl;
}
int main()
{
const Student student1(2, "张三", 2);
student1.Print();
}


[misaki@localhost 第二章-类和对象]$ ./DefaultFun
学号:2 姓名:张三 班级:2

  在成员函数后加const将其变为常成员,常成员会将函数的形参this类型改为const *,这样在常成员函数中则不可再修改调用对象的数据。

注意

  1、普通对象可以调用常成员函数。
  2、常对象不可以调用普通成员函数。
  3、建议将一切不会修改对象数据的成员函数都写为常成员函数。
  4、常对象只能初始化,不能进行修改。

取地址及const取地址操作符取地址

  这两个默认成员函数是最后的两个会默认生成的成员函数,一般情况下不需要我们显式定义,除非我们想让别人通过取地址符获得指定的数据。

1
2
3
4
5
6
7
8
9
10
//默认生成的取地址及const取地址操作符如下
Student* operator&()
{
return this ;
}

const Student* operator&() const
{
return this ;
}

【Cpp】第二章-类和对象(上)

发表于 2019-05-12 | 分类于 Cpp
字数统计: 2k

类和对象

  从本章开始我们就要开始学习C++ 中的最为重要的部分,也是让C++ 得以实现面向对象,得以更加方便的进行大型项目编程的最重要的部分——类和对象,类和对象的存在使C++得以完成封装。

类和对象初步认识

简介

  什么是类什么是对象呢? 类可以看作是一个类别,是一类事物的抽象和归纳。比如在现实世界中类可以是兔子,可以是人,可以是某一个职业,这是一类事物,我们将其抽象出来,而并非具体的。与之相对的是对象,对象就是类的实例化,比如说快递员这个类我们可以说快递员是一个类,而假如今天我们点了个外卖,呢么具体的今天来给我们送餐的这个人就是一个对象,对象是属于类的,,是类的具体实例。

类的定义

  类在C++ 中的语法更加类似于C语言中结构体的语法,不过在C语言中结构体内部只能定义成员变量,不能定义函数,在C++ 中结构体中可以定义成员函数,使得我们可以讲很多东西封装到类中支持面向对象的编程思想。并且在C++中我们更崇尚于用class来代替struct。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct AddStruct  
{
int num1;
int num2;
int AddNum()
{
return num1 + num2;
}
};
class AddClass
{
int num1;
int num2;
int AddNum()
{
return num1 + num2;
}
};

  以上我们用两种方法定义了两个类,可以看出在C++中struct与class的区别不是很大,但是即使如此它们之间依旧有所区别。其中最主要的区别就是在struct中的成员默认是公有的(public),而class中成员默认是私有的(private)。

类的访问限定符及封装

封装

  在C++中是如何实现封装的呢?其中最为重要的途径就是利用类和它*访问限定符,我们将对象的属性和方法封装在一个类中并为她们加上访问限定符让外界不能随意无序的进行访问我们就完成了封装。

访问限定符

  访问限定符一共有三个:private(私有);public(公有);protected(保护)。访问限定符的作用域是从当前访问限定符开始到下一个访问限定符结束或者到整个类的结束。

  这里重点介绍前两个,protected的主要作用起在类的继承方面,后续再做讨论。

public

  公有的访问限定符范围内的成员可以被类外部进行访问,我们可以在类外和类内随意使用类中public的成员,因此我们往往将类向外部提供的接口放在public中。

private

  私有的访问限定符范围内的成员不能被类外部进行访问,我们只能在类内调用或使用private的成员,因此private往往有保护类内数据的作用,为了方便起见我们往往将类内的成员变量全部定义为private,如果想要对类内数据机型修改则需要另写接口,这样的安全性更高。

  之后我们就用类访问限定符改造我们之前定义的Add类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class AddClass                  
{
private:
int _num1;
int _num2;
public:
void GetNum1(int num1)
{
_num1 = num1;
}
void GetNum2(int num2)
{
_num2 = num2;
}
int AddNum()
{
return _num1 + _num2;
}
};

  经过改造的类中的成员变量由于是private类型受到保护因此我们要再向外多提供几个接口以便让外部可以修改内部的值。同时为了将成员变量与函数中的变量加以区分我们往往会用_来修饰成员变量,这不是必须但是会提升代码可读性。

类的作用域

  其实类一旦声明再类内就形成了一个域,域外无法访问域内的成员。我们在域内可以直接声明并且定义成员函数,但是在域内定义的成员函数会默认作为内联函数对待,而且为了代码的可读性我们也并不能将所有的成员函数都定义在类内,那么如何在类外定义成员函数呢?为此我们再将之前写的类进行改造。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class AddClass  
{
private:
int _num1;
int _num2;
public:
//默认内联但为了可读性我们还是加上内联的标志
inline void GetNum1(int num1)
{
_num1 = num1;
}
inline void GetNum2(int num2)
{
_num2 = num2;
}
//在类内声明
int AddNum();
};
//在类外定义
int AddClass::AddNum()
{
return _num1 + _num2;
}

  我们可以通过域限定符来在类外定义函数,这样我们就可以将声明和定义分开,使代码可读性更高。

类的实例化

  在定义完类之后我们就需要用类来建立我们真正要使用的对象,我们称这一过程也叫类的实例化。类进行实例化十分方便,和用结构体定义变量无异。

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>
using namespace std;
class AddClass
{
private:
int _num1;
int _num2;
public:
//默认内联但为了可读性我们还是加上内联的标志
inline void GetNum1(int num1)
{
_num1 = num1;
}
inline void GetNum2(int num2)
{
_num2 = num2;
}
//在类内声明
int AddNum();
};
//在类外定义
int AddClass::AddNum()
{
return _num1 + _num2;
}
int main()
{
AddClass add;
add.GetNum1(1);
add.GetNum2(2);
cout << add.AddNum() << endl;
}


[misaki@localhost 第二章-类和对象]$ ./Class
3

  由此即完成了类的实例化即成员调用。

类的大小计算

  类的大小计算与结构体无异遵循内存对齐的规则,但有几点需要注意:

  1、类的大小只计算成员变量的大小,遵循内存对齐规则。

  2、类中的方法不算做类的大小,为了节省空间将方法存储在公共的区域且只存一个。

  3、空类的大小为1,这里占一个字节不进行数据存储只是为了在内存上占位。这里包括所有没有成员变量的类,哪怕有再多方法也是只有1字节大小。

this指针

什么是this指针

  既然我们类中的函数都存在同一块区域中,那么编译器使怎么区分是哪个对象调用了成员函数呢?这就牵扯到了每个类中隐藏的成员this指针。

  实际上这是一个指向类自身的指针。它会默认作为成员函数调用的第一个参数,将调用成员函数的对象地址传入好在函数内部找到对象中的成员。这一切都是隐式进行的都有编译器进行处理。因此实际上我们调用的代码是这样传参的。

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>
using namespace std;
class AddClass
{
private:
int _num1;
int _num2;
public:
//默认内联但为了可读性我们还是加上内联的标志
inline void GetNum1(int num1)
//inline void GetNum1(AddClass* this, int num1)
{
_num1 = num1;
}
inline void GetNum2(int num2)
{
_num2 = num2;
}
//在类内声明
int AddNum();
//int AddNum(AddClass* this);
};
//在类外定义
int AddClass::AddNum()
{
return _num1 + _num2;
}
int main()
{
AddClass add;
add.GetNum1(1);
//add.GetNum1(&add, 1);
add.GetNum2(2);
cout << add.AddNum() << endl;
//AddNum(&add);
}

this指针特性

  1、this指针不存储在类中,每个编译器对this指针存储的地方都有所不同,在vs中this指针存储在寄存器中。
  2、this指针可以为空,但是一旦调用需要访问对象中成员的函数就会由于this为空发生内存越界而导致崩溃。

【Cpp】第一章-Cpp入门

发表于 2019-05-11 | 分类于 Cpp
字数统计: 4.6k

第一章 C++入门

C++简介

什么是C++

  C语言是面向过程式的语言,在处理小规模的问题时则能体现出其简单易上手的的优势,但是在面对大型程序或需要高度抽象化的程序时,C语言就显得略有鸡肋。在20实际80年代,计算机界为了解决软件危机提出了面向对象(OOP)思想的变成模式,于是支持OOP的编程语言也应运而生。

  1982年Bjarne Stroustrup博士在C语言的基础上引入并且扩充了面向对象的概念,并且命名为C++,因此C++ 是在C语言基础上诞生的,它既可以支持面向过程编程也可以支持面向化程序设计。

  C++ 发展至今和C语言一样已经拥有很多各版本,并在不停的升级中,目前最为常用时C++ 98(引入STL,以模板方式重新编写标准库)和C++ 11(增加了很多特性,例如范围for,auto关键字),目前C++已经应用于互联网各个方向,例如大型系统开发,游戏开发,网络工具,嵌入式,数字图像处理……

如何学习

  多看书,目前市面上有很多C++ 优秀书籍,有些甚至成为了C++ 工程师心中的标杆(《Effective C++》)。

  多记录,每天的学习笔记,每周的学习总结,遇到的问题,这些都要多多记录,方便之后再次遇到相同的问题可以直接拿出来复习。

  思维导图,思维导图是学习中必不可少的,可以帮我们理清学习路线,学习思路,方便复习。

  多敲代码,语言都是如此多用才是巩固的基础,夺取在线OJ网上练习,或者自己敲几个小项目练练手都是练习的好思路。

命名空间

  在一门高级语言中,变量是大量存在的,那么难免在定义变量的时候就会重名,尤其在一个项目或工程中有多个工程师的时候就跟容易与他人定义相同变量名的变量,重名问题就会更加明显,在C语言中我们没有一个有效的办法来解决这个问题,因此C语言在大型项目方面会很吃力,因此在C++中加入了命名空间的语法,不同的程序员之间使用不同的命名空间,在不同的命名空间中允许变量重名,由此在项目合作中十分有用。

命名空间的定义

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>                         
//命名空间的定义
namespace N1
{
//在命名空间中可以定义函数、变量
int a = 1;
int b = 1;
int Add(int a, int b)
{
return a + b;
}
}
//命名空间的嵌套
namespace N2
{
int a = 2;
int b = 2;
int Add(int a, int b)
{
return a + b;
}
namespace N3
{
int a = 3;
int b = 3;
int Add(int a, int b)
{
return a + b;
}
}
}
//命名相同的命名空间
namespace N1
{
int c = 1;
}

  每一个命名空间都是一个作用域,空间中的内容都局限于该空间中,而我们要使用某空间中的某一变量或函数时指定命名空间即可找到指定的内容。
  命名空间支持嵌套,如果一个工程中同时存在多个相同名称的命名空间,则最后会合成到一个命名空间中。

命名空间的使用

  在使用命名空间时要加上作用域限定符::
进行作用域的限定。我们右三种使用命名空间的方法。

  加命名空间加作用域限定符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main()
{
//三种使用命名空间的方法
//::为作用域限定符
//分别打印三个命名空间中的a;
std::cout << "N1::a = " << N1::a << std::endl;
std::cout << "N2::a = " << N2::a << std::endl;
std::cout << "N2::N3::a = " << N2::N3::a << std::endl;
}



[misaki@localhost 第一章-C++入门]$ ./namespace
N1::a = 1
N2::a = 2
N2::N3::a = 3

  我们在进行输出的时候用到了cout函数以及endl换行函数,由于这两个函数都在std标准命名空间中,因此要想使用这两个函数也要用作用域限定符进行限定。

  使用using将命名空间中成员引入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using N1::a;
using N2::b;
int main()
{
//三种使用命名空间的方法
//::为作用域限定符
//分别打印三个命名空间中的a;
std::cout << "N1::a = " << a << std::endl;
std::cout << "N2::b = " << b << std::endl;
}


[misaki@localhost 第一章-C++入门]$ ./namespace
N1::a = 1
N2::b = 2

  使用命名空间名称引入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using namespace N1;
int main()
{
//三种使用命名空间的方法
//::为作用域限定符
//分别打印三个命名空间中的a;
std::cout << "N1::a = " << a << std::endl;
std::cout << "N1::b = " << b << std::endl;
}



[misaki@localhost 第一章-C++入门]$ ./namespace
N1::a = 1
N1::b = 1

缺省参数(默认参数)

  我们在C语言中书写函数时如果我们为一个函数设置了参数则在调用时必须对参数进行传入否则就会调用失败,但有的时候会出现大量的相同的冗余的参数,我嫩不得不手动将参数一一传入,但是C++中得益于缺省参数的语法是的我们可以在函数中提前设置默认的参数,如果有新参数传入则使用新的参数否则使用默认参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;
int func1(int a = 10, int b = 20, int c = 30)
{
return a + b + c;
}
int func2(int a, int b = 20, int c = 30)
{
return a + b + c;
}
int main()
{
cout << func1() << endl;
cout << func1(1, 2, 3) << endl;
cout << func2(1) << endl;
}


[misaki@localhost 第一章-C++入门]$ ./缺省参数
60
6
51

  在设置默认参数时一定要注意以下几点:

  1、为了不产生歧义我们只能从最后一个参数向前开始设置,并且中间不能跳过某个参数要保持默认参数连续。

  2、默认值只能是常量或是全局变量。

  3、缺省参数不能在函数的定义和声明中同时出现。

函数重载

  在C语言中我们要求函数名不能重名,因此往往我们需要功能相同的函数但向外提供不同的接口时就需要通过自己改变函数名的方式来进行区别。但在C++中我们有了函数重载,我们可以通过不同的参数对名字相同的函数进行修饰,使得函数的功能更加强大更加方便。

使用

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
#include <iostream>
using namespace std;
int Add(int num1, int num2)
{
cout << "call Add1" << endl;
return num1 + num2;
}
double Add(double num1, double num2)
{
cout << "call Add2" << endl;
return num1 + num2;
}
float Add(float num1, float num2)
{
cout << "call Add3" << endl;
return num1 + num2;
}
int main()
{
cout << "1 + 2 =" << Add(1, 2) << endl;
cout << "1.5 + 2.5 =" << Add(1.5, 2.5) << endl;
cout << "1.5f + 2.5f =" << Add(1.5f, 2.5f) << endl;
}


[misaki@localhost 第一章-C++入门]$ ./函数重载
call Add1
1 + 2 =3
call Add2
1.5 + 2.5 =4
call Add3
1.5f + 2.5f =4

  在C++如果定义同名函数我们必须保证参数列表不同,即参数的个数或参数类型或参数顺序不同才能完成重载,并且返回值不同不会进行函数的重载。

名字修饰

  在C++中我们之所以可以使函数重名是因为在编译过程中编译器根据函数的参数列表对我们的函数名进行了处理,使得其在最后得以唯一化,至于其名字修饰规则较为复杂,不同的编译器在修饰中的处理都不尽相同。我们可以利用反汇编看一下gcc的处理规则。

1
2
3
4
5
6
7
//三个Add()函数在编译中的修饰结果:
int Add(int, int):
000000000040089d <_Z3Addii>
double Add(double, double):
00000000004008d1 <_Z3Adddd>
float Add(float, float):
000000000040091d <_Z3Addff>

  从上面的例子中我们大致可以摸索出gcc函数修饰的规则,它会根据参数列表对函数名进行唯一化修饰。无论如何在编译中到最后一步链接之前我们的目标文件中一定不会出现重名的函数,不然在连接时就会产生歧义。

extern “C”

  C++ 支持向前兼容,就是说我们可以在C++ 中无缝调用C语言的代码,那么我们可以在C语言中调用C++ 的代码么?答案是肯定的。我们在C++ 的函数中只需要加上extern "C"就可以做到让编译器根据C语言的编译规则来进行编译,这样我们就可以在C语言中调用C++的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;
extern "C" int Add(int a, int b)
{
return a + b;
}
int main()
{
cout << Add(1, 2) << endl;
}


[misaki@localhost 第一章-C++入门]$ ./externC
3

总结

  C++ 支持重载而C语言不支持重载的原因是什么呢?C++支持函数名修饰,而C语言不支持。其实const也可以进行重载,但是要看情况,const只能支持指针和引用的重载。因为指针或引用指向不可更改的数据内容也是可以在编译过程中进行函数修饰的。

引用

  引用是C++中一种新的类似于指针的语法,但是我们在使用指针指向另外一个变量的时候会创建一个新的指针变量,这个变量会存储指向的变量的地址,但是我们在使用引用的时候并不会分配新的内存空间。所谓引用不过是给某一个变量起了一个别名。

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;
int main()
{
int a = 10;
cout << "a = " << a << endl;
int& b = a;//定义引用类型
cout << "a = " << b << endl;
}


[misaki@localhost 第一章-C++入门]$ ./Ref
a = 10
a = 10

  以上就是定义了一个引用b让它成为a的别名,之后我们就可以用b代替a。

特性

  在使用引用的过程中有以下规则:

  1、引用在定义的时候必须初始化,不像我们在定义指针的时候如果一开始不使用可以置空,但是引用相当于是别名因此不可以不进行初始化。

  2、一个变量可以有多个引用,就像是一个人可以有多个别名一样。

  3、一个引用一旦引用一个实体则不能再引用其他实体。一个引用一旦已经变成了某个实体的别名则它不可以再成为其他变量的别名了。

常引用

  常引用和常量指针一样是指向不可更改的数据的类型。被它所指向的数据不可进行更改。

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;
int main()
{
const int& c = 10;
cout << "a = " << c << endl;
}


[misaki@localhost 第一章-C++入门]$ ./Ref
a = 10

使用场景

做参数

  引用和指针一样在传参的使用上有独特的优势,他和指针一样可以将参数本身传入而不是传入副本,因此我们在函数内部可以进行函数参数值的更改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;
void Add(int a, int b, int& result)
{
result = a + b;
}
int main()
{
int result;
Add(1, 2, result);
cout << "1 + 2 = " << result << endl;
}



[misaki@localhost 第一章-C++入门]$ ./Ref
1 + 2 = 3

  这样我们就可以将传入参数的值在函数内进行更改,使用比指针更为简单。

做返回值

  就像我们如果一个函数的返回值是指针一样,我们如果用引用作为函数的返回值我们就要保证引用的实体在函数声明周期后依然存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;
int& Add(int a, int b)
{
int result = a + b;
return result;
}
int main()
{
int& result = Add(1, 2);
Add(2, 3);
cout << "result = "<< result << endl;
}


[misaki@localhost 第一章-C++入门]$ ./Ref
result = 5

  以上这个result的值为什么变成了5呢?我们在用引用的时候实际上是给一块内存地址取了别名,我们让函数返回了在函数结束后已经释放的空间的别名。在函数结束后空间中的数据并不会立刻删除,会保留直到下次使用进行覆盖,而我们紧接着再次调用函数,由于栈帧相同使相同的内存空间的数据进行更改,因此我们用别名得到它的数据也会更改。那么如果我们在之后执行一个其他的函数呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;
int& Add(int a, int b)
{
int result = a + b;
return result;
}
int main()
{
int& result = Add(1, 2);
Add(2, 3);
cout << "Ref"<< endl;
cout << "result = "<< result << endl;
}



[misaki@localhost 第一章-C++入门]$ ./Ref
Ref
result = 32635

  可以看到数据已经变成了完全无关的数据,因此我们如果用引用作为返回值就一定要保证我们引用所指内存空间在函数结束后并不会释放。

指针和引用

  1. 引用在定义时必须初始化,指针没有要求。

  2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实
体。

  3. 没有NULL引用,但有NULL指针。

  4. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4
个字节)。

  5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。

  6. 有多级指针,但是没有多级引用。

  7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。

  8. 引用比 指针使用起来 更为方便。

inline内联函数

  在C语言中我们可以用宏来定义常量,定义函数,宏只是单纯的文本替换,因此使用时缺点很多,例如其不支持调试,可读性差,不容易控制,因此在C++中诞生了内联函数,依旧可以帮助我们达到和宏一样的功能减少函数调用生成栈帧的开销。

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;
inline int Add(int num1, int num2)
{
return num1 + num2;
}
int main()
{
cout << Add(1, 2) << endl;
}



[misaki@localhost 第一章-C++入门]$ ./inline
3

  因此我们在C++中当我们需要写一些小型的函数时为了减少调用时形成栈帧的开销以减少时间就可以加上inline关键字

注意

  1、内联函数不能声明和定义分离 他们必须在通过一个文件中,因为内联函数在调用处会被展开是不会有内存地址的,如果分离是无法进行链接的。

  2、就算我们加上inline关键字到底是否会展开是由编译器决定的,我们仅仅是提个建议。

  3、在debug模式中编译器不会进行代码优化因此默认不会将inline进行展开。

C++11新增语法

auto关键字

  auto是C++11最新的关键字,可以自动检测类型,因此在某些类型名很长情况下可以起到简化代码的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
cout << b << endl;
cout << c << endl;
}


[misaki@localhost 第一章-C++入门]$ ./C++11
10
a

  但是使用auto要注意以下几点:

  1、auto在生命变量时一定要初始化。

  2、auto不能作为函数参数或者数组类型,因为无法计算其大小。

  3、auto*与auto无异,但是在定义引用时要加上&。

范围for

  这个语法十分类似JAVA中的范围for,也是为了简便代码而存在的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
for(auto e : arr)
{
cout << e << endl;
}
}


[misaki@localhost 第一章-C++入门]$ ./C++11
1
2
3
4
5

  范围for可以和auto共同使用,这样会更为方便,但是要注意范围for不能遍历传入函数中的数组,因为其实际上穿得是指针。

  以上这两种语法我们称之为语法糖,就是可以简化代码的语法,但是这种语法会让代码可读性变差。

nullptr

  在C++中有一个新的可以标记空指针的关键字,之前我们使用的NULL可以完全被他代替,并且我们之前使用的NULL为一个宏,值为0,未标记类型,因此我们在进行函数传参时,会将我们传入的空指针的值当作int处理,但是nullptr的值为0类型为int*,加入了值类型上的限定,因此在传参和重载中不会出现问题。

【Linux】第七章-进程信号

发表于 2019-05-02 | 分类于 Linux
字数统计: 2.6k

第七章-进程信号

信号基本认识

  生活中处处充满信号,信号的存在就是是为了传递和表达信息,通知事件的发生。并且信号是有生命周期的,信号在产生后到处理完毕之前才是信号的有效期。

  在对于系统来说,当软件中断,便会产生信号,通知发生了某件事情,为了说明不同的事件,所以系统中的信号有不同的种类。

信号的种类

  在Linux下我们可以使用kill -l产看信号的种类,会发现一共有62种不同的信号。其中1-31号信号每个都有各自对应的事件,它们继承于Unix,被称为非可靠信号/非实时信号;而34-64号信号是后续添加的信号,并无对应的事件,成为可靠信号/实时信号。所谓可靠信号与非可靠信号的区别在于非可靠信号是可能会丢失的,有可能产生多次信号才只会处理一次,而可靠信号产生多少次信号就必定会执行多少次。实时信号与非实时信号的区别在于实时信号在信号产生后便会立刻处理,而非实时信号则可能会延时处理。

信号的生命周期

  操作系统产生信号,注册在进程中,进程就收到了信号,但并不会立刻处理,在处理前会先注销信号,之后才会处理信号。

    产生->信号在进程中注册->信号在进程中注销->处理信号

信号的产生

  信号的产生分为两个途径:硬件产生,软件产生。

硬件产生

  我们在终止我们的进程时往往会使用快捷键ctrl + c,其实这个快捷键原理就是操作系统向进程发送了SIGINT信号造成进程结束,这就是硬件产生的信号。由硬件产生的信号还有很多例如ctrl + |:终止信号,ctrl + z 停止信号等等。

软件产生

  我们在强杀进程时会使用kill -9 pid,这里kill就是一个软件,会向指定进程发送指定信号,在使用强杀时则是发送了9号信号,将进程强制杀死。

  同时我们还可以在进程中向进程发送信号,C语言中将信号已经提前定义成了宏为了方便我们使用一些函数来发送信号。

kill()

  int kill(pid_t pid, int sig);函数来向指定进程发送指定信号。

raise

  使用int raise(int sig);向调用进程发送指定信号。

abort

  使用abort()向调用进程发送SIGABRT信号。

alarm

  使用unsigned int alarm(unsigned int seconds);在seconds秒后向调用进程发送信号。

核心转储

  核心转储文件(core dump)是在事件发生后记录事件发生的过程和原因的文件,方便我们事后调试,由于核心转储文件占用空间,默认是关闭的。我们可以用ulimit -a查看core dump是否开启,使用ulimit -c 大小设置核心转储文件大小,开启核心转储。在发生段错误后我们即可使用gdb core.pid来查看核心转储文件的信息。

  不过,我们直到在发生内存错误的时候Linux会报错说产生段错误并且退出进程,产生核心转储文件,但其实进程退出并非是由进程自己退出的,而是收到了操作系统发出的11号信号SIGSEGV,一旦进程收到这个信号便会终止并且报错说产生段错误随后生成核心转储文件,因此进程的退出和终止其实是由这个信号导致的。当发生内存错误时系统会自动调用kill()函数发送SIGSEGV信号到该进程,终止进程。

信号的注册

  给一个进程注册信号是在进程的pcb中进行了一个标记,这个标记由一个结构体sigset_t利用位图构成信号集进行标记,并且存入pcb中的pending信号集中,用来对应62种信号是否已经注册到进程中。同时还有另外一个sigqueque队列,用来按照注册顺序逐一处理信号。信号传入进程将pending上对应位置1表示此类信号存在,随后将信号存入sigqueque队列。但非可靠信号和可靠信号的注册还有区别。

非可靠信号的注册

  当信号传给进程时,进程会判断pending集合位图相应位是否为1;若为0则将其置1,并将其组织到sigqueque队列中;若为1则什么都不做(信号丢失)。

可靠信号的注册

  信号传递给进程时,进程不管位图上是否为1,都会将位图置1,并且再添加到sigqueque队列中,如果有多次传入则拥有多个结点(信号不会丢失)。

信号的注销

非可靠信号的注销

  因为非可靠信号的信号结点只有一个,因此删除结点,位图直接置0;

可靠信号的注销

  因为可靠信号的信号结点可能有多个,若还有相同结点,则位图依然置1,否则置0。

信号的处理

  信号处理有三大方式:默认,忽略,自定义。

信号捕捉初识

signal()

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
#include <stdio.h>                           
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
/**
*
* typedef void (*sighandler_t)(int);
* sighandler_t signal(int signum, sighandler_t handler);
* signum: 信号编号
* handler: 信号处理方式,函数指针
* SIG_DFL 默认处理方式
* SIG_IGN 忽略处理方式
* 修改信号的处理方式
*/
void sigcb(int signo)
{
printf("signal:%d\n", signo);
}
int main()
{
//signal(SIGINT, SIG_DFL);//执行默认的SIGINT功能
//signal(SIGINT, SIG_IGN);//忽略SIGINT信号的处理
signal(SIGINT, sigcb);//按自定义方式处理
while(1)
{
printf("----------\n");
sleep(1);
}
}



[misaki@localhost 第七章-进程信号]$ ./signal
----------
----------
^Csignal:2
----------
----------
----------
----------
^Csignal:2
----------
^Csignal:2
----------
^Csignal:2
----------
^Csignal:2
----------
----------
----------
----------
^\退出(吐核)

  可以看到我们再向进程发送SIGINT信号时,执行的已经是我们自定义的功能了。

  信号默认的处理方式和忽略处理方式都是在内核中定义好的,那么自定义处理方式又是怎么捕捉的呢?

  当进程因为系统调用、中断或者异常进入内核态,当完成内核功能,准备返回用户态主控流程时检测是否有信号待处理,如果有调用do_signal()函数处理信号,如果信号处理方式是自定义的则返回用户态执行自定义处理函数,再通过sigreturn()返回内核态,再次检查是否有信号待处理,若无则调用sys_sigreturn返回用户态从主控流程上次被中断的地方继续执行。

sigaction()

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 <stdio.h>                                                           
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
struct sigaction act, oldact;
void sigcb(int signo)
{
printf("signal:%d\n", signo);
//将原本的处理方式还原
sigaction(SIGINT, &oldact, NULL);
}
int main()
{
//int sigaction(int signum, struct sigaction* act, struct sigaction* oldact);
//使用act动作替换signum原有的处理动作,并且将原有处理动作拷贝到oldact中
//struct sigaction是预先定义好的结构体,用于存放信号修改的功能的数据
act.sa_handler = sigcb;
act.sa_flags = 0;
//清空信号集合
sigemptyset(&act.sa_mask);
sigaction(SIGINT, &act, &oldact);
while(1)
{
printf("----------\n");
sleep(1);
}
}


[misaki@localhost 第七章-进程信号]$ ./signal
----------
----------
^Csignal:2
----------
----------
^C

  这段代码中我们先将SIGINT的处理方式替换为我们自定义的方式,调用一次自定义处理的同时又将其还原。

信号的阻塞

  暂时阻止信号被递达,信号依然可以注册,只是暂时不处理,解除阻塞之后才会处理。

  信号的递达:动作-信号的处理。

  信号的未决:状态-信号冲产生到处理之前的状态。

  在进程的pcb中有一个blocked集合,当某一种信号被阻塞时,会将blocked集合中对应的位置1,表示这种信号被阻塞。因此信号的阻塞过程实际就是在pcb的blocked信号阻塞集合中标记信号,信号到来之后暂时不处理。

  但是有两个信号无法被阻塞——SIGKILL-9,SIGSTOP-19无法被阻塞,无法被自定义,无法被忽略。

竞态条件

  因为运行时序而造成数据竞争——导致数据二义性。函数中所完成的操作并非原子性操作,并且操作的数据是一个全局数据。如果一个函数中操作了全局性数据,并且这个操作不是原子性操作,并且这个操作不受保护,则这个函数是一个不可重入函数。不可重入函数指不能在多个时序的运行中重复调用(重复调用有可能会造成数据二义性)。可重入函数指在多个是虚的运行中重复调用,不会造成异常影响。malloc/free就是不可重入函数。

volatile关键字

  保持内存可见性——每次操作变量都需要重新从内存中获取。防止编译器过度优化。

SIGCHLD

  自定义信号处理方式sigcb,当子进程退出,操作系统发送信号给父进程,直接触发信号回调sigcb,用户主要在sigcb中调用wait/waitpid就可以处理子进程的退出。

  SIGCHLD是一个非可靠信号,假如有多个子进程同时退出了,则有可能造成时间丢失,造成sigcb只被回调一次,只处理了一个子进程。

  必须使用非阻塞,否则没有子进程退出的时候waitpid将阻塞导致程序无法回到主控流程。

【Linux】项目-minishell的实现

发表于 2019-05-01 | 分类于 项目
字数统计: 927

minishell的实现

实现原理

  我们在这里手动实现一个小型的shell,可以用来处理我们一般常规的指令如ls,还可以附加参数-l,并且还可以进行重定向操作。

  实现原理很简单,我们将用户输入的字符串读取到我们的缓冲区中,然后首先遍历一遍整个缓冲区看是否存在重定向符>或者>>,并且将重定向符改为\0并且读取出重定向的文件,随后保存判断结果。之后我们再次遍历一遍进行指令处理,我们利用字符指针数组让每一个指针指向每一个指令,并且将之间的空白符如(空格)改为\0。做完以上这一切,创建子进程利用进程替换将其替换为目标指令中的程序让其执行指定功能,如果有重定向符要先打开指定文件并且将标准输入重定向到指定文件中,者必须在子进程中完成,然后等待子进程关闭,父进程阻塞,并且让以上过程不断循环即可。

代码实现

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
/*minishell实现*/                                          

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <ctype.h>
#include <sys/wait.h>
char buf[1024] = {0};
char* argv[32];
int argc = 0;

//指令输入到缓冲区中
void do_face()
{
printf("[san@localhost]$ ");
fflush(stdout);
memset(buf, 0x00, 1024);
//%[^\n] 获取数据直到遇到\n
//%*c 取出一个字符丢弃
//利用正则表达式,取出指令输入字符串
if (scanf("%[^\n]%*c", buf) != 1)
{
//如果没有输入指令我们需要处理回车,将回车从缓冲区取出
getchar();
}
return ;
}
//处理指令,将字符指针数组每个指针指向每个指令
void do_parse()
{
char *ptr = buf;
argc = 0;
while(*ptr != '\0')
{
//当前位置非空白字符
if (!isspace(*ptr))
{
argv[argc++] = ptr;
while(!isspace(*ptr) && *ptr != '\0')
{
ptr++;
}
}
//所有空字符都换为'\0'
else
{
*ptr = '\0';
ptr++;
}
}
}
argv[argc] = NULL;
return;
}
int main()
{
// ls >> > a.txt
// int fd = open(a.txt);
// dup2(fd, 1);
// 将原先要写入到标准输出1中的数据,写入到指定文件中
while(1)
{
do_face();
//ls >> a.txt
//解析命令中是否有重定向指令
//没有则跳过此步骤
int redirect = 0;
char *file = NULL;
char *ptr = buf;
while(*ptr != '\0')
{
if (*ptr == '>')
{
redirect = 1;//清空重定向
*ptr++ = '\0';
if (*ptr == '>')
{
redirect = 2;//追加重定向
*ptr++ = '\0';
}
//isspace如果是空字符(制表符回车空格等则返回1)
//循环跳过中间所有空字符
while(isspace(*ptr) && *ptr != '\0')
{
ptr++;
}
file = ptr;
//拿到要写入的文件名存入file
while(!isspace(*ptr) && *ptr != '\0')
{
ptr++;
}
*ptr = '\0';
}
ptr++;
}
//解析流程:取出空白字符,获取程序名称和参数
do_parse();
//创建子进程
int pid = fork();
if (pid < 0)
{
exit(-1);
}
else if (pid == 0)
{
//重定向必须在子进程当中完成
//重定向处理
if (redirect == 1)
{

//清空重定向
int fd = open(file, O_CREAT|O_WRONLY|O_TRUNC, 0664);
//将标准输出重定向到file所指向的文件中
dup2(fd, 1);
}
else if (redirect == 2)
{
//追加重定向
int fd = open(file, O_CREAT|O_WRONLY|O_APPEND, 0664);
//将标准输出重定向到file所指向的文件中
dup2(fd, 1);
}
//将子进程替换为指令中要求的程序,执行相应的指令
execvp(argv[0], argv);
//防止子进程替换失败
exit(0);
}
wait(NULL);
}
return 0;
}

【Linux】第六章-进程间通讯

发表于 2019-04-25 | 分类于 Linux
字数统计: 3.4k

第六章 进程间通讯

基本介绍

为什么需要进程间通讯

  由于进程的独立性当我们想要一个进程把数据传输给另一个进程就需要进程间通讯的方式。但是想要传输数据就需要共同的媒介,以此达到数据传输,数据共享进程间访问控制等目的。

方式

  由于通讯目的不同,场景不同,因此操作系统提供了多种进程间通信的方式。

  1、管道(命名/匿名):传输数据。

  2、共享内存:共享数据

  3、消息队列:传输数据

  4、信号量:进程间访问控制。

管道

什么是管道

  管道是一种传输数据的媒介,所谓管道是操作系统在内核创建的一块缓冲区域,进程只要能够访问到这块缓冲区就能通过缓冲区相互通讯。并且只有在进程确认要发起进程间通讯时管道才会被创建,同时为了方便进程操作管道系统会给进程一个句柄(遥控器)方便进程操作,在LInux下这不过是给进程了两个文件描述符。为什么是两个文件描述符呢?系统为了操作更加方便易于管理功能更加全面,因此两个文件描述符一个描述符负责读管道,另一个负责写数据。

匿名管道

  匿名管道是只能用于具有亲缘关系的进程间通信(共同祖先)。用pipe()创建管道。

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
#include <stdio.h>                         
#include <unistd.h>
#include <string.h>
#include <errno.h>
/**
* int pipe(int pipefd[2]);
* pipefd[0] 用于从管道读数据
* pipefd[1] 用于从管道写数据
* 返回值:0 失败:-1
*/
int main()
{
int pipefd[2];
int ret = pipe(pipefd);
if(ret < 0)
{
perror("pipe error");
return -1;
}
int pid = fork();
if(pid < 0)
{
perror("fork error");
return -1;
}
else if(pid == 0)
{
//child read
char buf[1024] = {0};
read(pipefd[0], buf, 1023);
printf("read buf:%s\n", buf);
}
else
{
//parent write
char* ptr = "Misaki!!!";
write(pipefd[1], ptr, strlen(ptr));
}
}


[misaki@localhost 第六章-进程间通讯]$ ./ipc
read buf:Misaki!!!

  读写特性:若管道中没有数据,则read会阻塞,直到读到数据返回。若管道中数据满了,则write会阻塞,直到数据被读取,管道中有空闲位置,写入数据后返回。如果管道所有读端都被关闭,则write()会触发异常,发起SIGPIPE信号导致进程退出。如果管道所有写端都被关闭则read()读完管道所有数据后返回0。如果设置非阻塞,如果管道中暂时没有数据则返回-1,errno置为EAGAIN。

  同步与互斥特性:当读写大小小于PIPE_BUF时保证操作原子性,即操作不可被打断,保证读写一次性完成。互斥:对临界资源(即公共资源)同一时间的唯一访问性。同步:保证对临界资源访问的时许可控性。同时管道是一个半双工通信(可选择方向的单行通信)。以上的特性保证当有多个进程访问管道时向管道中读写数据不会出现顺序出错内容出错的问题。

  管道提供流式服务:面向字节流数据传输,传输十分灵活,但会造成数据粘连。

命名管道

  可见于文件系统,会在创建管道时创建一个命名管道文件,所有进程都可以通过打开管道文件进而获取管道的操作句柄,因此命名管道到可以用于同一主机上的任意进程间通信。但其本质依然是系统的内核,只是通过文件向所有的进程提供了访问的方法。我们在shell中用mkfifo filename创建命名管道。我们在进程中用mkfifo()创建命名管道。

  命名管道打开特性:

  若管道没有被以写的方式打开,这时如果只读打开则会阻塞,直到文件被以写的方式打开。

  若管道没有被以读的方式打开,这时如果只写打开则会阻塞,直到文件被以读的方式打开。

  这里需要两个进程一个负责写入一个负责读取才能让命名管道生效,如果同时以读写方式打开进程也可以让其生效。

fifo_read.c:

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
/*  命名管道的基本使用                            
* int mkfifo(const char *pathname, mode_t mode);
* pathname: 管道文件名
* mode: 创建权限 0664
* 返回值:0 失败:-1
*/

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>

int main()
{
//创建命名管道
char *file = "./test.fifo";
umask(0);
int ret = mkfifo(file, 0664);
if (ret < 0)
{
if (errno != EEXIST)
{
perror("mkfifo error");
return -1;
}
}
printf("open file\n");
int fd = open(file, O_RDWR);
if (fd < 0)
{
perror("open error");
return -1;
}
printf("open success!!\n");

//不停从管道中读取数据
while(1)
{
char buf[1024] = {0};
int ret = read(fd, buf, 1023);
if (ret > 0) {
printf("read buf:[%s]\n", buf);
}else if (ret == 0) {
printf("write closed~~~\n");
}else {
perror("read error");
}
printf("----------\n");
}
close(fd);
return 0;
}

fifo_write.c:

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
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>

int main()
{
char *file = "./test.fifo";
umask(0);
int ret = mkfifo(file, 0664);
if (ret < 0) {
if (errno != EEXIST) {
perror("mkfifo error");
return -1;
}
}
printf("open file\n");
int fd = open(file, O_WRONLY);
if (fd < 0) {
perror("open error");
return -1;
}
printf("open success!!\n");
//不停向管道中写入数据
while(1) {
char buf[1024] = {0};
scanf("%s", buf);
write(fd, buf, strlen(buf));
}
close(fd);
return 0;
}

结果:

结果

结果

  对于命名管道来说,匿名管道的读写特性,依然使用,并且依旧自带同步与互斥,采用字节流方式的数据传输。
  命名管道的生命周期与进程同步,及管道创建是在内核的一块缓冲区上,其实也是一块内存,与磁盘无关,而创建的管道文件不管是用于标记这块内核中的缓冲区用的。当进程使用到命名管道时内核中才会创建这块缓冲区供进程使用,当所有进程全部关闭管道时,这块缓冲区释放。

共享内存

  共享内存是在物理内存上开辟一块空间,将这块空间映射到进程的虚拟地址空间中,进程则可以用通过虚拟地址进行访问操作;如果一块内存被多个进程映射,那么多个进程访问同一块内存,则可以实现通讯。

特点

  共享内存是将两个进程共用同一块物理内存,是最快的进程间通信方式,因为相较于其他进程间通信方式(将数据从用户态拷贝到内核态,用的时候,再从内核态拷贝到用户态)其直接操作内存,少了两步拷贝操作。

  共享内存在创建时会在开辟一块物理内存空间,多块进程要共享同一块物理内存只需要将这块物理内存通过页表映射到多个进程的虚拟内存空间上即可使用。
  共享内存的生命周期随内核,操作系统重启,共享内存删除。共享内存不具备同步和互斥,因此其是不安全的。

共享内存的操作

shell内操作

  1、ipcs -m查看共享内存。

  2、ipcrm -m shmid删除共享内存。

程序内操作

  程序内使用共享内存基本分为以下4步:

  1、创建共享内存。shmget()不过在创建共享内存时我们会用到一个叫做key的参数,这个参数成为ipc键值,每一个键值对应一个唯一的ipc对象,我们可以通过ftok()生成一个key,也可以自己宏定义一个key,但是后者需要保证自己定义的key与系统中已经存在的不冲突,而前者则有系统自己保证。

  2、将共享内存映射到虚拟地址空间。shmat

  3、对共享内存进行基本的内存操作。memcpy()...

  4、解除映射关系。shmdt()

  5、删除共享内存。shmctl()

  在删除共享内存时会判断它的映射连接数,如果为0的话则直接删除,不为0则拒绝后续连接,直到为0删除。

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
shm_read.c

/*共享内存使用流程:
*1. 创建/打开共享内存 shmget
key_t ftok(const char *pathname, int proj_id);
pathname: 文件名
proj_id: 数字
通过文件的inode节点号和proj_id共同得出一个key值
int shmget(key_t key, size_t size, int shmflg);
key: 共享内存标识符
size: 共享内存大小
shmflg:打开方式/创建权限
IPC_CREAT 共享内存不存在则创建,存在则打开
IPC_EXCL 与IPC_CREAT同用,若存在则报错,不存在则创建
mode_flags 权限
返回值:操作句柄shmid 失败:-1
*2. 将共享内存映射到虚拟地址空间 shmat
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid: 创建共享内存返回的操作句柄
shmaddr: 用于指定映射在虚拟地址空间的首地址;通常置NULL
shmflg: 0--可读可写
返回值:映射首地址(通过这个地址对共享内存进行操作) 失败:(void*)-1
*3. 对共享内存进行基本的内存操作 memcpy.....
*4. 解除映射关系 shmdt
int shmdt(const void *shmaddr);
shmaddr: 映射返回的首地址
*5. 删除共享内存 shmctl IPC_RMID 删除共享内存
buf: 设置或者获取共享内存信息,用不着则置NULL
共享内存并不是立即删除的,只是拒绝后续映射连接,当共享内存
映射连接数为0时,则删除共享内存
*/

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/shm.h>
#define IPC_KEY 0x12345678
#define PROJ_ID 12345
#define SHM_SIZE 4096
int main()
{
int shmid;
//1. 创建共享内存
//key_t key = ftok(".", PROJ_ID);
//PROJ_ID只占用后八位,即1-255
shmid = shmget(IPC_KEY, SHM_SIZE, IPC_CREAT|0666);
if (shmid < 0)
{
perror("shmget error");
return -1;
}
//2. 将共享内存映射到虚拟地址空间
char *shm_start = (char *)shmat(shmid, NULL, 0);
if (shm_start == (void*)-1)
{
perror("shmat error");
return -1;
}
int i = 0;
while(1)
{
printf("%s\n", shm_start);
sleep(1);
}
//4. 解除映射
shmdt(shm_start);
//5. 删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}

shm_write.c:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/shm.h>
#define IPC_KEY 0x12345678
#define PROJ_ID 12345
#define SHM_SIZE 4096
int main()
{
int shmid;
//1. 创建共享内存
//key_t key = ftok(".", PROJ_ID);
shmid = shmget(IPC_KEY, SHM_SIZE, IPC_CREAT|0666);
if (shmid < 0) {
perror("shmget error");
return -1;
}
//2. 将共享内存映射到虚拟地址空间
char *shm_start = (char *)shmat(shmid, NULL, 0);
if (shm_start == (void*)-1) {
perror("shmat error");
return -1;
}
int i = 0;
while(1) {
sprintf(shm_start, "Misaki:%d\n", i++);
sleep(1);
}
//4. 解除映射
shmdt(shm_start);
//5. 删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}


[misaki@localhost ipc]$ ./shm_write
^C


[misaki@localhost ipc]$ ./shm_read
Misaki:0

Misaki:1

Misaki:2

Misaki:3

Misaki:4

Misaki:5

Misaki:6

Misaki:7

Misaki:8

Misaki:9

Misaki:10

Misaki:11

^C

  删除共享内存时,共享内存并不会立刻被删除,因为这样可能会影响正在使用共享内存的进程崩溃,而是将共享内存的key置为0,表示不再接收后续进程的映射,等所有进程都解除与它的映射后,共享内存才会完全被删除。

消息队列

  在使用消息队列时我们会在内核中创建一个消息队列结构,在进程中进程要想操作消息队列要自行创建一个消息队列结构体,将数据放进去构成队列,然后再将队列传输到内核。由于内核中存储数据的队列只有一个,为了方便区分不同进程的数据,在消息队列结构体中会有一个type变量以便区分这个数据属于哪个进程。在某个进程获取消息队列中的数据时可以指定type,得到自己希望得到的进程的数据。
  消息队列自带同步与互斥,生命周期随内核,数据传输自带优先级。
  使用接口

1
2
3
4
msgget 创建
msgsnd 添加节点
msgrcv 获取结点
msgctl 操作-删除消息队列

信号量

  信号量时内核中的一个计数器,并且具有等待队列,具有等待与唤醒的功能。

  信号量用于资源计数,若计数<=0则表示没有资源则读取需要等待资源,直到如果放置了资源,则计数+1,然后唤醒等待的进程;若资源>0则可以获取资源,之后计数-1。

  信号量实现进程间的同步与互斥。

  信号量中有两种重要操作分别是P操作和V操作,P操作表示获取资源,计数器-1,V操作表示归还资源,计数器+1。关于信号量在线程安全篇会细致讲解。

【Linux】第五章-基础IO

发表于 2019-04-25 | 分类于 Linux
字数统计: 3.7k

第五章 基础IO

回顾标准库IO接口

  我们在C语言中已经见过很多包含在标准库中的IO接口:fopen fclose fwrite fread fseek...,在fopen中有一些不同的文件打开模式:r r+ w w+ a...,而要想对文件进行操作我们需要用到句柄——FILE*,之前我们称之为句柄,但它实际上是一个文件流指针,stdout stdin stderr他们也是文件流指针,用来操作和控制文件和IO流。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//标准IO回顾
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main()
{
FILE* fp = NULL;//句柄,文件流指针
fp = fopen("./tmp.txt", "a");
if(fp == NULL)
{
perror("fopen error");
}
char* ptr = "Misaki";
fwrite(ptr, strlen(ptr), 1, fp);
char buf[1024] = { 0 };
fread(buf, 1023, 1, fp);
printf("buf = %s\n", buf);
return 0;
}

系统调用接口

  除过标准库中的IO接口我们的系统中也有系统调用接口可以直接进行IO流的控制,我们自然可以用这些系统调用接口来控制文件。标准库中的IO接口也不过是系统调用接口的一层封装。而直接使用系统调用接口可以实现的功能更为丰富,但同时也更难以操作。

open()

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>//要想使用系统IO必选加这个头文件
int main()
{
//int open(const char *pathname, int flags, mode_t mode);
//pathname:路径名
//flags:选项标志
// 必选:必选其一
// O_RDONLY 只读
// O_WRONLY 只写
// O_RDWR 可读可写
// 可选项:
// O_CREAT 文件不存在则创建,存在则打开
// O_EXCL 与O_CREAT同用时若文件存在则报错
// O_TRUNC 打开文件同时截断文件长度为0
// O_APPEND 写追加模式
//mode:创建文件时给定权限(八进制数字)
// mode & (umask)
//返回值:文件描述符-正整数 错误:-1
umask(0);//设置当前进程的权限掩码为0
int fd = open("./tmp.txt", O_RDWR | O_CREAT | O_TRUNC, 0777);
if(fd < 0)
{
perror("open error");
return -1;
}

  此时由于我们加上了截断选项文件中的内容会被全部清空。如果我们在语句中不加mode权限系统会默认给随机的权限,这是十分危险的,我们自己创建的文件可能连我们自己都打不开。

  我们可以看到我们创建的文件以及权限已经按照我们的预期设置好了。

open

write()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>//要想使用系统IO必选加这个头文件
int main()
{
umask(0);//设置当前进程的权限掩码为0
int fd = open("./tmp.txt", O_RDWR | O_CREAT | O_TRUNC, 0777);
if(fd < 0)
{
perror("open error");
return -1;
}
//ssize_t write(int fd, const void *buf, size_t count);
//fd:打开文件所返回的文件描述符
//buf:要想文件写入的数据
//count:要写的数据长度
//返回值:实际的写入字节数,错误:-1
char buf[1024] = "Misaki";
int ret = write(fd, buf, sizeof(buf));
}

  这样我们即可将数据写入到文件中。之后是读取函数。

read()

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>//要想使用系统IO必选加这个头文件
int main()
{
umask(0);//设置当前进程的权限掩码为0
int fd = open("./tmp.txt", O_RDWR | O_CREAT | O_TRUNC, 0777);
if(fd < 0)
{
perror("open error");
return -1;
}
char buf[1024] = "Misaki";
int ret = write(fd, buf, sizeof(buf));
//ssize_t read(int fd, void *buf, size_t count);
//fd:打开文件所返回的文件描述符
//buf:对读取到的数据进行存储的位置
//count:要读取得数据长度
//返回值:实际的读取的字节数,错误:-1
memset(buf, 0x00, 1024);//将缓冲区清零
ret = read(fd, buf, 1023);
if(ret < 0)
{
perror("read error");
return -1;
}
printf("read buf = %s\n", buf);
printf("ret = %d\n", ret);
}

[misaki@localhost 第五章-基础IO]$ ./SystemIO
read buf =
ret = 0

lseek()

  但是我们发现并没有向缓冲区中读取文件中的数据,这是因为我们在写入数据后文件中的描述符停留在文件末尾,之后就是文件结束符因此无法读取之前的数据,于是我们需要一个函数将描述符移动到文件最开始。

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>//要想使用系统IO必选加这个头文件
int main()
{
umask(0);//设置当前进程的权限掩码为0
int fd = open("./tmp.txt", O_RDWR | O_CREAT | O_TRUNC, 0777);
if(fd < 0)
{
perror("open error");
return -1;
}
char buf[1024] = "Misaki";
int ret = write(fd, buf, sizeof(buf));
//off_t lseek(int fd, off_t offset, int whence);
//fd:打开文件所返回的文件描述符
//offset:偏移量
//whence:偏移位置
// SEEK_CUR 当前位置
// SEEK_SET 文件起始位置
// SEEK_END 文件末尾
lseek(fd, 0, SEEK_SET);
memset(buf, 0x00, 1024);//将缓冲区清零
ret = read(fd, buf, 1023);
if(ret < 0)
{
perror("read error");
return -1;
}
printf("read buf = %s\n", buf);
printf("ret = %d\n", ret);
}


[misaki@localhost 第五章-基础IO]$ ./SystemIO
read buf = Misaki
ret = 1023

  这样读取就成功了。

close

  使用完文件一定别忘了关闭,否则会造成文件描述符泄漏。

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>//要想使用系统IO必选加这个头文件
int main()
{
umask(0);//设置当前进程的权限掩码为0
int fd = open("./tmp.txt", O_RDWR | O_CREAT | O_TRUNC, 0777);
if(fd < 0)
{
perror("open error");
return -1;
}
char buf[1024] = "Misaki";
int ret = write(fd, buf, sizeof(buf));
lseek(fd, 0, SEEK_SET);
memset(buf, 0x00, 1024);//将缓冲区清零
ret = read(fd, buf, 1023);
if(ret < 0)
{
perror("read error");
return -1;
}
printf("read buf = %s\n", buf);
printf("ret = %d\n", ret);
//int close(int fd);
close(fd);
}

文件描述符

文件描述符?文件流指针?

  我们知道了标准库使用的是文件流指针,而系统调用接口使用的是文件描述符,那么什么是文件描述符呢?我们通过对系统调用接口的练习使用知道了文件描述符是一个整数。而我们知道库函数与系统调用接口是上下级调用关系,那么文件描述符在其中又起到什么作用呢?

  我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files,指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件

文件描述符

  这张表存储在PCB中。

  文件流指针是一个结构体,其中就包括了文件描述符,当我们使用标准库接口进行io,则最终是通过文件流指针找到文件描述符进而对文件进行操作。

  而每个进程在执行时会默认打开三个文件stdin(标准输入) stdout(标准输出) stderr(标准错误)对应的物理设备一般是键盘,显示器,显示器,这三个默认打开的文件对应的文件描述符分别是0, 1, 2。

缓冲区

  缓冲区实际上是文件流指针为每个文件所维护的一个缓冲区(用户态)。其保存在文件流指针中,但是如果我们不使用标准库IO而直接使用系统调用接口我们则不会使用到文件流指针,自然系统就不会创建缓冲区,更不会先将数据搁在缓冲区中。

        write(1, "Misaki", 7);

  这段命令会直接在标准输出文件中打印数据,并不会通过缓冲区。

文件描述符分配规则

  最小未使用。文件描述符默认会给文件分配当前最小的且未使用的文件描述符。

1
2
3
4
5
6
7
8
9
10
11
12
//文件描述符分配原则:最小未使用
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
close(1);//我们关闭了标准输出,文件描述符1空闲
int fd = open("./tmp.txt", O_RDWR);//tmp.txt的文件描述符被分配为最小的未使用1
printf("%d\n", fd);
fflush(stdout);//刷新缓冲区
close(fd);
}

  这段代码执行后在屏幕上不会有打印,但在tmp.txt文件中会打印1。这是因为标准输出已经被我们关闭了,并且文件描述符1此时已经指向了tmp.txt因此我们使用printf()进行打印也会打印在tmp.txt中。

重定向

  我们之前关闭了stdout文件并且让新创建的文件的文件描述符自动变成了1,由此我们原本要打印到屏幕上的内容到了tmp.txt中,这个过程我们叫输出重定向,及将原本要输入到一个文件中的内容重定向使其输入到另一个文件中。我们可以用> >>来进行重定向,同时我们也可以在程序中利用dup()系统调用来达到重定向的目的。

  dup(int oldfd, int newfd),将newfd重定向到oldfd所指向的文件,如果newfd已有打开文件则关闭文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*文件描述符*/                                           
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
//int dup2(int oldfd, int newfd);
// 将newfd重定向到oldfd所指向的文件
// 若newfd本身已有打开文件,重定向时则关闭已打开文件
int fd = open("./tmp.txt", O_RDWR);

//让1也指向了fd所指向的文件
dup2(fd, 1);

printf("%d\n", fd);
fflush(stdout);
close(fd);
return 0;
}

文件系统

文件系统简介

  我们磁盘上没有一个分区就有一个文件系统用于管理文件存储。我们的文件系统是通过分块来存储文件数据的,为了方便的找到文件相关信息,我们文件系统创建了一个个inode结点用于存放一个个结点的文件信息,并且在inode中存放着文件数据具体存放的数据块的位置。并且还有一个inode table用来统一管理当前分区中所有的inode结点。同时我们还有两个工具inode_bitmap和data_bitmap用来分别在inode table和数据区找到空闲位置用来存储文件的inode和文件的数据。
  存储文件流程:通过inode_bitmap在inode table中找到空闲的inode结点,通过data_bitmap在数据区域找到空闲数据块,将数据块位置信息,记录到inode结点中,将文件数据写入数据块中,将文件名和inode节点号写入父目录文件中。父目录文件是目录下用于查看当前目录下有什么文件的表。如果我们要查询某个目录下的某个文件我们会现在目录文件中查询这个文件的inode结点信息(但其实这个结点是在inode_table中进行存储的),然后通过inode再找到这个文件的数据具体所存进的数据块,来查询数据。

硬链接/软链接

  硬链接是通过复制原文件的inode达到创建链接的目的,可以理解硬链接为原文件的别名。ln 原文件 硬链接,与原文件没有什么区别。

  软连接一个独立的文件,文件内容里存储着原文件的地址与名字,软连接有着新的inode结点,每次软连接通过它自身的数据找到原文件的文件名再查找到源文件。ln -s 源文件 软连接,是原文件的一个快捷方式。

  区别:

  1、删除原文件,软连接失效,硬链接无影响。

  2、软链接可以跨分区建立,硬链接不行。

  3、软连接文件可以针对目录创建,硬链接不行。

动态库/静态库

生成

  生成动态库:gcc -fPIC -c b.c -o b.o,gcc --share b.o -o libmytest.so

  生成静态库:gcc -c b.c -o b.o,ar -cr libmytest.a b.o

  gcc选项:-fPIC:产生位置无关代码。--share:生成一个共享库。

  ar选项:-c:创建一个静态库。-r:新增库文件。

库的使用

动态库

  由于我们动态库的默认查找路径是/lib64,我们可以将我们的库移到这个路径下就可以直接链接了,当然也可以使用一下这种方法来增加环境变量或者参数来让程序连接到库。

  1、我们要增加参数更改查找库的路径gcc a.c -o main -L. -lmytest。其中-L指定库的路径,-l指定库的名称。我们也可以增加环境变量来增加查找库的路径export LIBRARY_PATH=.。

  2、我们的动态库在程序运行时也需要库的存在,因此我们要增加环境变量来让程序运行时也能找的到库。export LD_LIBRARY_PATH=.。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
add.h:
int add(int num1, int num2);
add.c:
#include "add.h"

int add(int num1, int num2)
{
return num1 + num2;
}
main.c:
#include <stdio.h>
#include "add.h"
int main()
{
printf("%d\n", add(1,3));
}

  终端中执行代码:

1
2
3
4
5
6
[misaki@localhost add]$ gcc add.c -fPIC -c -o add.o
[misaki@localhost add]$ gcc --share add.o -o libmyMath.so
[misaki@localhost add]$ export LD_LIBRARY_PATH=.
[misaki@localhost add]$ gcc main.c -L. -lmyMath -o main
[misaki@localhost add]$ ./main
4

静态库

  通常链接静态库的时候并不会使用-static静态链接,而是将静态库放到指定路径下直接使用gcc -L路径 -l库名称直接指定库的链接路径去生成可执行程序。-static会将可执行程序全部使用静态链接生成,不依赖任何动态库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
add.h:
int add(int num1, int num2);
add.c:
#include "add.h"

int add(int num1, int num2)
{
return num1 + num2;
}
main.c:
#include <stdio.h>
#include "add.h"
int main()
{
printf("%d\n", add(1,3));
}

  终端中执行代码:

1
2
3
4
5
[misaki@localhost add]$ gcc -c add.c -o add.o
[misaki@localhost add]$ ar -cr libmyMath.a add.o
[misaki@localhost add]$ gcc main.c -L. -lmyMath -o main
[misaki@localhost add]$ ./main
4

【Linux】第四章-进程控制

发表于 2019-04-24 | 分类于 Linux
字数统计: 2.8k

第四章 进程控制

进程创建

fork()

  fork()可以用于创建一个子进程,并且父进程返回其子进程的PID,子进程返回0。子进程会完全复制父进程的PCB并且会优先执行父进程。

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
/**
*
* fork初使用
* 通过复制调用进程,创建一个新的进程(子进程)
*/
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("parent pid = %d\n", getpid());
pid_t pid = fork();//复制了父进程的PCB,因此程序计数器也会跟父进程一致,因此子进程下一句执行的代码和父进程一致
printf("child1:%d\n", pid);
if(pid < 0)
{
return -1;
}
else if(pid == 0)//fork有返回值,子进程返回0,父进程返回的是子进程的pid
{
printf("child:%d\n", getpid());//得到当前进程的PID
}
else
{
printf("parent:%d\n", getpid());//得到当前进程的PID
}
printf("Misaki\n");
}


[misaki@localhost 第三章]$ ./Creat
parent pid = 6751
child1:6752
parent:6751
Misaki
child1:0
child:6752
Misaki

  在创建子进程时,系统会完全复制一份相同的物理内存,共用相同的代码,但是这样会极大的浪费内存,在有些情况下如果我们只是单纯的让子进程重复父进程执行的操作,我们完全不需要复制一份物理内存,因此写时拷贝技术就会帮上我们很大的忙,在创建子进程时,并不会立刻复制一份物理内存,会优先使用同一份物理内存,直到父进程或者子进程出现了数据或者代码的修改才会分配新的物理内存空间,这样可以大大的减少内存空间的占用。

  fork()创建的子进程会完全复制父进程的PCB但是完全复制一份PCB有时在对空间要求很严格的情况下会导致浪费很多的空间,因此我们就有了vfork()。

vfork()

  vfork()函数的基本功能与fork()一致但有不同的地方是vfork()创建完子进程会优先执行子进程,并且子进程不会完全复制父进程的PCB,为了节省空间子进程会和父进程共用一块虚拟内存,为了不导致调用栈混乱,子进程会阻塞父进程,也就是说父进程会在子进程退出或替换后才会执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>                                
#include <unistd.h>

int main()
{
pid_t pid = vfork();//创建子进程,阻塞父进程
while(1)
{
if(pid == 0)
{
//child
printf("----child!! pid:[%d]\n", getpid());
_exit(0);
}
if(pid > 0)
{
printf("----parent!! pid:[%d]\n", getpid());
}
sleep(1);
}
}

  vfork和fork也会有调用失败的情况,比如说在系统中已经有了太多的进程或者用户的实际进程达到了上限,这样则系统不再允许我们创建新的进程。

进程终止

  进程退出往往分为以下几种方式,程序运行完毕正常退出和代码异常中止,我们平常使用的ctrl + c终止进程就是一种进程异常退出的方式,我们也可以通过调用接口和函数进行带退出状态的方式退出进程。

_exit()

  _exit()是一个函数,原型为:

        
void _exit(int status);

  可以让程序带状态进行退出,status是程序的退出状态,这个状态可以通过wait()函数接收到,也可以在命令行中通过echo $?命令进行查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main()
{
int pid = fork();
if (pid < 0) {
perror("fork error");
printf("fork error :%s\n", strerror(errno));
}
sleep(1);
_exit(-1);
}


[misaki@localhost process]$ echo $?
255

  明明我们返回了-1为什么终端中查看返回值却返回了255呢?

  因为status只有低8位可以被父进程查看到返回值信息,也就是说返回值信息一共只有255条,如果我们返回-1,实际上返回了255。

exit

  exit()也为退出函数,但是在退出进程前会对进程多很多的处理随后调用_exit()退出进程。这些退出前的处理包括:

  1、执行用户自定义的清理函数——atexit或on_exit。

  2、会关闭所有打开的流,所有的缓存数据都会被写入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
exit1.c

#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("Misaki");
exit(0);
}

exit2.c

#include <stdio.h>
#include <unistd.h>
int main()
{
printf("Misaki");
_exit(0);
}

[misaki@localhost process]$ ./exit2
[misaki@localhost process]$ ./exit1
Misaki[misaki@localhost process]$

  这个例子可以看出exit()退出进程会将缓冲区中的数据全部写入,但是_exit()却没有这些处理。

进程等待

  所谓进程等待是父进程等待子进程的终止并且得到子进程的退出信息,防止产生僵尸进程。进程等待是一种防止僵尸进程长生的重要方法,在合适的时间进行进程等待可以防止僵尸进程,确保不会产生内存泄漏,白白浪费资源。

wait()

  这个函数是最为基本的进程等待函数,原型为:

        
pid_t wait(int *status);

  返回等待到的子进程的pid,status为返回型参数,返回等待到的子进程的退出信息。但是如果在等待期间为等待到子进程会阻塞父进程一直等待子进程,因此必须要在合适的时间进行进程等待,不然也会影响父进程的执行速度。

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
/*进程等待-避免产生建时进程*/                

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
int pid = fork();
if (pid < 0) {
perror("fork error");
exit(-1);
}else if (pid == 0) {
//child
printf("child = %d\n", getpid());
sleep(5);
exit(1);
}
//pid_t wait(int *status);
// 等待任意一个子进程退出
// status:用于退出返回值
// 返回值:返回退出子进程的pid; 出错:-1
int status;
pid_t pid2 = wait(&status);
printf("pid = %d\n", pid2);
return 0;
}

[misaki@localhost process]$ ./wait
child = 4843
pid = 4843
status = 256

  这里我们已经等待到了我们的子进程退出,但是会发现返回的信息却不对,这是为什么呢?

  status的值不能单纯的看作是一个整形,而是应该用位图来理解。我们只讨论status的低16位。

  status的低8位用来保存异常终止信息,如果程序正常退出则低8位为0,否则会在低8位保存终止信号以及core dump标志及产生核心转储文件的标志。而高位才会保留我们自己返回的退出信息。并且C语言已经为我们实现准备好了宏来验证低7位是否为0以及提取高位返回信息。

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
/*进程等待-避免产生建时进程*/                

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
int pid = fork();
if (pid < 0) {
perror("fork error");
exit(-1);
}else if (pid == 0) {
//child
printf("child = %d\n", getpid());
exit(1);
}
//pid_t wait(int *status);
// 等待任意一个子进程退出
// status:用于退出返回值
// 返回值:返回退出子进程的pid; 出错:-1
int status;
pid_t pid2 = wait(&status);
printf("pid = %d\n", pid2);
if (WIFEXITED(status)) {
printf("child exit code:%d\n", WEXITSTATUS(status));
}
//WIFSIGNALED()若WIFEXITED为真则提取子进程退出码。
if (WIFSIGNALED(status)) {
printf("exit signal:%d\n", WTERMSIG(status));
}
return 0;
}


[misaki@localhost process]$ ./wait
child = 5529
pid = 5529
child exit code:1

  这样我们返回的信息码就被打印出来了。

waitpid()

  waitpid()比wait()功能更加全面,它拥有更多的选项,可以在不阻塞父进程的情况下进行进程等待。原型:

        pid_t waitpid(pid_t pid, int *status, int options);

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
/*进程等待-避免产生建时进程*/           

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
int pid = fork();
if (pid < 0) {
perror("fork error");
exit(-1);
}else if (pid == 0) {
//child
printf("child = %d\n", getpid());
sleep(5);
exit(1);
}
int status;
//pid_t waitpid(pid_t pid, int *status, int options);
// pid: 指定的进程id
// -1 等待任意子进程
// >0 等待指定子进程
// status: 用于获取返回值
// options:选项
// WNOHANG 将waitpid设置为非阻塞
// 返回值:<0:出错 ==0:没有子进程退出 >0: 退出子进程的pid
while (waitpid(pid, &status, WNOHANG) == 0) {
printf("no exit~~~smoking~~\n");
sleep(1);
}
//低8位为0则正常退出
//WIFEXITED()验证低8为是否为0,为0则为真
if (WIFEXITED(status)) {
printf("child exit code:%d\n", WEXITSTATUS(status));
}
//WIFSIGNALED()若WIFEXITED为真则提取子进程退出码。
if (WIFSIGNALED(status)) {
printf("exit signal:%d\n", WTERMSIG(status));
}
return 0;
}


[misaki@localhost process]$ ./wait
no exit~~~smoking~~
child = 6031
no exit~~~smoking~~
no exit~~~smoking~~
no exit~~~smoking~~
no exit~~~smoking~~
child exit code:1

  waitpid()可以不阻塞父进程,只等待一下如果当前没有子进程终止则便会继续执行父进程。

进程替换

  我们在创建子进程时现在往往只能让子进程执行和父进程一样的代码,这样我们想让子进程去执行另外的功能就需要用到进程替换。进程替换要用到exec系函数。我们先把他们的函数原型都先列出来再进行分析。

1
2
3
4
5
6
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);

  exec后跟不同的字符表示不同的功能:

  1、l:参数使用列表列出,结尾用NULL表示结束。

  2、p表示在环境变量中搜索参数,搜索到替换为环境变量中指定的文件。

  3、v表示参数用数组传递,数组最后一个元素用NULL表示结束。

  4、e表示需要自己传递环境变量。

  在系统中exec族函数最终都会调用execve,系统中只有这一个是系统接口,而其他函数不过是在execve上进行了一次封装。

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
exec.c:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
printf("Misaki\n");
execl("./env", "env", "-l", NULL);
return 0;
}

env.c:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[], char *env[])
{
int i;
for(i = 0; i < argc; i++) {
printf("argv[%d]=[%s]\n", i, argv[i]);
}
extern char **environ;
for (i = 0; environ[i] != NULL; i++) {
printf("env[%d] = [%s]\n", i, environ[i]);
}
char *ptr = getenv("MYENV");
printf("MYENV:[%s]\n", ptr);

return 0;
}


[misaki@localhost process]$ ./exec
Misaki
argv[0]=[env]
argv[1]=[-l]
env[0] = [XDG_SESSION_ID=1]
env[1] = [HOSTNAME=localhost.localdomain]
env[2] = [SELINUX_ROLE_REQUESTED=]
env[3] = [TERM=xterm]
env[4] = [SHELL=/bin/bash]
env[5] = [HISTSIZE=1000]
…………

  这样我们在exec.c中执行了进程替换代码后就用env.c中的代码替换了exec.c中剩下的代码,于是打印出了我们传入的参数和环境变量。

1…456…9
MisakiFx

MisakiFx

Hard working or giving up!!!

86 日志
10 分类
64 标签
GitHub E-Mail 网易云音乐 CSDN

© 2020 MisakiFx
我的网站的访客数:
|
主题 — NexT.Pisces v5.1.4
博客全站共273.4k字