线程及线程与进程的区别
一个线程类似于独立的进程,只有一点区别:它们共享地址空间,从而可以访问相同的内存数据。多个线程可以运行在同一个CPU上,也可以运行在不同的CPU上。
线程是程序员创建的实体,但是被操作系统调度。
为什么有多线程程序
经典观点是一个程序只有一个执行点(一个PC),但这在有些情境下不适用。
比如我们要实现一个定时器功能:当时间达到S时,使程序离开当前PC,执行另一个模块。那么我们就需要给这个定时器创建一个线程。
所谓多线程程序就是该程序可能同时在执行多条指令,这些线程或者运行在不同的CPU上(真正意义的并发),或者通过上下文切换进行时分共享。
线程API
//头文件
#include <pthread.h> //使用g++编译时需要添加属性 -l pthread
//数据结构(类似于进程列表)
pthread_t p1, p2;
//线程创建:pthread_create
void* mythred(void* arg);
rc = pthread_create(&p1, NULL, mythred, (void*)arg);assert(rc == 0)
rc = pthread_create(&p2, NULL, mythred, (void*)arg);assert(rc == 0);
//等待线程完成
pthread_join(p1, NULL);
pthread_join(p2, NULL);
我们创建线程的目的是为了让它执行特定的代码块(对应C语言的函数),所以在创建进程时,后两个参数指明了函数名与函数参数。
在等待线程完成时,第二个参数指明了函数的返回值。
具体细节请百度
共享内存引发的问题
一个简单的示例:
#include <iostream>
#include <pthread.h>
using namespace std;
static volatile int counter = 0;
void* mythred(void* arg)
{
cout << "begin" << static_cast<char*>(arg) << endl;
for (int i = 0; i < 1e7; i++)
counter += 1;
cout << "done" << static_cast<char*>(arg) << endl;
return nullptr;
}
int main()
{
cout << "hello world" << endl;
pthread_t p1, p2;
char A[] = "A", B[] = "B";
int rc = pthread_create(&p1, NULL, mythred, static_cast<void*>(&A));assert(rc == 0)
rc = pthread_create(&p2, NULL, mythred, static_cast<void*>(&B));assert(rc == 0);
pthread_join(p1, NULL);
pthread_join(p2, NULL);
cout << counter << endl;
}
A,B两个线程执行相同的任务:给计数器counter增加1e7次(使用1e7是希望它包含足够多的时间片),那么直观理解,结果应该是2e7。
但结果确实:
三次执行结果各不相同,且均小于2e7。
分析原因
原因出在不合时宜的中断上
分析这条语句:
counter += 1;
在CPU内部,这条高级语言的指令对应三条指令(取、+1、放回)
mov counter, ax
add 1, ax
mov ax, counter
假设counter值现在为50,A线程在第一次mov执行完毕后发生了上下文切换,轮到B线程对其增加,假如B线程对counter增加了20次。然后再次发生上下文切换,此时A线程恢复ax的值,从add指令开始执行,写回的counter值应该是51。
也就是说B线程的工作白费了。
核心来讲,是因为A,B线程共享内存缺乏调度引发的错误。
解决问题
一个思路是将 **counter += 1;**指令变成一条原子指令。
但对于更大的指令块呢,显然这个方式不具有扩展性。
原子:全都有或全没有,原子指令就是要么完全执行,要么还没有执行,不会出现执行一半时发生中断的情况。
原语:将具有原子性质的多条指令组成的指令块称为原语。
临界区:多个线程共同访问的变量或者代码块
静态条件:多个线程同时进入临界区,会产生不确定的结果。
对于这种A,B线程同时访问临界区的情况,更好的办法是加锁。
详见:操作系统并发性(二):锁
有时,A线程需要等待B线程执行完毕后再执行,这种情况的解决方法是条件变量。