33893


C言語で複数スレッドからファイルアクセスする場合、ロックする関数についてご教示ください。

下記のようなプログラムを作成しています。

・スレッド(1)は、ファイルに対してライトします。  
・スレッド(2)は、スレッド(1)によってライトされたファイルに対してリードします。  
・必ずしもスレッド(1)(2)は同じファイルにアクセスするわけではなく、スレッドごとにfopen()します。  
・タイミングによってスレッド(1)(2)は同じファイルにアクセスします。

・（不測の自体でロックをとったままスレッドが落ちてしまった場合、他方のスレッド側ロックを強制解除して処理は継続させたいです（別途エラーメッセージはログに出力します）。

flock(fd,LOCK_EX)をつかってロックを取る方針で検討していたのですが、  
「flock()はファイル記述子ごとにロックするので、  
fopen()を別々に行っていては正しくロックできないのでは？」  
との指摘を受けて、ロックのとり方を再検討している最中です。


CC BY-SA 3.0
2017-04-09T07:51:19.613
33879
2017-04-11T01:37:57.080
2017-04-09T07:58:09.630
21437
21437
question
linux c
複数スレッドからファイルアクセスする場合の、適切なロック方法


[前の質問](https://ja.stackoverflow.com/questions/33851/)の回答で sayuri
さんが言いたかったのは、ロックする時と解除する時とで  
`fp` (というか file descriptor) が違う点が問題、ということだと思います。

以下のように `flock()` でも問題ないと思います。

[code]

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/file.h>
    #include <pthread.h>
    
    #define N 2
    
    static FILE *fp0;
    
    static void *test0(void *parm)
    {
        int n = *(int *) parm;
    
        if ((fp0 = fopen("test.txt", "a+")) == NULL) {
            perror("fopen");
            exit(1);
        }
    
        if (flock(fileno(fp0), LOCK_EX | LOCK_NB) != 0) {
            perror("flock");
            exit(1);
        }
    
        return NULL;
    }
    
    static void *test1(void *parm)
    {
        int n = *(int *) parm;
        FILE *fp;
    
        if (flock(fileno(fp0), LOCK_UN | LOCK_NB) != 0) {
            perror("flock");
            exit(1);
        }
    
        if ((fp = fopen("test.txt", "a+")) == NULL) {
            perror("fopen");
            exit(1);
        }
    
        if (flock(fileno(fp), LOCK_EX | LOCK_NB) != 0) {
            perror("flock");
            exit(1);
        }
    
        fclose(fp);
    
        return NULL;
    }
    
    int main(void)
    {
        pthread_t thr[N];
        pthread_attr_t attr[N];
        int n[N];
    
        for (int i = 0; i < N; i++)
            n[i] = i;
    
        pthread_attr_init(&attr[0]);
        pthread_create(&thr[0], &attr[0], test0, &n[0]);
        sleep(1);
        pthread_attr_init(&attr[1]);
        pthread_create(&thr[1], &attr[1], test1, &n[1]);
    
        for (int i = 0; i < N; i++) {
            void *r;
            pthread_join(thr[i], &r);
        }
    
        return 0;
    }
    
[/code]

`test0()` 側スレッドで `fopen()` した `fp0` を、`test1()` 側スレッドに渡す必要があります。  
この `fp0` を放置すると、何度もスレッドが落ちた場合に `fclose()` されない `FILE *`
がどんどん残っていってしまいます。従って強制解除と同時に `fclose()` も必要になるでしょうから、どのみち `test1()`
側スレッドに渡す必要はあるので、この点は問題にはならないと思っています。


CC BY-SA 3.0
2017-04-09T13:52:53.950
33887
2017-04-09T13:52:53.950
2017-04-13T12:52:38.920
-1
5288
33879
answer


同一プロセス内のスレッド間のファイル読み書きの排他制御であれば、`pthread_mutex_lock()`で制御するほうが簡易かと思います。  
(と言うか、同一プロセス(PID)内ではスレッド間で`fcntl()`でのファイルロックはできないと思われます)

[code]

    /*
     * コード例(エラー判定は省略)
     */
    /* スレッド間で共有する排他制御変数 */
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZE;
    
    /*
     * スレッド1
     */
    void* thread1(void* arg) {
        /*
         * 中略
         */
        pthread_mutex_lock(&mutex);
        FILE* fp = fopen("file", "r");
        /*
         * ファイル操作(読み込み)
         */
        fclose(fp);
        pthread_mutex_unlock(&mutex);
        /*
         * 以下、略
         */
    }
    /*
     * スレッド2
     */
    void* thread2(void* arg) {
        /*
         * 中略
         */
        pthread_mutex_lock(&mutex);
        FILE* fp = fopen("file", "w+");
        /*
         * ファイル操作(書き込み)
         */
        fclose(fp);
        pthread_mutex_unlock(&mutex);
        /*
         * 以下、略
         */
    }
    
[/code]

なお、本論から外れますが、スレッドが異常終了したことを考慮に入れるのであれば、スレッドの生死や排他変数の状態監視を行う別スレッドが必要になってくると思います。  
複雑な処理の部類と思いますので、動作条件を見直した方がよいかと思います。


CC BY-SA 3.0
2017-04-09T14:41:43.747
33889
2017-04-09T14:41:43.747
20098
33879
answer


Linuxではあまり役に立たないかもしれませんが

C11で導入された[`fopen_s()`](http://en.cppreference.com/w/c/io/fopen)を使用すると排他オープンすることができます。ファイルシステムレベルでの排他なので別プロセス・別スレッドだけでなく同一スレッドであってもクローズするまでは再オープンすることができなくなります。

ただし、先行するファイル使用者がいつ完了するのか？という問題があります。完了待ちをすることを考えるとuser20098さんの提案されている`pthread_mutex_lock`などの排他・同期が適切に感じます。

* * *

> もスレッド(1)(2)は同じファイルにアクセスするわけではないので、mutexだと若干ロッ‌​ク範囲がおおきくなります。

ファイル毎にmutexを作成するまでです。ファイル名・mutexのペアを構造体で用意し、スレッド間で共有することになるでしょうか。  
というように適切な設計を行っていくと

> スレッド毎にfopen()して、できればスレッド間でファイルディスクリプタの受け渡しはしたくな‌​いです。

などとは言っていられないです。

* * *

> 排他を取るのはシングルプロセスです。

とのことですが、ファイルを書き込むのも読み込むのも単一プロセス内ということでしたら、そもそもファイルを介さず、プロセス内スレッド間でメモリを共有すればいいのではと思います。


CC BY-SA 3.0
2017-04-09T21:48:02.253
33893
2017-04-11T01:37:57.080
2017-04-11T01:37:57.080
4236
4236
33879
answer


参考までに、以下は [Open file description locks](https://gavv.github.io/blog/file-
locks/#open-file-description-locks-fcntl) を使ったサンプルコードです。

リンク先に記載がある通り、`POSIX record locks` との違いは以下で、

> The only difference is in fcntl command names:
[code]

>     F_OFD_SETLK instead of F_SETLK
>     F_OFD_SETLKW instead of F_SETLKW
>     F_OFD_GETLK instead of F_GETLK
>  
[/code]

Linux kernel 3.15 以上でのみ利用可能です。

※ `POSIX record locks` である `fcntl(fd, F_SETLKW, ...)` や `fcntl(fd, F_SETLK,
...)`
をコメントアウトしていますが、こちらを使うと出力ファイル(`/tmp/output.dat`)の中身は期待通りになりません(部分的に上書きされるなど)

[code]

    #define _GNU_SOURCE
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <pthread.h>
    
    #define OUTPUT "/tmp/output.dat"
    
    void *write_thread(void *id){
      long tid = (long)id;
      struct flock lock = {
        .l_whence = SEEK_SET,
        .l_start = 0,
        .l_len = 0,   // Lock whole file
      };
    
      int fd = open(OUTPUT, O_RDWR | O_CREAT, 0644);
    
      lock.l_type = F_WRLCK;
      fcntl(fd, F_OFD_SETLKW, &lock);
      //fcntl(fd, F_SETLKW, &lock);
    
      lseek(fd, 0, SEEK_END);
    
      int len; char buf[256];
      for (int i=0;i<5;i++) {
        len = sprintf(buf, "tid=%ld, fd=%d, i=%d\n", tid, fd, i);
        write(fd, buf, len);
      }
      fsync(fd);
    
      lock.l_type = F_UNLCK;
      fcntl(fd, F_OFD_SETLK, &lock);
      //fcntl(fd, F_SETLK, &lock);
      close(fd);
    
      pthread_exit(NULL);
    }
    
    int main() {
      int num_threads = 3;
      pthread_t threads[num_threads];
    
      truncate(OUTPUT, 0);
      for (int i=0;i<num_threads;i++) {
        pthread_create(&threads[i], NULL, write_thread, (void *)i);
      }
    
      for (int i=0;i<num_threads;i++) {
       pthread_join(threads[i], NULL);
      }
    
      return 0;
    }
    
[/code]


CC BY-SA 3.0
2017-04-09T22:10:03.813
33894
2017-04-09T22:10:03.813
33879
answer