赞
踩
线程是进程内的一个执行分支,线程的执行粒度,要比进程细。
地址空间是进程的资源窗口。在 Linux
中,线程在进程”内部“执行,即线程在进程的地址空间内运行,任何执行流要执行,都要有资源(代码,数据,CPU资源);在 Linux
中,线程的执行粒度要比进程更细,即线程执行进程代码的一部分;在 Linux
中,复用进程数据结构和管理算法来描述和组织线程,Linux
中没有真正意义上的线程,即在 Linux
中没有为线程创建独属于自己的 TCB
结构体(thread ctrl block
) ,而是用”进程“的内核数据结构(PCB
)模拟的线 程;CPU 只有执行流的概念,所以从原则上来说,CPU 是不区分进程和线程的,但是 Linux
操作系统要区分进程和线程;我们把 Linux
中的执行流叫做轻量级进程。
线程:我们认为线程是操作系统调度的基本单位。
进程:进程是承担分配系统资源(线程(执行流资源)、地址空间、页表、物理内存)的基本实体。
站在地址空间角度,线程分配资源本质就是分派地址空间范围。
PCB
对象就行。(生死)CPU
中的 cache
缓存、进程地址空间、页表等。只需要更新少量的上下文数据。创建线程不能给该线程重新申请时间片,而是将线程的时间片划分部分给线程。
cat /proc/cpuinfo
:查看 CPU
的信息。
创建一个新线程的代价要比创建一个新进程小的多。
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
线程占用的资源要比进程少很多。
能充分利用多处理器的可并行数量。
在等待慢速 I/O 操作结束的同时,程序可执行其他的计算任务。
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
I/O 密集型应用,为了提高性能,将 I/O 操作重叠。线程可以同时等待不同的 I/O 操作。
性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器,如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的新能损失(切换浪费时间),这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低:编写多线程需要更全面深入的考虑,在一个线程程序里,因时间分配上的细微偏差或因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说就是线程之间缺乏安全保护。
缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些 OS 函数会对整个进程造成影响。
编程难度提高:编写与调试一个多线程程序比单线程程序困难的多。
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
合理的使用多线程,能提高 CPU
密集型程序的执行效率。
合理的使用多线程,能提高 I/O 密集型程序的用户体验(如一边写代码一边下载开发工具,就是多线程运行的一种表现)
进程是资源分配的基本单位。
线程是调度的基本单位。
线程共享进程数据,但也拥有自己的一部分数据:
线程 ID
一组寄存器(线程的上下文)
栈
errno
信号屏蔽子
调度优先级
进程的多个线程之间共享同一地址空间,因此代码段、数据段都是共享的,如果定义一个函数,在各线程中都可以的调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各个线程还共享以下进程资源和环境:
SIG_IGN
、SIG_DFL
或者自定义的信号处理函数)id
和组 id
Linux
的内核中,没有很明确的线程概念,只有轻量级进程的概念。所以 Linux
操作系统不会给我们直接提供线程的系统调用,只会给我们提供轻量级进程的系统调用。伟大的 Linux
程序员将轻量级进程的接口进行封装,给用户在应用层开发出来了一个 pthread
线程库。几乎所有的 Linux
平台都是默认自带这个库的,Linux
中编写多线程代码,需要使用第三方 pthread
库。
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
thread
:输出型参数,返回线程 ID
attr
:设置线程的属性,为 NULL
表示使用默认属性
start_routine
:是个函数地址,线程启动后要执行的函数
arg
:传给启动线程的参数
返回值:创建成功返回0;创建失败返回对应的错误码
#include <iostream> #include <unistd.h> #include <pthread.h> using namespace std; void *threadRoutine(void *args) { while(true) { cout << "new thread, pid: " << getpid() << endl; sleep(2); } } int main() { pthread_t pid; pthread_create(&pid, nullptr, threadRoutine, nullptr); while(true) { cout << "main thread, pid: " << getpid() << endl; sleep(1); } return 0; }
在编译线程代码的时候,需要加上 -lpthread
选项。因为 pthread.h
是第三方库,但是 g++
编译器仅仅可以找到 pthread.h
和该库的位置,并不会默认帮我们去链接 pthread
库,因此我们要加 -lpthread
选项,告诉 g++
编译器,我们要链接这个库。
ps -aL
:其中 L
表示查看当前操作系统中的所有轻量级线程。
LWP
:一个轻量级进程的 ID,CPU 是按照 LWP 来进行调度的。CPU 调度的基本单位是线程,PID == LWP
的线程叫做主线程。
#include <iostream> #include <unistd.h> #include <pthread.h> using namespace std; int g_val = 100; void *threadRoutine(void *args) { const char *name = (const char*)args; while (true) { printf("%s, pid: %d, g_val: %d, &g_val: 0X%p\n", name, getpid(), g_val, &g_val); sleep(1); } } int main() { pthread_t pid; pthread_create(&pid, nullptr, threadRoutine, (void *)"Thread 1"); while (true) { printf("main thread, pid: %d, g_val: %d, &g_val: 0X%p\n", getpid(), g_val, &g_val); sleep(1); g_val++; } return 0; }
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
thread
:要等待的线程 IDretval
:输出型参数,获取线程函数的返回值线程等待的目的:
防止新线程内存泄露
主线程获取子线程的执行结果
#include <iostream> #include <unistd.h> #include <pthread.h> using namespace std; int g_val = 100; void *threadRoutine(void *args) { const char *name = (const char*)args; int cnt = 5; while (true) { printf("%s, pid: %d, g_val: %d, &g_val: 0X%p\n", name, getpid(), g_val, &g_val); sleep(1); cnt--; if(cnt == 0) break; } return (void *)100; } int main() { pthread_t pid; pthread_create(&pid, nullptr, threadRoutine, (void *)"Thread 1"); // while (true) // { // printf("main thread, pid: %d, g_val: %d, &g_val: 0X%p\n", getpid(), g_val, &g_val); // sleep(1); // g_val++; // } void *ret; pthread_join(pid, &ret); cout << "main thread quit..., Thread 1 return val: " << (long long int)ret << endl; return 0; }
线程执行完它的函数后就退出了,主线程在等待的时候,默认是阻塞等待。主线程等待子线程,只能获取到子线程执行函数的返回值,不考虑子线程出异常,因为子线程一旦出异常,主线程也会跟着遭殃。
exit 是用来终止进程的,不能用来直接终止线程。任何一个子线程在任何地方调用 exit
都表示整个进程退出。
pthread_exit
:终止调用该函数的线程
#include <pthread.h>
void pthread_exit(void *retval);
retval
:线程函数的返回值#include <iostream> #include <unistd.h> #include <pthread.h> using namespace std; int g_val = 100; void *threadRoutine(void *args) { const char *name = (const char*)args; int cnt = 5; while (true) { printf("%s, pid: %d, g_val: %d, &g_val: 0X%p\n", name, getpid(), g_val, &g_val); sleep(1); cnt--; if(cnt == 0) break; } pthread_exit((void *)200); return (void *)100; } int main() { pthread_t pid; pthread_create(&pid, nullptr, threadRoutine, (void *)"Thread 1"); void *ret; pthread_join(pid, &ret); cout << "main thread quit..., Thread 1 return val: " << (long long int)ret << endl; return 0; }
注意:pthread_exit
或者 return
返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
pthread_cancel
:取消一个已存在的目标线程。
#include <pthread.h>
int pthread_cancel(pthread_t thread);
thread
:要取消的进程 ID
返回值:取消成功返回0;取消失败,对应的错误码被返回
#include <iostream> #include <unistd.h> #include <pthread.h> using namespace std; int g_val = 100; void *threadRoutine(void *args) { const char *name = (const char*)args; int cnt = 5; while (true) { printf("%s, pid: %d, g_val: %d, &g_val: 0X%p\n", name, getpid(), g_val, &g_val); sleep(1); cnt--; if(cnt == 0) break; } pthread_exit((void *)200); return (void *)100; } int main() { pthread_t pid; pthread_create(&pid, nullptr, threadRoutine, (void *)"Thread 1"); sleep(1); pthread_cancel(pid); void *ret; pthread_join(pid, &ret); cout << "main thread quit..., Thread 1 return val: " << (long long int)ret << endl; return 0; }
一个进程被取消,它的返回值是 PTHREAD_CANCELED
一个宏:
#include <iostream> #include <unistd.h> #include <pthread.h> using namespace std; int g_val = 100; void Print(const string &name) { printf("%s is running, pid: %d, g_val: %d, &g_val: 0X%p\n", name.c_str(), getpid(), g_val, &g_val); } void *threadRoutine(void *args) { const char *name = (const char*)args; int cnt = 5; while (true) { Print(name);// 调用全局函数 sleep(1); cnt--; if(cnt == 0) break; } pthread_exit((void *)200); return (void *)100; } int main() { pthread_t pid; pthread_create(&pid, nullptr, threadRoutine, (void *)"Thread 1"); int cnt = 0; while (true) { Print("main thread");// 调用全局函数 sleep(1); g_val++; cnt++; if(cnt == 10) break; } void *ret; pthread_join(pid, &ret); cout << "main thread quit..., Thread 1 return val: " << (long long int)ret << endl; return 0; }
这说明代码区对所有的线程来说是共享的。
一个求和任务
#include <iostream> #include <unistd.h> #include <pthread.h> #include <string> #include <cstdlib> using namespace std; class Request { public: Request(int start, int end, const string &threadname) :start_(start) ,end_(end) ,threadname_(threadname) {} int sum() { int ret = 0; for(int i = start_; i <= end_; i++) { cout << threadname_ << " is running..." << endl; ret += i; usleep(10000); } return ret; } public: int start_; // 起始数 int end_; // 终止数 string threadname_; // 线程的名字 }; class Response { public: Response(int result, int exitcod) :result_(result) ,exitcode_(exitcod) {} public: int result_; // 计算结果 int exitcode_; // 标记结果的可靠性 }; void *SumCount(void *args) { Request *rq = static_cast<Request *>(args); Response *rp = new Response(rq->sum(), 0); delete rq; return rp; } int main() { pthread_t tid; // 创建一个线程 Request *rq = new Request(1, 100, "Thread 1"); pthread_create(&tid, nullptr, SumCount, rq); void *ret; pthread_join(tid, &ret); // 线程等待,获取线程的返回值 Response *rp = static_cast<Response *>(ret); cout << "result: " << rp->result_ << ", exitcode: " << rp->exitcode_ << endl; delete(rp); return 0; }
该实例证明了堆空间也是共享的。
pthread.h
是原生线程库。C++11 的线程库本质上是封装了原生线程库。在 Linux
下,C++11 的线程库底层封装的是 Linux
的系统调用,在 Windows
下,C++11 底层封装的是 Windows
的系统调用。这也是 C++ 语言具有跨平台性的体现。如果代码中直接使用系统调用,那么就不具有跨平台性。
#include <iostream> #include <unistd.h> #include <pthread.h> #include <thread> using namespace std; void threadrun() { while(true) { cout << "I am a new thread for C++" << endl; sleep(1); } } int main() { thread th(threadrun);// 创建一个线程 th.join(); return 0; }
返回调用该函数的线程 ID。
#include <pthread.h>
pthread_t pthread_self(void);
#include <iostream> #include <unistd.h> #include <pthread.h> #include <thread> #include <string> using namespace std; string toHex(int num) // 转十六进制接口 { char ret[64]; snprintf(ret, sizeof(ret), "%p", num); return ret; } void *threadroutine(void *args) { while(true) { sleep(2); cout << "thread id: " << toHex(pthread_self()) << endl; } return nullptr; } int main() { pthread_t tid; pthread_create(&tid, nullptr, threadroutine, nullptr); while(true) { cout << "creat a new thread, id: " << toHex(tid) << endl; sleep(1); } pthread_join(tid, nullptr); return 0; }
pthread
线程库Linux
内核中没有很明确线程的概念,只有轻量级进程的概念,clone
接口就是用来创建一个轻量级进程。pthread_creat
底层就是封装了 clone
。
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, .../* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
线程的概念是 pthread
线程库给我们提供的,我们在使用原生线程库的时候,g++
默认使用动态链接,所以该库是会被加载到内存中的,然后通过页表映射到进程的共享区中,线程肯定不止一个,所以线程库一定要把当前操作系统中创建的所有线程管理起来,管理的方式就是通过先描述再组织,因此在线程库中一定存在一个描述线程的结构体,将该结构体称作 TCB
,一个线程的 tid
(上文中提到的线程 ID)就是其在线程库中的 TCB
对象的起始地址(改地址是一个虚拟地址)。这个 tid
是用户层面的,给用户来使用的,LWP
是内核中的概念,因为 CPU
调度的最小单位是线程(也就是轻量级进程),所以操作系统需要有一个编号来唯一标识一个线程。在Linux
中,我们所说的线程是用户级线程,因为在 Linux
中,线程的概念是 pthread
为我们提供的,在 Windows
中的线程是内核级线程,因为它是由操作系统直接提供的。在 Linux
中一个用户级线程对应一个内核级线程。
每个线程在被创建出来之后,都要有自己独立的栈结构,因为每个线程都有自己的调用链,执行流的本质就是调用链,该栈空间会保存一个执行流在运行过程中产生的临时变量,函数调用进行的入栈操作。主线程直接使用地址空间中为我们提供的栈结构即可,其他子线程的独立栈,都在共享区,具体来说是在 pthread
库中,tid
指向的 TCB
中维护了该线程的独立栈。
#include <iostream> #include <pthread.h> #include <vector> #include <string> #include <unistd.h> using namespace std; #define NUM 10 class ThreaInfo { public: ThreaInfo(const string &threadname) :threadname_(threadname) {} public: string threadname_; }; string toHex(pthread_t tid) { char buffer[64]; snprintf(buffer, sizeof(buffer), "%p", tid); return buffer; } void *threadroutine(void *args) { int i = 0; ThreaInfo *ti = static_cast<ThreaInfo*>(args); while(i < 10) { cout << ti->threadname_.c_str() << " is running, tid: " << toHex(pthread_self()) << ", pid: " << getpid() << endl; i++; sleep(1); } return nullptr; } int main() { vector<pthread_t> tids; for(int i = 0; i < NUM; i++) { pthread_t tid; ThreaInfo *ti = new ThreaInfo("Thread-"+to_string(i)); pthread_create(&tid, nullptr, threadroutine, ti); tids.push_back(tid); sleep(1); } for(auto tid:tids) { pthread_join(tid, nullptr); } return 0; }
#include <iostream> #include <pthread.h> #include <vector> #include <string> #include <unistd.h> using namespace std; #define NUM 3 class ThreaInfo { public: ThreaInfo(const string &threadname) :threadname_(threadname) {} public: string threadname_; }; string toHex(pthread_t tid) { char buffer[64]; snprintf(buffer, sizeof(buffer), "%p", tid); return buffer; } void *threadroutine(void *args) { int i = 0; int num = 0; ThreaInfo *ti = static_cast<ThreaInfo*>(args); while(i < 10) { cout << ti->threadname_.c_str() << " is running, tid: " << toHex(pthread_self()) << ", pid: " << getpid() << ", num: " << num << ", &num: " << toHex((pthread_t)&num) << endl; i++; num++; usleep(10000); } return nullptr; } int main() { vector<pthread_t> tids; for(int i = 0; i < NUM; i++) { pthread_t tid; ThreaInfo *ti = new ThreaInfo("Thread-"+to_string(i)); pthread_create(&tid, nullptr, threadroutine, ti); tids.push_back(tid); // sleep(1); usleep(1000); } for(auto tid:tids) { pthread_join(tid, nullptr); } return 0; }
每个线程都去调用了 threadroutine
函数,但是每个线程都有自己的 num
,都是从0开始,并且 num
的地址都不同。这正是因为每个线程都有自己独立的栈结构,每个线程在调用该函数时,都将该函数中的局部变量压入自己所在的栈空间。
虽然每一个线程都有自己独立的栈结构,但是对于同一个进程创建的多个线程来说,它们都是在该进程的地址空间中,所以只要你想,一个进程是可以拿到另一个线程栈中的数据。
定义一个全局的指针变量,让其指向线程1栈空间中的一个变量,这样就能在主线程中去获取子线程栈空间的数据
#include <iostream> #include <pthread.h> #include <vector> #include <string> #include <unistd.h> using namespace std; #define NUM 3 int *p = nullptr; class ThreaInfo { public: ThreaInfo(const string &threadname) :threadname_(threadname) {} public: string threadname_; }; string toHex(pthread_t tid) { char buffer[64]; snprintf(buffer, sizeof(buffer), "%p", tid); return buffer; } void *threadroutine(void *args) { int i = 0; int num = 0; ThreaInfo *ti = static_cast<ThreaInfo*>(args); if(ti->threadname_ == "Thread-1") p = # // 将线程1中的 num 变量的地址存到 p 指针里面 while(i < 10) { cout << ti->threadname_.c_str() << " is running, tid: " << toHex(pthread_self()) << ", pid: " << getpid() << ", num: " << num << ", &num: " << &num << endl; i++; num++; usleep(10000); } return nullptr; } int main() { vector<pthread_t> tids; for(int i = 0; i < NUM; i++) { pthread_t tid; ThreaInfo *ti = new ThreaInfo("Thread-"+to_string(i)); pthread_create(&tid, nullptr, threadroutine, ti); tids.push_back(tid); // sleep(1); usleep(1000); } cout << "main thread get Thread-1 num: " << *p << ", &num: " << p << endl; for(auto tid:tids) { pthread_join(tid, nullptr); } return 0; }
虽然这样做可以,但是我们在代码中是禁止这样做的。
定义的普通全局变量是被所有线程所共享的,如果想让该全局变量在每个线程内部,各自私有一份,可以在定义全局变量的前面加上 __thread
,这并不是语言给我们提供的,而是编译器给我们提供。并且 __thread
只能用来修饰内置类型,不能用来修饰自定义类型。
#include <iostream> #include <pthread.h> #include <vector> #include <string> #include <unistd.h> using namespace std; #define NUM 3 int *p = nullptr; __thread int val = 100; class ThreaInfo { public: ThreaInfo(const string &threadname) :threadname_(threadname) {} public: string threadname_; }; string toHex(pthread_t tid) { char buffer[64]; snprintf(buffer, sizeof(buffer), "%p", tid); return buffer; } void *threadroutine(void *args) { int i = 0; // int num = 0; ThreaInfo *ti = static_cast<ThreaInfo*>(args); // if(ti->threadname_ == "Thread-1") p = # // 将线程1中的 num 变量的地址存到 p 指针里面 while(i < 10) { cout << ti->threadname_.c_str() << " is running, tid: " << toHex(pthread_self()) << ", pid: " << getpid() << ", val: " << val << ", &num: " << &val << endl; i++; // num++; val++; usleep(10000); } return nullptr; } int main() { vector<pthread_t> tids; for(int i = 0; i < NUM; i++) { pthread_t tid; ThreaInfo *ti = new ThreaInfo("Thread-"+to_string(i)); pthread_create(&tid, nullptr, threadroutine, ti); tids.push_back(tid); // sleep(1); usleep(1000); } // cout << "main thread get Thread-1 num: " << *p << ", &num: " << p << endl; for(auto tid:tids) { pthread_join(tid, nullptr); } return 0; }
此时 val
虽然定义在全局,但实际上在每一个进程的独立栈中间中都会为 val
开辟一块空间,来进行存储。
joinable
的,线程退出后,需要对其进行 pthread_join
操作,否则无法释放资源,从而造成资源泄露。join
是一种负担,这个时候,我们可以告诉操作系统,当线程退出时,自动释放线程资源。#include <pthread.h>
int pthread_detach(pthread_t thread);
thread
:要分离的线程 ID
返回值:分离成功返回0;失败错误码被返回
小Tips:该函数可以由主线程来调用,也可以由子线程来调用。
#include <iostream> #include <pthread.h> #include <vector> #include <string> #include <unistd.h> #include <cstdio> #include <cstring> using namespace std; #define NUM 3 int *p = nullptr; // __thread int val = 100; class ThreaInfo { public: ThreaInfo(const string &threadname) :threadname_(threadname) {} public: string threadname_; }; string toHex(pthread_t tid) { char buffer[64]; snprintf(buffer, sizeof(buffer), "%p", tid); return buffer; } void *threadroutine(void *args) { int i = 0; // int num = 0; ThreaInfo *ti = static_cast<ThreaInfo*>(args); // if(ti->threadname_ == "Thread-1") p = # // 将线程1中的 num 变量的地址存到 p 指针里面 while(i < 10) { cout << ti->threadname_.c_str() << " is running, tid: " << toHex(pthread_self()) << ", pid: " << getpid() << endl; i++; // num++; // val++; usleep(10000); } return nullptr; } int main() { vector<pthread_t> tids; for(int i = 0; i < NUM; i++) { pthread_t tid; ThreaInfo *ti = new ThreaInfo("Thread-"+to_string(i)); pthread_create(&tid, nullptr, threadroutine, ti); tids.push_back(tid); // sleep(1); usleep(1000); } // cout << "main thread get Thread-1 num: " << *p << ", &num: " << p << endl; for(auto tid:tids) { pthread_detach(tid); // 线程分离 } for(auto tid:tids) { int ret = pthread_join(tid, nullptr); printf("%p: ret: %d, messg: %s\n", tid, ret, strerror(ret)); } return 0; }
子线程各自只执行了一次,是因为,子线程在被创建出来之后,主线程立即将所有的子线程进行了分离,然后,主线程又去进行 join
,此时因为所有的子线程已经被分离了,主线程去 join
就不会阻塞等待了,而是直接出错返回,最后主线程执行完毕就直接退出了,主线程退出,也就意味着进程退出,进程退出所有的资源就要被释放,所有子线程赖以生存的环境也没了,所以子线程也就跟着没了。因此,线程分离后,要保证主线程最后退出,常见的情况是主线程跑死循环,一直不退出。
线程分离本质上是线程 TCB
中的一个属性,pthread_detach
本质上就是去修改改属性。
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,春人的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是春人前进的动力!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。