Skip to content

Files

Latest commit

8284d54 · Jul 11, 2019

History

History
261 lines (180 loc) · 9.51 KB

31.md

File metadata and controls

261 lines (180 loc) · 9.51 KB

同步,第 1 部分:互斥锁

原文:https://github.com/angrave/SystemProgramming/wiki/Synchronization%2C-Part-1%3A-Mutex-Locks

解决关键部分

什么是关键部分?

关键部分是一段代码,如果程序要正常运行,一次只能由一个线程执行。如果两个线程(或进程)同时在临界区内执行代码,则程序可能不再具有正确的行为。

只是将变量递增一个关键部分?

有可能。递增变量(i++)分三个步骤执行:将存储器内容复制到 CPU 寄存器。增加 CPU 中的值。将新值存储在内存中。如果只能通过一个线程访问存储器位置(例如下面的自动变量i),则不存在竞争条件和没有与i相关的临界区的可能性。但是,sum变量是一个全局变量,可由两个线程访问。两个线程可能会尝试同时递增变量。

#include <stdio.h>
#include <pthread.h>
// Compile with -pthread

int sum = 0; //shared

void *countgold(void *param) {
    int i; //local to each thread
    for (i = 0; i < 10000000; i++) {
        sum += 1;
    }
    return NULL;
}

int main() {
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, countgold, NULL);
    pthread_create(&tid2, NULL, countgold, NULL);

    //Wait for both threads to finish:
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    printf("ARRRRG sum is %d\n", sum);
    return 0;
}

上述代码的典型输出为ARGGGH sum is 8140268每次运行程序时都会打印一个不同的总和,因为存在竞争条件;代码不会阻止两个线程同时读写sum。例如,两个线程都将 sum 的当前值复制到运行每个线程的 CPU 中(让我们选择 123)。两个线程都将一个增加到它们自己的副本两个线程都回写值(124)。如果线程在不同时间访问了总和,则计数将为 125。

如何确保一次只有一个线程可以访问全局变量?

你的意思是,“帮助 - 我需要一个互斥体!”如果一个线程当前在一个临界区内,我们希望另一个线程等到第一个线程完成。为此,我们可以使用互斥(互斥的简称)。

对于简单的示例,我们需要添加的最少量代码只有三行:

pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; // global variable
pthread_mutex_lock(&m); // start of Critical Section
pthread_mutex_unlock(&m); //end of Critical Section

一旦我们完成了互斥锁,我们也应该调用pthread_mutex_destroy(&m)。请注意,您只能销毁未锁定的互斥锁。在被破坏的锁上调用 destroy,初始化已初始化的锁,锁定已锁定的锁,解锁未锁定的锁等都不受支持(至少对于默认的互斥锁)并且通常会导致未定义的行为。

如果我锁定互斥锁,是否会阻止所有其他线程?

不,其他线程将继续。只有在线程试图锁定已经锁定的互斥锁时,线程才会等待。一旦原始线程解锁互斥锁,第二个(等待)线程将获得锁定并能够继续。

还有其他方法可以创建互斥锁吗?

是。您只能将宏 PTHREAD_MUTEX_INITIALIZER 用于全局('静态')变量。 m = PTHREAD_MUTEX_INITIALIZER 等同于更通用的pthread_mutex_init(&m,NULL)。 init 版本包括用于交换性能的选项,以进行其他错误检查和高级共享选项。

pthread_mutex_t *lock = malloc(sizeof(pthread_mutex_t)); 
pthread_mutex_init(lock, NULL);
//later
pthread_mutex_destroy(lock);
free(lock);

关于initdestroy需要注意的事项:

  • 多线程 init / destroy 具有未定义的行为
  • 销毁锁定的互斥锁具有未定义的行为
  • 基本上尝试保持一个线程初始化互斥锁的模式以及一个且只有一个初始化互斥锁的线程。

Mutex Gotchas

So pthread_mutex_lock在读取同一个变量时会停止其他线程吗?

没有。互斥体不是那么聪明 - 它适用于代码(线程),而不是数据。只有当另一个线程在锁定的互斥锁上调用lock时,第二个线程才需要等到互斥锁被解锁。

考虑

int a;
pthread_mutex_t m1 = PTHREAD_MUTEX_INITIALIZER,
                 m2 = = PTHREAD_MUTEX_INITIALIZER;
// later
// Thread 1
pthread_mutex_lock(&m1);
a++;
pthread_mutex_unlock(&m1);

// Thread 2
pthread_mutex_lock(&m2);
a++;
pthread_mutex_unlock(&m2);

仍会造成竞争条件。

我可以在分叉之前创建互斥吗?

是 - 但是子进程和父进程不共享虚拟内存,并且每个进程都将具有独立于另一个的互斥锁。

(高级注释:使用共享内存的高级选项允许子级和父级共享互斥锁,如果它使用正确的选项创建并使用共享内存段。请参阅 stackoverflow 示例

如果一个线程锁定互斥锁,另一个线程可以解锁吗?

不可以。同一个线程必须解锁它。

我可以使用两个或更多互斥锁吗?

是!事实上,每个数据结构都需要更新一个锁。

如果你只有一个锁,那么它们可能是两个不必要的线程之间锁定的重要争用。例如,如果两个线程正在更新两个不同的计数器,则可能没有必要使用相同的锁。

然而,简单地创建许多锁是不够的:能够推断关键部分是很重要的,例如:重要的是,一个线程在更新时暂时处于不一致状态时无法读取两个数据结构。

调用锁定和解锁是否有任何开销?

调用pthread_mutex_lock_unlock会产生少量开销;但这是你为正确运行的程序付出的代价!

最简单的完整例子?

完整的示例如下所示

#include <stdio.h>
#include <pthread.h>

// Compile with -pthread
// Create a mutex this ready to be locked!
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;

int sum = 0;

void *countgold(void *param) {
    int i;

    //Same thread that locks the mutex must unlock it
    //Critical section is just 'sum += 1'
    //However locking and unlocking a million times
    //has significant overhead in this simple answer

    pthread_mutex_lock(&m);

    // Other threads that call lock will have to wait until we call unlock

    for (i = 0; i < 10000000; i++) {
    sum += 1;
    }
    pthread_mutex_unlock(&m);
    return NULL;
}

int main() {
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, countgold, NULL);
    pthread_create(&tid2, NULL, countgold, NULL);

    //Wait for both threads to finish:
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    printf("ARRRRG sum is %d\n", sum);
    return 0;
}

在上面的代码中,线程在进入之前获得对计数室的锁定。关键部分只是sum+=1,因此以下版本也正确但速度较慢 -

    for (i = 0; i < 10000000; i++) {
        pthread_mutex_lock(&m);
        sum += 1;
        pthread_mutex_unlock(&m);
    }
    return NULL;
}

这个过程运行得慢,因为我们锁定和解锁互斥锁一百万次,这是昂贵的 - 至少与增加一个变量相比。 (在这个简单的例子中,我们并不真正需要线程 - 我们本来可以加两次!)一个更快的多线程示例是使用自动(本地)变量添加一百万,然后将其添加到共享总数计算循环结束后:

    int local = 0;
    for (i = 0; i < 10000000; i++) {
       local += 1;
    }

    pthread_mutex_lock(&m);
    sum += local;
    pthread_mutex_unlock(&m);

    return NULL;
}

如果我忘记解锁会怎么样?

僵局!我们稍后会讨论死锁,但是如果被多个线程调用,这个循环会出现什么问题。

while(not_stop){
    //stdin may not be thread safe
    pthread_mutex_lock(&m);
    char *line = getline(...);
    if(rand() % 2) { /* randomly skip lines */
         continue;
    }
    pthread_mutex_unlock(&m);

    process_line(line);
}

我什么时候可以销毁互斥锁​​?

您只能销毁未锁定的互斥锁

我可以将 pthread_mutex_t 复制到新的内存位置吗?

不,将互斥锁的字节复制到新的存储位置然后使用副本是 _ 而不是 _ 支持。

互斥体的简单实现是什么样的?

一个简单(但不正确!)的建议如下所示。 unlock功能只是解锁互斥锁并返回。锁定功能首先检查锁定是否已被锁定。如果它当前被锁定,它将继续检查,直到另一个线程解锁了互斥锁。

// Version 1 (Incorrect!)

void lock(mutex_t *m) {
  while(m->locked) { /*Locked? Nevermind - just loop and check again!*/ }

  m->locked = 1;
}
void unlock(mutex_t *m) {
  m->locked = 0;
}

版本 1 使用“忙等待”(不必要地浪费 CPU 资源)但是存在更严重的问题:我们有竞争条件!

如果两个线程同时调用lock,则两个线程都可能将'm_locked'读为零。因此,两个线程都会相信它们具有对锁的独占访问权,并且两个线程都将继续。哎呀!

我们可能会尝试通过在循环中调用pthread_yield()来稍微减少 CPU 开销 - pthread_yield 建议操作系统线程不会短时间使用 CPU,因此 CPU 可能被分配给等待的线程跑。但不能解决竞争条件。我们需要一个更好的实施 - 你能工作如何防止竞争条件?

我怎么知道更多?

玩! 阅读手册页!