线程的概念(和进程的对比)
1,进程是分配系统资源的实体,线程是CPU调度的基本单位,共享进程资源的一部分。(所以线程并不拥有资源)
2,进程是程序的一次执行,而线程是程序的一个执行片段。
3,线程没有地址空间,只是在进程内部有自己最近的一块独立空间。因为线程共享进程资源,所以线程通信更加简单。
说了半天,可能你仍旧混论,看一张图就好:
线程的创建:只需要创建一个task_struct,共享进程的地址空间,让进程的资源(代码和数据)划分成若干份,
让每个task_struct去使用。 所以Linux的进程又被称为轻量级进程。
具体的来说,Linux是用进程来模拟线程的,优点:不用维护线程和进程之间复杂的关系,不需要为线程设置相关的算法,只需要聚焦于
OS如何去分配进程的资源给线程。
线程的资源是共享进程的资源,其实线程也有自己独立的资源,那线程那些资源是共享的,那些资源又是独立的呢?。
独立资源:
1,栈
2,上下文(线程运行时产生的临时数据)
3,线程id
4,errno (错误码)
5,信号屏蔽字(block位图)
6,调度优先级。
共享资源:
1,文件描述表
2,每种信号的处理方式
3,用户id,用户组id
4,进程的全局数据。
线程的优缺点分析:
优点:
1,创建一个线程比创建一个进程快。(线程只用创建task_struct,而进程还需要各种内核数据结构)
2,线程切换更快。
3,线程占用的资源更少。
4,计算密集型应用(大数据运算,主要利用CPU资源),在多处理机系统上,可以分给多个线程去计算。
5,I/O密集型应用,可以让多个线程同时等待I/O操作,提高性能。
缺点:
1,健壮性降低,在一个多线程程序中,可能因为时间上细微的差异导致一个线程访问了不该访问的全局数据,导致对整个进程都造成不良 影响。而且,一个线程因异常崩溃,会导致该进程崩溃,导致所有的线程都崩溃。
2,缺乏访问控制,一个线程调用系统调用接口,会对整个进程产生影响。
3,编程难度高。
线程函数的操作和使用
关于线程库:为了方便用户操作,顶级工程师将Linu轻量级进程接口进程了封装,给用户打包成库,直接让用户去使用原生的线程库。
使用线程库,包含头文件<pthread.h>
而且编译的时候要指明链接的哪一个库 -lpthread。
顺便复习下动静态库把:
生成静态库时 ar -rc
生成目标文件是 gcc -c
生成动态库时 gcc -shared
生成目标文件 gcc -fPIC -c
当用户去使用的时候:
gcc … -I -L -l
1,大写的i , 指明头文件路径,-L指明库的路径, -小写的l,指明库的名字。
运行时动态库要去设置环境变量。
言归正传,关于线程相关的接口。
线程等待是很有必要的:
已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
创建新的线程不会复用刚才退出线程的地址空间
另外,让某一个线程退出,而不是整个进程都退出的方法:
1,调用pthread_exit() 和 pthread_cancel()函数
2,在某一个线程函数内部用return。
7 void* thread_run(void* p)
8 {
9 while(1)
10 {
11 printf("我是新线程 %u\n",pthread_self());
12 sleep(1);
13 }
14 }
15
16
17 int main()
18 {
19 pthread_t tid;
20 pthread_create(&tid,NULL,thread_run,NULL);
21
22 while(1)
23 {
24 printf("我是主线程:%u ,创建的线程是::%u\n",pthread_self(),tid);
25 sleep(1);
26 }
27 return 0;
28 }
灵活使用线程函数:
6 void* func1(void* arg)
7 {
8
9 printf("I am a 1 thread....\n");
10 int* p = (int*)malloc(4);
11 *p = 10;
12 return (void*)p;
13 }
14
W> 15 void* func2(void* args)
16 {
17
18 printf("I am a 2 thread....\n");
19 int* p = (int*)malloc(4);
20 *p = 20;
21 pthread_exit((void*)p);
22 }
23
W> 24 void* func3(void* args)
25 {
26 while(1)
27 {
28 printf("I am a thread3 is run\n");
29 sleep(1);
30 }
31 return NULL;
32 }
33
34 int main()
35 {
36 pthread_t tid;
37 void* tmp;
38
39 pthread_create(&tid,NULL,func1,NULL);
40 pthread_join(tid,&tmp);
41 printf("thread1 run end %d\n",*(int*)tmp);
42 free(tmp);
43 pthread_create(&tid,NULL,func2,NULL);
44 pthread_join(tid,&tmp);
45 printf("thread2 run end %d\n",*(int*)tmp);
46 free(tmp);
47
48 pthread_create(&tid,NULL,func3,NULL);
49 sleep(5);
50 pthread_cancel(tid);
51 return 0;
52 }
这个thread_t的类型
其实就是进程地址空间中动态库里面用来存储线程属性和数据结构的起始地址
线程互斥
首先铺垫一下概念:
1,临界资源:在一个进程中,多线程执行流共享的资源。
2,临界区:每一个线程内部,访问临界资源的代码段。
3,互斥:任何时刻,只允许一个执行流去访问临界资源。本质是保护临界资源。
4,原子性:要么不做,要么做完
多线程去访问临界资源的时候可能会存在某一些安全问题:
int tickets = 1000; //全局的共享资源
pthread_mutex_t mtx;
void *take_ticked(void *args)
{
while (1)
{
if (tickets > 0)
{
usleep(1000);
printf("剩下的票:%d\n",tickets);
tickets--; //--操作并非是原子的
}
else
{
break;
}
}
}
int main()
{
pthread_mutex_init(&mtx, nullptr); //初始化锁
pthread_t tid[5];
for (int i = 0; i < 5; i++)
{
pthread_create(tid + i, nullptr, take_ticked, nullptr);
}
for (int i = 0; i < 5; i++)
{
pthread_join(tid[i], nullptr);
}
pthread_mutex_destroy(&mtx); //销毁锁
return 0;
}
所以要对临界区进行加锁:
互斥量字面意思理解:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,
那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁
class Ticket
{
public:
Ticket()
:_tickets(1000)
{
pthread_mutex_init(&_mtx,nullptr); //这是一种动态分配的方式,不用的时候要销毁的
//pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER 这是静态分配,不需要销毁的
}
bool get_ticket()
{
bool ref;
pthread_mutex_lock(&_mtx); //(加锁)
while(true)
{
if(_tickets > 0)
{
_tickets--;
cout << "剩余票数" << _tickets << endl;
ref = true;
}
else
{
ref = false;
break;
}
}
pthread_mutex_unlock(&_mtx); //(解锁)
return ref;
}
~Ticket()
{
pthread_mutex_destroy(&_mtx);
}
private:
int _tickets;
pthread_mutex_t _mtx; //定义互斥量(锁)
};
void* take_ticked(void* args)
{
Ticket* t = (Ticket*)args;
while(t->get_ticket()) //为空就退出了
{}
}
int main()
{
Ticket* t = new Ticket();
pthread_t tid[5];
for(int i = 0; i < 5; i++)
{
pthread_create(tid+i,nullptr,take_ticked,(void*)t);
}
for(int i = 0; i < 5; i++)
{
pthread_join(tid[i],nullptr);
}
return 0;
}
加锁的原理理解:
如果该线程在临界区收到信号而要被挂起的时候呢?
其实,锁本质是一个全局变量,当线程要被挂起的时候会保存上下文数据放入PCB中,这这时候锁也会被保存在
PCB中,相当于是:线程被挂起的时候,线程是“抱着锁“走的
可重入和线程安全
可重入函数一定是线程安全的。
线程安全不一定是可重入的。
//这里省略一大波概念性的知识点(偷一波小懒😄)。
主要是重入和线程安全之间的关系和区别等等,,,,,,
外加那个死锁的4个必要条件
死锁:这里就强调一点了,就是资源循环等待,你不舍得给我,我不舍得给你,只有你给我我才会释放手里的锁
我给你你才会放开。 (想想打架的时候,你扯着我的头发,我扯着你的脸,我和你都不放)
线程同步
同步:
在保证数据安全的前提条件下,让多线程访问临界资源具有一定的顺序性,避免有的线程得不到CPU资源(”饥饿问题“)
条件变量的引入:
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情
况就需要用到条件变量。
总而言之,条件变量就是告诉你线程什么时候可以运行了
举个例子:
你喜欢了一个很漂亮的女神小方,但是呢她有男朋友,你又是一个非常有正义感的骚年,必须要等她分手你才可以追。
你每次隔几个星期都要去问她闺蜜:小方分手了吗? 当小方的闺蜜告诉你:小方分手了。 这时候就是条件具备了
你可以大胆的追了。
pthread_mutex_t mtx;
pthread_cond_t con;
void* fun1(void* args)
{
while(true)
{
cout << "master say" << endl;
pthread_cond_signal(&con);
sleep(1);
}
}
void* fun2(void* args)
{
while(1)
{
pthread_cond_wait(&con,&mtx); //条件变量为什么要用到互斥锁???
cout << pthread_self() <<"worker is working... " << endl;
sleep(1);
}
}
int main()
{
pthread_mutex_init(&mtx,nullptr);
pthread_cond_init(&con,nullptr);
pthread_t master;
pthread_t worker[3];
pthread_create(&master,nullptr,fun1,nullptr);
for(int i = 0; i < 3; i++)
{
pthread_create(worker+i,nullptr,fun2,nullptr);
}
pthread_join(master,nullptr);
for(int i = 0; i < 3; i++)
{
pthread_join(worker[i],nullptr);
}
pthread_mutex_destroy(&mtx);
pthread_cond_destroy(&con);
return 0;
}
为什么pthread_cond_wait() 需要锁呢? 条件等待是线程同步的一种手段,本质是线程和线程之间在交流,一个线程告诉一个线程,
可以了,你可以执行了。他们肯定之间在共同操作一些共享资源,这样线程才有必要去条件等待,所以对于共享数据的访问和修改
肯定是要加锁去保证安全的。
pthread_cond_wait(&con,&mtx); //它调用的时候,会自动释放自己的锁然后才挂起,然后得到条件后再去竞争锁。